Here my approach. It has a cost in terms of time because it's a refactor-test in 4 phases.
What I'm going to expose may suite better in components with more complexity than the one exposed in the question's example.
Anyways the strategy is valid for any component candidate to be normalized by an interface (DAO, Services, Controllers,...).
1. The interface
Lets gather all public methods from MyDocumentService and lets put them all together into a interface. For instance. If it does exist already, use that one instead of setting any new one.
public interface DocumentService {
List<Document> getAllDocuments();
//more methods here...
}
Then we force MyDocumentService to implement this new interface.
So far so good. No major changes were made, we respected the current contract and behaivos remains untouched.
public class MyDocumentService implements DocumentService {
@Override
public List<Document> getAllDocuments(){
//legacy code here as it is.
// with no changes ...
}
}
2. Unit test of legacy code
Here we have the hard work. To set up a test suite. We should set as many cases as possible: successful cases and also error cases. These last are for the good of the result's quality.
Now, instead of testing MyDocumentService we are going to use the interface as the contract to be tested.
Im not going to go into details, so forgive me If my code looks too simple or too agnostic
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
}
}
This stage takes longer than any other in this approach. And it's the most important because it will set the point of reference for future comparisions.
Note: Due to no major changes were made and behaivor remains untouched. I suggest to do a tag here into the SCM. Tag or branch doesn't matter. Just do a version.
We want it for rollbacks, versions comparisions and may be for parallel executions of the old code and the new one.
3. Refactoring
Refactor is going to be implemented into a new component. We wont do any change on the existing code.
The first step is as easy as to do copy&paste of MyDocumentService and rename it to CustomDocumentService (for example).
New class keep implementing DocumentService. Then go and refactorize getAllDocuments(). (Lets start by one. Pin-refactors)
It may require some changes on DAO's interface/methods. If so, don't change existing code. Implement your own method in DAO interface. Annotate old code as Deprecated and you will know later on what should be removed.
It's important to don't break/change existing implementation. We want to execute both services in parallel and then compare results.
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. Updateing DocumentServiceTestSuite
Ok, now the easier part. To add the tests of the new component.
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);
}
}
Now we have oldResult and newResult both validated independently but we can also compare with each other. This last validation is optional and it is dependent to the result. May be it's not comparable.
May not make too much seense to compare two collections in this way, but would be valid for any other kind of object (pojos, data model entities, DTOs, Wrappers, native types...)
Notes
I would not dare to tell how to do unit tests or how to use mock libs. I don't dare neither to say how you have to do the refactor. What I wanted to do is to suggest a global strategy. How to take it forward depends on you. You know exactly how code is, its complexity and if such strategy worth a try. Facts like time and resources matters here. Also matters what do you expect from these tests in the future.
I have started my examples by a Service and I would follow with DAO and so on. Going deep into dependency levels. More or less it could be described as up-bottom strategy. However for minor changes/refactors (like the one exposed in tour example), a bottom up would do the task easier. Because the scope of the changes is little.
Finally, it is up to you to remove deprecated code and to redirect old dependencies to the new one.
Remove also deprecated tests and job is done. If you versioned the old solution with its tests, you can check and compare each other any time.
In consequence of so many work, you have legacy code tested, validated and versioned. And new code, tested, validated and ready to be versioned.