Стоит ли жестко кодировать свои данные во всех модульных тестах?

33

Большинство учебных пособий / примеров модульного тестирования обычно включают определение данных для тестирования для каждого отдельного теста. Я полагаю, что это является частью теории «все должно быть проверено изолированно».

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

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

Является ли общепринятой практикой заранее определять, если не все, то большинство данных испытаний по всем модульным тестам?

Обновить

Из комментариев ниже кажется, что я делаю больше интеграции, чем модульное тестирование.

Мой текущий проект - ASP.NET MVC, использующий Unit of Work над Entity Framework Code First и Moq для тестирования. Я издевался над UoW и репозиториями, но я использую реальные классы бизнес-логики и тестирую действия контроллера. Тесты часто проверяют, что UoW был зафиксирован, например:

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBaseстроит макет UoW и создает экземпляр userLogic.

Многие тесты требуют наличия существующего пользователя или продукта в базе данных, поэтому я предварительно заполнил то, что в этом примере возвращает фиктивный UoW userData, то есть просто IList<User>с одной записью пользователя.

mattdwen
источник
4
Проблема с учебниками / примерами в том, что они должны быть простыми, но вы не можете показать решение сложной проблемы на простом примере. Они должны сопровождаться «тематическими исследованиями», описывающими, как инструмент используется в реальных проектах разумного размера, но они редко используются.
Ян Худек
Может быть, вы могли бы добавить несколько небольших примеров кода, который вас не устраивает.
Люк Франкен
Если вам нужно много кода для запуска теста, вы рискуете запустить функциональный тест. Если тест не пройден при изменении кода, но в этом коде нет ничего плохого. Это определенно функциональный тест.
Reactgular
Книга "xUnit Test Patterns" дает веские основания для многоразовых приборов и помощников. Тестовый код должен быть таким же обслуживаемым, как и любой другой код.
Чак Круцингер,
Эта статья может быть полезна: yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

Ответы:

25

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

Я использую подход со стандартными классами TestHelper, которые предоставляют мне много типов данных, которые я регулярно использую, поэтому я могу создавать наборы стандартных классов сущностей или DTO для своих тестов, чтобы запрашивать и точно знать, что я получу каждый раз. Поэтому я могу позвонить, TestHelper.GetFooRange( 0, 100 )чтобы получить диапазон из 100 объектов Foo со всеми их зависимыми классами / полями.

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

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

Есть некоторые вещи, которые нужно соблюдать осторожно, хотя в модульных тестах:

  • Убедитесь , что издевается являются издевается . Классы, которые выполняют операции над тестируемым классом, должны быть фиктивными объектами, если вы выполняете модульное тестирование. Ваши классы типов DTO / entity могут быть реальными, но если классы выполняют операции, вам нужно их издеваться - в противном случае, когда меняется вспомогательный код и ваши тесты начинают проваливаться, вам придется искать намного дольше, чтобы выяснить, какие изменения на самом деле вызвал проблему.
  • Убедитесь, что вы тестируете свои классы . Иногда, когда кто-то просматривает набор модульных тестов, становится очевидно, что половина тестов на самом деле тестирует фальшивую среду больше, чем реальный код, который они должны тестировать.
  • Не используйте поддельные / поддерживающие объекты. Это очень важно. Когда вы начинаете пытаться научиться работать с кодом, поддерживающим модульные тесты, очень легко непреднамеренно создавать объекты, которые сохраняются между тестами, что может привести к непредсказуемым последствиям. Например, вчера у меня был тест, который проходил сам по себе, проходил при запуске всех тестов в классе, но не выполнялся при запуске всего набора тестов. Оказалось, что в помощнике по тестированию скрывался скрытый статический объект, который, когда я его создал, определенно никогда не вызывал бы проблемы. Просто помните: в начале теста все создано, в конце теста все уничтожено.
glenatron
источник
10

Что бы ни делало цель вашего теста более читабельной.

Как общее правило:

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

Если данные просто заполняют, чтобы убедиться, что с ними есть, с чем работать (например, не следует помечать запись как завершенную, если служба обработки выдает исключение), то непременно есть метод BuildDummyData или тестовый класс, который удерживает ненужные данные в тесте. ,

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

прецизионный самописец
источник
+1 согласен Это похоже на то, что он тестирует, чтобы он был тесно связан для модульного тестирования.
Reactgular
5

Различные методы тестирования

Сначала определите, что вы делаете: модульное тестирование или интеграционное тестирование . Количество слоев не имеет значения для модульного тестирования, поскольку вы, скорее всего, тестируете только один класс. Остальное ты издеваешься. Для тестирования интеграции неизбежно, что вы тестируете несколько слоев. Если у вас есть хорошие модульные тесты, задача состоит в том, чтобы сделать интеграционные тесты не слишком сложными.

