I have been tasked with making a small change to a codebase with no tests and am trying to implement the change using TDD for the first time but am struggling with the difference between testing implementation and behaviour. The system I am working on renders user-configurable list items. User can make changes to a list items behaviour through a global preference system, controlled by an XML DAO:
class Preference {
boolean getX();
boolean getY();
}
I have been tasked with adding Z to this DAO, I have implemented this change and added tests to ensure the value serialises and deserialises as I would expect.
The following list item class should then receive the Z value:
class ListItemA extends ListItem {
private boolean Z;
ListItemA(boolean Z){
this.Z = Z;
}
OpenAction getOpenAction(){
OpenAction action = new OpenAction();
if(Z){
action.addValue('Filter', true);
}
return action;
}
}
I have also written tests to ensure that the action returned reflects the Z field. The connection between these two is where I am struggling, there is a controller class that is called when rendering the list items:
class Controller {
private Preference preference;
Controller(Preference preference){
this.preference = preference;
}
List<ListItem> getListItems(State state){
// a bunch condition statements concerning the state
List<ListItem> items = new List();
if(state.items.includes('ListItemA')){
ListItemA item = new ListItemA();
}
return items;
}
}
When instantiate ListItemA, I now need to pass getZ() from the preference field but TDD says that I cannot make this change without a failing test first.
I could mock the preference, mock the state, generate the list and then write the same test to ensure the OpenAction reflects the value of Z but this seems as though I am testing implementation and writing a brittle test. In addition there would be a lot of duplicated test code from the ListItem test as they are making the same assertion. Another alternative would be to scrap the ListItem test and add all the assertions in the Controller test but this seems like I am testing another system.
My questions are:
How do I draw the line between testing implementation and writing tests that are too broad?
How would you approach testing this situation?
Am I overtesting?
I am approaching changes to this legacy system through TDD, as per the book I have just finished 'Working Effectively in Legacy Code', does the method described above match the process I should be undertaking when approaching a system like this?
Are there any good articles, books or resources I can read up on to further my understanding of this area?
How do I go about testing a legacy system in a workplace with a no-testing culture? I don't have any legacy systems of my own to work on, how do I gain experience?
How do I draw the line between testing implementation and writing tests that are too broad?
Broad tests tend to be most problematic when they span multiple unstable behaviors - each time one of those behaviors change, so too do the tests. That's not usually a problem in a single editing session - because even unstable behaviors tend to be "stable" in a short enough time period.
If the legacy code you are working with has its change history captured (ex: a source control system), then you can look to see which parts of the code have been stable, and assume that the heuristic "yesterday's weather" will hold.
I could mock the preference, mock the state,
You could do that, but it probably isn't your best starting point. Mocks work very well for testing certain kinds of designs -- particularly protocols -- but the benefits you get when testing protocols do not necessarily transfer to other styles.
If the test subject has no dependencies on shared mutable state, there are other techniques that you can use to produce finer grained tests without introducing mocks.
How do I go about testing a legacy system in a workplace with a no-testing culture?
In the worst case? write your tests, implement your changes, publish your changes and discard the tests. Concentrate your attention on delivering high quality code that could be tested, and defer the political conflict until later.