|
| 1 | +## 25. 2019-06-10-Laravel 基于 PHPUnit 的单元测试 |
| 2 | + |
| 3 | +### 简介 |
| 4 | +介绍完 PHPUnit 的基本使用和 Laravel 框架自带的编排文件 phpunit.xml 文件,今天开始我们正式准备在 Laravel 项目中基于 PHPUnit 编写单元测试和功能测试,通过上篇教程介绍的编排文件我们知道,Laravel 的单元测试用例位于 tests/Unit 目录下,框架本身也为我们提供了一个示例测试文件 ExampleTest.php: |
| 5 | + |
| 6 | +```php |
| 7 | +<?php |
| 8 | + |
| 9 | +namespace Tests\Unit; |
| 10 | + |
| 11 | +use Tests\TestCase; |
| 12 | +use Illuminate\Foundation\Testing\RefreshDatabase; |
| 13 | + |
| 14 | +class ExampleTest extends TestCase |
| 15 | +{ |
| 16 | + /** |
| 17 | + * A basic test example. |
| 18 | + * |
| 19 | + * @return void |
| 20 | + */ |
| 21 | + public function testBasicTest() |
| 22 | + { |
| 23 | + $this->assertTrue(true); |
| 24 | + } |
| 25 | +} |
| 26 | +``` |
| 27 | + |
| 28 | +其中包含了一行最基本的断言测试,用于判断指定的参数是否为真,并且这个测试永远是通过的。Laravel 的单元测试其实是原原本本继承了 PHPUnit 的单元测试功能,这里的父类 Tests\TestCase 从根源上继承自 PHPUnit\Framework\TestCase,所以我们可以在测试用例中使用所有 PHPUnit 支持的断言方法和测试注解。 |
| 29 | + |
| 30 | +下面我们通过几个常见的场景介绍下如何基于 PHPUnit 编写单元测试。 |
| 31 | + |
| 32 | +### 对变量进行测试 |
| 33 | +PHPUnit 底层提供了很多断言方法用于对变量进行测试,这些变量通常是业务代码类方法或函数的返回值,我们在 Unit\ExampleTest 中新增一个 testVariables 方法: |
| 34 | + |
| 35 | +``` |
| 36 | +public function testVariables() |
| 37 | +{ |
| 38 | + $bool = false; |
| 39 | + $number = 100; |
| 40 | + $arr = ['Laravel', 'PHP', '学院君']; |
| 41 | + $obj = null; |
| 42 | +
|
| 43 | + // 断言变量值是否为假,与 assertTrue 相对 |
| 44 | + $this->assertFalse($bool); |
| 45 | + // 断言给定变量值是否与期望值相等,与 assertNotEquals 相对 |
| 46 | + $this->assertEquals(100, $number); |
| 47 | + // 断言数组中是否包含给定值,与 assertNotContains 相对 |
| 48 | + $this->assertContains('学院君', $arr); |
| 49 | + // 断言数组长度是否与期望值相等,与 assertNotCount 相对 |
| 50 | + $this->assertCount(3, $arr); |
| 51 | + // 断言数组是否不会空,与 assertEmpty 相对 |
| 52 | + $this->assertNotEmpty($arr); |
| 53 | + // 断言变量是否为 null,与 assertNotNull 相对 |
| 54 | + $this->assertNull($obj); |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +相应的断言用途在注释中已经说明了,我们可以对各种类型的变量从各种维度进行断言,甚至还可以对文件、目录、正则表达式进行断言,并且很多断言都可以从正反两个方法进行,相关的调用都很简单,你可以在需要的时候查看官方文档选择相应的断言方法:https://phpunit.readthedocs.io/zh_CN/latest/assertions.html。 |
| 59 | + |
| 60 | +运行上面的测试用例,结果如下,每个测试方法都代表一个测试用例,所以上面的单元测试包含两个测试用例,7个断言: |
| 61 | + |
| 62 | + |
| 63 | + |
| 64 | +### 对输出进行测试 |
| 65 | +除了对变量进行测试外,还可以对页面输出进行测试,这可以通过 PHPUnit 提供的 expectOutputString 方法来实现: |
| 66 | + |
| 67 | +``` |
| 68 | +public function testOutput() |
| 69 | +{ |
| 70 | + $this->expectOutputString('学院君'); |
| 71 | + echo '学院君'; |
| 72 | +} |
| 73 | +``` |
| 74 | +此外还可以对输出数据进行正则判断: |
| 75 | + |
| 76 | +``` |
| 77 | +public function testOutputRegex() |
| 78 | +{ |
| 79 | + $this->expectOutputRegex('/Laravel/i'); |
| 80 | + echo 'Laravel学院'; |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +上述测试结果都是通过的: |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | +### 对异常进行测试 |
| 89 | +类似的,还可以通过 expectException 方法对异常进行测试,为了让测试用例更加符合真实场景,我们在 app 目录下新增一个 Services 子目录,然后在该子目录下创建一个 TestService 类并初始化代码如下: |
| 90 | + |
| 91 | +``` |
| 92 | +<?php |
| 93 | +namespace App\Services; |
| 94 | +
|
| 95 | +class TestService |
| 96 | +{ |
| 97 | + public function invalidArgument() |
| 98 | + { |
| 99 | + throw new \InvalidArgumentException('无效的参数'); |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | +然后回到 Unit\ExampleTest,编写一个新的测试用例如下: |
| 104 | + |
| 105 | +``` |
| 106 | +public function testException() |
| 107 | +{ |
| 108 | + $this->expectException(\InvalidArgumentException::class); |
| 109 | + $service = new TestService(); |
| 110 | + $service->invalidArgument(); |
| 111 | +} |
| 112 | +``` |
| 113 | +运行测试用例,结果为通过: |
| 114 | + |
| 115 | + |
| 116 | + |
| 117 | +如果传入的抛出异常类的是父类,也会通过: |
| 118 | + |
| 119 | +``` |
| 120 | +$this->expectException(\Exception::class); |
| 121 | +``` |
| 122 | + |
| 123 | +除此之外,还可以进一步对异常明细进行测试,比如通过 expectExceptionCode()、expectExceptionMessage() 和 expectExceptionMessageRegExp() 方法可以用于测试异常码、异常信息。 |
| 124 | + |
| 125 | +除了通过上述方法,还可以通过注解对异常进行测试,这种方式更加方便: |
| 126 | + |
| 127 | +``` |
| 128 | +/** |
| 129 | + * @expectedException \InvalidArgumentException |
| 130 | + */ |
| 131 | +public function testExceptionAnnotation() |
| 132 | +{ |
| 133 | + $this->service->invalidArgument(); |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +由于两个测试用例中都用到了 TestService,所以我们将其在 setUp 方法进行初始化: |
| 138 | + |
| 139 | +``` |
| 140 | +/** |
| 141 | + * @var TestService |
| 142 | + */ |
| 143 | +protected $service; |
| 144 | +
|
| 145 | +protected function setUp(): void |
| 146 | +{ |
| 147 | + parent::setUp(); |
| 148 | + $this->service = new TestService(); |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +上面两个测试用例从功能上说是等价的: |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +### 对错误进行测试 |
| 157 | +默认情况下,PHPUnit 会将 PHP 错误、警告和通知都转化为异常,在上一篇 PHP 编排文件 phpunit.xml 中我们提到,Laravel 也默认配置为做这些转化,所以我们可以通过测试异常的方式对业务代码中的错误进行测试。具体用法和异常测试一样,就不再赘述了。 |
| 158 | + |
| 159 | +### 测试的依赖关系 |
| 160 | +有的时候,我们需要测试的两个用例之间可能有依赖关系,比如我们在 TestService 定义如下个方法: |
| 161 | + |
| 162 | +protected $stack = []; |
| 163 | + |
| 164 | +public function init() |
| 165 | +{ |
| 166 | + $this->stack = ['学院君', 'Laravel学院', '单元测试']; |
| 167 | +} |
| 168 | + |
| 169 | +public function stackContains($value) |
| 170 | +{ |
| 171 | + return in_array($value, $this->stack); |
| 172 | +} |
| 173 | + |
| 174 | +public function getStackSize() |
| 175 | +{ |
| 176 | + return count($this->stack); |
| 177 | +} |
| 178 | +我们在测试 stackContains 方法时,往往要先调用 init 方法,但是在单元测试中,每个方法都有独立的测试用例,如果多次调用有可能会对数据造成污染,那我们能否在 init 方法测试用例的运行基础上运行 stackContains 方法的测试用例呢?这个时候,我们说这两个测试用例之间是具有依赖关系的,PHPUnit 中通过 @depends 注解对这种依赖关系进行了支持,我们可以在 Unit\ExampleTest 中编写测试用例如下: |
| 179 | + |
| 180 | +public function testInitStack() |
| 181 | +{ |
| 182 | + $this->service->init(); |
| 183 | + $this->assertEquals(3, $this->service->getStackSize()); |
| 184 | + |
| 185 | + return $this->service; |
| 186 | +} |
| 187 | + |
| 188 | +/** |
| 189 | + * @depends testInitStack |
| 190 | + * @param TestService $service |
| 191 | + */ |
| 192 | +public function testStackContains(TestService $service) |
| 193 | +{ |
| 194 | + $contains = $service->stackContains('学院君'); |
| 195 | + $this->assertTrue($contains); |
| 196 | +} |
| 197 | +在 testStackContains 用例中,我们将 testInitStack 用例返回的 $service 实例传递进来,并在此基础上进行测试。 |
| 198 | + |
| 199 | +### 数据提供器 |
| 200 | +除了支持测试用例之间的依赖之外,PHPUnit 还可以通过 @dataProvider 注解为多个测试用例提供初始化数据: |
| 201 | + |
| 202 | +public function initDataProvider() |
| 203 | +{ |
| 204 | + return [ |
| 205 | + ['学院君'], |
| 206 | + ['Laravel学院'] |
| 207 | + ]; |
| 208 | +} |
| 209 | + |
| 210 | +/** |
| 211 | + * @depends testInitStack |
| 212 | + * @dataProvider initDataProvider |
| 213 | + */ |
| 214 | +public function testIsStackContains() |
| 215 | +{ |
| 216 | + $arguments = func_get_args(); |
| 217 | + $service = $arguments[1]; |
| 218 | + $value = $arguments[0]; |
| 219 | + $this->assertTrue($service->stackContains($value)); |
| 220 | +} |
| 221 | +在这个测试用例中,我们通过 initDataProvider 方法作为数据提供器,数据供给器方法必须声明为 public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator 接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。 |
| 222 | + |
| 223 | +然后我们在需要用到这个数据提供器的测试用例上用 @dataProvider 注解进行声明,在这个示例中我们迭代数据提供器数组,将其中的数据作为参数传入 TestService 的 stackContains 方法以判断对应值在 stack 属性中是否存在。 |
| 224 | + |
| 225 | +以下是上述用例运行结果: |
| 226 | + |
| 227 | + |
| 228 | + |
| 229 | +关于 Laravel 基于 PHPUnit 编写单元测试用例我们就简单介绍到这里,在这篇教程中我们基本上使用的都是 PHPUnit 提供的原生测试功能,下一篇我们将围绕 Laravel 功能测试展开,主要是对控制器和 API 接口的测试,在那里,我们将开始就 Laravel 框架在 PHPUnit 基础上封装的新功能新特性进行介绍。 |
| 230 | + |
| 231 | + |
| 232 | +参考地址:[https://laravelacademy.org/post/19586.html](https://laravelacademy.org/post/19586.html) |
0 commit comments