Sunday, January 09, 2005

Unit Testing

Unit testing is an extremely important activity within any software development. In contrast to system testing and other test disciplines, Unit Testing is a task of the developer. For each functionality you write code, you must prove its conformance with your expectations using unit tests. Unit tests are themselves pieces of code with the sole purpose of proving and exercising the correctness of particular code unter test. Thus, unit tests should also be developed with quality aspects in mind. Basically, a developer should consider unit tests to be integral constituents of the overall code delivery. Whenever any part of the code is changed, e.g. by refactoring, all existing tests should still run successfully. Thus, unit testing provides you with a kind of safety net. Many developers develop the code first and then write the unit tests for verification. In a Test First approach the philosophy is more "design and refactor a little, implement a little, test a little". In this case the test is written first, even before the first line of production code is available.

In order to make unit testing acceptable and feasible special frameworks are available that parse through your test code using reflection and then automatically execute the tests which are implemented by methods each exercising a specific aspect of your production code. The order of activities in all test methods is pretty the same:


  • Preparing the required environment for the code under test, e.g., resources and objects
  • Executing the test method
  • Verifying that what the test methods retrieve from the production code is what they expect
  • Cleaning up
If you are interested in such a framework just refer to http://www.xprogramming.com where you'll find a lot of details on existing xUnit Frameworks for most programming languages even exotic ones.

Let me show you this using a C# example where a factorial is calculated instead of providing you with a theory-first approach.

The first thing I would expect from a factorial method would be that it does not accept negative arguments.

using NUnit.Framework;
[TestFixture]
public class UnitTestFactorial
{
[Test, ExpectedException(typeof(ArgumentException))]
public void TestNegative()
{
MathOperations.Factorial(-1);
}
}


In this first test method we specify that we expect an exception to be thrown when we pass a negative argument. In a Test-First approach: the question is how can we implement our factorial class in such a way that this expectation is met. However, the code should be as simple as necessary to pass the test.

Here is the result:

public class MathOperations {
public static int Factorial(int arg) {
if (arg < 0) throw new ArgumentException();
return 0;
}
}

This implementation is passing the first test. But wait, there is more we expect:


[Test]
public void Test0And1() {
Assert.AreEqual(1, MathOperations.Factorial(0));
Assert.AreEqual(1, MathOperations.Factorial(1));
}

We expect that fac(0) == fac(1) == 1. If we run the test again with our simple implementation, it will fail. We need to fefactor our code to let the test succeed.


public class MathOperations {
public static int Factorial(int arg) {
if (arg < 0) throw new ArgumentException();
return 1;
}
}

Now, both tests succeed. But we are not finished yet.
How about the following test?


[Test]
public void TestMoreThanOne() {
Assert.AreEqual(2, MathOperations.Factorial(2));
Assert.AreEqual(6 * MathOperations.Factorial(5),
MathOperations.Factorial(6));
Assert.AreEqual(10 * MathOperations.Factorial(9),
MathOperations.Factorial(10));
}

This test will fail again. So we have to refactor our code to succeed:


public class MathOperations {
public static int Factorial(int arg) {
if (arg < 0) throw new ArgumentException();
if ((arg == 0) (arg == 1))
return 1;
else
return arg * Factorial(arg - 1);
}
}

And finally we are finished. The example is oversimplified but it definitely lets you understand the idea.

As you recognize Unit Testing is an important tool for each programmer. You'll appreciate it as soon as anyone changes the production code and one of your tests that fails will show you that the code changes had additional side effects you weren't aware of. Integrate the idea into a configuration management or automatic build tool and you'll get a taste of its potential. Unit testing may cost a little time and a little of additional efforts but in the end you will save more than you invest.

No comments: