Как правильно издеваться и юнит-тест - PullRequest
24 голосов
/ 24 января 2009

Я в основном пытаюсь научиться кодировать, и я хочу следовать хорошей практике. Есть очевидные преимущества для модульного тестирования. Когда речь заходит о юнит-тестировании, здесь также много фанатизма, и я предпочитаю гораздо более прагматичный подход к кодированию и жизни в целом. В настоящее время я пишу свое первое «настоящее» приложение, которое представляет собой вездесущий движок блогов, использующий 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 = "joe@joeblow.com",
            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");
    }

Ответы [ 4 ]

19 голосов
/ 24 января 2009

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

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

7 голосов
/ 24 января 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);
}

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

6 голосов
/ 25 января 2009

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

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

2 голосов
/ 26 июня 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...