Обработка нескольких макетов и утверждений в модульных тестах

В настоящее время у меня есть хранилище, которое использует Entity Framework для моих операций CRUD.

Это вводится в мой сервис, который должен использовать этот репо.

Используя AutoMapper, я проецирую Entity Model на модель Poco, и poco возвращается службой.

Если мои объекты имеют несколько свойств, как правильно настроить, а затем утверждать мои свойства?

Если мой сервис имеет несколько зависимостей репо, как правильно настроить все мои макеты? * - Класс [setup], где все макеты и объекты настроены для этих тестовых приборов?*****

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

Я прочитал "Искусство модульного тестирования" и не нашел никаких предложений, как справиться с этим делом.

Я использую инструменты Rhino Mocks и NUnit.

Я также нашел это на SO, но это не отвечает на мой вопрос: правильно Unit Test Service / Взаимодействие с репозиторием

Вот пример, который выражает то, что я описываю:

public void Save_ReturnSavedDocument()
{
    //Simulate DB object
    var repoResult = new EntityModel.Document()
        {
            DocumentId = 2,
            Message = "TestMessage1",
            Name = "Name1",
            Email = "Email1",
            Comment = "Comment1"
        };

    //Create mocks of Repo Methods - Might have many dependencies
    var documentRepository = MockRepository.GenerateStub<IDocumentRepository>();
    documentRepository.Stub(m => m.Get()).IgnoreArguments().Return(new List<EntityModel.Document>()
        {
           repoResult
        }.AsQueryable());

    documentRepository.Stub(a => a.Save(null, null)).IgnoreArguments().Return(repoResult);

    //instantiate service and inject repo
    var documentService = new DocumentService(documentRepository);
    var savedDocument = documentService.Save(new Models.Document()
        {
            ID = 0,
            DocumentTypeId = 1,
            Message = "TestMessage1"
        });

    //Assert that properties are correctly mapped after save
    Assert.AreEqual(repoResult.Message, savedDocument.Message);
    Assert.AreEqual(repoResult.DocumentId, savedDocument.DocumentId);
    Assert.AreEqual(repoResult.Name, savedDocument.Name);
    Assert.AreEqual(repoResult.Email, savedDocument.Email);
    Assert.AreEqual(repoResult.Comment, savedDocument.Comment);
    //Many More properties here
}

4 ответа

Решение

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

Вы говорите, что не хотите устанавливать все макеты / заглушки в 10 тестах. Вот почему большинство фреймворков предлагают вам метод установки, который выполняется перед каждым тестом. Здесь вы можете поместить большую часть конфигурации макетов в одно место и использовать ее повторно. В NUnit вы просто создаете метод и украшаете его атрибутом [SetUp].

Если вы хотите протестировать метод с различными значениями параметра, вы можете использовать атрибуты [TestCase] ​​NUnit. Это очень элегантно, и вам не нужно создавать несколько одинаковых тестов.

Теперь поговорим о полезных инструментах.

AutoFixture - это удивительный и очень мощный инструмент, который позволяет вам создать объект класса, который требует нескольких зависимостей. Он устанавливает зависимости с помощью фиктивных макетов автоматически и позволяет вручную настраивать только те, которые вам нужны в конкретном тесте. Скажем, вам нужно создать макет для UnitOfWork, который принимает 10 репозиториев в качестве зависимостей. В вашем тесте вам нужно настроить только один из них. Autofixture позволяет вам создать этот UnitOfWork, настроить тот или иной конкретный макет репозитория (или больше, если вам нужно). Остальные зависимости будут установлены автоматически с помощью фиктивных макетов. Это экономит вам огромное количество бесполезного кода. Это немного похоже на контейнер IOC для вашего теста.

Он также может генерировать поддельные объекты со случайными данными для вас. Таким образом, вся инициализация EntityModel.Document будет всего одна строка

var repoResult = _fixture.Create<EntityModel.Document>();

Особенно взгляните на:

  • Создайте
  • замерзать
  • AutoMockCustomization

Здесь вы найдете мой ответ, объясняющий, как использовать AutoFixture.

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

Свободные утверждения Это просто дает вам лучший способ утверждать вещи. Вместо

Assert.AreEqual(repoResult.Message, savedDocument.Message);

Ты можешь сделать

repoResult.Message.Should().Be(savedDocument.Message);

Подводить итоги. Эти инструменты помогут вам создать свой тест с гораздо меньшим количеством кода и сделают его намного более читабельным. Требуется время, чтобы узнать их хорошо. Особенно AutoFixture, но когда вы это делаете, они становятся первым, что вы добавляете в свои тестовые проекты - поверьте мне:). Кстати, все они доступны от Nuget.

Еще один совет. Если у вас есть проблемы с тестированием класса, это обычно указывает на плохую архитектуру. Решение обычно состоит в том, чтобы извлечь меньшие классы из проблемного класса. (Принцип единой ответственности) Чем легко можно протестировать небольшие классы на предмет бизнес-логики. И легко проверить оригинальный класс на взаимодействие с ними.

Рассмотрите возможность использования анонимных типов:

public void Save_ReturnSavedDocument()
{
    // (unmodified code)...

    //Assert that properties are correctly mapped after save
    Assert.AreEqual(
        new
        {
            repoResult.Message,
            repoResult.DocumentId,
            repoResult.Name,
            repoResult.Email,
            repoResult.Comment,
        },
        new
        {
            savedDocument.Message,
            savedDocument.DocumentId,
            savedDocument.Name,
            savedDocument.Email,
            savedDocument.Comment,
        });
}

