(this post is adapted from my work-in-progress open source TDD tutorial)
(Note that I use "Statement" instead of "test" and "Specification" instead of "Test Suite" in this post)
This is the last installment of "how to start with a test" series. This time, we're gonna take on the last of the typical techniques, useful when there's already some existing code to fit in.
Start by invoking a method when you have one
Sometimes, we have to add a new class that implements an existing interface required by another class. The fact of implementing an interface imposes what methods should the new class support. If this point is already decided, we can start our Statement by first calling the method and then discovering what we need to supply.
A simple example
Suppose we have an application holding a lot of data that, among other things, handles importing am existing database from another instance of the application. As importing a database can be a lengthy process, a message box is displayed each time when user chooses to perform the import and this message box displays the following message: "Johnny, please sit down and enjoy your coffee for a few minutes as we take time to import your database" (given user name is Johnny). The class that implements it looks like this:
public class FriendlyMessages { public string HoldOnASecondWhileWeImportYourDatabase(string userName) { return string.Format("{0}, " + "please sit down and enjoy your coffee " + "for a few minutes as we take time " + "to import your database", userName); } }
Now, imagine that our management told us that they need to ship a trial version with some features disabled, including importing an existing database. Among all the things we need to do to make it happen, we also need to display a different string with message saying that this is a trial version and the feature is locked. We can do it by extracting an interface from the FriendlyMessages
class and using it to put in an instance of another class implementing this interface when the application discovers that it is being run as a trial version. The extracted interface looks like this:
public interface Messages { string HoldOnASecondWhileWeImportYourDatabase(string userName); }
So our new implementation is forced to support the HoldOnASecondWhileWeImportYourDatabase
method. Thus, we when implementing the class, we start with the following:
public class TrialVersionMessages : Messages { public string HoldOnASecondWhileWeImportYourDatabase(string userName) { throw new NotImplementedException(); } }
Now, we're ready to start writing a Statement. Assuming we don't know where to start, we just start with creating an object and invoking the method that needs to be implemented:
[Fact] public void TODO() { //GIVEN var trialMessages = new TrialVersionMessages(); //WHEN trialMessages.HoldOnASecondWhileWeImportYourDatabase(); //THEN Assert.True(false); //to remember about it }
As you can see, we added an assertion that always fails at the end because we don't have any assertions yet and the Statement would otherwise be already evaluated as true and we'd rather have it remind ourselves that it is not finished. Other than this, the Statement does not compile anyway, because the method HoldOnASecondWhileWeImportYourDatabase
takes a string argument and we passed none. This makes us ask the question what is this argument and what's its role in the behavior triggered by the HoldOnASecondWhileWeImportYourDatabase
method. Seems like it is a user name and we want it to be somewhere in the result of the method. Thus, we can add it to the Statement like this:
[Fact] public void TODO() { //GIVEN var trialMessages = new TrialVersionMessages(); var userName = Any.String(); //WHEN trialMessages. HoldOnASecondWhileWeImportYourDatabase(userName); //THEN Assert.True(false); //to remember about it }
Now, this compiles but is evaluated as false because of the guard assertion that we put at the end. Our goal is to substitute it with a real assertion for a real expected result. The return value of the HoldOnASecondWhileWeImportYourDatabase
is a string message, so all we need to do is to come up with the message that we expect in case of trial version:
[Fact] public void TODO() { //GIVEN var trialMessages = new TrialVersionMessages(); var userName = Any.String(); //WHEN var message = trialMessages. HoldOnASecondWhileWeImportYourDatabase(userName); //THEN var expectedMessage = string.Format("{0}, better get some pocket money!", userName); Assert.Equal(expectedMessage, message); }
and all that is left is to find a good name for the Statement. This isn't an issue since we already specified the desired behavior in the code, so we can just summarize it as something like ShouldYieldAMessageSayingThatFeatureIsLockedWhenAskedForImportDatabaseMessage
.