Как правильно издеваться и 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");
    }

Ответ 1

По существу, вы тестируете здесь, что методы вызываются, а не работают ли они или нет. Это то, что должны делать издевательства. Вместо вызова метода они просто проверяют, был ли вызван метод, и возвращаем все, что содержится в операторе 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)))

Теперь это имеет некоторое значение, потому что вы проверяете какое-то фактическое поведение. Конечно, вы могли бы просто проверить это непосредственно, передав "true" или "false". Вам даже не нужен макет для этого теста. Тестирование ожиданий хорошо, но я написал много таких тестов, и пришел к тому же выводу, что вы достигаете - это просто не так полезно.

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

Ответ 2

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

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

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-цикла позволило бы устранить эту проблему, но было бы даже лучше, если бы можно было обнаружить, что метод завершился неудачно из-за недействительного сообщения электронной почты, а не из-за другой проблемы.

Ответ 3

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

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

Ответ 4

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

Другим самым прекрасным ресурсом является книга Джерарда Мезароса "xUnit Test Patterns: Рефакторинг тестового кода"