Hard to Write Unit Tests Are a Code Smell

Complex test set up and brittle tests can make you hate unit tests. Especially when writing tests takes a lot more time than writing code.

Anything in your code that makes it hard to write unit tests is something that should be changed.

Terms

From Martin Fowler’s TestDouble post referencing Gerard Meszaros’s book.

  • SUT (System Under Test) — the unit being tested.
  • Dummy — compilers require all parameters when calling a method. If these parameters are not used in your test, you can set them to dummy values — usually null.
Func<int, int, int> multiplyDummy => null;
  • Stubs return canned answers — a constant or predictable sequence of constants that is “correct” only for one case.
Func<int, int, int> multiplyStub => (a, b) => 6;
  • Fakes are full implementations. We use fakes when it is easier to assert or set up data via a fake. In-memory implementations of ORM frameworks are a good example.
Func<int, int, int> multiplyFake => (a, b) =>
{
var res = 0;
// multiplication is repeated addition
for (var i = 0; i < a; i++)
{
res += b;
}
return res;
};
  • Test Doubles — a generic term for everything above (and then some more)
Test Doubles for IDateTime

Too Many Dependencies to Mock

Too many dependencies indicate a class is doing too much — violating the single responsibility principle.

public class Foo
{
private ICat _cat;
private IDog _dog;
private IDuck _duck;
private IHen _hen;
public Foo(ICat cat, IDog dog, IDuck duck, IHen hen)
{
_cat = cat;
_dog = dog;
_duck = duck;
_hen = hen;
}
public int DoFoo()
{
var mew = _cat.Mew();
var woof = _dog.Woof();
var quack = _duck.Quack();
var cluck = _hen.Cluck();
return mew + woof + quack + cluck;
}
}
[Fact]
public void DoFooTest()
{
var cat = Mock.Of<ICat>();
var dog = Mock.Of<IDog>();
var duck = Mock.Of<IDuck>();
var hen = Mock.Of<IHen>();
var sut = new Foo(cat, dog, duck, hen);
...
}

Moving some of the responsibilities into a separate class makes your test setup simpler.

public class Foo
{
private IAnimals _animals;
private IBirds _birds;
public Foo(IAnimals animals, IBirds birds)
{
_animals = animals;
_birds = birds;
}
public int DoFoo()
{
var animalSounds = _animals.AllShout();
var birdSounds = _birds.AllShout();
return animalSounds + birdSounds;
}
}
[Fact]
public void DoFooTest()
{
var animals = Mock.Of<IAnimals>();
var birds = Mock.Of<IBirds>();
var sut = new Foo(animals, birds);
...
}

We are combining the responsibilities into logical groups — not simply moving the dependencies to another class.

Complex Dependencies

Sometimes the dependency being mocked is actually very complex even if the code using it seems simple.

public class Foo
{
private readonly IDbContext _context;
public Foo(IDbContext context)
{
_context = context;
}
public int DoFoo(Breed breed)
{
return _context.puppies
.Where(p => p.breed == breed)
.Sum(p => p.Shout());
}
}

_context.puppies can take any expression and is not easily stubbed. Use a fake instead. You are now testing the SUT + fake — but assuming your fake is flawless, any test failures are due to SUT bugs.

Usually there are fakes available for common external dependencies (like the in-memory database for Entity Framework). If not, you can make your own or simplify your interface so that it is easier to stub.

Difficult to Mock Dependencies

When it is hard get at and mock a dependency, your interface may need to be changed.

public interface IBar
{
ILittleBar GetLittleBar();
}
public interface ILittleBar
{
void DoLittleBar();
}
public class Foo
{
private readonly IBar _bar;
public Foo(IBar bar)
{
_bar = bar;
}
public void DoFoo()
{
_bar.GetLittleBar().DoLittleBar();
}
}

The interface need expose only the specific functionality required. In this case, just use the nested interface.

public class Foo
{
private readonly ILittleBar _littleBar;
public Foo(ILittleBar littleBar)
{
_littleBar = littleBar;
}
public void DoFoo()
{
_littleBar.DoLittleBar();
}
}

And then in your dependency injection.

services.AddScoped<ILittleBar>(s => s.GetService<IBar>().GetLittleBar());

Static Dependencies

Static methods make bad dependencies.

