本文主要讨论写laravel集成/功能测试用例时如何断言。前面几篇文章主要讲了如何reseedtestdata和mockdata。本文主要讲一下assert的可行实践。虽然laravel官方文档讲了TestingJSONAPIs,也提供了一些辅助的assert方法,比如assertStatus(),assertJson()等,但是是可行的,不实用,不推荐这样做。最好的需求是对api生成的响应进行更精细的断言。更好的断言怎么样?简单一句话就是比较(断言)responsecode/headers/content的完整内容。方法是将response的内容存储在json文件中作为baseline。OK,接下来说说怎么做吧。写一个AccountControllerTest,调用是/api/v1/accounts,AccountController的内容参考写Laravel测试代码(三),然后写集成/功能测试用例:assertApiIndex();}publicfunctiontestShow(){$this->assertApiShow(1);}}显然,这里测试的是index/showapi,即/api/v1/accounts和/api/v1/accounts/{account_id},AssertApiBaseline是自定义的trait,主要功能是实现assertallresponse,并将其保存在json文件中作为基线。所以,重点就是AssertApiBaseline应该怎么写,这里就直接贴代码:['D'=>'DiJeb7IQHo8FOFkXulieyA',],'api'=>[],];privatestatic$servers=['web'=>['HTTP_ACCEPT'=>'application/json','HTTP_ORIGIN'=>'https://test.company.com','HTTP_REFERER'=>'https://test.company.com',],'api'=>['HTTP_ACCEPT'=>'application/json',],];公共静态函数assertJsonResponse(TestResponse$response,string$message='',array$ignores=[]):TestResponse{static::assertJsonResponseCode($response,$message);静态::assertJsonResponseContent($response,$message);static::assertJsonResponseHeaders($response,$message);返回$响应;}publicstaticfunctionassertJsonResponseCode(TestResponse$response,string$message=''):void{static::assert($response->getStatusCode(),$message);}publicstaticfunctionassertJsonResponseContent(TestResponse$response,string$message='',array$ignores=[]):void{static::assert($response->json(),$message);}publicstaticfunctionassertJsonResponseHeaders(TestResponse$response,string$message=''):void{$headers=$response->headers->all();$headers=array_except($headers,['date','set-cookie',]);//除了无用的标头static::assert($headers,$message);}publicstaticfunctionassert($actual,string$message='',float$delta=0.0,int$maxDepth=10,bool$canonicalize=false,bool$ignoreCase=false):void{//使用来自基线json文件的$expected断言$actual//如果没有基线json文件,将$actual数据放入基线文件(或-drebase)//基线文件路径//在测试用例中支持多个断言static$assert_counters=[];静态$baselines=[];$class=get_called_class();$function=static::getFunctionName();//'testIndex'$signature="$class::$function";如果(!isset($assert_counters[$signature])){$assert_counters[$signature]=0;}else{$assert_counters[$signature]++;}$test_id=$assert_counters[$signature];$baseline_path=static::getBaselinesPath($class,$function);if(!array_key_exists($signature,$baselines)){if(file_exists($baseline_path)&&array_search('rebase',$_SERVER['argv'],true)===false){//'-drebase'$baselines[$signature]=\GuzzleHttp\json_decode(file_get_contents($baseline_path),true);}else{$baselines[$signature]=[];$actual=static::prepareActual($actual);如果(array_key_exists($test_id,$baselines[$signature])){static::assertEquals($baselines[$signature][$test_id],$actual,$message,$delta,$maxDepth,$canonicalize,$ignoreCase);}else{$baselines[$signature][$test_id]=$actual;file_put_contents($baseline_path,\GuzzleHttp\json_encode($baselines[$signature],JSON_PRETTY_PRINT));静态::assertTrue(真);回声'R';}}/***@paramstring|string[]|null$route_parameters*@paramarray$parameters**@returnmixed*/protectedfunctionassertApiIndex($route_parameters=null,array$parameters=[]){returnstatic::assertApiCall('索引',$route_参数?(数组)$route_parameters:null,$parameters);}protectedfunctionassertApiShow($route_parameters,array$parameters=[]){assert($route_parameters!==null,'$route_parameterscannotbenull');returnstatic::assertApiCall('show',(array)$route_parameters,$parameters);}protectedstaticfunctiongetFunctionName():string{$stacks=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);做{$stack=array_pop($stacks);}while($stack&&substr($stack['function'],0,4)!=='test');返回$堆栈[‘函数’];//'testList'}受保护的静态函数getBaselinesPath(string$class,string$function):string{$class=explode('\\',$class);$dir=implode('/',array_merge([strtolower($class[0])],array_slice($class,1,-1),['_baseline',array_pop($class)]));如果(!file_exists($dir)){mkdir($dir,0755,true);}返回base_path()。DIRECTORY_SEPARATOR。$目录。DIRECTORY_SEPARATOR。$功能。'.json';}protectedstaticfunctionprepareActual($actual){if($actualinstanceofArrayable){$actual=$actual->toArray();}if(is_array($actual)){array_walk_recursive($actual,function(&$value,$key):void{if($valueinstanceofArrayable){$value=$value->toArray();}elseif($valueinstanceofCarbon){$value='Carbon:'.$value->toIso8601String();}elseif(in_array($key,['created_at','updated_at','deleted_at'],true)){$value=Carbon::now()->format(DATE_RFC3339);}});}返回$实际;}privatefunctionassertApiCall(string$route_action,array$route_parameters=null,array$parameters=[]){[$uri,$method]=static::resolveRouteUrlAndMethod(static::resolveRouteName($route_action),$route_parameters);/**@var\Illuminate\Foundation\Testing\TestResponse$响应*/$response=$this->call($method,$uri,$parameters,$this->getCookies(),[],$this->获取服务器(),空);返回静态::assertJsonResponse($response,'');}privatestaticfunctionresolveRouteName(string$route_action):string{returnstatic::ROUTE_NAME.'.'.$路线动作;}私有静态函数resolveRouteUrlAndMethod(string$route_name,array$route_parameters=null){$route=\Route::getRoutes()->getByName($route_name);assert($route,"路由[$route_name]必须存在。");返回[路线($route_name,$route_parameters),$route->methods()[0]];}privatefunctiongetCookies(array$overrides=[]):array{$cookies=$overrides+self::$cookies[static::$middlewareGroup];返回$cookies;}privatefunctiongetServers(array$overrides=[]):array{return$overrides+self::$servers[static::$middlewareGroup];}}AssertApiBaseline虽然有点长,但重点只放在assert()方法上。该方法的实现方式是:如果一开始没有基线文件,则将响应内容存储在json文件中。如果有json文件,使用baseline作为预期数据与这个api通信,生成的response的内容就是断言的实际数据。如果有'rebase'命令,则表示将此API生成的响应作为新基线存储在json文件中。它支持在一个测试用例中使用多个assert()方法。因此,执行phpunit命令后会生成对应的基线文件:OK,第一次执行时重新生成基线文件,看看是不是想要的结果。每次改api后,如果手写的api写错了,比如响应内容为空,此时执行测试时,会以基线作为预期数据和错误的实际数据进行assert和an会报错。很容易知道代码错了;如果gitdiff知道最新的响应,它就是你想要的(如果不需要将“名称”更改为另一个),只需使用phpunit-drebase将新响应用作新基线。与编写laravel文档中描述的jsonapi测试用例相比,这有什么优势?它是微调响应。响应的状态码、头部,尤其是响应内容都被精细控制(内容的每个字段都与assert进行比较)。这是我们这边写api测试用例的做法。如有任何问题,欢迎留言交流。
