Henrique Morbin
💻 Software Engineer @ Faster Platform (Remote Config, Experimentos e Pushes) no iFood
O código de teste é tão importante quando o código de produção. Por isso, vou compartilhar com você uma técnica para deixar a estrutura do seu teste muito mais limpa
Sempre buscamos maneiras melhores e mais eficientes de resolver os problemas que enfrentamos.
É com esse mindset que eu quero começar o meu texto. Não estou aqui para dizer qual é o certo ou errado. Mas quero mostrar que existem outras formas de escrever os seus testes. Principalmente no que diz respeito à declaração do seu System Under Test (que chamaremos de SUT daqui pra frente) e demais dependências.
Começando com testes
Lembro quando comecei a escrever testes. É engraçado, porque, mesmo que você já programe para aquela plataforma há um tempo, parece que você está começando tudo de novo. Falta padrão no código do teste, você não conhece os anti-pattern de teste e qualquer validação que você faça parece que já é o suficiente. Você se dá por satisfeito com muito pouco.
E, sendo bem sincero, no começo não tem problema nenhum. Para quem está começando, esse passo em prol da qualidade já é um grande avanço. Se o que está escrito está bem escrito ou se a validação cobre tudo que deveria cobrir, ***no começo*** não importa muito… pelo menos tem teste. 😆
Estou no iFood há pouco mais de 6 meses. Não parece muito, mas é o suficiente para aprender muita coisa legal! Entrei no time de Platforms, um time heterogêneo responsável pelas ferramentas proprietárias de Analytics, Remote Config, Push Notification, entre outros.
Como iOS Engineer, tenho muita interação com os demais times responsáveis pelas Features do nosso aplicativo. E foi em uma dessas interações, mais especificamente revisando o código de um colega, que descobri que existe uma maneira muito melhor de declarar o SUT e as demais dependências do XCTestCase. É um detalhe pouco conhecido e nada intuitivo por sinal, mas está disponível na documentação da Apple.
override func setUp()
Você provavelmente conhece e implementa o método setUp que a Apple disponibiliza. Você coloca nele código pertinente e comum a todos os testes, como inicialização de propriedades, principalmente para garantir que elas estejam limpas e isoladas para não afetarem o resultado dos demais testes etc. Certo?
Se a sua resposta foi sim fique tranquilo, não está errado. Até a escrita deste texto eu usava exatamente da mesma maneira.
É essencial termos nossos objetos limpos e isolados, como citado anteriormente, para que não impactem no resultado dos outros testes. Quando você declara a inicialização deles no método setUp do XCTestCase você conseqüentemente obtém esse resultado, pois esse método é invocado antes de cada um dos métodos de teste rodarem. Ou seja, o setUp resolve o problema. No big deal.
Agora aqui vai um balde de água fria: você não precisa implementar o setUp. Não precisa inicializar suas propriedades dentro dele. Não precisa deixar elas como var. Não precisa declarar elas como optional ou fazer force unwrap. A Apple garante o isolamento das propriedades declaradas no escopo da sua classe de teste quando herda de XCTestCase. Quer ver?
XCTestCase
Quando você declara uma propriedade no escopo da classe de teste e já atribui um valor a ela direto na sua declaração, a Apple inicializa a classe, juntamente com suas propriedades, e injeta essa versão recém instanciada da classe em cada um dos testes que rodam em processos separados.
Sim, isso mesmo. Cada teste roda em uma thread própria. Logo, se você tiver algum teste que altere o estado do SUT, ou de algum outro colaborador (Spy, Stub, etc), isso não irá se refletir nos demais testes. Vamos ver uns exemplos para entender melhor?
Vejo no teste de exemplo que a variável que chamamos de sut é iniciada com 0 (zero). Em todos os testes a estamos incrementando em +1, porém em todos os testes também estamos verificando se a variável é igual a 1. Ou seja, o incremento em um teste não reflete no estado das variáveis dos demais.
Está confirmado o isolamento. O código está menor e muito mais limpo. Você vai perceber melhor os benefícios quando começar a refatorar seus testes.
E como ficaria o primeiro exemplo sem o setUp? Já que naquele caso temos um cenário onde o sut tem dependência com outros objetos que precisam já estar instanciados antes do sut.
Percebem aqui que os colaboradores viraram constantes (let) e foi removido o force unwrap de todas as propriedades, inclusive do sut. Este último virou uma variável lazy para que quando for consumido nos testes ele seja instanciado com todas as suas dependências resolvidas.
Você deve estar se perguntando, mas então para que diabos serve o tal do setUp que a Apple disponibiliza?
A resposta é simples: serve para fazer exatamente o que você achava que ele fazia. Implementar comandos que sejam comuns a todos os testes da respectiva folha e que precisem ser executados antes de cada um dos testes rodar.
A única diferença é que inicialização de propriedades não precisa fazer parte desta lista de responsabilidades. Este não é o escopo do setUp. Mas outras coisas podem ser, por exemplo:
Neste cenários de teste, se não criarmos o arquivo antes teremos duas conseqüências:
1. Um falso/positivo no primeiro (testWriteContentWithoutOptions), pois ele irá escrever o conteúdo no arquivo, mas não poderíamos garantir que o arquivo foi realmente sobrescrito como diz na descrição do teste.
2 . E uma falha no segundo (testWriteContentWithoutOverwriting), pois como não haverá um arquivo original, a tentativa de escrita do novo conteúdo irá ser bem sucedida fazendo com que falhe na fase de verificação com a comparação dos conteúdos.
Bônus
E aqui vai um bônus extra: caso você não saiba existem 2 setUps disponíveis para você implementar no XCTestCase, um setUp de instância (não estático) e outro de classe (estático).
A diferença entre eles é bem simples. O primeiro, mais comumente utilizado, é invocado antes de cada um dos métodos de teste da respectiva folha, enquanto o segundo roda apenas uma vez e antes de todos os testes começarem.
Lembrando que em contrapartida também temos o tearDown, que segue a mesma dinâmica. Um tearDown de instância (não estático), que é invocado após cada execução de método de teste, e um tearDown (estático), que roda após todos os testes daquela folha terem sido executados.
Para terminar
Quis abordar esses detalhes que considero importantes do XCTestCase: isolamento das propriedades da classe, funcionamento dos dois setUp e dos dois tearDown, e como/quando usar cada um. Como já mencionei, você vai perceber como eles deixam a folha de teste mais limpa e simples de entender. Fora que quanto menos código melhor.
O melhor código é aquele que não precisou ser escrito.
Se você quiser saber mais ou tiver alguma sugestão, manda aí nos comentários.