Множественные Assertion’ы без прерываний в одном юнит-тесте на примере NUnit

от автора

В практике юнит-тестирования часто возникает желание сделать несколько Assertion’ов в одном тест-методе. В теории же, такой подход критикуется с двух основных позиций. Во-первых, семантически, один тест должен проверять только один кейс, не разрастаться. Во-вторых, при падении одного из Assertion’ов в цепочке, выполнение теста прервется и мы увидим сообщение об ошибке лишь от него, а до всех остальных дело не дойдет, что не даст наиболее полной картины произошедшего. Первый аргумент безусловно резонен и при написании тестов его всегда следует держать в голове, но фанатичное следование этому принципу зачастую не представляется разумным (пример далее). Устранению же второй проблемы посвящен этот пост. Будет представлен небольшой класс, позволяющий просто и лаконично обеспечить исполнение нескольких Assertion’ов без прерывания выполнения метода и с выводом сообщения об ошибке каждого из них.

Итак, предположим, у нас есть класс Size, который, помимо прочего, принимает параметром конструктора значение в дюймах, а в себе содержит аксессоры для получения количества целых футов и оставшихся дюймов, т.е., передав на вход 16, мы получим 1 фут и 4 дюйма (в одном футе 12 дюймов).

public class Size {     public int Feet { get; private set; }     public int RemainderInches { get; private set; }      public Size(int totalInches)     {         // код конструктора     }      //... } 

Чтобы не растекаться тестами конструктора по древу и вместе с тем обеспечить годное покрытие хочется написать что-то вроде:

[Test] public void ConstructorSuccess() {     var zeroSize = new Size(0);     var inchesOnlySize = new Size(2);     var mixedSize = new Size(15);     var feetOnlySize = new Size(36);      Assert.That(zeroSize.Feet == 0 && zeroSize.RemainderInches == 0, "Zero size");     Assert.That(inchesOnlySize.Feet == 0 && inchesOnlySize.RemainderInches == 2, "Inches-only size");     Assert.That(mixedSize.Feet == 1 && mixedSize.RemainderInches == 3, "Inches and feet size");     Assert.That(feetOnlySize.Feet == 3 && feetOnlySize.RemainderInches == 0, "Feet-only size"); } 

Disclaimer: Вместо одной проверки на истинность можно (и даже хорошо бы) использовать по две проверки на равенство, но в данном коротком примере это не принципиально, а код бы усложнило.

Ясно, что если выделять по методу на каждый такой Assertion, то наш тест-класс очень быстро обрастет огромным числом методов и в реальности, в результате, будем иметь сотни тестов, но толку не больше. Однако, в показанном подходе, как уже было сказано, при падении одного из Assertion’ов данных от остальных мы не увидим, т.к. выполнение метода остановится.

Приступим к устранению этого неудобства.

В NUnit падение теста происходит при возникновении любого непойманного Exception’a, а сам класс Assert при неудаче бросает AssertionException с полными сообщениями об ошибках. Таким образом, по сути, нам нужно обеспечить отлов исключений на протяжении тест-метода, накапливание их сообщений и вывод накопленного в конце. Естественно, что заниматься этим явно, прямо в коде самого теста — страшный ужас.

После некоторых размышлений, для этих целей был предложен класс-аккумулятор, использование которого внутри тест-метода из примера выше выглядит следующим образом:

var assertsAccumulator = new AssertsAccumulator();  assertsAccumulator.Accumulate(     () => Assert.That(zeroSize.Feet == 0 && zeroSize.RemainderInches == 0, "Zero size")); assertsAccumulator.Accumulate(     () => Assert.That(inchesOnlySize.Feet == 0 && inchesOnlySize.RemainderInches == 2, "Inches-only size")); assertsAccumulator.Accumulate(     () => Assert.That(mixedSize.Feet == 1 && mixedSize.RemainderInches == 3, "Inches and feet size")); assertsAccumulator.Accumulate(     () => Assert.That(feetOnlySize.Feet == 3 && feetOnlySize.RemainderInches == 0, "Feet-only size"));  assertsAccumulator.Release(); 

Другой пример использования (надеюсь, код говорит сам за себя и понятен без комментариев):

Result<User> signInResult = authService.SignIn(TestUsername, TestPassword);  var assertsAccumulator = new AssertsAccumulator(); assertsAccumulator.Accumulate(() => Assert.That(signInResult.IsSuccess)); assertsAccumulator.Accumulate(() => Assert.That(signInResult.Value, Is.Not.Null)); assertsAccumulator.Accumulate(() => Assert.That(signInResult.Value.Username, Is.EqualTo(TestUsername))); assertsAccumulator.Accumulate(() => Assert.That(signInResult.Value.Password, Is.EqualTo(HashedTestPassword))); assertsAccumulator.Release(); 

Результат выполнения этого примера с выводом двух ошибок одновременно показан на завлекающем скрине в начале поста.

Реализация AssertsAccumulator’a выглядит так:

public class AssertsAccumulator {     private StringBuilder Errors { get; set; }     private bool AssertsPassed { get; set; }      private String AccumulatedErrorMessage     {         get         {             return Errors.ToString();         }     }      public AssertsAccumulator()     {         Errors = new StringBuilder();         AssertsPassed = true;     }      private void RegisterError(string exceptionMessage)     {         AssertsPassed = false;         Errors.AppendLine(exceptionMessage);     }      public void Accumulate(Action assert)     {         try         {             assert.Invoke();         }         catch (Exception exception)         {             RegisterError(exception.Message);         }     }      public void Release()     {         if (!AssertsPassed)         {             throw new AssertionException(AccumulatedErrorMessage);         }     } } 

Как видно, наружу выставлены лишь два метода, Accumulate() и Release(), использование которых довольно прозрачно. Прием делегата методом Accumulate делает класс очень универсальным, можно передавать любые виды Assertion’ов (как и показано в примере с signInResult) и при необходимости класс можно очень легко адаптировать под любой другой тестовый фреймворк сменив только тип бросаемого Exception’a внутри Release().

Из примеров видно, что класс позволяет удобным образом писать тест-методы, содержащие в себе несколько Assertion’ов, при этом выполняющиеся всегда до конца и имеющие полный вывод информации об ошибках.

В заключение хочется напомнить, что фанатичное следование какому-либо принципу редко является чем-то хорошим, и чрезмерно использование такого класса — не исключение. Нужно понимать, что использовать его можно только тогда, когда несколько Assertion’ов действительно проверяют одну семантическую изолированную операцию или сценарий и размещение их в одном тесте оправдано.

ссылка на оригинал статьи http://habrahabr.ru/post/164479/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *