В предыдущем посте мы начали с бизнес-приложения с требованием:

Как пользователю, мне нужен способ ввода числа. После того, как номер был введен, мне нужно, чтобы он был распечатан обратно мне. Клиент любит приложение! Но есть несколько новых требований для нас:

Как пользователю, мне нужен способ ввода числа. После того, как номер был введен, мне нужно, чтобы он был распечатан обратно мне.

Если введено число, которое делится без остатка на 3, пользователь должен получить ответ «Шизз», а не число.

Если введено число, которое делится без остатка на 5, пользователь должен получить ответ «Buzz», а не число.

Требования теперь эквивалентны детской задаче и/или коду ката FizzBuzz.

Раньше в нашем коде была только одна ветвь логики. Получить номер, вернуть номер. Исходя из новых требований, мы видим, что будет еще несколько веток:

  1. число не делится без остатка ни на 3, ни на 5
  2. число без остатка делится на 3
  3. число без остатка делится на 5
  4. (не указано явно, но) число делится без остатка как на 3, так и на 5.

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

Наш оригинальный метод, который выглядел так:

public string ReturnNumberAsString(int numberToReturn)
{
    return numberToReturn.ToString();
}

Будет обновлено, чтобы теперь выглядеть так:

public string ReturnNumberAsString(int numberToReturn)
        {
            if (numberToReturn % 3 == 1 && numberToReturn % 5 == 1)
                return "FizzBuzz";
            else if (numberToReturn % 3 == 1)
                return "Fizz";
            else if (numberToReturn % 5 == 1)
                return "Buzz";
 
            return numberToReturn.ToString();
        }

Теперь имейте в виду, что все наши модульные тесты из прошлого раунда продолжают проходить, поскольку данные, которые использовались для тестирования метода, продолжают проходить с нашей новой реализацией. Это часто упоминается как потенциальная ловушка модульного тестирования и изменения требований — и, похоже, это серьезное беспокойство! Наши модульные тесты продолжают проходить, когда требования намного сложнее, чем раньше. Вот почему так важно учитывать как модульные тесты (и их утверждения), так и покрытие кода. Всегда существует вероятность того, что модульные тесты *не* сломаются из-за появления новых ветвей в вашем коде. НО, если вы взглянете на покрытие кода, особенно на покрытие кода применительно к нашему методу, вы увидите, что не все ветви кода покрываются модульными тестами.

Как вы можете видеть на приведенном выше снимке экрана, наше покрытие кода, указанное в процентах, и фиолетовый и желтоватый текст уменьшились. 3 новые ветки в нашем коде в настоящее время не покрываются модульными тестами. Вот репозиторий на момент обновления метода, но без модульных тестов для покрытия требований: https://github.com/Kritner/UnitTestingBusinessValue/tree/bb1f9bda9250fbdb85a8737c0c006f06e6daa788. Теперь напишем несколько модульных тестов:

        /// <summary>
        /// number mod 3 and 5 returns FizzBuzz
        /// number mod 3 returns Fizz
        /// number mod 5 returns Buzz
        /// </summary>
        [TestMethod]
        public void NumberReturner_ReturnNumberAsString_SpecialCasesReturnValid()
        {
            // Arrange
            NumberReturner rt = new NumberReturner();
            int modThree = 9;
            int modFive = 10;
            int modThreeAndFive = 15;
 
            // Act
            var resultsModThree = rt.ReturnNumberAsString(modThree);
            var resultsModFive = rt.ReturnNumberAsString(modFive);
            var resultsModThreeAndFIve = rt.ReturnNumberAsString(modThreeAndFive);
 
            // Assert
            Assert.AreEqual("Fizz", resultsModThree, nameof(resultsModThree));
            Assert.AreEqual("Buzz", resultsModFive, nameof(resultsModFive));
            Assert.AreEqual("FizzBuzz", resultsModThreeAndFIve, nameof(resultsModThreeAndFIve));
        }

Хм. В настоящее время у нас есть провальный тест. Fizz не возвращается из resultsModThree, а вместо этого 9. Давайте посмотрим, что здесь происходит.

Ой. Похоже, я непреднамеренно допустил ошибку в реализации требования №2.

if (numberToReturn % 3 == 1 && numberToReturn % 5 == 1)
    return "FizzBuzz";
else if (numberToReturn % 3 == 1)
    return "Fizz";
else if (numberToReturn % 5 == 1)
    return "Buzz";

Должны были быть:

if (numberToReturn % 3 == 0 && numberToReturn % 5 == 0)
    return "FizzBuzz";
else if (numberToReturn % 3 == 0)
    return "Fizz";
else if (numberToReturn % 5 == 0)
    return "Buzz";

Теперь, когда мы исправили код, наш новый модульный тест проходит успешно. Но наш исходный модульный тест:

// Arrange
int expected = 42;
NumberReturner biz = new NumberReturner();
 
// Act
var results = biz.ReturnNumberAsString(expected);
 
// Assert
Assert.AreEqual(expected.ToString(), results);

Сейчас терпит неудачу. Конечно, это так — «42 % 3 равно 0», поэтому мы фактически получили Fizz для 42. Вместо этого мы обновили этот тест, чтобы получить ожидаемое значение 7.

Что все это значит? Наши модульные тесты и помогли нам, и навредили нам в этом сценарии. Они помогли нам, потому что помогли определить, что у меня была логическая ошибка при реализации требования. Они причинили нам боль, потому что у нас был ложноположительный результат. Вот почему так важно, чтобы утверждения модульных тестов были релевантными, а покрытие кода оставалось высоким. Без сочетания обоих этих факторов ценность тестов для бизнеса будет менее значительной. Обновленная реализация и логика: https://github.com/Kritner/UnitTestingBusinessValue/tree/78f03b8550593b9576f28e8608561f4add989879

В сценарии без модульного тестирования вполне вероятно, что нашу бизнес-логику можно будет протестировать только через пользовательский интерфейс. Тестирование пользовательского интерфейса намного громоздче, медленнее и труднее воспроизвести последовательно. Представьте, что после каждого изменения нашей логики нам приходилось снова и снова тестировать все ветки через UI. Это, вероятно, означает компиляцию, запуск, вход в приложение, переход к вашей логике для тестирования и т. д. О, а затем повторите это еще три раза (из-за этой **простой** логики приложения). Это еще одна причина, по которой модульное тестирование настолько эффективно. Поскольку мы продолжаем вносить изменения в наш код, мы можем гарантировать, что вносимые нами изменения не повлияют на систему так, как мы этого не ожидаем. И мы делаем это за долю времени, которое потребовалось бы, чтобы сделать то же самое с помощью ручного тестирования пользовательского интерфейса.

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

По теме: В чем ценность модульного тестирования для бизнеса — часть 1

Первоначально опубликовано на сайте kritner.blogspot.com 22 февраля 2018 г.