Pseudorandom Knowledge

Unit testing: getting started

Unit testing is hailed by many as the best way to write and maintain code. Unit tests are written with the help of a unit testing framework. NUnit is the dominating framework in .NET.

namespace SpaceGame
{
    public class Enemy
    {
        private int hitpoints;
 
        public Enemy(int hitpoints)
        {
            this.hitpoints = hitpoints;
        }
 
        public bool Alive
        {
            get
            {
                return hitpoints > 0;
            }
        }
 
        public void Hit(int damage)
        {
            if (damage < 0)
            {
                throw new ArgumentOutOfRangeException("damage");
            }
            hitpoints -= damage;
        }
    }
}

In NUnit tests are contained in its own class. Normally with a test class for each class under test. It is also practical to put all tests in a separate project and assembly.

To run the tests we need a test runner. NUnit has its own runner in the form of a console or GUI program. However, it is much more convenient to use a runner that integrates with Visual Studio. If you already use ReSharper or CodeRush they might be a good choice.

namespace SpaceGame.UnitTests
{
    [TestFixture]
    public class EnemyTests
    {
        private Enemy enemy;
 
        [SetUp]
        public void Setup()
        {
            enemy = new Enemy(100);
        }
 
        [Test]
        public void NewEnemy_IsAlive()
        {
            Assert.IsTrue(enemy.Alive);
        }
 
        [TestCase(100)]
        [TestCase(101)]
        [TestCase(Int32.MaxValue)]
        public void Hit_OnceForMoreThanHitpoints_Kills(int damage)
        {
            enemy.Hit(damage);
            Assert.IsFalse(enemy.Alive);
        }
 
        [TestCase(0)]
        [TestCase(1)]
        [TestCase(50)]
        [TestCase(99)]
        public void Hit_OnceForLessThanHitpoints_RemainsAlive(int damage)
        {
            enemy.Hit(damage);
            Assert.IsTrue(enemy.Alive);
        }
 
        [TestCase(-1)]
        [TestCase(Int32.MinValue)]
        public void Hit_OnceForNegativeDamage_ThrowsException(int damage)
        {
            Assert.Catch<ArgumentOutOfRangeException>(() =>
            {
                enemy.Hit(damage);
            });
        }
 
        [Test]
        [Ignore("Currently fails due to bug 1")]
        public void Hit_TwiceForMaxInteger_Kills()
        {
            enemy.Hit(Int32.MaxValue);
            enemy.Hit(Int32.MaxValue);
            Assert.IsFalse(enemy.Alive);
        }
    }
}

An important goal when writing unit tests is to make them readable. Each test should test for one thing only and tests should not contain any logic of their own. Contrary to regular programming hardcoded values are a good thing when writing tests.

The last part shows how a test can be ignored which may be useful when a bug is uncovered but there isn’t time to fix it yet. This should be used sparingly of course.