Tudo tem uma interface. Quando visto meu chapéu de teste, uso uma visão de mundo específica para escrever um teste:
- Se algo existe, pode ser medido.
- Se não puder ser medido, não importa. Se isso importa, eu ainda não encontrei uma maneira de medir isso ainda.
- Os requisitos prescrevem propriedades mensuráveis ou são inúteis.
- Um sistema atende a um requisito quando faz a transição de um estado não esperado para o estado esperado prescrito pelo requisito.
- Um sistema consiste em componentes em interação, que podem ser subsistemas. Um sistema está correto quando todos os componentes estão corretos e a interação entre os componentes está correta.
No seu caso, seu sistema possui três partes principais:
- algum tipo de dados ou imagens, que podem ser inicializados a partir de arquivos
- um mecanismo para exibir os dados
- um mecanismo para modificar os dados
Aliás, isso me parece muito com a arquitetura original do Model-View-Controller. Idealmente, esses três elementos exibem acoplamentos frouxos - ou seja, você define limites claros entre eles com interfaces bem definidas (e, portanto, bem testáveis).
Uma interação complexa com o software pode ser traduzida em pequenas etapas que podem ser formuladas em termos dos elementos do sistema que estamos testando. Por exemplo:
Carrego um arquivo com alguns dados. Ele exibe um gráfico. Quando arrasto um controle deslizante na interface do usuário, o gráfico fica instável.
Parece ser fácil testar manualmente e difícil testar automatizado. Mas vamos traduzir essa história para o nosso sistema:
- A interface do usuário fornece um mecanismo para abrir um arquivo: o controlador está correto.
- Quando abro um arquivo, o Controller emite um comando apropriado para o Model: a interação Controller-Model está correta.
- Dado um arquivo de teste, o modelo analisa isso na estrutura de dados esperada: o modelo está correto.
- Dada uma estrutura de dados de teste, o View renderiza a saída esperada: o View está correto. Algumas estruturas de dados de teste serão gráficos normais, outras serão gráficos instáveis.
- A interação View – Model está correta
- A interface do usuário fornece um controle deslizante para deixar o gráfico instável: o controlador está correto.
- Quando o controle deslizante é definido com um valor específico, o Controlador emite o comando esperado para o Modelo: a interação Controlador-Modelo está correta.
- Ao receber um comando de teste referente à oscilação, o Modelo transforma uma estrutura de dados de teste na estrutura de dados do resultado esperado.
Agrupados por componente, terminamos com as seguintes propriedades para testar:
- Modelo:
- analisa arquivos
- responde ao comando de abertura de arquivo
- fornece acesso aos dados
- responde ao comando vacilante
- Visão:
- Controlador:
- fornece fluxo de trabalho aberto de arquivo
- emite o comando abrir arquivo
- fornece fluxo de trabalho instável
- emite comando instável
- todo sistema:
- a conexão entre os componentes está correta.
Se não decompormos o problema do teste em subtestes menores, o teste se tornará realmente difícil e realmente frágil. A história acima também pode ser implementada como "quando eu carrego um arquivo específico e defino o controle deslizante para um valor específico, uma imagem específica é renderizada". Isso é frágil, pois quebra quando qualquer elemento do sistema é alterado.
- Ele é interrompido quando altero os controles de oscilação (por exemplo, alças no gráfico em vez de um controle deslizante no painel de controle).
- Ele é interrompido quando altero o formato de saída (por exemplo, o bitmap renderizado é diferente porque alterei a cor padrão do gráfico ou porque adicionei anti-aliasing para tornar o gráfico mais suave. Observe isso nos dois casos).
Os testes granulares também têm a grande vantagem de me permitir evoluir o sistema sem medo de quebrar nenhum recurso. Como todo o comportamento necessário é medido por um conjunto de testes completo, os testes serão avisados caso algo pare. Como são granulares, eles me indicarão a área problemática. Por exemplo, se eu acidentalmente alterar a interface de qualquer componente, apenas os testes dessa interface falharão e nenhum outro teste que use indiretamente essa interface.
Se o teste é fácil, isso requer um design adequado. Por exemplo, é problemático quando conecto os componentes em um sistema: se eu quiser testar a interação de um componente com outros componentes em um sistema, preciso substituir esses outros componentes por stubs de teste que permitam registrar, verificar, e coreografar essa interação. Em outras palavras, preciso de algum mecanismo de injeção de dependência e as dependências estáticas devem ser evitadas. Ao testar uma interface do usuário, é uma grande ajuda quando essa interface do usuário é programável.
Naturalmente, a maior parte disso é apenas uma fantasia de um mundo ideal, onde tudo é dissociado e facilmente testável e os unicórnios voadores espalham amor e paz ;-) Embora qualquer coisa seja fundamentalmente testável, muitas vezes é proibitivamente difícil fazê-lo, e é melhor usos do seu tempo. No entanto, os sistemas podem ser projetados para serem testados e, normalmente, até os sistemas independentes de teste apresentam APIs internas ou contratos que podem ser testados (caso contrário, aposto que sua arquitetura é uma porcaria e você escreveu uma grande bola de barro). Na minha experiência, mesmo pequenas quantidades de teste (automatizado) afetam significativamente a qualidade.