Стратегии фиктивных данных и модульного тестирования в модульном стеке приложений

Как вы управляете фиктивными данными, используемыми для тестов? Хранить их вместе с соответствующими сущностями? В отдельном тестовом проекте? Загрузить их с помощью сериализатора из внешних ресурсов? Или просто воссоздать их там, где это необходимо?

У нас есть стек приложений с несколькими модулями, зависящими от другого, каждый из которых содержит объекты. Каждый модуль имеет свои собственные тесты и нуждается в фиктивных данных для запуска.

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

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

Есть ли лучший выход из этого или все решения компромиссы?


Подробнее

Наш стек выглядит примерно так:

Один модуль:

src/main/java --> gets jared (.../entities/*.java contains the entities)
src/main/resources --> gets jared
src/test/java --> contains dummy object setup, will NOT get jared
src/test/resources --> not jared

Мы используем Maven для обработки зависимостей.

пример модуля:

  • Модуль A содержит несколько фиктивных объектов
  • Модулю B нужны свои собственные объекты И такие же, как у модуля A

Вариант а)

Тестовый модуль T может содержать все фиктивные объекты и предоставлять их в тестовой области (так что загруженные зависимости не будут искажены) для всех тестов во всех модулях. Будет ли это работать? Значение: если я загружу T в A и запущу установку на A, он НЕ будет содержать ссылок, введенных T, особенно не B? Однако тогда A будет знать о модели данных B.

Вариант б)

Модуль A предоставляет фиктивные объекты где-то в src/main/java../entities/dummy, позволяя B получить их, в то время как A не знает о фиктивных данных B.

Вариант в)

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

Вариант г)

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

Чего мы не хотим

Мы не хотим создавать статическую базу данных со статическими данными, так как требуемая структура объектов будет постоянно меняться. Много сейчас, чуть позже. Итак, мы хотим, чтобы hibernate настраивал все таблицы и столбцы и заполнял их данными во время модульного тестирования. Кроме того, статическая база данных может привести к большому количеству потенциальных ошибок и тестовых взаимозависимостей.


Мои мысли идут в правильном направлении? Как лучше всего справляться с тестами, требующими большого количества данных? У нас будет несколько взаимозависимых модулей, которым потребуются объекты, заполненные какими-то данными из нескольких других модулей.


ИЗМЕНИТЬ

Еще немного информации о том, как мы это делаем прямо сейчас в ответ на второй ответ:

Итак, для простоты у нас есть три модуля: Person, Product, Order. Person проверит некоторые методы менеджера, используя объект MockPerson:

person/src/test/java :)

public class MockPerson {

    public Person mockPerson(parameters...) {
        return mockedPerson;
    }
}

public class TestPerson() {
    @Inject
    private MockPerson mockPerson;
    public testCreate() {
        Person person = mockPerson.mockPerson(...);
        // Asserts...
    }
}

Класс MockPerson не будет упакован.

То же самое относится и к тестированию продукта:

product/src/test/java:)

public class MockProduct() { ... }
public class TestProduct {
    @Inject
    private MockProduct mockProduct;
    // ...
}

MockProduct необходим, но не будет упакован.

Теперь для тестов порядка потребуются MockPerson и MockProduct, так что теперь нам нужно создать оба, а также MockOrder для проверки Order.

порядок/источник/тест/java :)

Это дубликаты, и их нужно будет менять каждый раз при изменении Person или Product.

public class MockProduct() { ... }
public class MockPerson() { ... }

Это единственный класс, который должен быть здесь:

public class MockOrder() { ... }

public class TestOrder() {
    @Inject
    private order.MockPerson mockPerson;
    @Inject
    private order.MockProduct mockProduct;
    @Inject
    private order.MockOrder mockOrder;
    public testCreate() {

        Order order = mockOrder.mockOrder(mockPerson.mockPerson(), mockProduct.mockProduct());
        // Asserts...
    }
}

Проблема в том, что теперь мы должны обновлять person.MockPerson и order.MockPerson всякий раз, когда изменяется Person.

Не лучше ли просто опубликовать Mocks с помощью jar, чтобы любой другой тест, который в любом случае имеет зависимость, мог просто вызвать Mock.mock и получить хорошо настроенный объект? Или это темная сторона - легкий путь?


person Pete    schedule 12.01.2012    source источник


Ответы (3)


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

То, что я обычно делал (и AFAIKT в Java это лучшая практика), пытался использовать шаблон Test Data Builder, как описано Нат Прайс в его построителях тестовых данных Почта.

Если вы считаете, что это несколько актуально, проверьте это:

person cwash    schedule 14.01.2012
comment
Эй, кваш! Спасибо за заводской указатель. Я помню, как использовал это в учебнике по рельсам;) Это может быть решением, мне придется проверить его дальше. - person Pete; 16.01.2012
comment
@Pete - круто, не могли бы вы оставить заметку / обновление, сообщив нам, что вы решили? - person cwash; 17.01.2012
comment
Итак, похоже, мы можем использовать его в качестве центрального фиктивного поставщика данных. В конце концов, однако, это просто вариант а), означающий, что какой-то внешний проект будет содержать все необходимые генераторы данных. Все еще интересно, если это лучший способ пойти. Надеюсь получить еще мнения по этому поводу.. - person Pete; 19.01.2012
comment
Круто - я хотел бы услышать, что другие говорят по этому поводу. Я уже видел централизованный подход при проведении интеграционного тестирования. - person cwash; 19.01.2012

Что ж, я внимательно прочитал все оценки, и это очень хороший вопрос. Я вижу следующие подходы к проблеме:

  1. Настроить (статическую) тестовую базу данных;
  2. Каждый тест имеет свои собственные данные настройки, которые создают (динамические) тестовые данные перед запуском модульных тестов;
  3. Используйте фиктивный или фиктивный объект. Все модули знают все фиктивные объекты, таким образом нет дубликатов;
  4. Уменьшить объем модульного теста;

Первый вариант довольно прямолинеен и имеет много недостатков, кто-то должен воспроизводить его время от времени, когда модульные тесты «косячат», если есть изменения в модуле данных, кто-то должен внести соответствующие изменения в тестовые данные, много накладных расходов на обслуживание. Нельзя сказать, что генерация этих данных из первых рук может быть сложной задачей. См. также второй вариант.

Второй вариант: вы пишете свой тестовый код, который перед тестированием вызывает некоторые из ваших «основных» бизнес-методов, которые создают вашу сущность. В идеале ваш тестовый код должен быть независим от производственного кода, но в этом случае вы получите дублированный код, который вы должны поддерживать дважды. Иногда полезно разделить производственный бизнес-метод, чтобы иметь точку входа для вашего модульного теста (я делаю такие методы закрытыми и использую Reflection для их вызова, также необходимо некоторое замечание по методу, рефакторинг теперь немного сложнее) . Главный недостаток заключается в том, что если вы должны изменить свои «основные» бизнес-методы, это внезапно повлияет на все ваши модульные тесты, и вы не сможете их протестировать. Таким образом, разработчики должны знать об этом и не делать частичных коммитов для «основных» бизнес-методов, если они не работают. Кроме того, при любых изменениях в этой области вы должны помнить, «как это повлияет на мой модульный тест». Иногда также невозможно динамически воспроизвести все необходимые данные (обычно это из-за стороннего API, например, вы вызываете другое приложение со своей БД, из которой вам требуется использовать некоторые ключи. Эти ключи (с связанные данные) создается вручную через стороннее приложение. В таком случае эти данные и только эти данные должны быть созданы статически. Например, вы создали 10000 ключей, начиная с 300000.

Третий вариант должен быть хорошим. Варианты а) и г) звучат для меня довольно неплохо. Для вашего фиктивного объекта вы можете использовать фиктивную структуру или не использовать ее. Mock Framework здесь только для того, чтобы помочь вам. Я не вижу проблемы в том, что все ваши подразделения знают все ваши сущности.

Четвертый вариант означает, что вы переопределяете, что такое «модуль» в вашем модульном тесте. Когда у вас есть пара взаимозависимых модулей, может быть сложно протестировать каждый модуль по отдельности. Этот подход говорит о том, что изначально мы тестировали интеграционное тестирование, а не модульное тестирование. Итак, мы разделяем наши методы, извлекаем небольшие «единицы работы», которые получают все свои взаимозависимости с другими модулями в качестве параметров. Эти параметры можно (надеюсь) легко смоделировать. Главный недостаток такого подхода в том, что вы тестируете не весь свой код, а только, так сказать, «фокусные точки». Вам нужно сделать интеграционный тест отдельно (обычно командой QA).

person alexsmail    schedule 20.01.2012
comment
Привет! Спасибо за отзыв. Особенно 4-й вариант был интересен. Вы правы, то, что мы делаем, на самом деле является не модульным тестированием, а интеграционным тестированием, поскольку мы вызываем функции менеджера из нижних модулей для создания объектов, необходимых для модульного теста в более высоком модуле, тем самым неявно проверяя также некоторые функции более низкого модуля. Я понимаю, что это делает тесты не строго независимыми. Я думаю, мы подумали: ну ладно, тогда просто увеличится плотность тестов для всех модулей. Я думаю, мне следует прочитать о полезности модульных тестов и интеграционных тестов... - person Pete; 20.01.2012
comment
@ Пит, это именно то, что нужно. Вы должны убедиться, что можете тестировать свои функции изолированно. Макет каждой зависимости и использование Builder для генерации тестовых данных. Также убедитесь, что вы помните о законе Деметры (не передавайте весь клиент, когда вам нужен только адрес), потому что это сделает ваш код намного лучше тестируемым. - person Wouter de Kort♦; 20.01.2012

Мне интересно, не могли бы вы решить свою проблему, изменив свой подход к тестированию.

Модульное тестирование модуля, который зависит от других модулей и, следовательно, от тестовых данных других модулей, не является настоящим модульным тестом!

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

Если вы представляете себе пирамиду, то в основе будут ваши модульные тесты, над которыми у вас будут функциональные тесты, а наверху у вас будут тесты сценариев (или, как их называет Google, малые, средние и большие тесты).

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

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

Шаблон Builder, упомянутый cwash, определенно поможет в ваших функциональных тестах. Мы используем .NET Builder, настроенный для построения полного дерева объектов и генерации значений по умолчанию для каждого свойства, поэтому, когда мы сохраняем это в базе данных, все необходимые данные присутствуют.

person Wouter de Kort♦    schedule 20.01.2012
comment
Спасибо за Ваш ответ. Можете ли вы предоставить какой-нибудь псевдокод для лучшего понимания? По крайней мере, для части модульного тестирования. Мы пока не проводим функциональное и сценарное тестирование. Насколько я вас понимаю, мы сейчас делаем именно то, что вы говорите. В каждом модуле есть классы Mock, которые предоставляют настраиваемые данные и внедряются в наборы тестов. Затем каждый тест вызывает фиктивный объект для создания необходимых данных. Я предоставлю некоторый исходный код и дополнительную информацию, чтобы более подробно изложить наш подход и проблемы, чтобы вы могли проверить, о чем вы говорите. - person Pete; 20.01.2012
comment
@Pete Я не разработчик Java, поэтому не могли бы вы объяснить, что вы имеете в виду под: «Проблема в том, что теперь мы должны обновлять person.MockPerson и order.MockPerson при каждом изменении Person». Что вам нужно изменить? Разве вы не издеваетесь только над важными свойствами и методами? - person Wouter de Kort♦; 20.01.2012
comment
Возможно, мой вопрос слишком длинный, поэтому трудно выделить необходимые детали. Извините... Я описал, как мы должны имитировать все поля, так как большинство из них nullable=false. Поэтому, если я добавлю новый столбец в объект Person, мне придется добавить насмешку ко всем вхождениям MockPerson в каждом модуле. - person Pete; 20.01.2012
comment
@Pete Тогда поможет шаблон Builder. Используйте Builder для создания тестовых данных. Используйте насмешки, чтобы имитировать зависимости. Человек - это не зависимость, это тестовые данные. PersonRepository для доступа к данным является зависимостью. Таким образом, вы можете издеваться над PersonRepository, чтобы вернуть TestPerson, созданный с помощью Builder. - person Wouter de Kort♦; 20.01.2012