public static MyFooExtension
{
public static IBar DoBar(this IFoo foo)
{
// do bar
}
}

In every SUT test that uses DoBar , you need to make sure that IFoo plays nice in MyFooExtension and the returned IBar plays nice in the test. Your “interface” now includes the entirety of the MyFooExtension code and IBar.

Treat your static and helper classes as inline code. The same goes for static helper classes.

Brittle Tests

Tests break because something changed — the requirement, the implementation or, the actual test and its dependencies.

A single test covering many requirements may seem like a good idea. Especially if setup takes a lot of code. But now the test breaks if any one of the requirements change.

[Fact]
public void Test()
{
var sut = new Foo
{
Bars = new Bar[]
{
new Bar { Key = 2, Value = 20 },
new Bar { Key = 3, Value = 30 }
}
};
var input = new ModifyBar[]
{
new ModifyBar { Key = 1, Value = 10, Operation = “add” },
new ModifyBar { Key = 2, Value = 5, Operation = “update” },
new ModifyBar { Key = 3, Operation = “delete” }
};
sut.Merge(input); Assert.Equal(true, sut.ContainsKey(1));
Assert.Equal(5, sut[2]);
Assert.Equal(false, sut.ContainsKey(3));
}

Separate the different scenarios. Most test frameworks support parameterizing test data so you can still re-use your setup code.

[Theory]
[MemberData(nameof(Data))]
public void Test(Bar[] existing, ModifyBar input, Bar[] expected)
{
var sut = new Foo { Bars = existing };
sut.Merge(new ModifyBar[] { input }); Assert.Equivalent(expected, sut.Bars);
}
public static IEnumerable<object[]> Data => new List<object[]>
{
// add
new object[] {
new Bar[0],
new ModifyBar { Key = 1, Value = 10, Operation = “add” },
new Bar[] { new Bar { Key = 1, Value = 10 } }
},
// update
new object[] {
new Bar[] { new Bar { Key = 1, Value = 10 } }
new ModifyBar { Key = 1, Value = 5, Operation = “update” },
new Bar[] { new Bar { Key = 1, Value = 5 } }
},
// delete
new object[] {
new Bar[] { new Bar { Key = 1, Value = 10 } }
new ModifyBar { Key = 1, Value = 10, Operation = “delete” },
new Bar[0]
}
};

When Coding is Unit Test Jenga

It may seem easier to treat the SUT as a white box to make sure you hit every condition in it. Suddenly you have many unit tests that fail for no reason when you change the implementation.

Unit tests should focus on what the SUT must be doing, not how it does it. When you cover all the requirements, all your conditions should be covered as a result. If not, you have extra functionality which you can remove or treat as a new requirement.

Difficult to Test a Particular Requirement

When it seems impossible to trigger a scenario, take a step back to see if it should be moved to a separate class.

public class Foo
{
public void DoFoo()
{
try
{
// do the foo
}
catch(Exception ex)
{
_logger.Warn(“Something bad happened”);
}
}
}
[Fact]
public void FooTestLogging()
{
// ???
}

The class is doing 2 things — doing the work and logging the exception. A better model would be to separate it out.

public interface IFoo
{
void DoFoo();
}
public class Foo : IFoo
{
public void DoFoo()
{
// do the foo
}
}
public class DecoratedFoo
{
private readonly IFoo _foo;
private readonly ILogger _logger;
public DecoratedFoo(IFoo foo, ILogger logger)
{
_foo = foo;
_logger = logger;
}
public void Bar()
{
try
{
_foo.DoFoo();
}
catch(Exception ex)
{
_logger.Warn(“Something bad happened”);
}
}
}

Change Code, Change All Unit Tests, Repeat

Seem familiar? This indicates an evolving design. A single requirement cycle should not cause more than one iteration where unit tests are changed extensively.

Take a step back — look at the requirements, figure out the design and then code and unit test. Design changes will require and extensive rewrite of your unit tests — test driven design (the other TDD) is not a good idea.

Sometimes your requirements change drastically. Your tests will break — not because they were brittle, they are no longer valid checks. Removing the invalid tests and writing new checks are better than trying to “fix” them.

Conclusion

If you feel unit tests are not worth the effort, figure out why it is taking so long and how to fix it.

Ultimately, whether you change your mind on the value of unit tests, writing code so it can be unit tested easily will make your code better.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store