Friday, June 25, 2010

When and Where to Fake (Mock/Isolate/Stub/etc.)

After learning how to make fakes (mocks, stubs, isolators, etc.) a part of our unit tests, it's easy to say, "Cool! I'm faking everything now!" I think that's especially true if we've just learned how to use a cool isolation framework at the same time. There is an elegance in ensuring that all we are testing in the code is the method we're looking at and that method only. However, this can be true even if there are dependencies.

At the right time and place, fakes are great tools for unit tests, or to serve as placeholders for unimplemented parts of production code. But I think it's important to think about why fakes are used to begin with.

When to Fake
So I ask myself a few things before I decide to fake a dependency in my system.

Does this dependency affect or rely on anything outside my system? In other words, do I read from or write to a file or database, call a web service, use an external framework and so on? Any external resource is considered unreliable and outside the scope of my tests. This is probably the most common reason to add a fake. In this case, writing fakes is almost without exception the right thing to do.

Is the code in this dependency fully tested? If there is code in my project that is not fully under test, I more or less expect it to have bugs. It may not, but I don't know that. The best solution is to get that code under test, but sometimes that isn't practical in the short term. So the next best thing is to fake the dependency. Then I at least know that errors are coming from the code I am testing. This incurs a bit of technical debt, however, since I have to remember to remove the fakes once the dependency is under test.

Does this dependency cause my tests to run significantly slower? While all of the dependencies may be fully tested code belonging to my project, there sometimes are processes that take a lot of time to complete. Those dependencies should be faked away so that I can run my tests frequently and maintain the TDD fail/fix/refactor cycle.

Why Not to Fake
There are also some good reasons to not use fakes, or at least use them as sparingly as possible.

Faking adds complexity to a test. Whether it is done manually by making a dummy class or by invoking some faking framework, the code becomes harder to read. Frameworks in particular have syntaxes that can seem cryptic to someone not familiar with them.

Also, faking will often cause more tests to be necessary, since a fake will break the data flow of a method, effectively short-circuiting the logic. Where I before just had to test a state or return value, I now also have to test an interaction to make sure that code behaves correctly before the insertion point of the fake. Most of the time, this means that I need an additional test to ensure that the fake is being called with the correct parameters.

Code Example
For this last point, I'm adding a code example here, since I'm not sure if I'm saying it clearly enough, and the code describes it better than I do.

Let's say I have a value generator class, which depends on a web service to deliver its values. By my guidelines above, this is an integration that definitely should be faked, but it's important to think about the effect that has on the tests.

First I establish a ValueGenerator class. It has a WebService dependency which has a method called GetAValue. The class can be instantiated without parameters, or by injecting a WebService object.
public class ValueGenerator
{
    private readonly WebService service;

    public ValueGenerator() : this(new WebService()) { }
    public ValueGenerator(WebService service)
    {
        this.service = service;
    }

    public int GetAValue()
    {
        return service.GetAValue();
    }
}
If I'm testing this without faking, which effectively makes it an integration test rather than unit test, then the test code is simple.
[TestClass]
public class ValueGeneratorTest
{
    // We happen to know that a call to GetAValue with
    // no parameters returns 10. Extremely bad form
    // however, since we don't control the external
    // resource.
    [TestMethod]
    public void GetAValue_CalledWithoutFake_10Received()
    {
        var generator = new ValueGenerator();
        var value = generator.GetAValue();
        Assert.AreEqual(10, value);
    }
}
However, we want to isolate the WebService and not be reliant on it being available, so we create a fake, called FakeWebService, which inherits from WebService. That results in the following tests.
[TestClass]
public class ValueGeneratorTest
{
    [TestMethod]
    public void GetAValue_CalledWithFake_ServiceIsCalled()
    {
        var service = new FakeWebService();
        var generator = new ValueGenerator(service);
        generator.GetAValue();
        Assert.IsTrue(service.ServiceCalled);
    }

    [TestMethod]
    public void GetAValue_CalledWithFake_5Received()
    {
        var service = new FakeWebService();
        var generator = new ValueGenerator(service);
        var value = generator.GetAValue();
        Assert.AreEqual(5, value);
    }

    private class FakeWebService : WebService
    {
        public bool ServiceCalled { get; private set; }

        public override int GetAValue()
        {
            ServiceCalled = true;
            return 5;
        }
    }
}

So in conclusion, faking is a great thing, but give it some thought before using it.

No comments:

Post a Comment