引言
新公司入职第一件事就是让我给一个用 Laravel 框架开发的项目写单元测试,我会说『当了这么多年的程序员没写过单元测试吗?』,于是我一边学习一边写,这篇文章是我写了一个月单元测试的经验分享。
单元测试
基本的断言
断言两边数据相等使用 assertEquals($a, $a1)
1
2
3
4
5
public function testComputePermission()
{
$totalPermission = TCH::computePermission([PointsAccountPermissions::TRANSFER]);
$this->assertEquals($totalPermission, PointsAccountPermissions::TRANSFER);
}
断言结果是 true
使用 assertTrue($a)
,与之相反的是 assertFalse($b)
断言文件是否存在使用 assertFileExists($path)
断言数据库没有数据使用 assertDatabaseMissing('user', [])
断言两个类一样使用 assertSame($a, $b)
……
断言返回数据数组包含某个键值
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @throws \App\Exceptions\ThirdPartyServiceErrorException
*/
public function testDecodeUnionId ()
{
$accountBaseService = app()-> make (AccountBaseService:: class );
$unionId = mt_rand(1 , 100 );
$response = $accountBaseService -> decodeUnionId ([$unionId ]);
$this -> assertArrayHasKey ('data' , $response );
$data = data_get($response , 'data.list.0' );
$this -> assertArrayHasKey ('brand_id' , $data );
$this -> assertArrayHasKey ('account_id' , $data );
}
多组数据测试
多组数据可以使用 @dataProvider
的方式传入数据来测试。比方说我测试一个金额,只支持正数、两位小数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
namespace App\Rules\Rules;
use App\Rules\AmountRule;
use PHPUnit\Framework\TestCase;
class AmountRuleTest extends TestCase
{
/**
* @var AmountRule()
*/
protected $rule ;
public function setUp ()
{
parent :: setUp ();
$this -> rule = new AmountRule();
}
/**
*
* @dataProvider evenPassData
* @param int|float $int
* @return void
*/
public function testEvenPass ($int )
{
$this -> assertTrue ($this -> rule -> passes ('test' , $int ));
}
/**
*
* @dataProvider evenFailData
* @param int|float $int
* @return void
*/
public function testEvenFail ($int )
{
$this -> assertFalse ($this -> rule -> passes ('test' , $int ));
}
public function evenPassData ()
{
return [
[100 ],
[100.11 ],
[2 ],
[2.00 ],
[2.01 ],
[2.1 ],
];
}
public function evenFailData ()
{
return [
[- 1 ],
[1.111 ],
[- 1.1 ],
[0 ],
];
}
}
Console 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace App\Console\Commands;
use App\Models\ClientApp;
use Illuminate\Console\Command;
use Webpatser\Uuid\Uuid;
class CreateClient extends Command
{
// something code
/**
* Execute the console command.
*
* @return mixed
* @throws \Exception
*/
public function handle ()
{
$title = $this -> ask ('客户端应用名称' );
$desc = $this -> ask ('客户端应用描述' );
$access = $this -> ask ('access (1: 私有 2: 内部 3: 公开)' );
$appId = Uuid:: generate ();
$secret = str_random(64 );
$data = [
// something code
];
ClientApp:: create ($data );
$this -> info ("Done! \n " );
$this -> info ('client_id: ' . $appId );
$this -> info ('secret: ' . $secret );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function testCreateClient ()
{
$application = new Application();
$testedCommand = $this -> app -> make (CreateClient:: class );
$testedCommand -> setLaravel (app());
$application -> add ($testedCommand );
$commandTester = new CommandTester($testedCommand );
$commandTester -> setInputs (['测试' , '单元测试' , 1 ]);
$commandTester -> execute (['command' => $testedCommand -> getName ()]);
$this -> assertRegExp ('/client_id: \w{8}(-\w{4}){3}-\w{12}\\n/' , $commandTester -> getDisplay ());
$this -> assertDatabaseHas (ClientApp:: TABLE_NAME , [
'title' => '测试' ,
'description' => '单元测试' ,
'access' => 1 ,
]);
}
断言类
1
2
3
4
5
6
7
8
9
/**
* @test
*/
public function getAccountByUnionId ()
{
$service = new PointsAccountService();
$record = $service -> getAccountByUnionId (1 );
$this -> assertInstanceOf (PointsAccount:: class , $record );
}
PS: 方法含有 test 或者使用注释 @test
都可以识别是单元测试的一个方法。
异常测试
默认没有测试异常的方法,所以我们自己实现一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Asserts that the given callback throws the given exception.
*
* @param string $expectClass The name of the expected exception class
* @param callable $callback A callback which should throw the exception
*/
protected function assertException (string $expectClass , callable $callback )
{
try {
$callback ();
} catch (\Throwable $exception ) {
$this -> assertInstanceOf ($expectClass , $exception , 'An invalid exception was thrown' );
return ;
}
$this -> fail ('No exception was thrown' );
}
1
2
3
4
5
6
7
public function testSomeException ()
{
$service = new PointsAccountService();
$this -> assertException (\InvalidArgumentException:: class , function () use ($service ) {
$service -> getAccountByUnionId (0 );
});
}
队列测试
1
2
3
4
5
6
7
8
9
10
11
12
use Illuminate\Support\Facades\Queue;
public function testJob ()
{
Queue:: fake ();
// 推送数据到队列
SomeJob:: dispatch ($vars );
// 断言已经在队列中
Queue:: assertPushed (SomeJob:: class );
}
需要补充的是队列这块注意 phpunit.xml
文件里面的 <env name="QUEUE_CONNECTION" value="sync"/>
代码,意思是使用同步的方式跑队列。这里要根据自己的情况修改配置。
API 接口测试
1
2
3
4
5
6
7
8
9
10
11
12
13
use Illuminate\Http\Request;
public function testUserCreate ()
{
$data = [
'username' => 'test' ,
];
$response = $this -> json (Request:: METHOD_POST , config('app.url' ) . '/users' , $data );
$response -> assertStatus (200 )
-> assertJson ([
'code' => 0 ,
]);
// something other assert
}
第三方接口依赖测试
如果我们项目中有依赖第三方接口,我们测试的时候不会直接去请求第三方接口,而是断言其接口成功返回。那么具体怎么测试呢?使用 mock 是一种方式,但是我使用的是另外一种方式:
添加接口返回的数据,比方说添加 tests/fixtures/simple_data/vendor/v1/member/info.json
文件,里面就贴上接口正常返回的数据。
添加控制器方法 app/Http/Controllers/TestController.php
文件,里面的方法做文件映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use Illuminate\Routing\Route;
class TestController extends Controller
{
public function index (Route $route ): Response
{
if ($url = str_replace('api/tests/v1' , '' , $route -> uri )) {
$path = base_path('/tests/fixtures/simple_data/vendor/v1' );
$data = file_get_contents(" { $path }{ $url } .json" );
return response($data , 200 , ['Content-Type' => 'application/json' ]);
}
return response('' );
}
}
1
2
3
4
5
6
// tests v1 版本 account-base-service 和 notification
Route:: group (['prefix' => 'tests/v1' ,], function () {
Route:: get ('member/info' , 'TestController@index' );
Route:: post ('member/create' , 'TestController@index' );
// ……
});
PS: 这里最后两步之所以不使用路由回调函数方式实现,是因为 php artisan route:cache
命令不支持路由 Closure
关于清除测试数据
Laravel 自带一个 RefreshDatabase
trait,如果你的测试类加入了 use RefreshDatabase;
代码,那么每跑一个测试会清除数据。但是我嫌这样跑测试太慢,基本上都不用这个方法。
Laravel 默认如果有 .env.testing
环境配置文件的话,跑单元测试会使用这个文件的配置,里面可以配置单独的数据库等其他配置。所以我目前本地使用的是这种方式来跑单元测试。
加速跑单元测试
跑单元测试我们一般用下面这个命令跑:
但是单元测试多了之后,上面这种方式跑单元测试会变慢,我们可以用 phpdbg 来加速跑运行单元测试,以节省时间。
安装:
1
sudo apt-get install php7.2-phpdbg -y
使用:
1
phpdbg -qrr ./vendor/bin/phpunit -dmemory_limit=1024M
如果还是嫌慢,还有优化空间,以后再补充。
最后
我们的单元测试只在本地和 travis 里面跑,自从写了单元测试之后,改代码要放心的很多。如果时间允许的话,推荐大家也多写写单元测试。养成好习惯。
打赏
微信打赏
支付宝打赏