Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

Loosely Couple Your Tests

, 16 May 2013 CPOL
Loosely couple your tests from your implementation to allow your implementation to be refactored, without having to change the tests
looselycouple.zip
Take-3.jpg
Take-1.jpg
Take-2.jpg
<p>
One of the most common set of tests that I see is the upfront naming of one fixture for one class.&nbsp; The difficulty occurs when later on the classes become refactored into other classes and you&#39;re left with fixtures named after classes that no longer exist.&nbsp; Perhaps more insidious though is that by having this coupling, you increase the amount of inertia that must be overcome in order to refactor a class.&nbsp; In order to maintain a perceived consistency, you have to rename the test fixtures.&nbsp; Even worse, many current source code implementations refuse to play happily with renaming of files, so the refactoring tools are less effective, and the resistance to change increases again.&nbsp; By decoupling the tests from the implementation, the tests can stand on their own, and the implementation is free to be refactored as needed. 
</p>
<p>
Consider a very simple implementation of a Blog Engine.&nbsp; The story is as follows...<br />
<em>A blog consists of a number of entries.&nbsp; Each entry must have a title and content.&nbsp; Each entry gets a date created which refers to when the entry was initially drafted, and the date posted which indicates when the entry was published.&nbsp; An entry can be created and added to the blog without being posted - a draft.&nbsp; An entry can be created and immediately posted.&nbsp; An draft can be posted at a later date.</em> 
</p>
<p>
<br />
<strong>Take 1</strong><br />
A fairly typical implementation would consist of the following fixtures...&nbsp; (after some refactoring) 
</p>
<p>
TestBase.cs
</p>
<pre class="code">
&nbsp;&nbsp;&nbsp; /// &lt;summary&gt;
&nbsp;&nbsp;&nbsp; /// Contains common constants and objects for testing blog and entry classes
&nbsp;&nbsp;&nbsp; /// &lt;/summary&gt;
&nbsp;&nbsp;&nbsp; public class TestBase
&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; protected Entry _entry;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; protected const string _testTitle = &quot;Test Title&quot;;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; protected const string _testContent = &quot;Test Content&quot;;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; protected void SetUp()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry = new Entry();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp; }&nbsp;
</pre>
<p>
EntryFixture.cs
</p>
<pre class="code">
&nbsp;&nbsp;&nbsp; /// &lt;summary&gt;
&nbsp;&nbsp;&nbsp; /// Tests to ensure Entrys are valid and have the correct defaults
&nbsp;&nbsp;&nbsp; /// &lt;/summary&gt;
&nbsp;&nbsp;&nbsp; /// 
&nbsp;&nbsp;&nbsp; [TestFixture]
&nbsp;&nbsp;&nbsp; public class EntryFixture : TestBase
&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [SetUp]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void SetUpEntryFixture()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SetUp();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void CanGetAndSetProperties()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Title = _testTitle;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Content = _testContent;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.AreEqual(_testTitle, _entry.Title);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.AreEqual(_testContent, _entry.Content);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void EntryCreatedGetsCreatedDate()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.AreEqual(DateTime.Today, _entry.Created);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void ValidEntryHasTitleAndContent()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Title = _testTitle;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Content = _testContent;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.IsTrue(_entry.IsValid);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void EntryWithoutTitleIsInvalid()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Content = _testContent;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.IsFalse(_entry.IsValid);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void EntryWithoutContentIsInvalid()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Title = _testTitle;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.IsFalse(_entry.IsValid);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp; }
</pre>
<p>
BlogFixture.cs
</p>
<pre class="code">
&nbsp;&nbsp;&nbsp; /// &lt;summary&gt;
&nbsp;&nbsp;&nbsp; /// Provides tests around the behaviour of the blog.
&nbsp;&nbsp;&nbsp; /// &lt;/summary&gt;
&nbsp;&nbsp;&nbsp; [TestFixture]
&nbsp;&nbsp;&nbsp; public class BlogFixture : TestBase
&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; private Blog _blog;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [SetUp]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void SetUpBlogFixture()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SetUp();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _blog = new Blog();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void PostingEntryProvidesPostedDate()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Title = _testTitle;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Content = _testContent;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _blog.Post(_entry);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.AreEqual(DateTime.Today, _entry.Posted );
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void PostingEntryIncreasesBlogEntryCount()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Title = _testTitle;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _entry.Content = _testContent;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _blog.Post(_entry);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Assert.AreEqual(1, _blog.Count);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [Test]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [ExpectedException(typeof(ArgumentException))]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public void AnInvalidBlogCannotBePosted()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Entry entry = new Entry();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _blog.Post(entry);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp; }
</pre>
<div style="text-align: center">
<img style="clear: both" src="/blog/image.axd?picture=Take-1.jpg" alt="" width="763" height="377" />
</div>
<p>
This leads to two classes - Entry and Blog.&nbsp; This makes sense of course, and the implementation is quite simple and neat.&nbsp; In short, this path of TDD leads to a successful implementation of Blog and Entry, I can post, get the dates, and have rudimentry validation on my blog. 
</p>
<p>
The downside though is that the reader / business analyst / new developer, that picks up these tests has an increased inertia in changing them.&nbsp; The very name of the test fixtures themselves forces an almost subliminal desire to maintain the current Blog / Entry structure, reducing the flexibility and creativity of the developer. 
</p>
<p>
<strong>Take 2.</strong> 
</p>
<p>
Same story, but remove any sort of artificial constructs and just add the tests one after the other refactoring mercilessly.&nbsp; I started with the simplest thing I could think of that provided some behaviour... 
</p>
<pre>
/// <summary>
/// Tests.  Note that there is currently no naming scheme - we leave that for refactoring to find...
/// </summary>
[TestFixture]
public class Fixture
{
    [Test]
    public void PostSingleItemIncreasesCount()
    {
        Blog.Post("Test Title", "Test Content");
        Assert.AreEqual(1, Blog.Count);
    }
}
</pre>
<p>
After the addition of the second test, which is to assert that the PostedDate is attached to the Blog, the following information arrises - one - we need an Entry class with which we can populate the blogs Posted with, and two, we can refactor out the setup of both tests into a setup method and give the fixture a readable name.&nbsp; This covers of the requirements of &quot;number of entries&quot; and &quot;contains a posted date&quot; from the story. 
</p>
<pre class="code">
/// <summary>
/// The tests are starting to flesh out - we can now rename things - for instance, the 
/// Fixture has now become SuccessfulPosting, and we've extracted the requirements for a successful posting into the setup. 
/// </summary>
[TestFixture]
public class SuccessfulPosting
{
    private int _index = 0;
    private Blog _blog;