Есть одна вещь, на которую нужно обратить внимание: обнуляемые типы (например, int?) И свойства, которые могут иметь несколько разные типы (float против double) - но вы можете обойти это, приведя свойства к определенным типам (например, (int?)repoResult.DocumentId).

Другой вариант - создать пользовательский класс / метод утверждения.

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

Несколько способов сделать это:

  1. Не объявляйте экземпляры ваших классов модели / poco внутри каждого теста, а используйте статический класс TestData, который представляет эти экземпляры как свойства. Обычно эти примеры полезны для более чем одного теста. Для повышения надежности, сделайте так, чтобы свойства класса TestData создавали и возвращали новый экземпляр объекта каждый раз, когда к ним обращаются, чтобы один юнит-тест не мог повлиять на следующий, изменив тестовые данные.

  2. В вашем тестовом классе объявите вспомогательный метод, который принимает (обычно проверяемые) репозитории и возвращает тестируемую систему (или "SUT", то есть ваш сервис). Это в основном полезно в ситуациях, когда настройка SUT занимает более 2-х и более операторов, поскольку приводит в порядок ваш тестовый код.

  3. В качестве альтернативы 2, пусть ваш тестовый класс предоставляет свойства для каждого из фиктивных репозиториев, чтобы вам не нужно было объявлять их в ваших тестах юнитов; вы даже можете предварительно инициализировать их с поведением по умолчанию, чтобы еще больше уменьшить конфигурацию для каждого теста.
    Вспомогательный метод, который возвращает SUT, затем не принимает в качестве аргументов фиктивные репозитории, а скорее создает SUT, используя свойства. Возможно, вы захотите переинициализировать каждое свойство репозитория на каждом [TestInitialize],

  4. Чтобы уменьшить беспорядок при сравнении каждого свойства вашего Poco с соответствующим свойством объекта Model, объявите вспомогательный метод в вашем тестовом классе, который делает это для вас (т.е. void AssertPocoEqualsModel(Poco p, Model m)). Опять же, это устраняет некоторые помехи, и вы получаете возможность повторного использования бесплатно.

  5. Или, в качестве альтернативы 4, не сравнивайте все свойства в каждом юнит-тесте, а скорее проверяйте код отображения только в одном месте с отдельным набором юнит-тестов. Это дает дополнительное преимущество, заключающееся в том, что, если в отображение будут включены новые свойства или какие-либо другие изменения, вам не нужно обновлять 100 с лишним юнит-тестов.
    Когда вы не тестируете сопоставления свойств, вы должны просто убедиться, что SUT возвращает правильные экземпляры объектов (т.е. на основе Id или же Name), и что только те свойства, которые могут быть изменены (проверяемой в настоящее время бизнес-логикой), содержат правильные значения (например, общее количество заказов).

Лично я предпочитаю 5 из-за его ремонтопригодности, но это не всегда возможно, и тогда 4 - обычно жизнеспособная альтернатива.

Ваш тестовый код будет выглядеть следующим образом (непроверенный, только для демонстрационных целей):

[TestClass]
public class DocumentServiceTest
{
    private IDocumentRepository DocumentRepositoryMock { get; set; }

    [TestInitialize]
    public void Initialize()
    {
        DocumentRepositoryMock = MockRepository.GenerateStub<IDocumentRepository>();
    }

    [TestMethod]
    public void Save_ReturnSavedDocument()
    {
        //Arrange
        var repoResult = TestData.AcmeDocumentEntity;

        DocumentRepositoryMock
            .Stub(m => m.Get())
            .IgnoreArguments()
            .Return(new List<EntityModel.Document>() { repoResult }.AsQueryable());

        DocumentRepositoryMock
            .Stub(a => a.Save(null, null))
            .IgnoreArguments()
            .Return(repoResult);

        //Act
        var documentService = CreateDocumentService();
        var savedDocument = documentService.Save(TestData.AcmeDocumentModel);

        //Assert that properties are correctly mapped after save        
        AssertEntityEqualsModel(repoResult, savedDocument);
    }

    //Helpers

    private DocumentService CreateDocumentService()
    {
        return new DocumentService(DocumentRepositoryMock);
    }

    private void AssertEntityEqualsModel(EntityModel.Document entityDoc, Models.Document modelDoc)
    {
        Assert.AreEqual(entityDoc.Message, modelDoc.Message);
        Assert.AreEqual(entityDoc.DocumentId, modelDoc.DocumentId);
        //...
    }
}

public static class TestData
{
    public static EntityModel.Document AcmeDocumentEntity
    {
        get
        {
            //Note that a new instance is returned on each invocation:
            return new EntityModel.Document()
            {
                DocumentId = 2,
                Message = "TestMessage1",
                //...
            }
        };
    }

    public static Models.Document AcmeDocumentModel
    {
        get { /* etc. */ }
    }
}

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

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

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

Наконец, попытка tdd может помочь. Закомментируйте все, что может в вашем сервисе. Затем напишите тест, который не проходит. Затем откомментируйте достаточно кода, чтобы пройти тест. Им напишут ваш следующий провальный тест. Не можете написать тест, который не проходит? Тогда вы сделали.

Другие вопросы по тегам