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 .
public interface DocumentService {
List<Document> getAllDocuments();
//more methods here...
}
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.
public class MyDocumentService implements DocumentService {
@Override
public List<Document> getAllDocuments(){
//legacy code here as it is.
// with no changes ...
}
}
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
public class DocumentServiceTestSuite {
@Mock
MyDependencyA mockDepA;
@Mock
MyDependencyB mockDepB;
//... More mocks
DocumentService service;
@Before
public void initService(){
service = MyDocumentService(mockDepA, mockDepB);
//this is purposed way to inject
//dependencies. Replace it with one you like more.
}
@Test
public void getAllDocumentsOK(){
// here I mock depA and depB
// wanted behaivors...
List<Document> result = service.getAllDocuments();
Assert.assertX(result);
Assert.assertY(result);
//... As many you think appropiate
}
}
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.
public class CustomDocumentService implements DocumentService {
@Override
public List<Document> getAllDocuments(){
//new code here ...
//due to im refactoring service
//I do the less changes possible on its dependencies (DAO).
//these changes will come later
//and they will have their own tests
}
}
4. Atualizando o DocumentServiceTestSuite
Ok, agora a parte mais fácil. Para adicionar os testes do novo componente.
public class DocumentServiceTestSuite {
@Mock
MyDependencyA mockDepA;
@Mock
MyDependencyB mockDepB;
DocumentService service;
DocumentService customService;
@Before
public void initService(){
service = MyDocumentService(mockDepA, mockDepB);
customService = CustomDocumentService(mockDepA, mockDepB);
// this is purposed way to inject
//dependencies. Replace it with the one you like more
}
@Test
public void getAllDocumentsOK(){
// here I mock depA and depB
// wanted behaivors...
List<Document> oldResult = service.getAllDocuments();
Assert.assertX(oldResult);
Assert.assertY(oldResult);
//... As many you think appropiate
List<Document> newResult = customService.getAllDocuments();
Assert.assertX(newResult);
Assert.assertY(newResult);
//... The very same made to oldResult
//this is optional
Assert.assertEquals(oldResult,newResult);
}
}
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.