Saturday, 8 September 2012

What is the scope of a "unit test" in TDD?

I often get asked the question: "What is the scope of a "test" in TDD? Is it method scope, class scope, feature scope?".

Let's try to answer the question by examining some TDD "unit tests":

Is it class scope?

Let's see the first example and try to answer this question:

[Test] public void
ShouldThrowValidationExceptionWithFatalErrorLevelWhenPassedStringIsEmpty()
{
  var emptyString = string.Empty;
  var validation = new Validation();

  var exceptionThrown = Assert.Throws<CustomException>(
    () => validation.Perform(emptyString) 
  );
  
  Assert.IsTrue(exceptionThrown.IsError);
}

Ok, so let's see... how many real classes take part in this spec? Three: a string, an exception and the validation. So the class scope is not the most accurate description.

Or a method scope?

So, maybe the scope covers a single method, meaning a spec always exercises one method of a specified object?

Let's consider the following example:

[Test] public void 
ShouldBeFulfilledWhenEventOccursThreeTimes()
{
  var rule = new FullQueueRule();
  rule.Queued();
  rule.Queued();
  rule.Queued();
  Assert.IsTrue(rule.IsFulfilled);
}

Count with me: how many methods are called? Depending on how we count, it's two (Queued() and IsFulfilled) or four (Queued(), Queued(), Queued(), IsFulfilled). In any case, not one. So it's not method scope either.

It's the scope of class behavior!

The proper answer is: behavior! Each TDD test specifies a single behavior. Amir Kolsky and Scott Bain even teach that each TDD test should "introduce a behavioral distinction not existing before".

It may look that "behavior" scope is actually broader than method or class levels, since such test can span multiple classes and multiple methods. This is only partially true. That's because e.g. tests with method scope can span multiple behaviors. Let's take a look at an example:

[Test] public void 
ShouldReportItCanHandleStringWithLengthOf3ButNotOf4AndNotNullString()
{
  var bufferSizeRule = new BufferSizeRule();
  Assert.IsTrue(bufferSizeRule.CanHandle("aaa"));
  Assert.IsFalse(bufferSizeRule.CanHandle("aaaa"));
  Assert.IsFalse(bufferSizeRule.CanHandle(null));
}

Note that it specifies three (or two - depending on how you count) behaviors: acceptance of string of allowed size, refusal of handling string above the allowed size and a special case of null string. This is an antipattern by the way and is sometimes called a "check-it-all test". The issue with this kind of test is that it can fail for at least two reasons - when the allowed string size changed and when null handling is done in another way.

Let's see how this works out with class-scope test:

[Test] public void
ShouldReportItIsStartedAndItDoesNotYetTransmitVoiceWhenItStarts()
{
  var call = new DigitalCall();
  call.Start();
 
  Assert.IsTrue(call.IsStarted);

  Assert.Throws<Exception>(
    () => call.Transmit(Any.InstanceOf<Frame>()));
}

Again, there are two behaviors here: reporting the call status after start and not being able to transmit frames after start. That's why this test should be split into two.

How to catch that you're writing a test that specifies more than one behavior? First, take a look at the test name - if it looks strange and contains some "And" or "Or" words, it may (but does not have to) be about more than one behavior. Another way is to write the description of a behavior in a Given-When-Then way. If you have more than one item in the "When" section - that's also a signal.

So, it's all about behaviors! Of course, if you examine the styles of some famous TDD authorities, most of them understand "behavior" and "scope" a little bit different - so it's always good to read some books or blogs to get a grasp of the big picture.

See ya and have a good weekend!

No comments: