Изучаю NUnit. Часть 2

В прошлый раз разобрали самые простые варианты реализации тестов с использованием фреймворка NUnit.

Сегодня остановлюсь на несколько более сложных примерах с использованием стандартных атрибутов NUnit: SetUp, ExpectedException и Ignore.

В существующий проект добавим класс Account:

  public class Account
  {
    private decimal balance;

    public void Deposit(decimal amount)
    {
      balance += amount;
    }

    public void Withdraw(decimal amount)
    {
      balance -= amount;
    }

    public void TransferFunds(Account destination, decimal amount)
    {
    }

    public decimal Balance
    {
      get { return balance; }
    }
  }

Напишем первый тест. Для этого добавим класс AccountTest:

  [TestFixture]
  public class AccountTest
  {
    [Test]
    public void TransferFunds()
    {
      Account source = new Account();
      source.Deposit(200m);

      Account destination = new Account();
      destination.Deposit(150m);

      source.TransferFunds(destination, 100m);

      Assert.AreEqual(250m, destination.Balance);
      Assert.AreEqual(100m, source.Balance);
    }
  }

Атрибут TestFixture означает, что класс содержит тесты. Класс AccountTest должен быть public и содержать конструктор по-умолчанию.

Единственный метод класса TransferFunds помечен атрибутом Test. Данным атрибутом помечаются все тестовые случаи.

Тестовый метод должен иметь тип void и не должен принимать параметры. В нашем примере мы инициализируем тестовые объекты, выполняем бизнес-логику и проверяем состояние тестового объекта. Класс Assert содержит множество методов для проверки результатов работы. В нашем тесте воспользуемся методом AreEqual для того, чтобы проверить, что счета, которые участвовали в транзакции имеют корректный баланс.

Сохраним проект. Далее необходимо выполнить вновь созданный тест в меню Test | Run | All Tests либо комбинацией Ctrl + R + A.

Внезапно получили сообщение:

TransferFunds : expected <250> but was <150>

Это говорит о том, что в код закралась ошибка.

Очевидно, проблема лишь в том, что метод TransferFunds не реализован. Исправим это и запустим тесты еще раз:

public void TransferFunds(Account destination, decimal amount)
{
  destination.Deposit(amount);
  Withdraw(amount);
}

На этот раз тест проходит успешно.

Добавим в класс Account чуть больше логики: пусть у каждого счета есть минимальный возможный баланс.

private decimal minimumBalance = 10m;

public decimal MinimumBalance
{
  get{ return minimumBalance; }
}

Создадим исключение, которое будет выдаваться, если на счете не достаточно средств, чтобы совершить транзакцию:

public class InsufficientFundsException : ApplicationException
{
}

Добавим новый тест в наш класс AccountTest:

[Test]
[ExpectedException(typeof(InsufficientFundsException))]
public void TransferWithInsufficientFunds()
{
  Account source = new Account();
  source.Deposit(200m);

  Account destination = new Account();
  destination.Deposit(150m);

  source.TransferFunds(destination, 300m);
}

Атрибут ExpectedException, которым помечен новый тест, говорит о том, что этот тест будет считаться пройденным успешно, если в ходе его выполнения будет сгенерировано исключение типа InsufficientFundsException.

В очередной раз сохраним проект и выполним тесты. Новый тест завершился с ошибкой TransferWithInsufficentFunds : InsufficientFundsException was expected – исключения не было. Исправим метод TransferFunds для того, чтобы учитывались новые требования про минимально допустимый баланс:

public void TransferFunds(Account destination, decimal amount)
{
  destination.Deposit(amount);

  if(balance - amount < minimumBalance)
    throw new InsufficientFundsException();

  Withdraw(amount);
}

Еще раз запустим наши тесты. В этот раз все должно завершиться удачно. Если нет, то, вероятно, вы где-то совершили ошибку.

Если присмотреться внимательнее к коду выше, то можно заметить, что на каждой транзакции банк будет терять деньги, если она завершится неудачей.

Напишем тест, который проверит данное утверждение:

[Test]
public void TransferWithInsufficientFundsAtomicity()
{
  Account source = new Account();
  source.Deposit(200m);

  Account destination = new Account();
  destination.Deposit(150m);

  try
  {
    source.TransferFunds(destination, 300m);
  }
  catch(InsufficientFundsException expected)
  {
  }

  Assert.AreEqual(200m, source.Balance);
  Assert.AreEqual(150m, destination.Balance);
}

Как и ожидали, тест упал на проверке Assert.AreEqual(150m, destination.Balance); с сообщением о том, что фактическое значение destination.Balance – 450, а ожидаемое – 150.

Исправим метод так, чтобы данный тест проходил. Для этого необходимо проверять баланс счетов до выполнения транзакции:

public void TransferFunds(Account destination, decimal amount)
{
  if(balance - amount < minimumBalance)
    throw new InsufficientFundsException();

  destination.Deposit(amount);

  Withdraw(amount);
}

Теперь все тесты проходят успешно.

Однако, стоит задуматься, о методе Withdraw. Что будет, если этот метод выдаст какое-то другое исключение? Об этом будем думать позже, а сейчас добавим заглушку для тестирования метода Withdraw:

[Test]
[Ignore("Decide how to implement transaction management")] // пропустить тест
public void TransferWithInsufficientFundsAtomicity()
{
  // будут написаны тысячи строк кода здесь
}

Атрибут Ignore говорит о том, что данный тест будет игнорироваться и не будет помечаться в Test Explorer как пройденный или проваленный – в списке тестов будет отображаться сообщение, которое указано параметром к этому методу.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s