Статьи

Как написать модульный тест


На прошлой неделе я имел удовольствие участвовать в
Sela Developer Practice . Перед сессией я
проходил сессию Гила Зильберфельда «
7 шагов для написания вашего первого модульного теста », и я подумал — какие шаги я предпринимаю при написании нового модульного теста? Я писал их так долго и никогда не замечал, что следую «методологии» своего рода. И так без лишних слов — вот шаги, которые
я предпринимаю при написании нового модульного теста:

Тестируемый код

Для написания юнит-теста мне понадобится пример, и я придумал следующий сценарий:


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

Код, который мы хотим протестировать, выглядит примерно так (читай: точно):

public class BugTracker
{
    private readonly IBugRepository _repository;
    private readonly IEmailClient _emailClient;




    public BugTracker(IBugRepository repository, IEmailClient emailClient)
    {
        _repository = repository;
        _emailClient = emailClient;
    }




    public Bug CreatNewBug(string title, Severity severity)
    {
        if (string.IsNullOrEmpty(title))
        {
            throw new ApplicationException("Title cannot be empty");
        }




        var newBug = new Bug
             {
                 Title = title,
                 Severity = severity
             };




        SaveBugToDb(newBug);




        // Here be code        




        return newBug;
    }

И так как мы заядлые практики TDD (Test Driven Design), мы начнем с написания теста в первую очередь.

Решите, что вы тестируете

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

Я обнаружил, что называние метода тестирования таким образом, что заставляет меня (и моих коллег-разработчиков) думать о том, что они собираются делать:

[TestFixture]
public class BugTrackerTests
{
    [Test]
    public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
    {
        
    }
}

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

  1. Метод, который я тестирую — не описание, а сценарий, только название метода
  2. Сценарий, который я тестирую
  3. Что я ожидаю, что произойдет

Дополнительным преимуществом является то, что, когда этот тест не пройден — все, что мне нужно, это прочитать название теста, чтобы узнать, что пошло не так, — возьмите этот F5!

Существуют и другие похожие схемы именования — выберите одну и будьте настойчивы в этом.

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

Напишите тестируемый метод

Я начинаю с нуля и строю свой тест изнутри.

Сначала я создаю класс, который я тестирую, а затем я запускаю метод, который я тестирую.

[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
    var cut = new BugTracker();




    cut.CreatNewBug("my title", Severity.OhMyGod);
}

К сожалению, это даже не компилируется. Проблема в том, что мне нужно «накормить»
класс
BugTracker двумя зависимостями типа
IBugRepository и
IEmailClient — так что давайте добавим их в качестве основы Isolation (в данном случае FakeItEasy):

[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
    var fakeBugRepository = A.Fake<IBugRepository>();
    var fakeEmailClient = A.Fake<IEmailClient>();




    var cut = new BugTracker(fakeBugRepository, fakeEmailClient);




    cut.CreatNewBug("my title", Severity.OhMyGod);
}

И теперь мы можем написать фактическое утверждение.

Напишите утверждение

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

[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
    var fakeBugRepository = A.Fake<IBugRepository>();
    var fakeEmailClient = A.Fake<IEmailClient>();




    var cut = new BugTracker(fakeBugRepository, fakeEmailClient);




    cut.CreatNewBug("my title", Severity.OhMyGod);




    A.CallTo(() => fakeEmailClient.Send("manager@project.com", "Don't Panic!")).MustHaveHappened();
}

Запустить тест

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

Тест не удался, но по неправильной причине:
кажется, у меня есть код внутри метода «SaveBugToDb», который выдает исключение:

образ

private void SaveBugToDb(Bug newBug)
{
    if (!_repository.Connected)
    {
        throw new ApplicationException("Cannot access bug repository");
    }




    _repository.Save(newBug);
}

Это означает возвращаться к чертежной доске для нас.

Добавить больше кода

Чтобы сделать тест неудачным из-за правильной причины, я добавлю еще одну строчку, любезно предоставленную нашей платформой Isolation, чтобы убедиться, что вызов
Connected всегда будет возвращать true:

[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
    var fakeBugRepository = A.Fake<IBugRepository>();
    A.CallTo(() => fakeBugRepository.Connected).Returns(true);




    var fakeEmailClient = A.Fake<IEmailClient>();




    var cut = new BugTracker(fakeBugRepository, fakeEmailClient);




    cut.CreatNewBug("my title", Severity.OhMyGod);




    A.CallTo(() => fakeEmailClient.Send("manager@project.com", "Don't Panic!")).MustHaveHappened();
}

Запустите тест (снова)

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

Напишите код для прохождения теста

Это просто — просто добавьте код, который проходит тест:

public Bug CreatNewBug(string title, Severity severity)
{
    if (string.IsNullOrEmpty(title))
    {
        throw new ApplicationException("Title cannot be empty");
    }




    var newBug = new Bug
         {
             Title = title,
             Severity = severity
         };




    SaveBugToDb(newBug);




    if (severity == Severity.OhMyGod)
    {
        _emailClient.Send("manager@project.com", "Don't Panic!");
    }




    return newBug;
}

Вывод

  1. Так что мы сделали:
  2. Решили что тестировать
  3. Напишите тестируемый метод
  4. Добавить утверждение
  5. Запустить тест
  6. Добавить больше кода
  7. При необходимости повторите шаги 4,5
  8. Напишите код, который делает тест пройден

Несколько комментариев до конца:

  • В этом нет ничего нового, если вы писали модульные тесты в прошлом — вы, вероятно, следовали аналогичному процессу — мне просто нужно было написать явно.
  • Кажется, много шагов, чтобы создать один тест? Не отчаивайтесь — написание всех их не должно занять слишком много времени и, кроме того, — можете ли вы написать тест, не пройдя все их так или иначе.
  • Я действительно сторонник написания тестов перед кодом, но не волнуйтесь, этот метод без шага 7 будет работать, даже если вы пишете свои тесты задним числом.

Удачного кодирования …