В практике юнит-тестирования часто возникает желание сделать несколько 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/
Добавить комментарий