Гостевая статья Краткое введение в Code Katas, TDD и Red-Green-Refactor

  • Автор темы Автор темы Zer0must2b
  • Дата начала Дата начала
Каждые несколько месяцев выполняет код kata du jour. Все объединяются и используют общие практики, такие как TDD и красно-зеленый-рефакторинг. Пару недель назад я был партнером с кем-то, кто раньше этого не делал, поэтому у меня была возможность поговорить с ним через это.

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

Давайте сначала определим некоторые термины
Так что же такое юнит-тестирование, TDD, красно-зелёный и т. Д., И прочие глупости, которые я только что упомянул?

(Уровень опыта у всех разный, поэтому не стесняйтесь пропускать все, что вам уже знакомо.)

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

i-dont-always-test-but-when-i-do-i-test-in-prod.png



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

Итак, мы включили компьютер в работу, тестируя себя. Эти автоматизированные тесты бывают самых разных типов - от тестирования одного компонента до полного тестирования всей системы, тестирования только пользовательского интерфейса, нагрузочного тестирования и т. Д.

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

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

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

Давайте рассмотрим пример ...

Вам было поручено создать калькулятор, и первая функция - это возможность «Разделить». Метод берет два числа, делит их и возвращает частное. Новаторский, верно?

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

Код:
public class Calculator
{
    public decimal Divide(decimal dividend, decimal divisor)
    {
        return dividend / divisor;
    }
}


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

Дело в том, что метод «Разделить» сейчас терпит неудачу, и никто не знает точно, почему, или даже когда он мог начаться. Ваши пользователи любят складывать и вычитать, но они редко делятся. Вся ваша прекрасная работа, недооцененная. :п

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

Пришло время начать ** автоматизацию ваших тестов **. Вы можете добавить второй класс, полный методов, которые создают экземпляр Calculator, пропускают через него несколько чисел и возвращают значение, указывающее, совпадают ли ожидаемый ответ и фактический ответ. Следующие два «тестовых» метода возвращают true, если ответы совпадают; ложь в противном случае.

Код:
public class CalculatorTests
{
    Calculator c = new Calculator();
    public bool Is6DividedBy3EqualTo2()
    {
        var quotient = c.Divide(6, 3);
        if (quotient == 2)
            return true;
        else
            return false;
    }
    public bool Is9DividedBy2EqualTo4Point5()
    {
        var quotient = c.Divide(9, 2);
        if (quotient == 4.5m)
            return true;
        else
            return false;
    }
}


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

Вот консольное приложение, которое делает именно это, но может быть трудно следовать, если вы не знакомы с C #, и, конечно, вы не рекомендовали запускать тесты, поэтому не тратьте на это слишком много времени. :)

Код:
public class Program
{
    public static void Main()
    {
        var cTests = new CalculatorTests();

        var failedTests = new List<string>();

        // using reflection, run every test method and record the names of those methods that fail
        foreach (var m in typeof (CalculatorTests).GetMethods()
                                                  .Where(m => m.DeclaringType != typeof (object)))
        {
            if (Convert.ToBoolean(m.Invoke(cTests, null)) != true)
                failedTests.Add(m.Name);
        }

        // display all the failed tests, or a message that everything passed
        if (failedTests.Any())
            Console.WriteLine("Failed Tests: \r\n\r\n{0}", string.Join("\r\n", failedTests));
        else
            Console.WriteLine("All tests passed!");

        Console.ReadLine();
    }
}

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

tests-passed-failed-console.png



Так что это лучше, чем руководство, но все же смешно.

