Eu li algumas respostas para perguntas de uma linha semelhante, como "Como você mantém seus testes de unidade funcionando ao refatorar?". No meu caso, o cenário é um pouco diferente, pois recebi um projeto para revisar e alinhar com alguns padrões que temos; atualmente, não há testes para o projeto!
Eu identifiquei várias coisas que acho que poderiam ter sido feitas melhor, como NÃO misturar código de tipo DAO em uma camada de serviço.
Antes de refatorar, parecia uma boa ideia escrever testes para o código existente. O problema que me parece é que, quando refatorar, esses testes serão interrompidos à medida que vou mudando onde certas lógicas são feitas e os testes serão escritos com a estrutura anterior em mente (dependências simuladas etc.)
No meu caso, qual seria a melhor maneira de proceder? Estou tentado a escrever os testes em torno do código refatorado, mas sei que existe o risco de refatorar coisas incorretamente que podem alterar o comportamento desejado.
Seja um refator ou um novo design, fico feliz em corrigir esses termos. Atualmente, estou trabalhando na seguinte definição para refatoração "Com a refatoração, por definição, você não altera o que o software faz, você muda como faz. " Então, eu não estou mudando o que o software faz. Eu estaria mudando como / onde ele faz.
Da mesma forma, posso ver o argumento de que, se estou alterando a assinatura de métodos, isso pode ser considerado um redesenho.
Aqui está um breve exemplo
MyDocumentService.java
(atual)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.java
(refatorado / reprojetado qualquer que seja)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
fonte
Respostas:
Você está procurando testes que verifiquem regressões . isto é, quebrar algum comportamento existente. Começaria identificando em que nível esse comportamento permanecerá o mesmo, e que a interface que conduz esse comportamento permanecerá o mesmo, e começaria a fazer testes nesse ponto.
Agora você tem alguns testes que afirmam que o que você faz abaixo desse nível, seu comportamento permanece o mesmo.
Você está certo em questionar como os testes e o código podem permanecer sincronizados. Se sua interface com um componente permanecer a mesma, você poderá escrever um teste em torno disso e afirmar as mesmas condições para ambas as implementações (à medida que cria a nova implementação). Caso contrário, você deve aceitar que um teste para um componente redundante é um teste redundante.
fonte
A prática recomendada é começar a escrever "testes de pin-down" que testam o comportamento atual do código, possivelmente incluindo erros, mas sem exigir que você mergulhe na loucura de discernir se um determinado comportamento que viola os documentos de requisitos é um bug, solução alternativa para algo que você não conhece, ou representa uma mudança não documentada nos requisitos.
Faz mais sentido que esses testes de pinagem estejam em um nível alto, ou seja, integração em vez de testes de unidade, para que eles continuem trabalhando quando você começar a refatorar.
Mas algumas refatorações podem ser necessárias para tornar o código testável - apenas tome cuidado com as refatorações "seguras". Por exemplo, em quase todos os casos, os métodos privados podem ser tornados públicos sem quebrar nada.
fonte
Sugiro - se você ainda não leu - a leitura de Trabalhando efetivamente com código legado e refatoração - Melhorando o design do código existente .
Eu não necessariamente vejo isso como um problema: escreva os testes, altere a estrutura do seu código e ajuste a estrutura do teste também . Isso fornecerá feedback direto se sua nova estrutura é realmente melhor que a antiga, porque, se for, os testes ajustados serão mais fáceis de escrever (e, portanto, a alteração dos testes deve ser relativamente direta, diminuindo o risco de uma nova introdução). erro passar nos testes).
Além disso, como outros já escreveram: Não escreva testes muito detalhados (pelo menos não no começo). Tente permanecer em um alto nível de abstração (portanto, seus testes provavelmente serão melhor caracterizados como regressão ou até testes de integração).
fonte
Não escreva testes de unidade rigorosos, onde você zomba de todas as dependências. Algumas pessoas dirão que esses não são testes de unidade reais. Ignore-os. Esses testes são úteis, e é isso que importa.
Vejamos o seu exemplo:
Seu teste provavelmente se parece com isso:
Em vez de zombar do DocumentDao, zombe de suas dependências:
Agora, você pode mover a lógica de
MyDocumentService
paraDocumentDao
sem que os testes sejam interrompidos. Os testes mostrarão que a funcionalidade é a mesma (desde que você a tenha testado).fonte
Como você diz, se você mudar o comportamento, será uma transformação e não um refator. Em que nível você muda o comportamento é o que faz a diferença.
Se não houver testes formais no nível mais alto, tente encontrar um conjunto de requisitos que os clientes (código de chamada ou humanos) precisam manter o mesmo após a sua reformulação para que seu código seja considerado funcionando. Essa é a lista de casos de teste que você precisa implementar.
Para responder à sua pergunta sobre alterações nas implementações que exigem alterações nos casos de teste, sugiro que você dê uma olhada no TDD de Detroit (clássico) vs Londres (mockista). Martin Fowler fala sobre isso em seu ótimo artigo. Mocks não são stubs, mas muitas pessoas têm opiniões. Se você começar no nível mais alto, onde seus externos não podem mudar, e seguir o seu caminho, os requisitos deverão permanecer razoavelmente estáveis até que você chegue a um nível que realmente precise mudar.
Sem nenhum teste, isso será difícil, e você pode considerar executar clientes por caminhos de código duplo (e registrar as diferenças) até ter certeza de que seu novo código faz exatamente o que precisa.
fonte
Aqui está a minha abordagem. Tem um custo em termos de tempo, porque é um teste de refatoração em 4 fases.
O que vou expor pode se encaixar melhor em componentes com mais complexidade do que o exposto no exemplo da pergunta.
De qualquer forma, a estratégia é válida para que qualquer candidato a componente seja normalizado por uma interface (DAO, Serviços, Controladores, ...).
1. A interface
Vamos reunir todos os métodos públicos do MyDocumentService e vamos reuni-los em uma interface. Por exemplo. Se ele já existir, use esse em vez de definir outro novo .
Em seguida, forçamos o MyDocumentService a implementar essa nova interface.
Por enquanto, tudo bem. Não foram efetuadas grandes alterações, respeitamos o contrato atual e o comportamento permanece intocado.
2. Teste de unidade do código legado
Aqui temos o trabalho duro. Para configurar uma suíte de testes. Deveríamos definir o maior número possível de casos: casos bem-sucedidos e também casos de erro. Estes últimos são para o bem da qualidade do resultado.
Agora, em vez de testar o MyDocumentService , usaremos a interface como o contrato a ser testado.
Não vou entrar em detalhes, então me perdoe Se meu código parecer muito simples ou agnóstico
Esse estágio leva mais tempo do que qualquer outro nesta abordagem. E é o mais importante porque definirá o ponto de referência para futuras comparações.
Nota: Devido a nenhuma grande alteração, o comportamento permanece intocado. Sugiro fazer uma tag aqui no SCM. Tag ou ramo não importa. Basta fazer uma versão.
Nós queremos isso para reversões, comparações de versões e pode ser para execuções paralelas do código antigo e do novo.
3. Refatoração
O refator será implementado em um novo componente. Não faremos nenhuma alteração no código existente. A primeira etapa é tão fácil quanto copiar e colar MyDocumentService e renomeá-la para CustomDocumentService (por exemplo).
Nova classe continua implementando o DocumentService . Em seguida, refatorize getAllDocuments () . (Vamos começar por um. Refatores de pinos)
Pode exigir algumas alterações na interface / métodos do DAO. Nesse caso, não altere o código existente. Implemente seu próprio método na interface DAO. Anote o código antigo como Descontinuado e você saberá mais tarde o que deve ser removido.
É importante não quebrar / alterar a implementação existente. Queremos executar os dois serviços em paralelo e depois comparar os resultados.
4. Atualizando o DocumentServiceTestSuite
Ok, agora a parte mais fácil. Para adicionar os testes do novo componente.
Agora temos oldResult e newResult ambos validados de forma independente, mas também podemos comparar. Essa última validação é opcional e depende do resultado. Pode ser que não seja comparável.
Pode não fazer muito sentido comparar duas coleções dessa maneira, mas seria válido para qualquer outro tipo de objeto (pojos, entidades de modelo de dados, DTOs, Wrappers, tipos nativos ...)
Notas
Eu não ousaria dizer como fazer testes de unidade ou como usar bibliotecas simuladas. Não me atrevo nem a dizer como você deve refatorar. O que eu queria fazer é sugerir uma estratégia global. Como seguir adiante depende de você. Você sabe exatamente como é o código, sua complexidade e se essa estratégia vale a pena tentar. Fatos como tempo e recursos são importantes aqui. Também importa o que você espera desses testes no futuro.
Comecei meus exemplos por um Serviço e seguia com o DAO e assim por diante. Aprofundando nos níveis de dependência. Mais ou menos, poderia ser descrito como estratégia de baixo para cima . No entanto, para pequenas mudanças / refatores ( como o exposto no exemplo do tour ), um processo ascendente facilitaria a tarefa. Porque o escopo das mudanças é pequeno.
Por fim, cabe a você remover o código obsoleto e redirecionar as dependências antigas para a nova.
Remova também testes obsoletos e o trabalho está concluído. Se você testou a solução antiga com seus testes, é possível verificar e comparar a qualquer momento.
Em consequência de tantos trabalhos, você tem código legado testado, validado e com versão. E novo código, testado, validado e pronto para ser versionado.
fonte
tl; dr Não escreva testes de unidade. Escreva testes em um nível mais apropriado.
Dada sua definição de refatoração de trabalho:
existe um espectro muito amplo. Por um lado, há uma alteração independente de um método específico, talvez usando um algoritmo mais eficiente. Por outro lado, está migrando para outro idioma.
Qualquer que seja o nível de refatoração / reprojeto que esteja sendo executado, é importante ter testes que operem nesse nível ou acima.
Testes automatizados geralmente são classificados por nível como:
Testes unitários - Componentes individuais (classes, métodos)
Testes de integração - interações entre componentes
Testes do sistema - a aplicação completa
Escreva o nível de teste que pode suportar a refatoração essencialmente intocado.
Pensar:
fonte
Não perca tempo escrevendo testes que se conectam em pontos onde você pode antecipar que a interface mudará de maneira não trivial. Isso geralmente é um sinal de que você está tentando testar classes de natureza 'colaborativa' por natureza - cujo valor não está no que elas fazem, mas em como elas interagem com várias classes intimamente relacionadas para produzir um comportamento valioso. . É esse comportamento que você deseja testar, o que significa que deseja testar em um nível superior. Testar abaixo desse nível geralmente requer muita zombaria feia, e os testes resultantes podem ser mais um empecilho para o desenvolvimento do que um auxílio à defesa do comportamento.
Não fique muito preocupado se você está fazendo um refator, um novo design ou o que quer que seja. Você pode fazer alterações que, no nível inferior, constituam uma reformulação de vários componentes, mas que, em um nível de integração mais alto, isso equivale a um refator. O ponto é ter clareza sobre qual comportamento é de valor para você e defender esse comportamento à medida que avança.
Pode ser útil considerar ao escrever seus testes - eu poderia descrever facilmente para um controle de qualidade, proprietário de um produto ou usuário, o que esse teste está realmente testando? Se parecer que descrever o teste for muito esotérico e técnico, talvez você esteja testando no nível errado. Teste nos pontos / níveis que 'fazem sentido' e não agregue seu código com testes em todos os níveis.
fonte
Sua primeira tarefa é tentar criar a "assinatura do método ideal" para seus testes. Esforce-se para torná-lo uma função pura . Isso deve ser independente do código que está realmente sendo testado; é uma pequena camada adaptadora. Escreva seu código nesta camada do adaptador. Agora, quando você refatorar seu código, você precisará alterar apenas a camada do adaptador. Aqui está um exemplo simples:
Os testes são bons, mas o código em teste tem uma API ruim. Posso refatorá-lo sem alterar os testes simplesmente atualizando minha camada do adaptador:
Este exemplo parece uma coisa bastante óbvia a ser feita de acordo com o princípio Não se repita, mas pode não ser tão óbvio em outros casos. A vantagem vai além do DRY - a vantagem real é a dissociação dos testes do código sob teste.
Obviamente, essa técnica pode não ser aconselhável em todas as situações. Por exemplo, não haveria razão para escrever adaptadores para POCOs / POJOs porque eles realmente não têm uma API que pudesse mudar independentemente do código de teste. Além disso, se você estiver escrevendo um pequeno número de testes, uma camada adaptadora relativamente grande provavelmente seria um esforço desperdiçado.
fonte