Eu não tenho tempo para testar
Falta de tempo para testar é a desculpa mais comum para abandonar os testes de unidade no meio de um projeto. Quantas vezes você ignorou os testes, ou começou com eles e desistiu quando o prazo apertou?
Testar é polir a sua aplicação. Embora isso consuma tempo, é exatamente esse cuidado que diferencia o seu produto dos concorrentes.
Neste artigo, vamos olhar para os motivos reais que fazem o tempo para testar desaparecer. Além disso, vamos ver como SOLID, um domínio bem modelado e uma cultura de testes devolvem esse tempo ao time.
Pare de tirar tempo para testar das tarefas
É incrível como a cena se repete. O prazo aperta e o primeiro item descartado é o teste de unidade.
A tarefa estimada em 8 horas atrasa. Então, em vez de cumprir o prometido e testar, usamos a desculpa do atraso e deixamos os testes de lado.
Muitas funcionalidades também ficam para ser testadas no final da Sprint, e a mesma história acontece de novo. Ou seja, 40 horas para produzir uma funcionalidade e 1 hora de tempo para testar tudo.
Seja sincero: você acredita que uma entrega com o mínimo de testes possível tem chance de dar certo? De passar tudo redondo?
Talvez o seu cliente receba um bug e nem reclame mais, porque já se acostumou com os erros. Isso não significa que ele está satisfeito, mas sim que desistiu de brigar por algo melhor.
Não testar porque está atrasado é como sair de casa com o tanque na reserva e não parar no posto para não perder tempo. Cedo ou tarde, o carro para no meio do caminho.
Se você já atrasou, muitas vezes para entregar algo melhor, entregue ao menos algo testado. Afinal, se você sempre tirar o tempo para testar das tarefas, você nunca terá tempo para testar.
O custo de deixar os testes para depois
A documentação da Microsoft sobre boas práticas de testes de unidade no .NET lista benefícios que ajudam a enxergar esse custo. O primeiro deles é gastar menos tempo com testes funcionais manuais.
Segundo o texto, testes de unidade rodam em milissegundos e podem ser executados com um clique. Por outro lado, um teste funcional manual exige abrir a aplicação, seguir passos e conferir o resultado, o que leva de segundos a minutos.
Além disso, a suíte de testes protege contra regressões. Toda vez que você altera o código, roda os testes de novo e descobre na hora se quebrou algo que já funcionava.
Portanto, cortar o tempo para testar hoje é contratar retrabalho para amanhã. O bug que o teste pegaria em milissegundos vira um chamado do cliente, uma investigação e um hotfix.
Esqueça os CRUDs
Já rodei muitas empresas fazendo consultoria. Em várias delas, os times falavam sobre DDD, domínios ricos e outros termos bonitos.
Em diversos cenários, vi equipes implementando interfaces, modelando entidades e aplicando Design Patterns no cadastro de categoria, de estado civil e de unidade de produto. No entanto, esses itens não têm sequer regras de negócio.
Lembro de chegar a um cliente que comentou que 90% do sistema dele era apenas CRUD, e realmente era. Então, sentamos na frente do computador, escrevemos as entidades anêmicas, geramos o banco e o scaffold da API.
Pronto, 90% do sistema estava concluído em menos de duas horas. Em outras palavras, o que parecia meses de trabalho cabia em uma tarde.
Se você não tem regras de negócio, não implemente complexidade. Da mesma forma, se tem apenas validações simples e entidades simples, seja simples com elas e economize esse tempo.
Com essa decisão, 90% de um projeto de seis meses ficou pronto em um dia. Assim, sobraram meses para o que realmente importava: a modelagem do domínio, com testes de unidade.
Talvez você esteja gastando tempo e esforço na parte errada do sistema. Por esse motivo, você nunca terá tempo para testar o que de fato importa.
Onde investir o tempo para testar
A pergunta certa não é testar ou não testar, mas sim onde colocar o esforço. Um cadastro de estado civil dificilmente quebra o negócio, enquanto um cálculo de comissão errado gera prejuízo real.
Por isso, direcione o tempo para testar às partes que concentram regras. Pense em cálculos, validações de negócio, mudanças de estado e integrações críticas.
Martin Fowler descreve essa distribuição no conceito de pirâmide de testes. Segundo ele, você deve ter muito mais testes de unidade de baixo nível do que testes de ponta a ponta pela interface.
Além disso, Fowler afirma que testes de ponta a ponta pela interface são frágeis, caros de escrever e demorados para rodar. Logo, a base sólida de testes de unidade no domínio é onde o tempo rende mais.
SOLID, Clean Code e tempo para testar
Um código bom é um código testável. Eu sempre repito essa frase e, sempre que crio algo, penso em como vou testá-lo e se está fácil de entender.
SOLID e Clean Code são assuntos muito discutidos na comunidade, com muitos artigos e até cursos aqui no balta.io. Mas será que são realmente usados no dia a dia?
Quanto mais tempo você investe escrevendo código com esses princípios, mais fáceis ficam os testes de unidade. Dessa forma, o tempo para testar diminui, porque cada classe tem uma responsabilidade clara.
A própria documentação da Microsoft cita esse efeito ao listar o código menos acoplado como benefício dos testes. Afinal, um código muito acoplado é difícil de testar, então escrever testes força o desacoplamento.
Se quiser revisar os princípios, leia o artigo sobre os princípios SOLID e o de Clean Code. Além disso, o texto sobre o Dependency Inversion Principle aprofunda o ponto mais ligado à testabilidade.
Se você não se preocupa em escrever um bom código, você nunca terá tempo para testar.
A propósito, se existe um curso mandatório para todo dev, é o 1975: Modelando Domínios Ricos, que está gratuito no balta.io.
Regras de negócio no domínio
Quantas vezes você já ouviu que a regra de negócio deve ficar na aplicação, mais especificamente no domínio?
Mas por quê? Qual é o real motivo dessa afirmação?
O negócio é o pilar da empresa e o motivo pelo qual o software existe. Da mesma forma, o domínio é o pilar do software, o coração dele, então as regras ficam ali.
Parte dessa afirmação está correta. No entanto, a meu ver, trazemos as regras para a aplicação porque a orientação a objetos é mais flexível que um MER e o C# é mais poderoso que o T-SQL.
E, principalmente, porque podemos testar unitariamente nossas entidades e regras. Uma procedure no banco é difícil de isolar, enquanto um método de uma entidade roda em um teste de milissegundos.
Se você faz todo o esforço para levar as regras ao domínio e não escreve testes de unidade, perde a melhor parte dessa mudança. O artigo Data Driven vs Domain Driven discute bem essa troca de abordagem.
Dependências externas roubam tempo para testar
Você provavelmente já ouviu que o domínio não deve ter dependências externas, apenas do CLR, e que deve ser o mais puro possível.
As dependências externas, além de trazer acoplamento ao domínio, também dificultam os testes. Lembre-se: um bom código é um código testável.
Use sempre interfaces, dependa de abstrações e não de implementações. Assim, você mantém o domínio livre de dependências externas e reduz o tempo para testar cada regra.
Se você precisa de informações que vêm do banco de dados ou de serviços externos, receba-as na assinatura dos métodos. Não é problema do domínio resolver isso.
Afinal, o domínio não precisa saber de onde vem a taxa XYZ. Ele só precisa saber como usá-la no cálculo.
Exemplo: a taxa na assinatura do método
Veja uma entidade que calcula o total de um pedido com uma taxa. Repare que ela não busca a taxa em lugar nenhum, apenas recebe o valor.
public class Order
{
public decimal Subtotal { get; private set; }
public decimal Total { get; private set; }
public Order(decimal subtotal)
{
Subtotal = subtotal;
Total = subtotal;
}
// A taxa chega pronta: o domínio não sabe se veio do banco ou de uma API
public void ApplyFee(decimal feeRate)
{
if (feeRate < 0)
return;
Total = Subtotal + (Subtotal * feeRate);
}
}
Como a entidade não depende de banco, de HTTP nem de arquivo, o teste fica direto. Com isso, o tempo para testar essa regra cai para poucos minutos.
public class OrderTests
{
[Fact]
public void ApplyFee_ValidRate_IncreasesTotal()
{
// Arrange
var order = new Order(100m);
// Act
order.ApplyFee(0.1m);
// Assert
Assert.Equal(110m, order.Total);
}
[Fact]
public void ApplyFee_NegativeRate_KeepsTotal()
{
// Arrange
var order = new Order(100m);
// Act
order.ApplyFee(-0.1m);
// Assert
Assert.Equal(100m, order.Total);
}
}
Os nomes seguem o padrão recomendado pela Microsoft: método, cenário e comportamento esperado. Além disso, o corpo segue o formato Arrange, Act e Assert, o que transforma o teste em documentação executável.
Se a taxa viesse de dentro da entidade, por meio de uma chamada ao banco, você precisaria de mocks, configuração e talvez um banco de testes. Ou seja, o mesmo teste custaria muito mais.
O mesmo raciocínio vale para data e hora, que também são dependências externas disfarçadas. O artigo sobre como testar data e hora no .NET mostra essa técnica na prática.
Códigos testáveis
Se você chegou até aqui, já tem insumo suficiente para escrever um código testável, usando SOLID, Clean Code e afins.
Mas o principal ponto na escrita de um código testável é pensar em como ele será testado antes de escrevê-lo. Além disso, você precisa calcular o tempo de escrita e de execução dos testes junto com a criação do código.
Estime o tempo para testar junto com a tarefa
Sei que muitas vezes deixamos essa estimativa de lado, mas ela é importante. Embora os conceitos anteriores facilitem os testes, ainda leva tempo escrever bons testes de unidade.
Então não deixe de lado a estimativa para escrever bons testes. Leve em conta o tempo para testar, refatorar os testes e tudo mais que envolve a tarefa.
Na prática, o teste faz parte da tarefa, e não é uma tarefa separada. Uma funcionalidade sem teste ainda não está pronta, mesmo que funcione na sua máquina.
A documentação da Microsoft resume as características de um bom teste de unidade. Ele deve ser rápido, isolado, repetível, autoverificável e escrito no momento certo.
O último item é importante aqui. Segundo o texto, escrever um teste não deve levar um tempo desproporcional em relação ao código testado; caso leve, vale repensar o design.
Também não persiga números a qualquer custo. A mesma documentação lembra que um percentual alto de cobertura não garante código de qualidade, e metas muito altas podem trazer retorno decrescente.
Para ir além, o curso abaixo mostra como refatorar código existente para torná-lo testável.
Curso 7182: Refatorando para Testes de Unidade
Se quiser revisar a base conceitual antes, o artigo sobre fundamentos dos testes de unidade é um bom ponto de partida.
Cultura de testes cria tempo para testar
A diferença entre os testes serem uma cultura na sua empresa ou apenas tarefas é simples. A cultura sempre será levada a sério.
Cultura é aquilo que está no sangue e que se mantém em qualquer situação. Um time com uma forte cultura de testes entende sua importância e não deixa uma entrega sem testes de unidade, mesmo com o prazo apertado ou já estourado.
Implementar uma cultura de testes também não é um trabalho fácil. Todos os envolvidos precisam conhecer as vantagens e, principalmente, acreditar que a prática funciona.
Se você não acredita que testes de unidade são importantes, sempre terá uma desculpa para não escrevê-los. Nesse caso, nunca vai faltar motivo para cortar o tempo para testar.
Comece pelo que mais dói
Uma dica é começar testando o mais importante. Por exemplo, algo que envolva o financeiro, um cálculo crítico ou qualquer regra que não pode falhar.
Assim, o time percebe o valor dos testes logo nas primeiras semanas. Afinal, é justamente nessas áreas que um bug custa mais caro.
De fato, às vezes é complicado enxergar a necessidade dos testes. Por isso, faço uma analogia com o seguro do carro.
Enquanto você não tem o carro roubado nem bate, parece que todo o dinheiro investido no seguro não tem valor algum. Porém, no dia do problema, ele paga todo o investimento.
Com os testes acontece o mesmo. Imagine que você modifica uma linha no contexto A e um teste do contexto B, que não tem nada a ver com a mudança, falha.
Nesse momento, todos percebem a importância de uma boa cobertura de testes. Além disso, ninguém mais questiona o tempo para testar que entrou na estimativa.
Sinais de que o time está cortando tempo para testar
Alguns sinais indicam que os testes viraram tarefa e não cultura. O primeiro é a frase "depois a gente testa" aparecer em toda reunião de planejamento.
Outro sinal é a suíte de testes quebrada há dias sem que ninguém se incomode. Da mesma forma, testes ignorados ou comentados para o build passar mostram que o tempo para testar foi sacrificado.
Também desconfie quando os bugs chegam sempre pelo cliente, e não pela pipeline. Nesse caso, a pipeline existe, mas não protege o que importa.
Por fim, observe se o code review cobra testes. Quando ninguém pergunta onde está o teste de uma regra nova, a cultura ainda não se formou.
Para medir se a suíte realmente pega erros, uma técnica interessante é o teste de mutação. O artigo sobre testes de mutação com Stryker no .NET explica como aplicar a ideia.
Conclusão
Quase tudo pode ser testado, e os testes não tomam tanto tempo quanto parece. Talvez você esteja focando em algo que consome mais tempo do que deveria, como complexidade em CRUDs sem regra.
Foque sempre no seu domínio e escreva códigos legíveis e testáveis. Dessa forma, será muito fácil testá-los unitariamente e o tempo para testar deixa de ser um problema.
Lidere uma cultura de testes no seu ambiente de trabalho. Seja você a mudança que espera que aconteça.
FAQ
Testes de unidade atrasam a entrega?
No curto prazo, eles exigem algumas horas a mais na estimativa. No médio prazo, reduzem testes manuais, regressões e retrabalho.
Preciso testar CRUDs simples?
Cadastros sem regra de negócio raramente justificam testes de unidade. Concentre o esforço nas entidades e regras do domínio.
Qual cobertura de testes devo buscar?
Não existe um número mágico. A Microsoft alerta que cobertura alta não garante qualidade, então priorize as regras críticas.
Como começar uma cultura de testes no time?
Comece testando a área mais crítica, como cálculos financeiros. Depois, inclua o teste na definição de pronto e cobre isso no code review.
Por que dependências externas dificultam os testes?
Elas exigem banco, rede ou arquivos durante o teste, o que deixa tudo lento e instável. Receber os valores por parâmetro ou por interfaces resolve isso.