In this Learn Series I am going to write about the Sprout Method which is in the book “Working Effectively with Legacy Code by Michael Feathers“, all I have to say is what a great book if you don’t have it, you should pick it up asap.
Sprout Method is a technique that one can use when one has to write a new feature into a system. The code will be formulated as new code, and you want to put the new code into a new method and call it where the new functionality needs to be.
The reason for Sprout method is that sometimes you cannot write unit test for the code that you want to change but you can use Test Driven Development to write unit test for the new code, so that at least the new code would be tested.
Let look at some code to see what that means.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class FileUpdateManager { public void AppendFilesWithText(IList<string> filePath, string textToAppend) { foreach (var file in filePath) { if (File.Exists(file) && string.IsNullOrEmpty(textToAppend)) File.AppendAllText(file, textToAppend); } NotifyThatFilesHaveChangedByEmail(filePath); } } |
The code looks simple and now a change request has come in, the clients is requesting that they want to be notified of unique files names that would change. Not every file since there could be other files with same name but in different directory, and what is important is only one of them should be notified. Which make sense, since you don’t want to receive multiple emails on the same name files.
Your first instinct would be to call the method and pass in unique files, but you cannot do that since the call is being used somewhere that is out of your control or is ghetto code calling your code, code that no one wants to touch or change due to XYZ reason, so you need to think back and code into the method. Let see how we can change it.
1 2 3 4 5 6 7 8 9 10 11 12 |
public void AppendFilesWithText(IList<string> filePath, string textToAppend) { filePath = filePath.Select(x => x).Distinct(); foreach (var file in filePath) { if (File.Exists(file) && string.IsNullOrEmpty(textToAppend)) File.AppendAllText(file, textToAppend); } NotifyThatFilesHaveChanged(filePath); } |
We have added a LINQ query to find out the distinct elements but how do we know that our LINQ was correct or is doing the right thing. And we are making this method do two things, filtering and then appending.
So what we do is move that call to another method and write test against it. I have left out the test code just to make things shorter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public void AppendFilesWithText(IList<string> filePath, string textToAppend) { filePath = GetUniqueFilePath(filePath); foreach (var file in filePath) { if (File.Exists(file) && string.IsNullOrEmpty(textToAppend)) File.AppendAllText(file, textToAppend); } NotifyThatFilesHaveChanged(filePath); } public IList<string> GetUniqueFilePath(IList<string> filePath) { filePath = filePath.Select(x => x).Distinct(); return filePath; } } |
So here it is the Sprout Method and the advantages of it is you got your new code under test, the disadvantages is that you are not working on getting the entire code in test due to hard to break dependency or other reason that you may have and you might just not get it under test at all.
I would suggest the Sprout Method if you are starting TDD and would like to write little parts to get it under test and go from there at least you would get your feet wet.
All finally the methods as listed out in “Working Effectively with Legacy Code” for Sprout Method are:
- Identify where you need to make your code change.
- If the change can be formulated as a single sequence of statements in one place in a method, write down a call for a new method that will do the work involved and then comment it out. (I like to do this before I even write the method so that I can get a sense of what the method call will look like in context.)
- Determine what local variables you need from the source method, and make them arguments to the call.
- Determine whether the sprouted method will need to return values to source method. If so, change the call so that its return value is assigned to a variable.
- Develop the sprout method using test-driven development.
- Remove the comment in the source method to enable the call.
Concise and great explanation of Sprout method. Spotted something in the sample code which may not be logically correct-
string.IsNullOrEmpty(textToAppend) should be negated. Though it doesn’t change the message of this great post anyway.
Base on the example, GetUniqueFilePath method should be protected or private. In this case, we couldn’t write the tests for GetUniqueFilePath method. How can we apply TDD for protected or private methods?
In TDD you are usually testing the interface to the class rather than the internal private method. you may want to change the implementation of GetUniqueFilePath to GetUniquePathFromDatabase, but you code should still work since you are relying on the contract to the object. Hope that makes sense, let me know if you have more questions.