Lazy Unit Testing

Unit tests form a wonderful safety net for your code and can help to give you serious confidence when adding new features or fixing bugs in complex systems without breaking existing functionality but they can become quite fragile and this can lead to people swearing loudly and doing unspeakable things such as excluding the unit test project from their build.

One thing that can help to make your tests less fragile and more specific is to make lazy unit tests. This means creating tests that test one thing, one expected condition and avoiding the trap of letting one test check several required outcomes or check needless conditions which should not fall within its area of responsibility.

Examples

This test is an example of the sort of thing I'm talking about:

[Test]
public void WhenIAddARecordToARecordSet_ItShouldBeAddedAndNotChangedAndStillTheSameLength()  
{
    // Arrange:
    RecordSet recordSet = new RecordSet();
    Record record = new Record("Record Name", 1);

    // Act:
    recordSet.AddRecord(record);

    // Assert:
    recordSet.Records.Should().Not.Be.Null();
    recordSet.Records.Should().Contain(record);
    recordSet.Records.Count.Should().Be.EqualTo(1);
    recordSet.Records.First().Name.Length.Should().Be.EqualTo(11);
}

As you can see, many different changes to how this object works could easily break this test, it makes far too many assertions.

For example, if we decide to append the date the new Record object was inserted to the Record's Name property, we have already broken several assertions:

public class Record  
{
    public string Name { get; set; }

    public int Value { get; set; }

    public Record(string name, int value, DateTime currentDate)
    {
        this.Name = String.Format("{0} ({1})", name, currentDate.ToShortDateString());
        this.Value = value;
    }
}

Some of these assertions are probably actually immaterial to the way the app works and as we start to fix issues with our code this same test will continue to break for different reasons. A unit test should test one unit of functionality.

Solutions

To solve this problem all we need to do is decide which of these assertions are really valid and important and then split them out into their own tests:

[Test]
public void WhenIAddARecordToARecordSet_ItShouldBeAdded()  
{
    // Arrange:
    RecordSet recordSet = new RecordSet();
    Record record = new Record("Record Name", 1, DateTime.Today);

    // Act:
    recordSet.AddRecord(record);

    // Assert:
    recordSet.Records.Should().Not.Be.Null();
    recordSet.Records.Should().Contain(record);
}

[Test]
public void WhenIAddARecordToARecordSet_TheRecordSetShouldContainOneRecord()  
{
    // Arrange:
    RecordSet recordSet = new RecordSet();
    Record record = new Record("Record Name", 1, DateTime.Today);

    // Act:
    recordSet.AddRecord(record);

    // Assert:
    recordSet.Records.Should().Not.Be.Null();
    recordSet.Records.Count.Should().Be.EqualTo(1);
}

[Test]
public void WhenIConstructARecord_ItShouldHaveTheCorrectValue()  
{
    // Arrange:
    var value = 1;
    var currentDate = DateTime.Today;

    // Act:
    Record record = new Record("Record Name", value, currentDate);

    // Assert:
    record.Value.Should().Be.EqualTo(value);
}

This means that only tests related directly to the broken functionality will actually break and this will make diagnosing the issue much quicker and easier (assuming you have well-named tests with specific responsibilities) and this will in turn make the error or tests easier to fix, depending on what needs fixing.

Unit tests should have a simple enough set-up that you can easily copy and change the tests to test different assertions and if you do end up with a large, complex test setup you can extract the setup to a setup method and at least split up your assertions.