15 июля 2011 г.

Шпаргалка по NUnit: Основы unit тестирования

NUnit шпаргалка
Попробовав писать на C# очень быстро понял что все хитросплетения программы очень сложно держать в голове. А так же когда отлаживаешь код с помощью отладчика то выполняешь те же тесты только они уже не unit, и охватывают обычно большой участок хитросплетений кода. Когда же немного почитав интернет и книги, узнал что есть очень удобные библиотеки тестов, а так же подход в программировании «разработка через тестирование» или TDD. Основной идеей TDD является, то что тест пишется до того как будет написан основной код программы. Но вернемся к нашей теме. Сейчас планирую написать несколько статей с примерами использования Nunit библиотеки.
Планируемые статьи:
Дальше больше и интересней Подмигивающая рожица
Не буду вникать в подробности подключения и запуска тестов NUnit. Об этом можно почитать в приложении [2], [3] и [5]. Далее по тексту предполагается что Вы подключили NUnit и он заработал.
Дабы не тестировать класс Class1 тестом Test1 в качестве примера возьмем класс для управления двигателем. Опишем исходные условия:
У двигателя можно читать текущее состояние направления вращения (left, right, stopped) и скорость вращения (0 – 10), останавливать и запускать двигатель с указанием направления вращения, а так же увеличивать и уменьшать скорость. Если не остановить двигатель и сменить ему направление вращения то создается исключение. Исходные условия описаны приступим к программированию. Создадим консольный проект MotorExample. В проекте MotorExample добавляем два класса MotorTest и Motor. Оба класса помечаем модификатором public. Что бы не задумываться об области видимости классов. Для описания направления вращения создадим перечисление.  Создадим так же интерфейс IMotor, который позволит одинаково управлять различными двигателями и наследуем от него наш класс Motor. Visual Studio попросим для интерфейса IMotor создать заглушки. Класс MotorTest помечаем атрибутом TestFixture. Описание атрибутов Nunit отложим. Скажу лишь что сейчас мы задействуем еще два атрибута Test и SetUp.
Для простоты примера класс с тестами MotorTest и разрабатываемый класс Motor находятся в одном проекте. Обычно я их разношу в разные проекты что бы файла тестов и файлы проекта не путались, а в проект тестов добавляю ссылку на тестируемый проект.
В результате вот что у нас должно получиться:
public enum DirectionOfRotation
{
    Stopped,
    Right,
    Left
}
interface IMotor
{
    int Speed { get; }
    DirectionOfRotation CurrentDirection { get; }

    void Start(DirectionOfRotation senseOfRotation);
    void Stop();
    void SpeedUp(int increment);
    void SpeedDown(int increment);
}
public class Motor:IMotor
{
    public int Speed
    {
        get { throw new NotImplementedException(); }
    }
    public DirectionOfRotation CurrentDirection
    {
        get { throw new NotImplementedException(); }
    }
    public void Start(DirectionOfRotation senseOfRotation)
    {
        throw new NotImplementedException();
    }
    public void Stop()
    {
        throw new NotImplementedException();
    }
    public void SpeedUp(int increment)
    {
        throw new NotImplementedException();
    }
    public void SpeedDown(int increment)
    {
        throw new NotImplementedException();
    }
}
[TestFixture]
public class MotorTest
{
}
Приступим к разработке класса Motor. Начнем с конструктора. Скорее всего при инициализации двигателя  он не будет вращаться. Опишем это в тестовом классе.
NUnit поддерживает две модели проверок на основе Assert и на основе Constraint. Пока не будем вдаваться в подробности скажу лишь что разработчики рекомендуют пользоваться Constraint моделью из-за большей гибкости. И начиная с версии 2.4 тесты основанные на классической модели проверок, являются лишь оберткой Constraint модели для совместимости со старыми версиями. Для тестирования конструктора приведу синтаксис классической и constraint модели, далее будем использовать только новую constraint модель.

Motor motor;
[SetUp]
public void Init()
{
    motor = new Motor();
}
[Test]
public void TestConstructorClassic()
{
    Assert.AreEqual(0, motor.Speed);
    Assert.IsInstanceOf<DirectionOfRotation>(motor.CurrentDirection);
    Assert.AreEqual(DirectionOfRotation.Stopped, motor.CurrentDirection);
}
[Test]
public void TestConstructorConstraint()
{
    Assert.That(motor.Speed, Is.EqualTo(0));
    Assert.That(motor.CurrentDirection, Is.TypeOf<DirectionOfRotation>());
    Assert.That(motor.CurrentDirection, Is.EqualTo(DirectionOfRotation.Stopped));
}
MotorTest.TestConstructorClassic : Failed
System.NotImplementedException : Метод или операция не реализована.
Тесты провалились потому что мы еще ничего не реализовали в нашем классе Motor. Для этого давайте создадим конструктор, две private переменных и проинициализируем их. А так же реализуем свойства Speed и CurrentDirection.
private int _currentSpeed;
private DirectionOfRotation directionOfRotation;
public Motor ()
{
    _currentSpeed = 0;
    directionOfRotation = DirectionOfRotation.Stopped;
}
public int Speed
{
    get { return _currentSpeed; }
}
public DirectionOfRotation CurrentDirection
{
    get { return directionOfRotation; }
}
TestConstructorClassic, Success
TestConstructorConstraint, Success
Тесты конструктора проходят, теперь давайте подумаем как должен работать тест метода Start. Метод Start задает направление вращения и минимальную скорость, а так же создает исключение InvalidOperationException(), если двигатель был не остановлен. Опишем это в тесте:
[Test]
public void TestStart()
{
    motor.Start(DirectionOfRotation.Right);
    //Проверка min скорости
    Assert.That(motor.Speed, Is.Not.EqualTo(0));
    //Проверка на генерацию исключения
    Assert.That(()=>motor.Start(DirectionOfRotation.Left), Throws.InvalidOperationException);
}
Тест не проходит потому что мы еще не вносили изменений в реализацию метода Start. Опишем реализацию (пока опишем старт без генерации исключения):
public void Start(DirectionOfRotation senseOfRotation)
{
directionOfRotation = senseOfRotation;
_currentSpeed = 1;
}
Запустим тест.
MotorTest.TestStart : Failed
Expected: <System.InvalidOperationException>
But was: no exception thrown
Тест не проходит из-за того что не реализовано формирование исключения. Реализуем формирование исключения.
public void Start(DirectionOfRotation senseOfRotation)
{
    if (directionOfRotation == DirectionOfRotation.Stopped)
    {
        directionOfRotation = senseOfRotation;
        _currentSpeed = 1;
    }
    else
    {
        throw new InvalidOperationException();
    }
}
TestConstructorClassic, Success
TestConstructorConstraint, Success
TestStart, Success
Аналогично реализуем тесты и методы, оставшиеся еще не реализованными. Посмотреть код можно в прилагаемом проекте.
Ссылка на проект с тестами.

Полезные ссылки по NUnit (приложение):

PS Если какие либо вопросы остались для Вас не раскрыты или не понятны пишите в комментариях. С радостью дополню пост.