Если ваши юнит-тесты хороши, вам не нужно повторять тестирование всех деталей при проведении интеграционного тестирования.

Термины, которые мы используем, немного зависят от платформы, но вы можете найти их практически на всех платформах тестирования / разработки:

Пример приложения

В зависимости от технологии, которую вы используете, имена могут отличаться, но я буду использовать это в качестве примера:

Если у вас есть простое приложение CRUD с моделью Product, ProductsController и представлением индекса, которое генерирует таблицу HTML с продуктами:

Конечным результатом приложения является отображение таблицы HTML со списком всех активных продуктов.

Модульное тестирование

модель

Модель вы можете протестировать довольно легко. Есть разные методы для этого; мы используем светильники. Я думаю, что это то, что вы называете "поддельные наборы данных". Поэтому перед каждым тестом мы создаем таблицу и вводим исходные данные. У большинства платформ есть методы для этого. Например, в вашем тестовом классе - метод setUp (), который запускается перед каждым тестом.

Затем мы запускаем наш тест, например: testGetAllActive products.

Таким образом, мы тестируем непосредственно в тестовой базе данных. Мы не копируем источник данных; мы делаем это всегда одинаково. Это позволяет нам, например, протестировать новую версию базы данных, и возникнут любые проблемы с запросами.

В реальном мире вы не можете всегда следовать 100% единоличной ответственности . Если вы хотите сделать это еще лучше, вы можете использовать источник данных, который вы издеваетесь. Для нас (мы используем ORM) это похоже на тестирование уже существующих технологий. Также тесты становятся намного более сложными, и они на самом деле не тестируют запросы. Так что мы продолжаем в том же духе.

Жестко закодированные данные отдельно хранятся в приборах. Таким образом, фикстура похожа на файл SQL с оператором create table и вставками для используемых нами записей. Мы оставляем их небольшими, если только нет реальной необходимости проводить тестирование с большим количеством записей.

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

контроллер

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

Таким образом, мы смоделируем метод модели getAllActive () и добавим в него фиксированные данные (например, две записи). Теперь мы проверяем данные, которые контроллер отправляет в представление, и сравниваем, действительно ли мы получаем эти две записи обратно.

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

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

Таким образом, контроллеру обычно требуется один макет и небольшой фрагмент жестко закодированных данных. Для системы входа в систему может быть другой. В нашем тесте у нас есть вспомогательный метод для этого: setLoggedIn (). Это упрощает тестирование с логином или без логина.

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

Взгляды

Тестирование просмотров сложно. Сначала мы выделяем логику, которая повторяется. Мы помещаем это в Помощники и проверяем те классы строго. Мы ожидаем, что всегда один и тот же результат. Например, generateHtmlTableFromArray ().

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

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

echo $this->tableHelper->generateHtmlTableFromArray($products);

Интеграционное тестирование

Здесь, в зависимости от вашей платформы, вы можете работать с историями пользователей и т. Д. Это может быть веб-интерфейс, например, Selenium или другие аналогичные решения.

Обычно мы просто загружаем базу данных с приборами и утверждаем, какие данные должны быть доступны. Для полного интеграционного тестирования мы обычно используем глобальные требования. Итак: установите продукт активным, а затем проверьте, доступен ли продукт.

Мы не проверяем все снова, например, доступны ли нужные поля. Мы проверяем большие требования здесь. Так как мы не хотим дублировать наши тесты с контроллера или представления. Если что-то действительно является ключевой / основной частью вашего приложения или по соображениям безопасности (проверьте, что пароль недоступен), то мы добавляем их, чтобы убедиться, что это правильно.

Жестко закодированные данные хранятся в приборах.

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}
Люк Франкен
источник
Это отличный ответ на совершенно другой вопрос.
фунтовые
Спасибо за ответ. Возможно, вы правы, что я не упомянул это слишком конкретно. Причина подробного ответа состоит в том, что я вижу одну из самых трудных вещей при тестировании в заданном вопросе. Обзор того, как изолированное тестирование соответствует различным видам тестов. Вот почему я добавил в каждой части, как данные обрабатываются (или выделяются). Посмотрим, смогу ли я сделать это более ясно.
Люк Франкен
Ответ был обновлен с некоторыми примерами кода, чтобы объяснить, как тестировать, не вызывая все виды других классов.
Люк Франкен
4

Если вы пишете тесты с большим количеством DI и проводных соединений, вплоть до использования «реальных» источников данных, вы, вероятно, вышли из области простого модульного тестирования и попали в область интеграционного тестирования.

Я думаю, что для интеграционных тестов неплохо иметь общую логику настройки данных. Основная цель таких тестов - доказать, что все правильно настроено. Это скорее не зависит от конкретных данных, отправляемых через вашу систему.

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

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

Свен Аманн
источник
-1

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

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

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

Тем не менее, я думаю, что имеет смысл иметь некоторые случайные данные в ваших тестах.

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