В прошлый раз разобрали самые простые варианты реализации тестов с использованием фреймворка NUnit. Сегодня остановлюсь на несколько более сложных примерах с использованием стандартных атрибутов NUnit: SetUp, ExpectedException и Ignore. В существующий проект добавим класс Account: [code lang=CSharp] 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; } } } [/code] Напишем первый тест. Для этого добавим класс AccountTest: [code lang=CSharp] [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); } } [/code] Атрибут TestFixture означает, что класс содержит тесты. Класс AccountTest должен быть public и содержать конструктор по-умолчанию. Единственный метод класса TransferFunds помечен атрибутом Test. Данным атрибутом помечаются все тестовые случаи. Тестовый метод должен иметь тип void и не должен принимать параметры. В нашем примере мы инициализируем тестовые объекты, выполняем бизнес-логику и проверяем состояние тестового объекта. Класс Assert содержит множество методов для проверки результатов работы. В нашем тесте воспользуемся методом AreEqual для того, чтобы проверить, что счета, которые участвовали в транзакции имеют корректный баланс. Сохраним проект. Далее необходимо выполнить вновь созданный тест в меню Test | Run | All Tests либо комбинацией Ctrl + R + A. Внезапно получили сообщение: [code lang=CSharp] TransferFunds : expected <250> but was <150> [/code] Это говорит о том, что в код закралась ошибка. Очевидно, проблема лишь в том, что метод TransferFunds не реализован. Исправим это и запустим тесты еще раз: [code lang=CSharp] public void TransferFunds(Account destination, decimal amount) { destination.Deposit(amount); Withdraw(amount); } [/code] На этот раз тест проходит успешно. Добавим в класс Account чуть больше логики: пусть у каждого счета есть минимальный возможный баланс. [code lang=CSharp] private decimal minimumBalance = 10m; public decimal MinimumBalance { get{ return minimumBalance; } } [/code] Создадим исключение, которое будет выдаваться, если на счете не достаточно средств, чтобы совершить транзакцию: [code lang=CSharp] public class InsufficientFundsException : ApplicationException { } [/code] Добавим новый тест в наш класс AccountTest: [code lang=CSharp] [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); } [/code] Атрибут ExpectedException, которым помечен новый тест, говорит о том, что этот тест будет считаться пройденным успешно, если в ходе его выполнения будет сгенерировано исключение типа InsufficientFundsException. В очередной раз сохраним проект и выполним тесты. Новый тест завершился с ошибкой TransferWithInsufficentFunds : InsufficientFundsException was expected - исключения не было. Исправим метод TransferFunds для того, чтобы учитывались новые требования про минимально допустимый баланс: [code lang=CSharp] public void TransferFunds(Account destination, decimal amount) { destination.Deposit(amount); if(balance - amount < minimumBalance) throw new InsufficientFundsException(); Withdraw(amount); } [/code] Еще раз запустим наши тесты. В этот раз все должно завершиться удачно. Если нет, то, вероятно, вы где-то совершили ошибку. Если присмотреться внимательнее к коду выше, то можно заметить, что на каждой транзакции банк будет терять деньги, если она завершится неудачей. Напишем тест, который проверит данное утверждение: [code lang=CSharp] [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); } [/code] Как и ожидали, тест упал на проверке Assert.AreEqual(150m, destination.Balance); с сообщением о том, что фактическое значение destination.Balance - 450, а ожидаемое - 150. Исправим метод так, чтобы данный тест проходил. Для этого необходимо проверять баланс счетов до выполнения транзакции: [code lang=CSharp] public void TransferFunds(Account destination, decimal amount) { if(balance - amount < minimumBalance) throw new InsufficientFundsException(); destination.Deposit(amount); Withdraw(amount); } [/code] Теперь все тесты проходят успешно. Однако, стоит задуматься, о методе Withdraw. Что будет, если этот метод выдаст какое-то другое исключение? Об этом будем думать позже, а сейчас добавим заглушку для тестирования метода Withdraw: [code lang=CSharp] [Test] [Ignore(“Decide how to implement transaction management”)] // пропустить тест public void TransferWithInsufficientFundsAtomicity() { // будут написаны тысячи строк кода здесь } [/code] Атрибут Ignore говорит о том, что данный тест будет игнорироваться и не будет помечаться в Test Explorer как пройденный или проваленный - в списке тестов будет отображаться сообщение, которое указано параметром к этому методу.