Monday, February 27, 2012

Better mocking for tests

The common approach I've seen to TDD and SOLID design in static languages requires a lot of interfaces and constructors that accept those interfaces: dependency injection.

If you only develop in a static language, you probably don't see anything wrong with this.  Spend some time in a dynamic language though, and these overly indirect patterns really start to stand out and look like a lot of extra work.

Here's the rub.  When your code legitimately needs to be loosely coupled, then IoC, DI, and interfaces rock.  But when it doesn't need to be loosely coupled for any reason OTHER THAN TESTING, why riddle the code with indirection?

Indirection costs.  It takes time, adds significantly more files to your code base, adds IoC setup requirements, forces more complicated design patterns (like Factories), and most importantly makes code harder to understand.

Testing is important.  So if we have to make our code more abstract than strictly necessary to test, it's worth it.  But there are techniques other than DI that we can use to make our code unit testable while still reducing the amount of abstraction!

Limited Integration Testing
The first way to unit test without interfaces is to not mock out the object-under-test's dependencies.  To not unit test.  I'm not talking about doing a full stack test though.  You can still mock out the database or other third party services, you just might not mock out your object's direct dependencies.
A -> B -| C
  -> D -| E
Here A depends on B and D which depend on C and E.  We've mocked out C and E but not B and D.

The benefit of this approach is that there is absolutely no indirection or abstraction added to your code.  The question is what should you test?  I'm still thinking of this as a unit test, so I really only want to test class A.  It's still going to be calling out to B and D though.  So what I want to do is write the test so that it's focused on verifying the behavior of A.  I'll have separate unit tests for B and D.

I'll reuse the example from Demonstrating the Costs of DI again.  If I wanted to test PresentationApi I could mock out the database layer but still let it call into OrdersSpeakers.  In my test I need to verify that PresentationApi did in fact cause the speakers to be ordered, but I don't care if they were ordered correctly.  I can do this by verifying that the speaker I added has it's Order property set.  I don't care what it is set to, as long as it's set I know PresentationApi called OrdersSpeakers.  The OrdersSpeakers tests will verify that the ordering is actually done correctly.

The downside of this technique is that your test must have some knowledge about the object-under-test's dependencies and frequently those object's dependencies.  You might expect these tests to be brittle, but surprisingly I haven't had any problems with that.  It's more just that to conceptually understand and write the test, you have to think about the object's dependencies.

Static Funcs
I learned this technique from a blog post Ayende wrote some time ago.  And he recently showed another example.  In this technique you expose a lambda method which is set to a default implementation.  The system code calls the lambda which wraps the actual implementation.  But the tests can change the implementation of the lambda as needed.

I'll use an example I think is probably common enough that you'll have had code like this somewhere: CurrentUser.
public static class CurrentUser
{
  public static Func GetCurrentUser = GetCurrentUser_Default;

  public static User User { get { return GetCurrentUser(); } }

  private static User GetCurrentUser_Default()
  {
    // ... does something to find the user in the current context ...
  }

  public static ResetGetCurrentUserFunc() { GetCurrentUser = GetCurrentUser_Default; }
}

[TestFixture]
public class DemoChangingCurrentUser
{
  [TearDown]
  public void TearDown() { CurrentUser.ResetGetCurrentUserFunc(); }

  [Test]
  public void TestChangingCurrentUser()
  {
    var fakeCurrentUser = new User();
    CurrentUser.GetCurrentUser = () => fakeCurrentUser;
    
    Assert.AreEqual(fakeCurrentUser, CurrentUser.User);
  }
}
The previous approach could not have handled a case like this, because here the test actually needs to change the behavior of it's dependencies.  The benefit of this approach is that we have poked the smallest possible hole in the code base to change only what the test needs to change.  And also, the added complexity is constrained within the class it applies to.

But the biggest downside is exemplified by the TearDown method in the test.  Since the func is static, it has to be reset to the default implementation after the test runs, otherwise other tests could get messed up.  It's nice to build this resetting INTO your test framework.  For example, a test base class could reset the static funcs for you.

Built In Mocking
This is by far my favorite technique, which is why I saved it for last ;)  I first saw this in practice with the Ruby on Rails ActionMailer.  The idea is that you don't actually mock, stub, or substitute anything.  Instead the framework you are calling into is put in test mode.  Now the framework will just record what emails were sent but not actually send them.  Then the test just queries on the sent mails to verify what mail was attempted to be sent.

In the case of ActionMailer, every email you try to sends gets stored in an array, ActionMailer::Base.deliveries.  The tests can just look in this array and see if the expected email is there, as well as verify that the email is in the correct format.

With the right mindset this can be applied in all kinds of places.  The most powerful place I've found is to the database layer.  The database access layer we wrote at Pointe Blank includes this style of mocking/testing.  So for example, a typical unit test that mocks out the database in our codebase might look something like this:
[Test]
public void TestAddSpeaker()
{
  ...
  presentationApi.AddSpeaker(speaker);

  TestLog.VerifyInserted(speaker);
}

The upsides to this technique are really awesome.  First off, there are no interfaces or dependency injection between the code and the persistence layer.  Yet the test can still "mock out" the actual persistence.

Secondly, there is no ugly mocking, stubbing, or faking code in your tests.  Instead of testing that certain method calls occurred, you are simply verifying results!  By looking in the TestLog, you're verifying that the actions you expected really did occur.

Thirdly, the TestLog becomes a place where a sophisticated query API can be introduced allowing the test to express exactly what it wants to verify as succinctly as possible.

Fourthly, this code is completely orthogonal to any changes that may happen in how a speaker is saved, or how the actual persistence framework works.  The code is only dependent on the TestLog.  This means the test will not break if irrelevant changes occur in the code.  It is actually written at the correct level of abstraction, something that is frequently very difficult to achieve in unit tests.

The only downside isn't even a downside to this technique.  The downside is that since built in mocking is so great, you may have a tendency to use the limited integration testing approach more than you should.  You may find yourself integration testing everything down to the database layer because the database layer is so easy to mock out.  Sometimes that's OK, but sometimes you really shouldn't test through a class dependency.

So those are three techniques I've discovered so far for writing unit tests without riddling my code with interfaces, dependency injection, and IoC. Applying these allows me to keep my code as simple as possible. I only need to apply indirection where it is actually warranted. There is a trade off, as I pointed out in the downsides of these techniques, namely in that your tests may be a bit more error prone. But I feel those downsides are more than acceptable given how much simpler the overall code base becomes!

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.