Как правильно делать mock и unit test

Я в основном пытаюсь научиться программировать и хочу следовать передовым методам. Есть очевидные преимущества модульного тестирования. Когда дело доходит до модульного тестирования, также проявляется много фанатизма, и я предпочитаю гораздо более прагматичный подход к кодированию и жизни в целом. Что касается контекста, в настоящее время я пишу свое первое «настоящее» приложение, которое является повсеместным движком блогов, использующим asp.net MVC. Я вольно слежу за архитектурой MVC Storefront с моими собственными корректировками. Таким образом, это мой первый настоящий набег на издевательства над объектами. Я поставлю пример кода в конце вопроса.

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

Я пытаюсь понять ценность следующих тестов. UserService зависит от IUserRepository. Ценность уровня обслуживания состоит в том, чтобы отделить вашу логику от вашего хранилища данных, но в этом случае большинство вызовов UserService просто передаются прямо в IUserRepository. Тот факт, что реальной логики для тестирования не так много, тоже может быть источником моих опасений. У меня есть следующие опасения.

  • Такое ощущение, что код просто проверяет, работает ли фреймворк для фиксации.
  • Чтобы смоделировать зависимости, в моих тестах слишком много знаний о реализации IUserRepository. Это необходимое зло?
  • Какую ценность я получаю от этих тестов? Заставляет ли меня сомневаться в ценности этих тестов простота тестируемой услуги.

Я использую NUnit и Rhino.Mocks, но должно быть довольно очевидно, чего я пытаюсь достичь.

    [SetUp]
    public void Setup()
    {
        userRepo = MockRepository.GenerateMock<IUserRepository>();
        userSvc = new UserService(userRepo);
        theUser = new User
        {
            ID = null,
            UserName = "http://joe.myopenid.com",
            EmailAddress = "[email protected]",
            DisplayName = "Joe Blow",
            Website = "http://joeblow.com"
        };
    }

    [Test]
    public void UserService_can_create_a_new_user()
    {
        // Arrange
        userRepo.Expect(repo => repo.CreateUser(theUser)).Return(true);

        // Act
        bool result = userSvc.CreateUser(theUser);

        // Assert
        userRepo.VerifyAllExpectations();
        Assert.That(result, Is.True, 
          "UserService.CreateUser(user) failed when it should have succeeded");
    }

    [Test]
    public void UserService_can_not_create_an_existing_user()
    {
        // Arrange
        userRepo.Stub(repo => repo.IsExistingUser(theUser)).Return(true);
        userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);
        // Act
        bool result = userSvc.CreateUser(theUser);

        // Assert
        userRepo.VerifyAllExpectations();
        Assert.That(result, Is.False, 
            "UserService.CreateUser() allowed multiple copies of same user to be created");
    }

person Ben Robbins    schedule 24.01.2009    source источник


Ответы (4)


По сути, вы здесь тестируете то, что методы вызываются, а не работают ли они на самом деле. Это то, что должны делать насмешки. Вместо вызова метода они просто проверяют, был ли вызван метод, и возвращают все, что указано в операторе Return (). Итак, в вашем утверждении здесь:

Assert.That(result, Is.False, "error message here");

Это утверждение ВСЕГДА будет успешным, потому что ваше ожидание ВСЕГДА вернет false из-за оператора Return:

userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);

Полагаю, в данном случае это не так уж и полезно.

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

public string displayMessage(bool userWasCreated) {
    if (userWasCreated)
        return "User created successfully!";
    return "User already exists";
}

тогда ваш тест будет

userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);
Assert.AreEqual("User already exists", displayMessage(userSvc.CreateUser(theUser)))

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

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

person Doug R    schedule 24.01.2009
comment
Гораздо больше смысла проверять поведение. Я не мог понять ценность написания тестов, которые будут проходить автоматически, из-за того, как я настроил макеты. Поведение при тестировании фактически дает представление о функции тестируемого кода. - person Ben Robbins; 25.01.2009

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

Вы можете рассмотреть несколько таких тестов:

CreateUser_fails_if_email_is_invalid()
CreateUser_fails_if_username_is_empty()

Еще один комментарий: похоже, что ваши методы возвращают логические значения, указывающие на успех или неудачу, похоже на запах кода. У вас может быть веская причина для этого, но обычно вы должны позволить исключениям распространяться. Это также усложняет написание хороших тестов, поскольку у вас возникнут проблемы с определением того, отказал ли ваш метод по «правильной причине», f.x. вы можете написать CreateUser_fails_if_email_is_invalid () - тест следующим образом:

[Test]
public void CreateUser_fails_if_email_is_invalid()
{
    bool result = userSvc.CreateUser(userWithInvalidEmailAddress);
    Assert.That(result, Is.False);
}

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

person Rasmus Faber    schedule 24.01.2009
comment
У меня есть информация о сервисе, но я опубликовал только пару примеров, потому что они были довольно избыточными. Я исключил возвращаемые значения bool в исключения, и это дало мне больше информации о том, как код давал сбой. Спасибо за понимание. - person Ben Robbins; 25.01.2009

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

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

person tvanfosson    schedule 24.01.2009
comment
Системный дизайн - это то, что я все еще изучаю. Я не занимаюсь чистым TDD, потому что мне нравится иметь представление о том, как меньшая часть системы вписывается в целое. Я пишу, как, по моему мнению, будет работать мой интерфейс, и реорганизую интерфейс, когда мои тесты подтверждают или опровергают мои предположения. - person Ben Robbins; 25.01.2009

Вы сталкиваетесь с вопросом о подходах к тестированию «классический» или «издевательский». Или «проверка состояния» или «проверка поведения», как описано Мартином Фаулером: http://martinfowler.com/articles/mocksArentStubs.html#ClassicalAndMockistTesting

Еще один замечательный ресурс - это книга Джерарда Месароса «Тестовые шаблоны xUnit: рефакторинг тестового кода».

person ayang    schedule 26.06.2009