Net Objectives

Net Objectives
If you are interested in coaching or training in ATDD or TDD please click here.

Friday, September 25, 2015

TDD: Specifying the Negative

One of the issues that frequently comes up is "how do I write a test about a behavior that the system is specified not to have?"  It's an interesting question given the nature of unit tests.  Let's examine it.

The things that you cannot do

Sometimes part of the specification of a system is to disallow certain actions to be taken on it.

The simplest example of this is an immutable object.  Let's say there exists in our system a USDollar class that represents an amount of money in US dollars.  Such a class might exist in order to restrict, validate, or perfect the data it holds.

But it's believable that we'd want an instance of USDollar, once created, to be immutable... that is, the value it was created to represent cannot be changed afterwards.  We might want this for thread-safety, or security, or any number of reason.  Many developers strongly believe in immutable objects as a general principle of good design.  Whether you agree or not, it would bring up the question "how do I specify in a test that you cannot change the value?"

public class USDollar
    private readonly double myAmount;
    public USDollar(double anAmount)
        myAmount = anAmount;

    public double GetValue()
        return myAmount;

How can we test-drive such an entity when part of what we wish to specify is that the amount, once established in an instance of this class, cannot be changed from the outside?  The most common way this question is phrased is "how can I show in a test that there is no SetAmount() method?"  Any test that references this method simply will not compile because the method does not exist.

Developers will typically suggest two different ideas:
  1. Add the SetValue() method, but make it throw an exception if anyone ever calls it.  Write a test that calls this method and fails if the exception is not thrown.[1]  Sometimes other "actions" are suggested if the method gets called, but an exception is quite common.
  2. Use reflection in the test to examine the object and, if SetValue() is found, fail the test.
The problem with option #1 is that this is not what the specification is supposed to say, it is not what was wanted.  The specification should be "you cannot change the value" not "if you change the value, thing x will happen."  So here, the developer is creating his own specification and ignoring the actual requirements.

The problem with option#2 is twofold:  First, reflection is typically a very sluggish thing, and in TDD we want our tests to be extremely fast so that we can run them frequently without this slowing down our process.  But even if we overcame that somehow, what would we have the test look for?  SetValue()ChangeValue()PutValue()AlterValue()?  The possibilities are infinite.

The key to solving this is in reminding ourselves, once again, that TDD is not about testing, but creating a specification.  Professional developers have always worked from some form of specification, it's just that the form was usually a document or something.

So think about the traditional specification, the one you're likely more familiar with.  Ask yourself this: Does a specification indicate everything the system does not do?  Obviously not, for this would create a document of infinite length.  Every system does a finite set of things, and then there is an infinite set of things it does not do.

So here's the test:

public class USDollarTest
    public void TestUSDollarPersistence()
        const double anyAmount = 10.50d;
        USDollar testDollar = new USDollar(anyAmount);

        double retrievedValue = testDollar.GetValue();

        Assert.AreEqual(retrievedValue, anyAmount);

"Huh?" you're probably saying.  "How does this specify that you cannot change the value?"

Examine the test, then the code, and ask yourself the following question:  If we are doing TDD to create this USDollar object, and if the object had a method allowing the value to be changed (SetValue() or whatever), how would it have gotten there?  Where is the test that drove that mechanism into existence?  It's not there.

In TDD we never add code to the system without have a failing test first, and we only add the code that is needed to make the test pass, and nothing more.  It must be this way, or the process does not work.

Put yet another way, if a developer on our team added a method that allowed such a change, and did not have a failing test written first, then she would not be following the process.  TDD does not work if you don't do it.  I don't know of any process that does.

In fact, if someone did add the method without a failing test, we would say that person was maliciously attacking the code.  If you have someone doing that, there is nothing that will save you.  If I wanted to attack you, and I can change your code, I'll write something that checks to see if the code is currently running in the test environment and, if so, will behave correctly. Otherwise I'll launch the nuclear arsenal. How would a test ever detect that?  Impossible.

And if we think back to the concept of a specification there is an implicit rule here, which basically has two parts.
  1. Everything the system does, every behavior must be specified.
  2. Given this, anything that is not specified is by default specified as not a behavior of the system.
In TDD anything the system does must have a test, which is ensured if we are disciplined about following the process.  Thus, rule two is also ensured.

But this may not be quite enough, it may not be clear enough or reliable enough.  We never want to lose knowledge.  So if we think of this requirement in terms of acceptance testing [3] we could express it using the Given/When/Then nomenclature:

Given: A USDollar class exists in the system
When: An attempt is made to change the value it holds after it is created
Then: The system will fail to compile

This, of course, implies a strongly-typed, compiled language with access-control idioms (like making things "private" and so forth).  What if your technology does not support this view?  What if it is an interpreted language, or one with no enforcement mechanism to prevent access to internal variables?

The answer is: You have to ask the customer.  You have to tell them that you cannot do precisely what they are asking for, and consider other alternatives in that investigation.

The things that must not occur

This is subtly different.  Let's add a requirement to our USDollar class.  If the context of this object was, say, an online book store, the customer might have a maximum amount of money that he allows to be entered into a transaction.

We used a double-precision number to hold the value in USDollar. A double can hold an incredibly large value.  In .net, for example, it can hold a value as high as 1.7976931348623157E+308[2].  It does not seem credible that any purchase made at our customer's site could total up to something like that!  So the requirement is: Any USDollar object that is instantiated with a value greater than the customer's maximum credible value should raise a visible alarm, because this probably means the system is being hacked or has a very serious calculation bug.

As developers, we know a good way to raise an alarm is to thrown an exception.  We can do that, but we also capture the customer's view of what the maximum credible value is, so we specify it.  Let's say he says "nothing over $1,000.00 makes any sense".  But... how much "over"?  A dollar?  A cent?  We have to ask, of course.  Let's say the customer says "one cent"."

In TDD everything must be specified, all customer rules, behaviors, values, everything.  So we start with this:

public void SpecifyMaximumDollarValue()
    Assert.AreEqual(1000d, USDollar.MAXIMUM);

...which won't compile, of course, so we add the constant to the USDollar but give it a 0 amount.  So we run the test, and watch it fail (which proves the test is valid).  Then we set the amount to the correct one, 1,000, and watch it pass.

Now we can write this test, which will also fail initially of course:

public void TestUSDollarThowsUSDollarValueTooLargeException()
        new USDollar(USDollar.MAXIMUM + .01);
        Assert.Fail("USDollar created with amount over the maximum"+

                    " should have thrown an exception");
    catch (USDollarValueTooLargeException)

But now the question is, what code do we write to make this test pass?  The temptation would be to add something like this to the constructor of USDollar:

if(anAmount > MAXIMUM) throw new USDollarValueTooLargeException();

But this is a bit of a mistake.  Remember, it's not just "add no code without a failing test", it is "add only the needed code to make the failing test pass."

Your spec is your pal.  He's there at your elbow saying "don't worry.  I won't let you make a mistake.  I won't let you write the wrong code, I promise."  He's not just your pal, he's your best pal.  Here, the spec/test is just a mediocre friend because he will let you write the wrong code and say nothing about it.  He'll let you do this, and still pass:

throw new USDollarValueTooLargeException();

No conditional.  Just throw the exception all the time.  That's wrong, obviously. This behavior has a boundary (as we discussed in our blog about test categories) and every boundary has two sides.  We need a little more in the test.  We need this:

    new USDollar(USDollar.MAXIMUM);
catch (USDollarValueTooLargeException)
    Assert.Fail("USDollar created with amount at the maximum"+

                " should not have thrown an exception");

Now the "if(anAmount > MAXIMUM)" part must be added to the production code or your best buddy will let you know you're off track.

[TODO: The spec is the guys who takes your keys away]


[1] There are a variety of ways to do this, but most frameworks have built-in assertions that cause a failure if an expected exception is not thrown.
[2] For those who dislike exponential notation, this is:
...and no cents. :)
[3] [TODO] Link to ATDD blog

No comments:

Post a Comment