Skip to content

01 Welcome to TDD

Luke Usher edited this page Sep 2, 2014 · 4 revisions

Welcome to TDD

This tutorial serves as both an introduction to TDD and an introduction to AAATest. For those unfamiliar with TDD, it will provide a step by step example, outlining related principles on patterns along the way. It is still recommended for anyone familiar with TDD, as it goes over all the basics of AAATest and will help you map what you already know to the AAATest way of doing it.

This tutorial starts with a simple test application, loosely base on an ASP MVC one. If you don't know MVC then don't worry, the actual framework isn't something we will be discussing.

To get started, we'll create a product controller and a product test fixture.

The ProductController is very straight forward and at the moment does not contain any real code:

public class ProductController {
    public ActionResult View(int id) {
        throw new NotImplementedException();
    }
}

Next we need to create a test fixture. A test fixture is just a way of grouping tests together. Tests fixtures generally live in their own tests project, separate from the main application code. With the AAATest framework, we create the test fixture by declaring a class that inherits from TestFixture<T>, where T is the type of class that you will be testing. This is how our test fixture initially looks:

public class ProductController_View : TestFixture<ProductController> {
}

Notice that the name of the class id ProcutController_View. This is because we will only test the View method and any other methods will go into a different test fixture. Tests with dozens of methods and hundreds of lines become just as hard to maintain as any other code.

Of course, we need some tests in there as well but before we can do that, we'll need some requirements.

Requirements

Create the ability for any user to be able to view a product

This is generally what a feature request will sound like, our job is to break this down. Remember, for this tutorial, we only care about the controller, not the view, not the database or anything else it depends on. So our functional requirements will look something like this:

  1. The product will be loaded from the database.
  2. The method will return a view result.
  3. The DataItem of the result will be a type of ProductViewVM

As with most requirements, our functional requirements leave a great deal of undefined behaviour. Let's put our testing hat on and think of some of the errors that could occur and what we will do about them, as well as other assumptions we make based on our knowledge of the application:

  1. If id is 0 throw an exception.
  2. If id is negative throw an exception.
  3. When loading the product from the database, our repository should be used.
  4. If the Product is not found, throw an exception.
  5. The product returned from the database will be used as the source of the ProductViewVM.

There, that's something we can use to actually get started with.

The Best Offense

We'll start off simple and test that an ArgumentException is thrown if the method receives 0 as the id. With the AAATest framework, any public method on a test fixture is considered a test. In ProductController_View we will create our first test:

public void ExceptionWhenId0() {
    Act(x => x.View(0));
    AssertException<ArgumentException>("id must be provided. Provided value was: '0'");
}

AAATest is based off the "Arrange, Act, Assert Pattern". This test happens to be so trivial that no arrange step is necessary, so we'll leave that for later.

The Act method takes an Action and executes it with a new instance of the type under test (ProductController), which is defined by the test fixture. In this case the AAATest framework creates a new ProductController and passes it to the action that the test provides (x.View(0)). The test also passes the parameters to the method, 0 for the id.

This is one of the features that makes AAATest stand out. The AAA pattern is built in (as you might have guessed by the name), this allows the test to be more self documenting than with other unit test frameworks.

By handling the instantiation of ProductController and it's dependencies it takes away much of the tedious, repetitive setup that would otherwise be required.

Finally, in the Assert phase, we check what the result of the Act phase was. Here we are testing that an ArgumentException was thrown and that the Message property of the exception matches the string. If this error ever comes up in our hands on testing or in production then we will have a description of exactly what the error was.

We should now run this and check that it fails. This is now as red-green-refactoring and is the basis of TDD. First we make a test. Then we run it to make sure it failed. Then we refactor our production code (the ProductController) until it passes. In this case however, we are going to ad our second test first, as they are so similar:

public void ExceptionWhenIdNegative() {
    Act(x => x.View(-2893));
    AssertException<ArgumentException>("id must be provided. Provided value was: '-2893'");
}

As you can see, it's essentially the same as the previous test, but uses a negative number instead of zero. In real code you could make the id variable an unsigned int. I haven't done so here to demonstrate that it's perfectly ok to have many, similar looking tests. It is called the "Single Assert Principle". Each test should only be verifying one thing, without this our tests become hard to maintain.

Now, onto the real code. Let's make our tests pass! It's a simple matter of adding a guard clause at the start of the method:

public ActionResult View(int id) {
    if (id <= 0)
        throw new ArgumentException(string.Format("id must be provided. Provided value was: '{0}'", id));
    return null;
}

Why test for invalid arguments?

This is known as a guard clause or a pre-condition, which is part of the much broader practice of defensive programming.

If id is invalid then we won't find out until much later on, probably when a NullReferenceException is thrown. When we are analysing why a NullReferenceException was thrown, we have to start at the source of the exception and trace our steps back through the application until we find the original cause. The time it takes to go from a fairly meaningless NullReferenceException to the real cause could be significant, depending on how far up the chain the actual error is.

Therefore, the small amount of time (about a minute) spent checking that the precondition is met and the associated test is trivial, compared to the time that may be saved.

In the next section (Making Arrangements) our tests will get a little more involved