Entre as discussões na comunidade, muitas vezes ouço a opinião de que o teste de unidade no Laravel é errado, complicado e os testes são longos e não trazem nenhum benefício. Por esse motivo, poucos escrevem esses testes, limitando-se apenas aos testes de recursos, e o uso de testes de unidade tende a 0.
Eu também pensei isso uma vez, mas uma vez pensei nisso e me perguntei - talvez eu não saiba como cozinhá-los?
Por algum tempo eu entendi e, na saída, tive um novo entendimento sobre os testes de unidade, e os testes se tornaram claros, amigáveis, rápidos e começaram a me ajudar.
Quero compartilhar meu entendimento com a comunidade e, ainda melhor, entender este tópico, tornar meus testes ainda melhores.
Um pouco de filosofia e limitações
O Laravel é um tipo de framework em alguns lugares. Especialmente em termos de fachadas e Eloquent. Não tocarei em discussões ou condenações desses pontos, mas mostrarei como os combino com testes de unidade.
Escrevo testes depois (ou ao mesmo tempo) de escrever o código principal. Talvez minha abordagem não seja compatível com a abordagem TDD ou exija ajustes parciais.
A pergunta mais importante que me faço antes de escrever um teste é "o que exatamente eu quero testar?". Esta é uma questão importante. Foi essa ideia que me permitiu reconsiderar meus pontos de vista sobre a escrita de testes de unidade e o próprio código do projeto.
Os testes devem ser estáveis e minimamente dependentes do ambiente. Se, quando você fizer mutações, seus testes falharem, provavelmente serão bons. Por outro lado, se eles não caem, provavelmente não são muito bons.
Pronto para uso, o Laravel suporta 3 tipos de testes:
Vou falar principalmente sobre testes de unidade.
Não testei todo o código por meio de testes de unidade (talvez isso não esteja correto). Não testei nenhum código (mais sobre isso abaixo).
Se moques forem usados nos testes, não esqueça de usar Mockery :: close () no tearDown.
Alguns exemplos de testes são "retirados da Internet".
Como eu teste
Abaixo, agruparei exemplos de teste por grupo de turmas e tentarei dar exemplos de teste para cada grupo de turmas. Para a maioria dos grupos de turmas, não darei exemplos do código em si.
Middleware
Para o teste de unidade do middleware, crio um objeto da classe Request, um objeto do Middleware desejado, depois chamo o método handle e executo as declarações necessárias. O middleware de acordo com as ações executadas pode ser dividido em 3 grupos:
- objeto de solicitação de alteração (alteração de solicitação do corpo ou sessões)
- redirecionando (alterando o status da resposta)
- não fazendo nada com o objeto de solicitação
Vamos tentar dar um exemplo de teste para cada grupo:
Suponha que tenhamos o seguinte Middleware, cuja tarefa é modificar o campo de título:
class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } }
Um teste para um Middleware semelhante pode ser assim:
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); }); }
Os testes para os grupos 2 e 3 terão esse plano, respectivamente:
$response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302);
Solicitar classe
A principal tarefa desse grupo de classes é a autorização e a validação de solicitações.
Não testei essas classes através de testes de unidade (admito que isso pode não ser verdade), apenas através de testes de recursos. Na minha opinião, os testes de unidade são redundantes para essas classes, mas encontrei alguns exemplos interessantes de como isso pode ser feito. Talvez eles o ajudem se você decidir testar sua classe de unidade de solicitação com testes:
Controlador
Também não testo controladores através de testes de unidade. Mas, ao testá-los, uso um recurso sobre o qual gostaria de falar.
Controladores, na minha opinião, devem ser leves. A tarefa deles é obter a solicitação correta, ligar para os serviços e repositórios necessários (já que esses dois termos são “estranhos” para o Laravel, explicarei minha terminologia abaixo), retorne a resposta. Às vezes, aciona um evento, trabalho, etc.
Assim, ao testar através de testes de recursos, precisamos não apenas chamar o controlador com os parâmetros necessários e verificar a resposta, mas também bloquear os serviços necessários e verificar se eles realmente estão sendo chamados (ou não). Às vezes - crie um registro no banco de dados.
Um exemplo de teste de controlador com uma classe de serviço simulada:
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); }
Um exemplo de teste de controlador com simulação de fachada (no nosso caso, um evento, mas por analogia, é feito para outras fachadas do Laravel):
public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [
Serviço e Repositórios
Esses tipos de classes estão prontos para uso. Eu tento manter os controladores magros, então dedico todo o trabalho extra a um desses grupos de turmas.
Eu determinei a diferença entre eles da seguinte maneira:
- Se eu precisar implementar alguma lógica de negócios, coloquei isso na camada de serviço apropriada (classe).
- Em todos os outros casos, eu coloquei isso no grupo de classes do repositório. Por via de regra, o funcional com Eloquent vai para lá. Entendo que essa não é a definição correta do nível do repositório. Também ouvi dizer que alguns suportam tudo relacionado ao Eloquent no modelo. Minha abordagem é uma espécie de compromisso, na minha opinião, embora "academicamente" não seja inteiramente verdadeiro.
Para classes de repositório, quase não escrevo testes.
Exemplo de teste de classe de serviço abaixo:
public function testUpdateCart() { Event::fake(); $cartService = resolve(CartService::class); $cartRepo = resolve(CartRepository::class); $user = factory(User::class)->make(); $cart = $cartRepo->getCart($user);
Ouvinte de eventos, Trabalhos
Essas classes são testadas quase pelo princípio geral - preparamos os dados necessários para o teste; Chamamos a classe desejada a partir da estrutura e verificamos o resultado.
Exemplo para o Ouvinte:
public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id,
Console do console
Considero os comandos do console como algum tipo de controlador que pode gerar adicionalmente (e executar manipulações mais complexas com os dados de entrada e saída de console descritos na documentação). Assim, os testes são semelhantes ao controlador: verificamos se os métodos de serviço necessários são chamados, eventos são acionados (ou não) e também verificamos a interação com o console (saída ou solicitação de dados).
Um exemplo de teste semelhante:
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); }
Bibliotecas externas separadas
Como regra, se bibliotecas separadas tiverem recursos para testes de unidade, elas serão descritas na documentação. Noutros casos, o trabalho com este código é testado de forma semelhante à camada de serviço. Não faz sentido cobrir as próprias bibliotecas com testes (somente se você quiser enviar PR para essa biblioteca) e considere-as como algum tipo de caixa preta.
Em muitos projetos, tenho que interagir através da API com outros serviços. O Laravel geralmente usa a biblioteca Guzzle para esses fins. Pareceu-me conveniente colocar todo o trabalho com outros serviços em uma classe separada do serviço NetworkService. Isso facilitou a escrita e o teste do código principal e ajudou a padronizar as respostas e o tratamento de erros.
Dou exemplos de vários testes para minha classe 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', '/'); }
Conclusões
Essa abordagem permite que eu escreva um código melhor e mais compreensível, aproveitando as abordagens do SOLID e SRP ao escrever o código. Meus testes se tornaram mais rápidos e, o mais importante - começaram a me beneficiar.
Com a refatoração ativa ao expandir ou alterar a funcionalidade, vemos imediatamente o que exatamente está caindo e podemos corrigir com rapidez e precisão os erros sem liberá-los do ambiente local. Isso torna a correção de erros o mais barata possível.
Espero que os princípios e abordagens que descrevi o ajudem a lidar com o teste de unidade no Laravel e faça com que os testes de unidade sejam seus assistentes no desenvolvimento de código.
Escreva suas adições e comentários.