在社区中的讨论中,我经常听到这样的观点,即Laravel中的单元测试是错误的,复杂的,并且测试本身很长并且没有任何好处。 因此,很少有人编写这些测试,仅将它们限于功能测试,而单元测试的使用趋向于0。
我也曾经这样想过,但是一旦我想到了并问自己-也许我不知道如何煮它们?
一段时间以来,我了解了,在出口时,我对单元测试有了新的了解,测试变得清晰,友好,快速,并开始为我提供帮助。
我想与社区分享我的理解,甚至更好地理解这个主题,使我的测试变得更好。
一点哲学和局限性
Laravel在某些地方是一种框架。 特别是在外墙和口才方面。 我不会涉及这些问题的讨论或谴责,但会展示如何将它们与单元测试结合起来。
我在编写主代码后(或同时)编写测试。 也许我的方法与TDD方法不兼容或需要部分调整。
在编写测试之前,我问自己最重要的问题是“我到底要测试什么?”。 这是一个重要的问题。 正是这种想法让我重新考虑了对编写单元测试和项目代码本身的看法。
测试应该稳定并且对环境的依赖性最小。 如果在进行突变时测试失败,则很有可能是好的。 相反,如果它们不跌倒,则可能不是很好。
开箱即用,Laravel支持3种类型的测试:
我将主要讨论单元测试。
我没有通过单元测试来测试所有代码(也许这是不正确的)。 我根本不测试一些代码(下面有更多内容)。
如果测试中使用了Moque,请不要忘记对tearDown进行Mockery :: close()。
测试的一些示例是“从Internet上获取的”。
我如何测试
下面,我将按班级对测试示例进行分组,并尝试给出每个班级的测试示例。 对于大多数类组,我不会提供代码本身的示例。
中间件
对于中间件单元测试,我创建一个Request类的对象,一个所需中间件的对象,然后调用handle方法并执行必要的断言。 中间件根据执行的动作可以分为3组:
- 更改请求对象(更改正文请求或会话)
- 重定向(更改响应状态)
- 不处理请求对象
让我们尝试给出每个组的测试示例:
假设我们有以下中间件,其任务是修改标题字段:
class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } }
对类似中间件的测试可能如下所示:
public function testChangeTitleToTitlecase() { $request = new Request; $request->merge([ 'title' => 'Title is in mixed CASE' ]); $middleware = new TitlecaseMiddleware; $middleware->handle($request, function ($req) { $this->assertEquals('Title Is In Mixed Case', $req->title); }); }
第2组和第3组的测试分别是这样的计划:
$response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302);
要求等级
该类组的主要任务是请求的授权和确认。
我不通过单元测试来测试这些类(我承认这可能不正确),而仅通过功能测试来测试。 在我看来,单元测试对于这些类是多余的,但是我发现了一些有趣的示例来说明如何做到这一点。 如果您决定使用测试来测试您的请求单元类,也许它们会为您提供帮助:
控制者
我也不通过单元测试来测试控制器。 但是在测试它们时,我使用了一个我想谈的功能。
我认为控制器应该轻巧。 他们的任务是获取正确的请求,调用必要的服务和存储库(由于这两个术语都是Laravel的“外来”,我将在下面解释我的术语),并返回答案。 有时会触发事件,工作等。
因此,在通过功能测试进行测试时,我们不仅需要使用必要的参数调用控制器并检查答案,还需要锁定必要的服务并验证它们是否被真正调用(或未被调用)。 有时-在数据库中创建一条记录。
使用服务类模拟进行控制器测试的示例:
public function testProductCategorySync() { $service = Mockery::mock(\App\Services\Product::class); app()->instance(\App\Services\Product::class, $service); $service->shouldReceive('sync')->once(); $response = $this->post('/api/v1/sync/eventsCallback', [ "eventType" => "PRODUCT_SYNC" ]); $response->assertStatus(200); }
使用外观模拟的控制器测试的示例(在我们的例子中,是一个事件,但以此类推,其他Laravel外观也完成了):
public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [
服务和存储库
这些类型的类是开箱即用的。 我试图使控制器变薄,所以我将所有额外的工作放在这些类组之一中。
我确定它们之间的区别如下:
- 如果需要实现一些业务逻辑,则可以将其放在适当的服务层(类)中。
- 在所有其他情况下,我将其放在存储库类组中。 通常,Eloquent的功能就在那里。 我了解这并不是对存储库级别的正确定义。 我还听说有些人忍受了模型中与Eloquent相关的一切。 我认为我的方法是一种折衷,尽管“学术上”并不完全正确。
对于存储库类,我几乎不编写测试。
服务类测试示例如下:
public function testUpdateCart() { Event::fake(); $cartService = resolve(CartService::class); $cartRepo = resolve(CartRepository::class); $user = factory(User::class)->make(); $cart = $cartRepo->getCart($user);
事件监听器,乔布斯
这些类几乎都是按照一般原则进行测试的-我们准备测试所需的数据; 我们从框架中调用所需的类并检查结果。
侦听器示例:
public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id,
控制台控制台
我认为控制台命令是可以额外输出(并使用文档中描述的控制台输入输出执行更复杂的操作)数据的某种控制器。 因此,测试与控制器类似:我们检查是否调用了必要的服务方法,是否触发了事件,还检查了与控制台的交互(输出或数据请求)。
类似测试的示例:
public function testSendCartSyncDataEmptyJobs() { $service = m::mock(CartJobsRepository::class); app()->instance(CartJobsRepository::class, $service); $service->shouldReceive('getAll') ->once()->andReturn(collect([])); $this->artisan('sync:cart') ->expectsOutput('Get all jobs for sending...') ->expectsOutput('All count for sending: 0') ->expectsOutput('Empty jobs') ->assertExitCode(0); }
单独的外部库
通常,如果单独的库具有用于单元测试的功能,则将在文档中对其进行描述。 在其他情况下,与服务层类似地测试使用此代码。 用测试覆盖库本身是没有意义的(仅当您想将PR发送到该库时),并且您应该将它们视为某种黑盒。
在许多项目中,我必须通过API与其他服务进行交互。 Laravel通常将Guzzle库用于这些目的。 在我看来,将所有与其他服务一起的工作放到NetworkService服务的单独的类中似乎很方便。 这使我可以更轻松地编写和测试主要代码,并有助于标准化答案和错误处理。
我提供了一些针对NetworkService类的测试示例:
public function testSuccessfulSendNetworkService() { $mockHandler = new MockHandler([ new Response(200), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); app()->instance(\GuzzleHttp\Client::class, $client); $networkService = resolve(NetworkService::class); $response = $networkService->sendRequestToSite('GET', '/'); $this->assertEquals('200', $response->getStatusCode()); } public function testUnsupportedMethodSendNetworkService() { $networkService = resolve(NetworkService::class); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToSite('PUT', '/'); } public function testUnsetConfigUrlNetworkService() { $networkService = resolve(NetworkService::class); Config::shouldReceive('get') ->once() ->with('app.api_url') ->andReturn(''); Config::shouldReceive('get') ->once() ->with('app.api_token') ->andReturn('token'); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToApi('GET', '/'); }
结论
这种方法使我可以编写更好和更易理解的代码,以在编写代码时利用SOLID和SRP方法。 我的测试变得更快,最重要的是,它们开始使我受益。
通过在扩展或更改功能时进行主动重构,我们可以立即看到确切的下降情况,并且可以快速而准确地纠正错误,而无需从本地环境中释放错误。 这使得纠错尽可能便宜。
我希望我所描述的原理和方法将帮助您处理Laravel中的单元测试,并使单元测试成为代码开发的助手。
写下您的添加内容和评论。