[AJUDA] Dúvida sobre testes.

Introdução ao problema

Olá, gostaria de retirar uma dúvida sobre testes com um exemplo abaixo. Estou estudando essa matéria há alguns dias, mas surgiu-me uma dúvida e gostaria de saná-la com vocês. Creio que, por ser uma dúvida conceitual, você não precisa entender PHP ou PHPUnit para saná-la. Ainda assim, deixei alguns exemplos de código para expressar melhor a minha dúvida.

Cenário do problema

Suponha que, na aplicação, tenhamos a seguinte classe no diretório ./src/Models:

namespace App\Models;

class User
{
    public function __construct(
        private string $firstName,
        private string $lastName,
        private int $age
    )
    {
        /**
        * Essa é uma funcionalidade da versão 8.0 do PHP.
        * Com ela, podemos fazer com que os parâmetros do método
        * construtor tornem-se propriedades.
        */
    }
    
    public function getName(): string
    {
        return "{$this->firstName} {$this->lastName}";
    }
    
    public function isAdult(): bool
    {
        return $this->age >= 18;
    }
}

Essa classe é simples, mas será suficiente para explicar a minha dúvida.

Agora, no diretório ./test/Models teremos a seguinte classe de teste.

namespace Test\Model;

use PHPUnit\Framework\TestCase;

/**
*@covers App\Model\User
*/

//A annotation acima serve apenas para explicitar qual
//classe estamos testando.

class UserTest extends TestCase
{
    /**
    *@dataProvider usersProvider
    */
    public function testIfUserIsAnAdultOrNot($firstName, $lastName, $age): void
    {
        $user = new User($firstName, $lastName, $age);
        
        $age >= 18 ? self::assertEquals(true, $user->isAdult()) : self::assertEquals(false, $user->isAdult());
    }
    
    //O método estático abaixo retornará um conjunto de dados que podem ser 
    //utilizados no teste acima
    public static function usersProvider(): array
    {
        return [
            'firstUser' => ['Natan', 'Matos', 19],
            'secondUser' => ['Oliver', 'Souza', 27],
            'thirdUser' => ['Giovanna', 'Matsumoto', 16]
        ];
    }
}

Cerne da dúvida

Repare na maneira como realizamos o teste do método isAdult. Essa maneira é correta? Digo isso porque o teste possui asserções condicionais. O que é um pouco estranho. Será que a melhor maneira seria testar esse método com dois métodos de teste diferentes? Exemplo

  • testIfUserIsAnAdult: possui apenas uma asserção, onde insiro apenas dados onde o usuário é um adulto, aguardando que o valor final seja true.
  • testIfUserIsNotAnAdult: possui apenas uma asserção, onde insiro apenas dados onde o usuário não é um adulto, aguandando que o valor final seja false.

Agradeço por terem lido até aqui!

Olha não entendo bulhufas de PHP, muito menos testes exclusivamente em PHP, mas existem coisas que funcionam para testes em qualquer linguagem.

Um teste unitário em primeiro lugar deve testar apenas uma coisa. Não é boa prática testar vários retornos em um teste unitário, da mesma forma que você expôs. Então deveria ficar algo como:

public function testIfUserIsAnAdult($firstName, $lastName, $age): void
{
    $user = new User($firstName, $lastName, $age);

    self::assertEquals(true, $user->isAdult());
}
    
public function testIfUserIsNotAnAdult($firstName, $lastName, $age): void
{
    $user = new User($firstName, $lastName, $age);

    self::assertEquals(false, $user->isAdult());
}

Se atente também a massa de testes, deve ser adequada a execução. Você não conseguirá testar todos os cenários com o mesmo resultado. Eu diria até mesmo para realizar a inserção da massa dentro do método de teste. Construa o objeto dentro do teste e construa o teste, com o cenário esperado.

Existem outras boas práticas para testes unitários, como nomeclatura. Isso depende muito dos idioms usados em sua empresa, mas em geral existe a prática given_when_then, ou Dado_Quando_Então. Isso servirá de documentação para outros usuários que lerem seu teste.

Um teste deve ter a possibilidade de falhar. Isso é uma prática excelente quando citamos resiliência de regras de negócio. Geralmente fazemos isso criando um outro cenário para encontrar essas falhas.

Por fim, devemos ter em mente que testes unitários não garantem 100% de segurança ou resiliência a bugs em produção. Bons testes em software são além de testes unitários, testes de integração, end to end, carga, smoke, api, e por ai vai. Dependendo do escopo, empregue tudo que for possível.

Obrigado pela resposta!

Eu faria diferente, da seguinte forma.

class UserTest extends TestCase
{
    /**
    *@dataProvider usersProvider
    */
    public function testIfUserIsAnAdultOrNot($firstName, $lastName, $age, $isAdult): void
    {
        $user = new User($firstName, $lastName, $age);        
        self::assertEquals($isAdult, $user->isAdult());
    }
    
    //O método estático abaixo retornará um conjunto de dados que podem ser 
    //utilizados no teste acima
    public static function usersProvider(): array
    {
        return [
            'firstUser' => ['Natan', 'Matos', 19, true],
            'secondUser' => ['Oliver', 'Souza', 27, true],
            'thirdUser' => ['Giovanna', 'Matsumoto', 16, false]
        ];
    }
}

Assim eu removeria de dentro do teste esse if ternário, pois acho que está adicionando ruído na intenção do teste.

outra forma seria alterar a condição:

$age >= 18 ? self::assertEquals(true, $user->isAdult()) : self::assertEquals(false, $user->isAdult());

que está fazendo uma comparação desnecessária e gerando uma carga cognitiva maior para entender para:

self::assertEquals($age >= 18, $user->isAdult())

Olá, Natan. Houveram boas respostas antes, então vou só adicionar um detalhe. Seu erro foi aplicar a mesma lógica da sua função na declaração de expectativa do teste e isso não é correto. Testes unitários devem validar se, dada uma entrada, o resultado é o esperado. Logo o teste para o "isAdult" deve verificar apenas se a saída esperada é um True, ou False. Neste caso, se você fosse descrever seu teste, seria como:

Se Eu criar um usuário com idade de 20 anos, Eu espero que a função isAdult me retorne True; Se Eu criar um usuário com idade de 15 anos, Eu espero que a função isAdult me retorne False.

Sendo assim, a escrita do teste vira uma descrição de comportamento esperado.

Um forte abraço!