Вместо этого вы должны использовать зрелые инструменты тестирования, такие как и (языки .NET, такие как C #, F #, VB.NET), (Java), (Ruby) и т. Д. Существуют платформы для различных типов тестов почти в каждом язык, и они облегчают выполнение тестов (по отдельности или все сразу), и расскажут вам больше о том, что конкретно может пойти не так.

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

Что такое TDD и Red-Green-Refactor?

В последнем разделе мы сначала написали метод «Разделить», а затем написали тесты, чтобы проверить его намного позже. Это распространено в устаревшем коде, который изначально был написан без каких-либо тестов.

TDD, или разработка, управляемая тестами, решает эту проблему. Мы пишем тесты * перед * написанием кода. По сути, тесты должны вести программу, указав, что должен делать код.

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

Код:
public decimal Divide(decimal dividend, decimal divisor)
{
    return 0;
}


Теперь мы напишем тест, в котором будет точно указано, что мы ожидаем от этого кода . Теперь я собираюсь перейти на синтаксис NUnit, который должен быть достаточно простым, чтобы следовать, но с большей вероятностью будет соответствовать тому, что вы могли бы увидеть, когда проводите собственное тестирование. NUnit доступен в Visual Studio через NuGet.

Конечно, будучи программистами, мы зацикливаемся на каждой детали, включая то, как мы называем наши тесты. Существуют разные мнения, и вы можете просмотреть несколько и , но я буду придерживаться соглашения об именах, которое определяет, что мы тестируем, ожидаемый результат и когда этот результат должен произойти (aka MethodName_ExpectedBehavior_StateUnderTest во второй статье, указанной выше) ,

Код:
[TestFixture]
public class CalculatorTests
{
    Calculator c;

    [SetUp]
    public void Setup()
    {
        c = new Calculator();
    }

    [Test]
    public void Divide_Returns2_WhenDividing6By3()
    {
        var quotient = c.Divide(6, 3);

        Assert.IsTrue(quotient == 2);
    }

    [Test]
    public void Divide_Returns4_5_WhenDividing9By2()
    {
        var quotient = c.Divide(9, 2);

        Assert.IsTrue(quotient == 4.5m);
    }
}


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

Пара быстрых заметок, касающихся приведенного выше кода ...

Метод, помеченный атрибутом «SetUp», запускается перед каждым тестом. Создавая новый экземпляр Calculator перед каждым тестом, мы изолируем наши тесты друг от друга (помните, изоляция хороша - мы не хотим, чтобы один тест изменял некоторые значения в одиночном экземпляре Calculator, а затем следующий тест не выполнялся из-за эти измененные значения).

Кроме того, методы больше не возвращают значение. Класс «Assert» и его методы фиксируют результаты теста и сообщают нам об этом. Большинство тестирующих библиотек имеют встроенные аналогичные методы.

Вышесказанное можно еще больше сократить в NUnit, используя атрибут «TestCase» для объединения похожих тестов. Это не имеет отношения к обсуждению TDD, но я включу его сюда, если вам интересно. Метод теста был обновлен, чтобы принимать параметры, которые мы передаем при запуске тестов.

Код:
[TestCase(6, 3, 2,   Description = "6 / 3 = 2")]
[TestCase(9, 2, 4.5, Description = "9 / 2 = 4.5")]
public void Divide_ReturnsExpectedQuotient(decimal dividend, decimal divisor, decimal expectedQuotient)
{
    var actualQuotient = c.Divide(dividend, divisor);

    Assert.AreEqual(expectedQuotient, actualQuotient);
}


Термин «красный-зеленый-рефактор» тесно связан с TDD.

Когда мы впервые запустим наши модульные тесты, тесты пройдут неудачно. Метод «Разделить» возвращает 0 во всех случаях, поэтому исходное состояние наших тестов - красный. Обратите внимание, как NUnit сообщает, каковы были ожидаемые и фактические значения, а также предоставляет трассировку стека и некоторую другую полезную информацию.

tests-failed-1.png


Теперь мы можем исправить оригинальный метод «Разделить», изменив его так, чтобы он снова возвращал дивиденд / делитель ; Теперь тесты проходят (и отображаются зелёным цветом):

tests-pass-1.png


Хорошо, теперь новое требование приходит от пользователей. Если коэффициент отрицательный, сделайте его положительным, прежде чем возвращать его. Это странно, но, к счастью, вы тот тип, который плывет по течению.

Давайте напишем еще один тест, чтобы указать ожидаемое поведение (-10 / 5 должно возвращать 2, а не -2) и посмотрим, как провалились первые два теста:

Код:
[TestCase(-10,  5, 2, Description = "-10 / 5 = 2")]
[TestCase( 10, -5, 2, Description = "10 / -5 = 2")]
[TestCase(-10, -5, 2, Description = "10 / 5 = 2")]
public void Divide_ReturnsPositiveQuotient_WhenInput(decimal dividend, decimal divisor, decimal expectedQuotient)
{
    var actualQuotient = c.Divide(dividend, divisor);

    Assert.GreaterOrEqual(actualQuotient, 0);
}

tests-failed-2.png

Теперь мы исправим код, чтобы тесты снова прошли (зеленый).

Код:
public decimal Divide(decimal dividend, decimal divisor)
{
    // make negative dividends positive
    if (dividend < 0)
        dividend = -dividend;
   
    // make negative divisors positive
    if (divisor < 0)
        divisor = -divisor;

    return dividend / divisor;
}

Часть рефакторинга может прийти в любой момент, когда наши тесты проходят.

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

Замените приведенный выше код на предоставляемую .NET функцию Math.Abs и снова запустите тесты. Они проходят, поэтому изменения ничего не сломали.

Код:
public decimal Divide(decimal dividend, decimal divisor)
{
    return Math.Abs(dividend / divisor);
}

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

Я пройду еще один.

Теперь кто-то приходит и говорит: «Ух ты, посмотри, что произойдет, если я разделю на 0!» И некоторые котята попадают в закрученный вихрь. Симпатичные. Действительно неудачно.

divide-by-zero.jpg


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

Код:
[TestCase(5,  Description = "5 / 0 = 0")]
[TestCase(0,  Description = "0 / 0 = 0")]
[TestCase(-5, Description = "-5 / 0 = 0")]
[Test]
public void Divide_ReturnsZero_WhenDivisorIsZero(decimal input)
{
    var actualQuotient = c.Divide(input, 0);

    Assert.AreEqual(0, actualQuotient);
}

tests-failed-3.png


Проверьте «сообщение» выше. Новые тесты не прошли (я свернул прохождение тестов), потому что оригинальный метод вызвал DivideByZeroException.

Время снова стать зеленым. Мы * можем * поймать это конкретное исключение, но (по крайней мере, в мире .NET) исключения являются дорогостоящими, и лучше по возможности их предотвратить. В конце концов, если вы можете предвидеть условие и код вокруг него, это не так уж и исключительно.

Код:
public decimal Divide(decimal dividend, decimal divisor)
{
    if (divisor == 0)
        return 0;

    return Math.Abs(dividend / divisor);
}

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

tests-pass-2.png


Ясно как грязь? Если вам нужны разъяснения, оставьте комментарий ниже! ( .)

Что такое парное программирование?
Парное программирование - это то, на что это похоже. Одна голова хорошо, а две - лучше.

Если вы когда-либо обращались за помощью к коллеге-программисту, а затем вы оба сидели вместе, решая проблему, вы запрограммировали пару. Есть два способа, которыми люди видят это:
  • Некоторые люди считают это пустой тратой ресурсов - две зарплаты с половиной выработки
  • Другие люди видят в этом совместную работу - вместо того, чтобы два человека застряли на отдельных задачах или отвлеклись на последние видеофильмы о кошках, они могут обмениваться идеями друг с другом и продолжать двигаться вперед.
Некоторые адвокаты доводят это до крайности и ничего не делают, кроме парного программирования, каждый день. Я никогда не пробовал, поэтому я не могу много говорить об этом. За исключением того, что я надеюсь, что никто не будет в паре с Уолли.

dilbert-pair-programming.gif


Как это все сочетается с Code Katas?
Когда вы объединяетесь во время ката кода, вы учитесь друг у друга и обсуждаете проблемы по мере их возникновения.

В основном вы будете «пинг-понг» взад и вперед, следуя схеме, подобной следующей:

  1. Вы пишете модульный тест, который не проходит ( красный ), а затем передаете контроль над клавиатурой своему партнеру.
  2. Она изменяет код, чтобы сделать ваш тест успешным ( зеленый ), затем пишет свой собственный модульный тест (чтобы указать, что программа должна делать дальше), который также не проходит ( снова красный ). Она передает вам контроль.
  3. Вы пишете больше кода для прохождения ее теста ( зеленый ), а затем пишете следующий (провальный) тест ( красный! ). И так далее…
Это продолжается, пока не истечет время. Правильно, обычно есть ограничение по времени. Это может быть полчаса, или, может быть, продолжительность встречи группы пользователей.

Видите ли, вы на самом деле не стремитесь «завершить» ката, хотя с некоторыми более короткими вы наверняка это сделаете. Вы больше сосредоточены на следовании дисциплинированному процессу:
  1. Что следующая наша программа должна уметь делать?
  2. Какой тип теста мы можем написать, чтобы отразить следующее требование?
  3. Тест не пройден. Как мы можем изменить код, чтобы пройти тест? (требование выполнено)
  4. Тест проходит. Как мы можем реорганизовать код, чтобы сделать его более эффективным? (убедитесь, что тесты все еще проходят)
  5. Повторение
Можем ли мы пройти через образец ката?
Конечно. Рад, что ты спросил.

Популярным (и коротким) является ката Fizz Buzz: (есть еще много на )
Напишите программу, которая печатает числа от 1 до 100. Но для кратных трех выведите «Fizz» вместо числа и для кратных пяти выведите «Buzz». Для чисел, кратных трем и пяти, выведите «FizzBuzz».
Это помогает перечислить требования, если ката это еще не делает:
  • Выведите числа от 1 до 100.
  • Если число кратно 3, вместо этого выведите «Fizz».
  • Если число кратно 5, вместо этого выведите «Buzz».
  • Если число кратно 3 * и * 5, вместо этого выведите «FizzBuzz».
Теперь у нас есть 4 различных шага, и мы можем начать писать тесты для них.

Шаг 1: вернуть тот же номер

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

Код:
public class FizzBuzz
{
    public string FizzyOutput(int input)
    {
        return "";
    }
}

Давайте просто проверим 1 и 2, чтобы начать. Конечно, это не получается, потому что мы всегда возвращаем пустую строку:

Код:
[TestFixture]
public class FizzBuzzTests
{
    private FizzBuzz fizzBuzz;

    [SetUp]
    public void Setup()
    {
        fizzBuzz = new FizzBuzz();
    }

    [Test]
    public void FizzyOutput_OutputsOne_WhenInputIsOne()
    {
        var output = fizzBuzz.FizzyOutput(1);

        Assert.AreEqual("1", output);
    }

    [Test]
    public void FizzyOutput_OutputsTwo_WhenInputIsTwo()
    {
        var output = fizzBuzz.FizzyOutput(2);

        Assert.AreEqual("2", output);
    }
}

fizzbuzz-test-failed-1.png


Теперь вы передадите клавиатуру своей паре, чтобы исправить код и выполнить тест.

Код:
public string FizzyOutput(int input)
{
    return input.ToString();
}

Шаг 2: верните «Fizz» для кратных 3

Второе требование - напечатать «Fizz» для кратных 3. Ваш партнер может создать несколько тестов или с помощью инструмента, такого как NUnit, использовать атрибут TestCase. Запустите новый тест и посмотрите, как он провалится. В конце концов, мы все равно возвращаем одно и то же число, несмотря ни на что.

Код:
[TestCase(3)]
[TestCase(6)]
[TestCase(9)]
[Test]
public void FizzyOutput_OutputsFizz_WhenInputIsMultipleOfThree(int input)
{
    var output = fizzBuzz.FizzyOutput(input);

    Assert.AreEqual("Fizz", output);
}


fizzbuzz-test-failed-2.png


Чтобы это произошло, вы можете сделать что-то глупое, например, вернуть «Fizz» точно для указанных входных данных. Или используйте более практичный подход, который обрабатывает любое число, кратное 3. Всегда выполняйте тесты снова, когда закончите, чтобы убедиться, что они пройдены.

Код:
public string FizzyOutput(int input)
{
    if (input % 3 == 0)
        return "Fizz";

    return input.ToString();
}

Шаг 3: верните «Buzz» для кратных 5

Теперь вы пишете следующий тест, показывающий, что кратные 5 возвращают «Buzz», и снова проходите клавиатуру.

Код:
[TestCase(5)]
[TestCase(10)]
public void FizzyOutput_OutputsBuzz_WhenInputIsMultipleOfFive(int input)
{
    var output = fizzBuzz.FizzyOutput(input);

    Assert.AreEqual("Buzz", output);
}

Тест не пройден, и ваша пара исправляет его очень похоже на предыдущее требование.

Шаг 4: верните «FizzBuzz» для кратных 15

Последнее требование, и ваш партнер пишет тест для него .., кратный 3 * и * 5:

Код:
[TestCase(15)]
[TestCase(30)]
[TestCase(45)]
public void FizzyOutput_OutputsFizzBuzz_WhenInputIsMultipleOfThreeAndFive(int input)
{
    var output = fizzBuzz.FizzyOutput(input);

    Assert.AreEqual("FizzBuzz", output);
}

Ваш ход, чтобы закончить ката, и вы делаете это, проверяя на кратные 15:

Код:
public string FizzyOutput(int input)
{
    if (input % 15 == 0)
        return "FizzBuzz";
 
    if (input % 3 == 0)
        return "Fizz";
 
    if (input % 5 == 0)
        return "Buzz";
 
    return input.ToString();
}

Запустите тесты еще раз и убедитесь, что все они пройдены. Вот полный набор тестов, которые мы проводим.

fizzbuzz-test-passed-2.png


Хотя я думаю, что приведенных выше тестов достаточно, вы можете проверить каждое значение, просто чтобы быть уверенным:

Код:
[TestCase(1, "1")]
[TestCase(2, "2")]
[TestCase(3, "Fizz")]
[TestCase(4, "4")]
[TestCase(5, "Buzz")]
// ....
[TestCase(14, "14")]
[TestCase(15, "FizzBuzz")]
[TestCase(16, "16")]
// ...
[TestCase(95, "Buzz")]
[TestCase(96, "Fizz")]
[TestCase(97, "97")]
[TestCase(98, "98")]
[TestCase(99, "Fizz")]
[TestCase(100, "Buzz")]
public void FizzyOutput_OutputsExpectedValues(int input, string expectedOutput)
{
    var actualOutput = fizzBuzz.FizzyOutput(input);
 
    Assert.AreEqual(expectedOutput, actualOutput);
}




Последние мысли
На этом этапе мы могли бы снова провести рефакторинг, поскольку все тесты проходят успешно. Я не думаю, что есть еще много чего сделать. Ты видишь что-нибудь, что я пропустил? Опечатки?

Я использовал C #, потому что я наиболее знаком с ним, но вы могли бы соединиться с кем-то, кто знает другой язык. Я пару раз делал пару для создания каты в Ruby - никогда не повредит изучать что-то новое (и встречать кого-то нового!) И видеть, как другие программисты / языки подходят к тестированию.

mr-bean-pick-a-partner.jpg




Там нет давления бизнес-требований или сроков ... просто учиться у других и делиться знаниями. И, может быть, посмеяться, когда время истекло, и вы понимаете, что пришли к отвратительному решению (не то, что * такое * когда-либо случается).

Спасибо, что прочитали это далеко ... Я надеюсь, что вы узнали что-то новое или интересное.

Мысли? У вас есть вопросы или что-то добавить? Позвольте мне знать в комментариях ниже!

Источник:
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!