Existem muitas ferramentas no ecossistema PHP que fornecem testes convenientes de PHP. Um dos mais famosos é o
PHPUnit , que é quase um sinônimo de teste nessa linguagem. No entanto, não se escreve muito sobre bons métodos de teste. Existem muitas opções para por que e quando escrever testes, que tipo de testes e assim por diante. Mas, para ser sincero,
não faz sentido escrever um teste se você não conseguir lê-lo mais tarde .
Os testes são um tipo especial de documentação. Como
escrevi anteriormente sobre TDD em PHP , o teste sempre (ou pelo menos deveria) claramente fala sobre qual é a tarefa de um pedaço de código específico.
Se um teste não puder expressar essa ideia, o teste será ruim.
Eu preparei um conjunto de técnicas que ajudarão os desenvolvedores de PHP a escrever testes bons, legíveis e úteis.
Vamos começar com o básico
Há um conjunto de técnicas padrão que muitos seguem sem nenhuma pergunta. Vou mencionar muitos deles e tentar explicar por que eles são necessários.
1. Os testes não devem conter operações de entrada e saída
O principal motivo : as operações de E / S são lentas e não confiáveis.
Lento : mesmo se você tiver o melhor hardware do mundo, a E / S ainda será mais lenta do que a memória acessa. Os testes devem sempre funcionar rápido, caso contrário, as pessoas os executam muito raramente.
Não confiável : alguns arquivos, binários, soquetes, pastas e registros DNS podem não estar disponíveis em algumas máquinas nas quais você está testando. Quanto mais você confia nos testes de E / S, mais os seus testes estão vinculados à infraestrutura.
Quais operações estão relacionadas à E / S:
- Lendo e gravando arquivos.
- Chamadas de rede.
- Chamadas para processos externos (usando
exec
, proc_open
, etc.).
Há situações em que a presença de operações de entrada e saída permite que você escreva testes mais rapidamente. Mas tenha cuidado: verifique se essas operações funcionam da mesma maneira em suas máquinas para desenvolvimento, montagem e implantação; caso contrário, você poderá ter sérios problemas.
Isole os testes para que eles não precisem de operações de E / S: Forneci uma solução de arquitetura abaixo que impede que os testes executem operações de E / S compartilhando responsabilidade entre interfaces.
Um exemplo:
public function getPeople(): array { $rawPeople = file_get_contents( 'people.json' ) ?? '[]'; return json_decode( $rawPeople, true ); }
Quando você inicia o teste usando esse método, um arquivo local será criado e, de tempos em tempos, instantâneos serão criados:
public function testGetPeopleReturnsPeopleList(): void { $people = $this->peopleService ->getPeople();
Para fazer isso, precisamos configurar os pré-requisitos para executar os testes. À primeira vista, tudo parece razoável, mas na verdade é terrível.
Ignorar um teste devido ao fato de que os pré-requisitos não são atendidos não garante a qualidade do nosso software. Isso apenas oculta bugs!
Corrigimos a situação : isolamos as operações de E / S transferindo a responsabilidade para a interface.
Agora eu sei que o
JsonFilePeopleProvider
usará a E / S em qualquer caso.
Em vez de
file_get_contents()
você pode usar uma camada de abstração como
o sistema de arquivos Flysystem , para o qual é fácil criar stubs.
E então por que precisamos do
PeopleService
? Boa pergunta Para isso, são necessários testes: desafiar a arquitetura e remover código inútil.
2. Os testes devem ser conscientes e significativos.
O principal motivo : os testes são uma forma de documentação. Mantenha-os claros, concisos e legíveis.
Clareza e brevidade : sem bagunça, sem milhares de linhas de stubs, sem seqüências de declarações.
Legibilidade : Os testes devem contar uma história. A estrutura “dado, quando, então” é excelente para isso.
Características de um teste bom e legível:
- Contém apenas as chamadas necessárias para o método
assert
(de preferência um).
- Ele explica claramente o que deve acontecer sob determinadas condições.
- Ele testa apenas um ramo da execução do método.
- Ele não faz um esboço para o universo inteiro por causa de qualquer afirmação.
É importante observar que, se sua implementação contiver expressões condicionais, operadores de transição ou loops, todos eles deverão ser explicitamente cobertos por testes. Por exemplo, para que as respostas iniciais sempre contenham um teste.
Repito: não é uma questão de cobertura, mas de documentação.
Aqui está um exemplo de um teste confuso:
public function testCanFly(): void { $noWings = new Person(0); $this->assertEquals( false, $noWings->canFly() ); $singleWing = new Person(1); $this->assertTrue( !$singleWing->canFly() ); $twoWings = new Person(2); $this->assertTrue( $twoWings->canFly() ); }
Vamos adaptar o formato "dado quando, então" e ver o que acontece:
public function testCanFly(): void {
Como a seção "Dados", "quando" e "então" podem ser transferidos para métodos privados. Isso tornará seu teste mais legível.
assertEquals
bagunça inútil. A pessoa que está lendo isso deve rastrear a declaração para entender o que isso significa.
O uso de instruções específicas tornará seu teste muito mais legível.
assertTrue()
deve receber uma variável booleana, não uma expressão como
canFly() !== true
.
No exemplo anterior, substituímos
assertEquals
entre
false
e
$person->canFly()
por um
assertFalse
simples:
Agora está tudo muito claro! Se uma pessoa não tem asas, ela não deve poder voar! Leia como um poema
Agora, a seção "Outros casos", que aparece duas vezes em nosso texto, é uma indicação clara de que o teste faz muitas declarações. O método
testCanFly()
é completamente inútil.
Vamos melhorar o teste novamente:
public function testCanFlyIsFalsyWhenPersonHasNoWings(): void { $person = $this->givenAPersonHasNoWings(); $this->assertFalse( $person->canFly() ); } public function testCanFlyIsTruthyWhenPersonHasTwoWings(): void { $person = $this->givenAPersonHasTwoWings(); $this->assertTrue( $person->canFly() ); }
Podemos até renomear o método de teste para que ele corresponda ao cenário real, por exemplo, em
testPersonCantFlyWithoutWings
, mas tudo me convém de
testPersonCantFlyWithoutWings
.
3. O teste não deve depender de outros testes
O principal motivo : os testes devem ser executados e executados com êxito em qualquer ordem.
Não vejo razões suficientes para criar interconexões entre testes. Recentemente me pediram para fazer um teste de função de login, vou dar aqui como um bom exemplo.
O teste deve:
- Gere um token JWT para efetuar login.
- Execute a função de login.
- Aprovar a mudança de status.
Foi assim:
public function testGenerateJWTToken(): void {
Isso é ruim por vários motivos:
- O PHPUnit não pode garantir essa ordem de execução.
- Os testes devem poder executar independentemente.
- Testes paralelos podem falhar aleatoriamente.
A maneira mais fácil de contornar isso é usar o esquema dado, quando e depois. Portanto, os testes serão mais ponderados, eles contarão uma história, demonstrando claramente suas dependências, explicando a função que está sendo testada.
public function testAmazingFeatureChangesState(): void {
Também precisamos adicionar testes para autenticação, etc. Essa estrutura é tão boa que o
Behat é usado por padrão .
4. Sempre implemente dependências
A principal razão : um tom muito ruim - para criar um esboço para o estado global. A incapacidade de criar stubs para dependências não permite testar a função.
Dica útil:
esqueça as classes estáticas e instâncias estáticas . Se sua classe depende de algo, faça-o para que possa ser implementado.
Aqui está um exemplo triste:
class FeatureToggle { public function isActive( Id $feature ): bool { $cookieName = $feature->getCookieName();
Como posso testar esta resposta inicial?
Isso mesmo. De jeito nenhum.
Para testá-lo, precisamos entender o comportamento da classe
Cookies
e garantir que possamos reproduzir todo o ambiente associado a ela, resultando em certas respostas.
Não faça isso.
A situação pode ser corrigida se você implementar uma instância de
Cookies
como uma dependência. O teste terá a seguinte aparência:
O mesmo vale para singletones. Portanto, se você deseja tornar um objeto único, configure corretamente seu injetor de dependência, em vez de usar o padrão (anti) singleton. Caso contrário, você escreverá métodos que são úteis apenas para casos como
reset()
ou
setInstance()
. Na minha opinião, isso é loucura.
É completamente normal alterar a arquitetura para facilitar o teste! E criar métodos para facilitar o teste não é normal.
5. Nunca teste métodos protegidos / privados
O principal motivo : eles afetam a maneira como testamos as funções determinando a assinatura do comportamento: sob essa condição, quando digito A, espero obter B.
Métodos particulares / protegidos não fazem parte das assinaturas da função .
Eu nem quero mostrar uma maneira de "testar" métodos privados, mas darei uma dica: você só pode fazer isso usando a API de
reflexão .
Sempre se castigue de alguma forma quando você pensa em usar a reflexão para testar métodos privados! Desenvolvedor ruim, ruim!
Por definição, métodos privados são chamados apenas internamente. Ou seja, eles não estão disponíveis ao público. Isso significa que apenas métodos públicos da mesma classe podem chamar esses métodos.
Se você testou todos os seus métodos públicos, também testou todos os métodos privados / protegidos . Se não for esse o caso, remova livremente métodos privados / protegidos; ninguém os usa de qualquer maneira.
Dicas avançadas
Espero que você não esteja entediado ainda. Ainda assim, eu tive que falar sobre o básico. Agora vou compartilhar minha opinião sobre como escrever testes e decisões limpas que afetam meu processo de desenvolvimento.
A coisa mais importante que não esqueço ao escrever testes:
- Estudo.
- Feedback rápido.
- Documentação
- Refatoração
- Design durante o teste.
1. Testes no começo, não no fim
Valores : estudo, feedback rápido, documentação, refatoração, design durante o teste.
Esta é a base de tudo. O aspecto mais importante, que inclui todos os valores listados. Quando você escreve testes com antecedência, isso ajuda a entender primeiro como o esquema "dado, quando, então" deve ser estruturado. Ao fazer isso, você primeiro documenta e, mais importante, lembra e define seus requisitos como os aspectos mais importantes.
É estranho ouvir sobre a gravação de testes antes da implementação? E imagine como é estranho implementar algo e, ao testar para descobrir, todas as suas expressões "dadas quando, então" não fazem sentido.
Além disso, essa abordagem verificará suas expectativas a cada dois segundos. Você recebe feedback o mais rápido possível. Não importa o tamanho ou a aparência do recurso.
Testes verdes são uma área ideal para refatoração. A idéia principal: sem testes - sem refatoração. A refatoração sem testes é simplesmente perigosa.
Finalmente, definindo a estrutura “dada quando, então”, ficará óbvio para você quais interfaces seus métodos devem ter e como devem se comportar. Manter o teste limpo também forçará você a tomar constantemente diferentes decisões de arquitetura. Isso forçará você a criar fábricas, interfaces, interromper a herança etc. E sim, os testes se tornarão mais fáceis!
Se seus testes são documentos ativos que explicam como o aplicativo funciona, é imperativo que eles deixem claro.
2. Melhor sem testes do que com testes ruins
Valores : estudo, documentação, refatoração.
Muitos desenvolvedores pensam nos testes da seguinte maneira: escreverei um recurso, dirigirei a estrutura de teste até que os testes abranjam um certo número de novas linhas e os coloque em operação.
Parece-me que você precisa prestar mais atenção à situação em que um novo desenvolvedor começa a trabalhar com esse recurso.
O que os testes dirão a essa pessoa?Os testes geralmente são confusos se os nomes não forem detalhados o suficiente. O que é mais claro:
testCanFly
ou
testCanFlyReturnsFalseWhenPersonHasNoWings
?
Se seus testes são apenas um código confuso que faz com que a estrutura cubra mais linhas, com exemplos que não fazem sentido, é hora de parar e pensar em escrever esses testes.
Mesmo bobagens como atribuir
$a
e
$b
variáveis ou atribuir nomes que não estão relacionados a um uso específico.
Lembre -
se : seus testes são documentos ativos, tentando explicar como seu aplicativo deve se comportar.
assertFalse($a->canFly())
não documenta muito. E
assertFalse($personWithNoWings->canFly())
já é bastante.
3. Execute testes de forma intrusiva
Valores : estudo, feedback rápido, refatoração.
Antes de começar a trabalhar nos recursos, execute os testes. Se eles falharem antes que você comece a trabalhar, você saberá disso
antes de escrever o código e não precisará gastar minutos preciosos na depuração de testes quebrados com os quais nem se importava.
Depois de salvar o arquivo, execute os testes. Quanto mais cedo você descobrir que algo está quebrado, mais rápido você o corrige e segue em frente. Se a interrupção do fluxo de trabalho para resolver um problema parecer improdutiva para você, imagine que mais tarde você precisará voltar várias etapas se não souber sobre o problema.
Após conversar por cinco minutos com os colegas ou verificar as notificações do Github, execute os testes. Se eles coraram, então você sabe de onde parou. Se os testes estiverem verdes, você poderá continuar trabalhando.
Após qualquer refatoração, mesmo nomes de variáveis, execute os testes.
Sério, execute os malditos testes. Sempre que você pressionar o botão Salvar.
O PHPUnit Watcher pode fazer isso por você e até enviar notificações!
4. Grandes testes - grande responsabilidade
Valores : estudo, refatoração, design durante o teste.
Idealmente, cada classe deve ter um teste. Este teste deve abranger todos os métodos públicos desta classe, bem como todas as expressões condicionais ou operadores de transição ...
Você pode pegar algo assim:
- Uma classe = um caso de teste.
- Um método = um ou mais testes.
- Uma ramificação alternativa (se / switch / try-catch / exception) = um teste.
Portanto, para esse código simples, você precisará de quatro testes:
Quanto mais métodos públicos você tiver, mais testes serão necessários.
Ninguém gosta de ler documentação longa. Como seus testes também são documentos, o tamanho pequeno e a significância apenas aumentarão sua qualidade e utilidade.
Também é um sinal importante de que sua classe está acumulando responsabilidades e é hora de refatorá-la transferindo várias funções para outras classes ou redesenhando o sistema.
5. Apoie um conjunto de testes para resolver problemas de regressão
Valores : estudo, documentação, feedback rápido.
Considere a função:
function findById(string $id): object { return fromDb((int) $id); }
Você acha que alguém está transmitindo "10", mas na verdade "10 bananas" está sendo transmitida. Ou seja, dois valores vêm, mas um é supérfluo. Você tem um bug.
O que você fará primeiro? Escreva um teste que marque esse comportamento errôneo !!!
public function testFindByIdAcceptsOnlyNumericIds(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( 'Only numeric IDs are allowed.' ); findById("10 bananas"); }
Obviamente, os testes não transmitem nada. Mas agora você sabe o que precisa ser feito para que eles transmitam. Corrija o erro, torne os testes verdes, implante o aplicativo e seja feliz.
Mantenha este teste com você. Sempre que possível, em um conjunto de testes projetados para resolver problemas com regressão.
Isso é tudo! Feedback rápido, correções de erros, documentação, código resistente à regressão e felicidade.
Palavra final
Muito do exposto é apenas minha opinião pessoal, desenvolvida durante minha carreira. Isso não significa que o conselho seja verdadeiro ou falso, é apenas uma opinião.