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
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:
Post a Comment