    [SetUp]
    public void SetUp()
    {
        _blog = new Blog();
        _index = _blog.Post("Test Title", "Test Content");
    }

    [Test]
    public void PostSingleItemIncreasesCount()
    {
        SetUp();
        Assert.AreEqual(1, _blog.Count);
    }

    [Test]
    public void PostSingleItemProvidesPostedDate()
    {
        Assert.AreEqual(DateTime.Today, ((Entry)_blog.Entries[_index]).Posted);
    }
}
</pre>
<p>
Focussing next on invalid entries leads to the extraction of a simpler setup super class which contains methods to instantiate a blog and provide clean entry classes for testing succesful postings, and invalid posting data. 
</p>
<pre class="code">

/// <summary>
/// Manage overall setups for blog test
/// </summary>
public class TestBase
{
    protected Entry _cleanEntry;
    protected Blog _blog;

    protected void Prepare()
    {
        _cleanEntry = GetCleanEntry();
        _blog = GetTheBlog();
    }

    public static Blog GetTheBlog()
    {
        return new Blog();
    }

    public static Entry GetCleanEntry()
    {
        return new Entry();
    }
}

/// <summary>
/// Test cases focussing on the normal flow of operations and the successful outcome
/// </summary>
[TestFixture]
public class SuccessfulPosting : TestBase
{
    private const string testTitle = "Test Title";
    private const string testContent = "Test Content";
    protected Entry _validEntry;

    [SetUp]
    public void SetUp()
    {
        Prepare();
        _validEntry = GetValidEntry();
        _blog.Post(_validEntry);
    }

    [Test]
    public void IsContainedInBlog()
    {
        Assert.Contains(_validEntry, _blog.Entries);
    }

    [Test]
    public void PopulatesDatePosted()
    {
        Assert.AreEqual(DateTime.Today.Date, ((Entry) _blog.Entries[0]).PostedDate);
    }

    public static Entry GetValidEntry()
    {
        Entry entry = GetCleanEntry();
        entry.Title = testTitle;
        entry.Content = testContent;
        return entry;
    }
}

/// <summary>
/// Tests to ensure that only valid data gets posted (Alternative flows)
/// </summary>
[TestFixture]
public class PostingValidation : TestBase
{
    [SetUp]
    public void SetUp()
    {
        Prepare();
    }

    [Test]
    [ExpectedException(typeof(ArgumentNullException))]
    public void FailsIfTitleNotPopulated()
    {
        _cleanEntry.Content = _testContent;
        _blog.Post(_cleanEntry);
    }

    [Test]
    [ExpectedException(typeof(ArgumentNullException))]
    public void FailsIfContentNotPopulated()
    {
        _cleanEntry.Title = _testTitle;
        _blog.Post(_cleanEntry);
    }
}
</pre>
<div style="text-align: center;">
<img style="clear: both" src="/blog/image.axd?picture=Take-2.jpg" alt="" width="763" height="377" /> 
</div>
<p>
Note that this is now inherently more readable.&nbsp; Posting validation rules have been moved and renamed into a group, as have the rules around successful postings.&nbsp; The last thing two things to deal with are the default values on creating an entry, and the addition of draft entries to the blog without posting. 
</p>
<pre class="code">
/// <summary>
/// Ensure that entries are created with default values
/// </summary>
[TestFixture]
public class EntryDefault : TestBase
{
    [SetUp]
    public void SetUp()
    {
        Prepare();
    }

    [Test]
    public void CreatedDateIsToday()
    {
        Assert.AreEqual(DateTime.Today.Date, _cleanEntry.CreatedDate.Date);
    }
}
</pre>
<p>
And finally, the addition of entries as drafts... 
</p>
<pre class="code">
/// <summary>
/// Ensure that draft entries can be persisted
/// </summary>
[TestFixture]
public class DraftAddition : TestBase
{
    [SetUp]
    public void SetUp()
    {
        Prepare();
        _blog.Add(_cleanEntry);
    }

    [Test]    
    public void IncrementsBlogCount()
    {
        Assert.AreEqual(1, _blog.Entries.Count);    
    }

    [Test]
    public void IsContainedInBlog()
    {
        Assert.Contains(_cleanEntry, _blog.Entries);
    }
}
</pre>
<div style="text-align: center">
<img style="clear: both" src="/blog/image.axd?picture=Take-3.jpg" alt="" width="763" height="377" />
</div>
<p>
This now reads almost like a set of business rules... 
</p>
<ul>
	<li>DraftAddition.IncrementsBlogCount</li>
	<li>DraftAddition.IsContainedInBlog</li>
	<li>EntryDefault.CreatedDateIsToday</li>
	<li>PostingValidation.FailsIfTitleNotPopulated</li>
	<li>PostingValidation.FailsIfContentNotPopulated</li>
	<li>SuccessfulPosting.IsContainedInBlog</li>
	<li>SuccessfulPosting.PopulatesDatePosted</li>
	<li>SuccessfulPosting.IncrementsBlogCount</li>
</ul>
<p>
The best thing though is that none of the rules or tests are constraining the implementation.&nbsp; The rules stand on their own.&nbsp; Any changes to the implementation through refactoring tools will change the tests, which is a good thing, but they aren&#39;t artificially constrained by the tests. 
</p>
<p>
In short - by removing a preconceived structure from the test fixtures you allow a more organic growth of the code as it adapts to new business rules and constraints.&nbsp; Refactoring mercilessly leads to removed duplication, and frequently, the promotion of &quot;TestHelpers&quot; that create valid objects into product code &quot;Factory&quot; objects.&nbsp; The readability of the tests is enhanced, and even non-developers are able to read them.&nbsp; Finally, the tests are less brittle as you are no longer focussing on forcing the code to fit the test structure, but rather, focussing on how the code solves the behavioural requirements of the tests. 
</p>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Peter Hancock
Program Manager Amazon
United States United States
Nutcase triathlete that likes doing long course triathlons. Planning on competing in the Hawaiian Ironman at some stage - in fact - just as soon as I qualify.
Follow on   Twitter   LinkedIn

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.1411023.1 | Last Updated 16 May 2013
Article Copyright 2007 by Peter Hancock
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid