Test driven development is hard. Perhaps it would not be if we were taught to think about OO development from a TDD perspective in the first place; but those muscles are poorly developed, and the exercise leaves you sore and panting a bit. As with physical exercise, there is a reward in the pain. Perhaps others do not see it, but I can already see the benefits accruing in Reggie as I rebuild it with SOLID principles in mind, driven by tests. To help me consolidate where I’m going, and help others whose TDD muscles are likewise under-developed, let us walk through a test, shall we?
First, some context. I’m working on adding persistence to the application: ability to save and re-open session data. I have a ViewModel, called ReggieBasicViewModel
, which initially contains the data to persist and which binds the View to my business logic. The ViewModel is being instantiated with a factory object, which allows the ViewModel to build concrete instances of various dependencies. This illustrates the Abstract Factory pattern, and the Open-Closed Principle, but arguably violates Single Responsibility Principle [same link as OCP] by grouping un-related functionality into the factory. The proper factory object is configured in the application’s bootstrapper class, or or it is setup in a unit test using an alternate factory implementation.
I’d like to save to / retrieve from an XML file. But what if my requirements change in a few days? I’m told to save to a database, or a web service. It would not be wise to design for that – but I can easily make the system flexible enough to handle addition of other types of persistence in the future. So I create an interface in my business layer, called ISessionPersistence
. As you can see, I’ve added a factory method to the IHelperFactory
, for building an instance of ISessionPersistence
.
[TestMethod]
public void SaveSessionLoadsSessionIntoPersistenceService()
{
// Prepare Input
string sampleText = "Reggie";
string regularExpressionPattern = "^(Reggie)$";
// Setup mocks
Mock mockSession = m_mockFactory.Create<IReggieSession>();
mockSession.SetupSet(ms => ms.RegularExpressionPattern =
It.Is<string>(x => x == regularExpressionPattern));
mockSession.SetupGet(ms => ms.RegularExpressionPattern)
.Returns(regularExpressionPattern);
mockSession.SetupSet(ms => ms.SampleText =
It.Is<string>(x => x == sampleText));
mockSession.SetupGet(ms => ms.SampleText).Returns(sampleText);
m_helperFactory.Setup(hf => hf.BuildReggieSession())
.Returns(mockSession.Object);
m_persistence.Setup(p => p.Save(It.Is<IReggieSession>(x =>
x.SampleText == sampleText
&& x.RegularExpressionPattern == regularExpressionPattern)));
// Call the system under test
m_systemUnderTest.SampleText = sampleText;
m_systemUnderTest.RegularExpressionPattern = regularExpressionPattern;
m_systemUnderTest.SaveSession();
// Evaluate results
m_persistence.Verify(p => p.Save(It.IsAny<IReggieSession>()), Times.Once());
}
Let’s step through that…
// Setup mocks
Mock<IReggieSession> mockSession = m_mockFactory.Create<IReggieSession>();
Use MoQ to create a mockup of an IReggieSession
. I forgot to mention this: it is a small interface for holding the Sample Text and Regular Expression Pattern that will be saved.
mockSession.SetupSet(ms => ms.RegularExpressionPattern = It.Is<string>(x => x == regularExpressionPattern));
mockSession.SetupGet(ms => ms.RegularExpressionPattern).Returns(regularExpressionPattern);
mockSession.SetupSet(ms => ms.SampleText = It.Is<string>(x => x == sampleText));
mockSession.SetupGet(ms => ms.SampleText).Returns(sampleText);
Now add some meat to that mock object by setting up the Get and Set for the two properties, RegularExpressionPattern
and SampleText
:
- The SetupSet line can be read this way: RegularExpressionPattern_Set is allowed to be called with a value matching the regularExpressionPattern variable.
- The SetupGet can can be read as saying: RegularExpressionPattern_Get will return the value of regularExpressionPattern.
- Etc.
m_helperFactory.Setup(hf => hf.BuildReggieSession())
.Returns(mockSession.Object);
When the factory’s BuildReggieSession method is called, return the mock Session object.
m_persistence.Setup(p => p.Save(It.Is<IReggieSession>(x => x.SampleText == sampleText
&& x.RegularExpressionPattern == regularExpressionPattern)));
Create a mock persistence engine – this helps us remember that the method we’re implementing does not care how the session is persisted! It will simply use whatever method is configured in the factory, which in turn was configured by the main application’s bootstrapper class and passed into the ViewModel’s constructor. FYI, the factory and persistence mocks were instantiated with Strict behavior in the test fixture’s TestInitialize method, so these are member variables for the class and the test will fail if any method is called that has not be explicitly setup. Read this line thusly: setup the persistence mock so that the Save command is allowed to be called with a IReggieSession
whose SampleText
has the value of sampleText and RegularExpressionPattern
has the value of “regularExpressionPattern”.
// Call the system under test
m_systemUnderTest.SampleText = sampleText;
m_systemUnderTest.RegularExpressionPattern = regularExpressionPattern;
m_systemUnderTest.SaveSession();
This is clear enough. The system under test is an instance of ReggieViewModel
, that was constructed in the TestInitialize
method. Normally I would put the construction directly in the test, but in this test fixture, all of the tests need to be initialized in the same way. Moving that to TestInitialize
reduces redundant code.
// Evaluate results
m_persistence.Verify(p => p.Save(It.IsAny<IReggieSession>()), Times.Once());
This line verifies that the Save method was called exactly one time, with any IReggieSession
object. The exact object values do not matter, because I already configured the test to accept the particular values in the local variables.
Run the test. It fails. Great! Fill in the method body. It does not matter that I do not have a concrete version of ISessionPersistence
yet, since I am programming against the interface. Now the test passes! The next step is clear: write a test for a concrete persistence class that saves sessions to XML. This will, by definition, be an integration test, writing an actual file. That is, unless I add another layer of abstraction: wrap the XML serialize/deserialize in a reusable custom class, and call that custom class from the persistence class.
/// Save the current input values for future use.
///
public void SaveSession()
{
var session = m_helperFactory.BuildReggieSession();
session.SampleText = this.SampleText;
session.RegularExpressionPattern = this.RegularExpressionPattern;
m_persistence.Save(session);
}
Posted with : Microsoft .NET Framework, Software Testing