Olá!
Este post é uma sequência de Design: Monolitos Modulares - Parte 3, e nele falaremos sobre como testar nosso monolito modular. A intenção é seguir com a aplicação de demonstração do post passado, testando os diferentes módulos da aplicação da maneira mais adequada a cada necessidade.
Sigamos!
Entendendo o que importa.
Antes de pensarmos em testes, precisamos considerar o que faz sentido ser testado. Existe uma discussão longa sobre cobertura de testes, que está fora do escopo deste post, mas há uma questão mais importante que essa para que atinjamos o objetivo de conferir alguma confiabilidade, porque ela jamais será total, ao sistema: quais os caminhos possíveis em nosso código? Quais ramificações (branches) existem e quais cenários as envolvem?
Perceba que sequer entro no mérito da cobertura. Ou seja, se todos os componentes de uma dada classe foram submetidos a testes ou não. A preocupação é com o caminho que os dados podem fazer entre sua origem e seu destino.
Getters, setters e afins.
Essa é uma pergunta frequente quando se fala em testes, e vou deixar meu humilde ponto de vista: por que eu deveria testar algo que o responsável pela linguagem já testou? Getters e setters, por exemplo, não deveriam demandar testes. Dito de forma direta: são testes inúteis, uma vez que, no caso do C#, o time da Microsoft já se deu este trabalho.
É claro que há os casos de propriedades computadas (computed properties) como, por exemplo, a soma dos valores dos negócios (Trades
) na classe Order
, que podem precisar de validação por se tratar de uma operação e não uma mera recuperação de valor. Mas, em casos como este, importa muito mais testar sua participação em uma operação maior da aplicação como, por exemplo, a execução de uma ordem, que a propriedade em si.
Testar o sistema desta forma é eficiente porque, além de prover um modelo mental mais preciso sobre como o sistema funciona, e não como cada classe funciona, torna os testes não apenas uma forma de validar a solução do problema como, também, explicita as operações possíveis e serve de documentação complementar, ilustrando na prática o que foi implementado.
Nota: é possível que você tenha percebido aqui que a abordagem comentada lembra muito o conceito do chamado "Desenvolvimento Orientado a Comportamento" (Behavior-Driven Development). E isso não é à toa. A ideia é relacionar os casos de uso conhecidos pelo negócio com o que foi implementado, de modo a dar clareza sobre a conformidade entre ambos.
Encapsulamento é Lei! E agora?
O que pode saltar aos olhos neste momento é o fato de que nossos módulos possuem classes marcadas como internal
. Ou seja, são visíveis, apenas, a seu assembly de origem. Isso deveria impedir a realização de testes ou obrigaria o uso do recurso friend assemblies disponíveis no .NET. Certo?
Sim. E não!
Sim porque, de fato, estamos impedidos de realizar testes diretamente contra nossos modelos de domínio por uma questão de controle de acesso. Mas não porque, graças a este encapsulamento, limitamos a superfície de contato com os mesmos modelos por meio das APIs (não confundir com Web API) dos nossos módulos. Ou seja, cada API servirá de ponto de entrada para nossos testes, o que os torna mais realistas ao refletir o comportamento esperado da aplicação quando em execução.
Unidade vs Integração.
Dada a limitação imposta pelo controle de acesso aos nossos modelos de domínio, uma nova questão aparece: e os testes de unidade?
E a resposta neste cenário é: esqueça-os!
Reforçando: o que vimos sobre getters e setters também se aplica aos testes de unidade. Mais importante que testar componentes independentemente é validar seu comportamento e interação em cada caso de uso. Assim sendo, podemos ignorar testes de unidade em favor de testes de integração.
A esta altura é possível que você tenha reparado em um detalhe interessante: estamos reduzindo o escopo dos testes mas, ao mesmo tempo, não estamos reduzindo sua capacidade de cobrir os casos de uso. Isso porque, ao definir uma API, com pontos de entrada bem definidos para cada caso de uso, conseguimos validar cada classe de acordo com o que delas é esperado.
Nota: não estou, de forma alguma, condenando testes de unidade nem menosprezando seu valor. Meu ponto aqui é outro: um recurso tem valor apenas quando seu emprego é imprescindível. Se não houver a necessidade de implementar testes de unidade, como no caso da aplicação de exemplo, e como também pode ser em uma implementação real deste estilo arquitetural, deixar de usá-los não é nenhum problema. YAGNI (em inglês) não existe à toa. Não é mesmo?
Mocks? Melhor não!
Seguindo nossa abordagem, digamos minimalista, nos colocamos de frente com outra questão bastante polêmica quando o assunto é testes: usar ou não usar mocks?
Uma resposta curta seria outra pergunta: pra quê?
A resposta longa começa com: por que precisamos de mocks?
O uso indiscriminado de mocks cria um grande problema em diversos sistemas: testes que não testam! Ou seja, quanto mais mocks, maiores os riscos de induzirmos um comportamento incorreto porque somos nós os responsáveis por configurar seu estado e comportamento.
Muitas vezes mocks são utilizados com o intuito de simular dependências de uma dada classe. Mas a questão que se levanta é: por quê? Por que não utilizar uma instância no lugar de um mock?
E a resposta é pouco palatável: o uso mocks demais costuma indicar uma gestão ruim de dependências, um acoplamento exagerado, o que se faz notar pela quantidade de interfaces injetadas em um dado construtor por exemplo.
Se você precisa utilizar mocks para criar instâncias de classe sobre as quais você tem controle, talvez seja um bom momento para repensar como você tem lidado com acoplamento.
Neste caso, uma regra do polegar que costumo usar é: empregar mocks apenas sobre componentes que não podem ser testados por não estarem sob meu controle. Ou seja, se preciso chamar uma Web API externa, de um fornecedor, dificilmente terei acesso a uma versão estável, e portanto confiável, em ambientes não produtivos. Por este motivo, o emprego de mocks que reflitam o comportamento desta Web API é necessário para que meu sistema tenha acesso a uma versão que reflita, ainda que com dados falsos, o comportamento desejado para esta dependência externa.
Mocks, bancos de dados e brokers de mensagens
Preciso admitir: este post está repleto de polêmicas sobre testes. E esta é mais uma!
Muitas vezes utilizamos mocks para simular o acesso aos dados da aplicação ou a publicação e consumo de mensagens mas, sinceramente, não é algo que recomende pois mocks não refletem as capacidades e comportamentos do banco/broker de produção.
Este ponto é crucial para entender o motivo pelo qual devemos buscar utilizar uma versão do banco/broker de produção em nossos testes. Não são apenas os dados/mensagens que importam. Também importam os comportamentos do gerenciador de banco de dados/broker com o qual estamos trabalhando. Precisamos validar o modelo físico de persistência (linhas e colunas, documentos ou pares chave-valor)/mensagem, e também os recursos extras que podem ser utilizados na aplicação como, por exemplo, stored procedures.
Utilizar mocks neste cenário é abrir mão de parte importante da aplicação, e há também a chance de introduzirmos comportamentos indesejados por adequação a um mecanismo que não é o mesmo do mundo real.
Há 10 anos teria uma opinião diferente mas, uma vez que podemos utilizar Docker para criar instâncias reais dos mecanismos mencionados, ainda mais integrado diretamente a nossos projetos, não vejo razão para que mocks sejam utilizados como primeira escolha.
Nota: No caso específico da aplicação de exemplo, como utilizo um mecanismo simples de persistência em memória, não utilizei Docker para realizar os testes. Apenas injetei o serviço (interface) responsável por ela onde necessário e verifiquei seu estado na fase de asserção dos testes.
End-to-end
A esta altura já temos clareza sobre o fato de que buscamos uma abordagem minimalista para nossos testes e que, até agora, fundamentamos bem os motivos para tal.
Agora temos uma última necessidade: testar nossa fachada (façade) para o envio de ordens e, também, os eventos disparados quando uma operação é realizada. Para isso um teste do tipo end-to-end é necessário.
Testes end-to-end são muito úteis em cenários como este porque nos permitem avaliar operações do ponto de vista dos acionadores de cada caso de uso (usuário, tarefa agendada, eventos de outros sistemas etc).
Mostre-me o código!
Como de praxe o código foi disponibilizado no Github. Para realizar os testes end-to-end basta acessar o projeto de testes da Web API. Recomendo uma leitura minuciosa para relacionar o comportamento dos testes com o que foi mostrado neste post.
Além do projeto de testes da Web API, cada módulo oferece sua própria suíte de testes, seguindo os mesmos princípios. Esses são testes de integração de cada módulo, demonstrando como cada serviço lida com suas próprias requisições.
Considerações Finais
Este post fez mais que ilustrar os testes do projeto, também trouxe uma forma de lidar com testes no geral. É óbvio que o que foi descrito aqui não é uma regra, é apenas uma demonstração da forma como lido com testes em projetos do tipo. Fique à vontade para realizar seus testes como preferir, mas recomendo a ponderação sobre o que foi descrito aqui, pode ser muito útil e te fazer economizar muito trabalho testando apenas o que realmente importa.
Caso tenha gostado desta série me deixe saber pelos indicadores, comentários ou por minhas redes sociais. Em breve voltamos com mais conteúdo.
Até mais!