如何在PHPUnit的文本断言中允许已知位置的预期差异(Golden Master测试场景)
我太懂这种头疼的情况了——搞Golden Master测试本来就是要抓精准的文本匹配,但那些暂时没法根除的副作用(比如固定位置的动态数字、临时ID这类)总在同一个地方搞破坏,直接用$this->assertSame($expected, $actual)每次都红,完全没法正常跑测试。
给你几个实用的解决方案,都是我自己在项目里用过的,完全适配你的场景:
方法一:正则标准化可变内容后再断言
最直接的思路:把预期和实际文本里的可变部分都替换成统一的占位符,然后再做严格相等断言。这样既忽略了允许的差异,又能在其他不允许修改的地方出现变化时立刻失败。
针对你的例子,可变的是DIFF后面的数字,还有containing an后面的数字,我们可以写个简单的清理函数:
// 定义标准化函数,把已知的可变内容替换成统一标记 function normalizeGoldenText(string $text): string { // 替换"DIFF 数字"为"DIFF XX" $text = preg_replace('/DIFF \d+/', 'DIFF XX', $text); // 替换"containing an 数字 expected diff"为"containing an XX expected diff" $text = preg_replace('/containing an \d+ expected diff/', 'containing an XX expected diff', $text); return $text; } // 测试里的断言改成这样 $this->assertSame(normalizeGoldenText($expected), normalizeGoldenText($actual));
完全适配你的场景:
- 第一个测试用例里,两个文本的可变数字被替换成XX后完全一致,断言通过;
- 第二个失败案例里,“Also this SHOULD be the same but is not”这一行没被标准化处理,替换后还是和预期不一样,断言直接失败,正好是我们要的效果。
方法二:封装成自定义PHPUnit断言(适合高频使用场景)
如果你的项目里很多测试都要处理这种Golden Master差异,不如把逻辑封装成自定义断言,写在测试基类里,用起来更简洁:
// 在你的测试基类(比如BaseTestCase)里添加这个方法 protected function assertGoldenMasterMatch(string $expected, string $actual): void { $normalize = function(string $text) { $text = preg_replace('/DIFF \d+/', 'DIFF XX', $text); $text = preg_replace('/containing an \d+ expected diff/', 'containing an XX expected diff', $text); return $text; }; $normalizedExpected = $normalize($expected); $normalizedActual = $normalize($actual); // 自定义错误信息,方便定位问题 $this->assertSame( $normalizedExpected, $normalizedActual, "Golden Master不匹配:已忽略允许的差异,但仍存在未预期的文本变化" ); } // 之后在测试里直接调用就行 $this->assertGoldenMasterMatch($expected, $actual);
这样每次用的时候不用重复写正则,代码更干净,错误信息也更友好。
方法三:行级精准校验(适合复杂可变场景)
如果可变部分的格式有规则,但位置固定,你也可以把文本按行拆分,对每一行做针对性校验:允许固定行的格式符合规则,其他行必须完全一致。
比如你的例子里,第2行和第6行是可变行,其他行必须严格匹配:
$expectedLines = explode("\n", trim($expected)); $actualLines = explode("\n", trim($actual)); // 先检查行数是否一致,避免少行/多行的情况 $this->assertCount(count($expectedLines), $actualLines, "Golden Master文本行数不匹配"); foreach ($expectedLines as $lineIndex => $expectedLine) { $actualLine = $actualLines[$lineIndex]; // 第2行(索引1,从0开始计数)和第6行(索引5)是允许可变的行 if ($lineIndex === 1 || $lineIndex === 5) { if ($lineIndex === 1) { // 校验行格式:必须是"Here might be an expected DIFF 数字" $this->assertMatchesRegularExpression( '/^Here might be an expected DIFF \d+$/', $actualLine, "第{$lineIndex}行格式不符合预期" ); } else { // 校验第6行格式:必须是"Yet another line containing an 数字 expected diff" $this->assertMatchesRegularExpression( '/^Yet another line containing an \d+ expected diff$/', $actualLine, "第{$lineIndex}行格式不符合预期" ); } } else { // 其他行必须完全一致 $this->assertSame( $expectedLine, $actualLine, "第{$lineIndex}行与Golden Master不匹配" ); } }
这种方法的好处是:不仅忽略了允许的数字差异,还能确保可变行的格式完全符合预期(比如不能突然变成字母),适合对可变内容有格式要求的场景。
我自己平时做Golden Master测试用得最多的是第一种方法,简单高效,只要把所有已知的可变模式都覆盖到就行。如果断言失败了,还可以把标准化后的$normalizedExpected和$normalizedActual打印出来,一眼就能找到未预期的变化。
内容来源于stack exchange

