使用 PHPUnit 进行数据驱动测试
发布于 作者: Jeff
测试您的代码是开发流程中必不可少的一部分,但当您尝试根据一组不同的输入数据模拟许多用例时,它有时也会很昂贵。
在许多情况下,您最终可能会得到一个庞大的测试目录,在每个可能的用户交互中重复相同的代码块。
以这个案例为例:假设您正在编写一个应用程序将 Markdown 文档转换为 HTML,因此您可能需要处理至少以下方法
- Transform h1 tags- Transform code- Transform links- ...
只是想提几个。
为每种情况添加测试的一种解决方案是为每个 Transformers
创建一个方法
class ExampleTest extends TestCase{ /** @test */ public function transform_title() { // } /** @test */ public function transform_code() { // } /** @test */ public function transform_links() { // }}
有什么问题呢?
在前面的示例中,所有方法更有可能共享相同的结构:给定的输入、转换方法或类以及预期的输出。
现在想象一下,如果您的应用程序中有 50 个不同的转换器,这个测试文件将会非常大。
介绍 数据驱动测试
TL;DR 数据驱动测试 (DDT) 是在计算机软件测试中使用的一个术语,用于描述使用条件表直接作为测试输入和可验证输出进行的测试,以及测试环境设置和控制不硬编码的过程。
在这种方法中,您定义一组数据,这些数据将驱动每个可能组合的测试。
$dataSet = [ ['some A', TransformerA::class, 'expected output A'], ['some B', TransformerA::class, 'expected output B'], ['some C', TransformerA::class, 'expected output C'],]
然后在您的测试中,您可以使用类似以下内容
class ExampleTest extends TestCase{ /** @test */ public function transform_content() { $dataSet = $this->dataSet(); foreach($dataSet as $data) { $value = $data[0] $class = new $data[1]; $expected = $data[2] $this->assertEquals($expected, $class->process($value)); } } public function dataSet() { return [ ['# Title #', TitleTransformer::class, '<h1>Title</h1>'], ['`$var`', CodeTransformer::class, '$var'], ['[Link](https://news.laravel.net.cn)', LinkTransformer::class, '<a href="https://news.laravel.net.cn">Link</a>'] ] }}
这里我们假设所有转换器类共享相同的接口或契约,并使用
process()
方法处理给定的输入。
数据驱动测试和 PHPUnit
虽然这似乎是一个不错的解决方案,但 PHPUnit 有一个更优雅的方式来处理这种模式。
PHPUnit 使用一些有用的 注释,您可以使用它们来简化您的代码,例如,而不是
public function test_some_method_name(){ }
PHPUnit 使用
test
前缀来识别测试类中哪些方法是测试方法。
您可以使用
/** @test **/public function some_method_name(){ }
在这种情况下,您可以通过添加包含文本 @text
的注释来删除 test_
前缀,以识别测试方法。
现在让我们在前面的示例中使用 @dataProvider
注释
class ExampleTest extends TestCase{ /* * @test * @dataprovider dataSet */ public function transform_content($input, $class, $expected) { $class = new $class; $this->assertEquals($expected, $class->process($value)); } public function dataSet() { return [ ['# Title #', TitleTransformer::class, '<h1>Title</h1>'], ['`$var`', CodeTransformer::class, '$var'], ['[Link](https://news.laravel.net.cn)', LinkTransformer::class, '<a href="https://news.laravel.net.cn">Link</a>'] ] }}
这里发生的事情是,PHPUnit 在内部迭代 dataSet
方法中定义的每个数据集,并将每个参数传递给 transform_content()
方法。
现在,如果您在任何数据集上收到失败,您将看到类似以下内容
There was 1 failure: 1) TestsUnitExampleTest::transform_content with data set #1 ('# Title #', TitleTransformer::class, '<h1>Title</h1>')...
如您所见,这可能会非常令人困惑,但我们可以通过向每个数据集添加一个键名来改进它,如下所示
public function dataSet(){ return [ 'Transform titles' => ['# Title #', TitleTransformer::class, '<h1>Title</h1>'], 'Transform code text' => ['`$var`', CodeTransformer::class, '$var'], 'Transform links' => ['[Link](https://news.laravel.net.cn)', LinkTransformer::class, '<a href="https://news.laravel.net.cn">Link</a>'] ]}
然后,您可以在失败的情况下看到类似以下内容
There was 1 failure: 1) TestsUnitExampleTest::transform_content with data set "Transform titles" ('# Title #', TitleTransformer::class, '<h1>Title</h1>')...
额外提示
您甚至可以通过编写一个实现 Iterator 接口 的类来拥有一个自定义迭代器
use PHPUnitFrameworkTestCase; class CustomIterator implements Iterator { protected $key = 0; protected $current; public function __construct() { // } public function __destruct() { // } public function rewind() { // } public function valid() { // } public function key() { // } public function current() { // } public function next() { // }}
在这里,您可以了解更多关于 如何使用返回 Iterator 对象的数据提供者。
本文的灵感来自 Paul Redmond 和他在这方面的部分工作 HTML 到 AMP 包。