Ho Ho Ho Feliz Natal com um artigo fresquinho, repleto de conhecimento para vocês!!
Meu objetivo é elucidar testes automáticos em Laravel, explicando algumas funcionalidades legais, como ele se comporta, riscos, facilitadores e contextualizar explicando o princípio que testes automatizados estão mais intrínsecos na gama cultural da empresa, sem a criação dos códigos que fazem os testes automáticos... Eles não existiriam.
O Laravel tem algumas características muito particulares em seus testes que devem ser sempre prestado atenção. Vou deixar um código exemplo abaixo explicando o que vem por padrão nos testes quando você os cria.
O comando para criar testes automátizados é o seguinte:
No exemplo abaixo estamos criando um feature test.
php artisan make:test NameTest
No exemplo abaixo vamos criar um teste unitário ou unit test.
php artisan make:test NameUnitTest --unit
Importante ressaltar que se faz necessário o uso de Test no nome do arquivo para sua fácil identificação e também nos métodos para que o PHPUnit reconheça o que é teste por padrão.
Unit test é a menor porção isolada possível a ser testada, normalmente testa-se somente 1 método sem integrações.
Os testes unitários na pasta tests/Unit não iniciam sua aplicação Laravel e obviamente o banco de dados de sua aplicação se torna inacessível ou outros serviços dos frameworks utilizados.
Feature test são testes mais robustos que utiliza uma maior porção de seu código, incluindo como objetos interagem uns com os outros, ou uma requisição http completa que tem um endpoint JSON, onde inclusive é possível validar o retorno JSON e se contém certas chaves e afins, os features test iniciam sua aplicação Laravel, ou seja você tem acesso a tudo e a maior partes dos testes gerados no Laravel serão feature test.
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class NameTest extends TestCase
{
/**
* A basic feature test example.
*
* @return void
*/
public function test_example()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
Serve para limpar todo o banco de dados e evitar que os testes tenham dados fantasmas, que possam impactar e atrapalhar os testes automatizados.
Acesso fácil ao faker para poder utilizar dados falsos para seus testes automatizados com facilidade, rapidez e precisão, também é possível usar seed, factory entre outros para poder agilizar o processo.
Todo test deve inicar com test_nome_teste, sendo o nome_teste uma descrição rápida e eficiente para descrever o que aquele método se propõe a testar.
O maior risco de um test é quando roda ele na produção onde você pode popular um sistema de produção com dados inúteis e ainda pior se utilizar a trait RefreshDatabase você limparia os dados de produção, dito isso aqui está o código exemplo de um teste em Laravel.
Quando os testes são executados, Laravel automáticamente seta a configuração de ambiente para testing, por conta das variáveis de ambiente setadas no arquivo phpunit.xml .
Pode configurar o ambiente de teste da forma como quiser configurando o arquivo phpunit.xml.
.env.testing
Particularmente gosto de usar esse arquivo em ambientes que irão em algum momento do tempo se tornar ambiente de produção, pois o Laravel em modo teste preferirá usar esse arquivo e ignorará o .env, assim poderá criar um banco de dados por exemplo, pra fazer os testes e reduzir riscos relacionados a limpeza de banco de dados ou acumulo de dados inutilizáveis em ambiente de produção caso um teste seja executado indevidamente lá, obviamente que devemos tomar outras medidas para evitar que seja possível alguém executar um teste em ambiente de produção.
Vou mostrar um teste automatizado que fiz para um teste de emprego, onde eu utilizo algumas coisas legais que vou explicar ainda nesse artigo, prepare-se para entrar numa jornada interessante pelo mundo dos testes.
Meu repositório do teste utilizado https://github.com/andmarruda/otherlaraveltest/tree/main
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
use Tests\CommonTrait;
use Illuminate\Support\Facades\Artisan;
class UserBadgeTest extends TestCase
{
use RefreshDatabase, CommonTrait;
private $users;
/**
* Set up the test seeding data and choosing five users for test
*/
public function setUp(): void
{
parent::setUp();
Artisan::call('db:seed', [
'--force' => true,
]);
$this->users = User::whereDoesntHave('comments')->select(['id', 'name', 'email'])->limit(7)->get();
}
/**
* Test badge begginer with no achievement
*/
public function test_no_achievement_begginer()
{
$response = $this->get($this->getUrl($this->users[0]->id));
$response->assertStatus(200);
[$unlocked_achievements, $next_achievements, $current, $next, $remaining] = $this->getResponseJsonAchievements($response);
$this->assertEquals($unlocked_achievements, $this->getUnlockAchievements($this->users[0]->id));
$this->assertEquals($next_achievements, $this->getNextAvailableAchievements($this->users[0]->id));
$this->assertEquals($current, 'Beginner');
$this->assertEquals($next, 'Intermediate');
$this->assertEquals($remaining, 4);
}
/**
* Test badge begginer with one achievement
*/
public function test_begginer()
{
$this->createComment(1, $this->users[1]->id);
$response = $this->get($this->getUrl($this->users[1]->id));
$response->assertStatus(200);
[$unlocked_achievements, $next_achievements, $current, $next, $remaining] = $this->getResponseJsonAchievements($response);
$this->assertEquals($unlocked_achievements, $this->getUnlockAchievements($this->users[1]->id));
$this->assertEquals($next_achievements, $this->getNextAvailableAchievements($this->users[1]->id));
$this->assertEquals($current, 'Beginner');
$this->assertEquals($next, 'Intermediate');
$this->assertEquals($remaining, 3);
}
/**
* Test badge intermediate with four achievement
*/
public function test_intermediate()
{
$this->createComment(3, $this->users[2]->id);
$this->createLessonWatched(5, $this->users[2]);
$response = $this->get($this->getUrl($this->users[2]->id));
$response->assertStatus(200);
[$unlocked_achievements, $next_achievements, $current, $next, $remaining] = $this->getResponseJsonAchievements($response);
$this->assertEquals($unlocked_achievements, $this->getUnlockAchievements($this->users[2]->id));
$this->assertEquals($next_achievements, $this->getNextAvailableAchievements($this->users[2]->id));
$this->assertEquals($current, 'Intermediate');
$this->assertEquals($next, 'Advanced');
$this->assertEquals($remaining, 4);
}
/**
* Test badge intermediate with five achievement
*/
public function test_intermediate_with_five_achievements()
{
$this->createComment(5, $this->users[3]->id);
$this->createLessonWatched(5, $this->users[3]);
$response = $this->get($this->getUrl($this->users[3]->id));
$response->assertStatus(200);
[$unlocked_achievements, $next_achievements, $current, $next, $remaining] = $this->getResponseJsonAchievements($response);
$this->assertEquals($unlocked_achievements, $this->getUnlockAchievements($this->users[3]->id));
$this->assertEquals($next_achievements, $this->getNextAvailableAchievements($this->users[3]->id));
$this->assertEquals($current, 'Intermediate');
$this->assertEquals($next, 'Advanced');
$this->assertEquals($remaining, 3);
}
/**
* Test badge advanced with eight achievement
*/
public function test_advanced()
{
$this->createComment(10, $this->users[4]->id);
$this->createLessonWatched(25, $this->users[4]);
$response = $this->get($this->getUrl($this->users[4]->id));
$response->assertStatus(200);
[$unlocked_achievements, $next_achievements, $current, $next, $remaining] = $this->getResponseJsonAchievements($response);
$this->assertEquals($unlocked_achievements, $this->getUnlockAchievements($this->users[4]->id));
$this->assertEquals($next_achievements, $this->getNextAvailableAchievements($this->users[4]->id));
$this->assertEquals($current, 'Advanced');
$this->assertEquals($next, 'Master');
$this->assertEquals($remaining, 2);
}
/**
* Test badge advanced with nine achievement
*/
public function test_advanced_with_nine_achievement()
{
$this->createComment(20, $this->users[5]->id);
$this->createLessonWatched(25, $this->users[5]);
$response = $this->get($this->getUrl($this->users[5]->id));
$response->assertStatus(200);
[$unlocked_achievements, $next_achievements, $current, $next, $remaining] = $this->getResponseJsonAchievements($response);
$this->assertEquals($unlocked_achievements, $this->getUnlockAchievements($this->users[5]->id));
$this->assertEquals($next_achievements, $this->getNextAvailableAchievements($this->users[5]->id));
$this->assertEquals($current, 'Advanced');
$this->assertEquals($next, 'Master');
$this->assertEquals($remaining, 1);
}
/**
* Test badge master
*/
public function test_master()
{
$this->createComment(20, $this->users[6]->id);
$this->createLessonWatched(50, $this->users[6]);
$response = $this->get($this->getUrl($this->users[6]->id));
$response->assertStatus(200);
[$unlocked_achievements, $next_achievements, $current, $next, $remaining] = $this->getResponseJsonAchievements($response);
$this->assertEquals($unlocked_achievements, $this->getUnlockAchievements($this->users[6]->id));
$this->assertEquals($next_achievements, $this->getNextAvailableAchievements($this->users[6]->id));
$this->assertEquals($current, 'Master');
$this->assertEquals($next, '');
$this->assertEquals($remaining, 0);
}
}
O teste consiste no seguinte pensamento, as ações do usuário levam a conquistas e essas conquistas acumuladas levam a medalhas e meu intuito aqui é testar se os events e listeners estavam se comportando de maneira adequada e esperada.
Utilizo o RefreshDatabase como dito anteriormente para limpar todo o banco de dados e evitar que tenham dados fantasmas que possam atrapalhar o teste.
Nesse teste não utilizei o .env.testing pois o intuito era avaliar capacidade técnica e não utilizar esse código em produção.
CommonTrait
É uma Trait que fiz para centralizar os comandos comuns que iria utilizar dentro de cada feature test, vou deixar o código aqui também para poderem entender o raciocínio por completo.
<?php
namespace Tests;
use App\Models\Achievement;
use App\Models\Comment;
use App\Models\User;
use App\Models\Lesson;
trait CommonTrait {
const URL = 'users/%d/achievements';
private function createComment(int $count, int $user_id)
{
for($i = 0; $i < $count; $i++) {
Comment::factory()->create(['user_id' => $user_id]);
}
}
private function createLessonWatched(int $count, User $user)
{
$usedLesson = [];
for($i = 0; $i < $count; $i++) {
$lesson = Lesson::whereNotIn('id', $usedLesson)->inRandomOrder()->first();
$user->attachWatched($lesson);
$usedLesson[] = $lesson->id;
}
}
private function getUnlockAchievements(int $user_id)
{
return Achievement::whereHas('user_achievement', function($query) use($user_id) {
$query->where('user_id', $user_id);
})->orderBy('model')->orderBy('min_count')->get()->pluck('name')->toArray();
}
private function getNextAvailableAchievements(int $user_id)
{
return Achievement::whereDoesntHave('user_achievement', function($query) use($user_id) {
$query->where('user_id', $user_id);
})->orderBy('model')->orderBy('min_count')->get()->pluck('name')->toArray();
}
private function getResponseJsonAchievements($response) : array
{
$json = $response->json();
return [
$json['unlocked_achievements'], $json['next_available_achievements'],
$json['current_badge'], $json['next_badge'], $json['remaing_to_unlock_next_badge']
];
}
private function getUrl(int $user_id)
{
return config('app.url').'/'.sprintf(self::URL, $user_id);
}
}
setUp
O setUp é o método que vai preparar esse teste, no meu caso utilizei ele para poder db:seed pois gostaria de preparar os dados massivamente sem perder muito tempo fazendo manualmente, $this->users = User::whereDoesntHave('comments')->select(['id', 'name', 'email'])->limit(7)->get(); serve para selecionar 7 usuários para serem alvos dos testes, claro que hoje fazendo a review desse código vejo como é desnecessário verificar se o usuário não tem comentários, mas no ímpeto de entregar o teste acabou passando despercebido, pois o teste limpa o banco de dados e popula o banco de dados com usuários novos e no caso esses não teriam mesmo comentários.
Métodos de teste
Todos os testes seguem o mesmo princípio o que muda é a quantidade de conquistas que se transformam em medalhas e o retorno da rota estimulada.
Como disse é um introdutório especial de Natal, só pra colocarmos o pézinho em testes automáticos utilizando Laravel, tem muito o que se abordar é muito mais complexo do que isso, tendo inclusive métodos de boot do feature test que ainda não foi dado boot na aplicação do Laravel sendo impossível acesso ao banco de dados naquele momento, outra boa prática é CRIE UM BANCO DE DADOS SOMENTE PARA TESTES AUTOMATIZADOS, fiquem a vontade para andar pelo código de teste que fiz, façam perguntas a vontade.
Se quiserem mais artigos desse assunto deixe nos comentários pra eu saber que o tema os agradou e que eu devo continuar a abordar esse assunto.
Até a próxima e FELIZ NATAL HO HO HO
Seja o primeiro a comentar o nosso artigo!