Зачем нужны юнит-тесты для тестирования методов репозитория?

19

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

Допустим, я построил следующий модульный тест:

[TestMethod]
public void GetOrderByIDTest()
{
   //Uses Moq for dependency for getting order to make sure 
   //ID I set up in 'Arrange' is same one returned to test in 'Assertion'
}

Поэтому, если я настрою OrderIdExpected = 5и мой фиктивный объект вернется 5в качестве идентификатора, мой тест пройдет. Я понял Я протестировал код, чтобы убедиться, что преформы моего кода возвращают ожидаемый объект и идентификатор, а не что-то еще.

Аргумент, который я получу, таков:

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

Я мог бы защитить его с помощью определения из учебника, например: «Ну, это интеграционный тест, и нам нужно тестировать код отдельно как модульный тест, и, да, яда, яда ...» Это тот случай, когда пуристское объяснение практики против реальности теряется. Иногда я сталкиваюсь с этим, и если я не могу отстоять обоснование кода модульного тестирования, которое в конечном итоге опирается на внешние зависимости, то я не могу доказать это.

Любая помощь по этому вопросу с благодарностью, спасибо!

atconway
источник
2
я говорю, отправьте это прямо на тестирование пользователей ... они все
равно
+1 за сарказм, чтобы время от времени
оставаться легким
2
Простой ответ заключается в том, что у каждого метода может быть х число граничных случаев (скажем, вы хотите проверить положительные идентификаторы, идентификаторы 0 и отрицательные идентификаторы). Если вы хотите протестировать некоторые функциональные возможности, использующие этот метод, который сам по себе имеет 3 граничных случая, вам нужно написать 9 тестовых случаев для проверки каждой комбинации граничных случаев. Выделив их, вам нужно всего лишь написать 6. Кроме того, тесты дают вам более конкретное представление о том, почему что-то сломалось. Возможно, ваш репозиторий возвращает ноль в случае сбоя, и исключение NULL выдает несколько сотен строк кода.
Роб
Я думаю, что вы слишком строги в своем определении «модульного» теста. Что такое «единица работы» для класса репозитория?
Калеб
И убедитесь, что вы обдумываете следующее: если ваш модульный тест вычеркивает все, что вы пытаетесь проверить, что вы на самом деле тестируете?
Калеб

Ответы:

20

Модульные и интеграционные тесты имеют разные цели.

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

Модульные тесты, если они выполнены правильно, легче настроить, чем интеграционные тесты. Если вы полагаетесь исключительно на интеграционные тесты, ваши тесты будут:

  1. В общем, труднее писать
  2. Будьте более хрупкими из-за всех необходимых зависимостей и
  3. Предлагайте меньше покрытия кода.

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


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

Роберт Харви
источник
Да, спасибо за быстрый ответ, я ценю это. Моя единственная критика ответа заключается в том, что он относится к моему последнему абзацу. Моя оппозиция все еще услышит абстрактные объяснения и определения, а не более глубокие рассуждения. Например, не могли бы вы привести обоснование, примененное к моему варианту использования для тестирования хранимой процедуры / БД в отношении моего кода, и почему модульные тесты по-прежнему полезны в отношении этого конкретного варианта использования?
atconway
1
Если SP просто возвращает результат из базы данных, может быть достаточно интеграционного теста. Если в нем есть нетривиальная логика, указываются юнит-тесты. См. Также stackoverflow.com/questions/1126614/… и msdn.microsoft.com/en-us/library/aa833169(v=vs.100).aspx (специфично для SQL Server).
Роберт Харви
12

Я на стороне прагматиков. Не проверяйте свой код дважды.

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

  1. Они могут заменить модульные тесты при выполнении TDD. Несмотря на то, что есть еще один пример тестового кода, который нужно написать, прежде чем вы сможете начать с реального кода, он очень хорошо работает с подходом красный / зеленый / рефакторинг, когда все на месте.
  2. Они проверяют реальный код репозитория - код, содержащийся в строках SQL или командах ORM. Более важно проверить правильность запроса, чем проверить, действительно ли вы отправили некоторую строку в StatementExecutor.
  3. Они превосходны в качестве регрессионных тестов. Если они терпят неудачу, это всегда связано с реальной проблемой, такой как изменение схемы, которая не была учтена.
  4. Они незаменимы при изменении схемы базы данных. Вы можете быть уверены, что не изменили схему таким образом, чтобы это нарушало работу вашего приложения в течение всего периода тестирования. (В этом случае модульный тест бесполезен, поскольку, когда хранимая процедура больше не существует, модульный тест все равно будет проходить.)
свиристель
источник
Очень прагматично. Я сталкивался с ситуацией, когда база данных в памяти ведет себя иначе (не с точки зрения производительности) по сравнению с моей реальной базой данных из-за немного отличающегося ORM. Позаботьтесь об этом в своих интеграционных тестах.
Марсель
1
@ Марсель, я столкнулся с этим. Я решил это, иногда проводя все свои тесты на реальной базе данных.
Уинстон Эверт
4

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

Кроме того, в том числе модульное тестирование имеет следующие преимущества:

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

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

Захари Йейтс
источник
4

Тесты полезны, когда они ломаются: активно или повторно.

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

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

Например, если мы создаем систему ETL, наиболее релевантные тесты будут касаться данных (постановочных, фальсифицированных или живых). В той же системе будет несколько модульных тестов, но только вокруг проверки, фильтрации и т. Д.

Шаблон репозитория не должен иметь вычислительной или бизнес-логики. Это очень близко к базе данных. Но хранилище также близко к бизнес-логике, которая его использует. Так что это вопрос достижения баланса.

Модульные тесты могут проверить, КАК ведет себя репозиторий. Интеграционные тесты могут проверять ЧТО ДЕЙСТВИТЕЛЬНО ПРОИЗОШЛО, когда вызывался репозиторий.

Теперь, «ЧТО ДЕЙСТВИТЕЛЬНО СЛУЧИЛОСЬ» может показаться очень заманчивым. Но это может быть очень дорогим для запуска последовательно и многократно. Классическое определение хрупких испытаний применяется здесь.

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

Otpidus
источник
Мне нравится это: «Теперь,« ЧТО ДЕЙСТВИТЕЛЬНО СЛУЧИЛОСЬ »может показаться очень заманчивым. Но это может быть очень дорогим для последовательной и многократной работы».
atconway
2

Есть 3 отдельные вещи, которые требуют тестирования: хранимая процедура, код, который вызывает хранимую процедуру (т. Е. Ваш класс репозитория), и потребитель. Задача репозитория - сгенерировать запрос и преобразовать возвращенный набор данных в объект домена. Существует достаточно кода для поддержки модульного тестирования, которое отделено от фактического выполнения запроса и создания набора данных.

Итак (это очень очень упрощенный пример):

interface IOrderRepository
{
    Order GetOrderByID(Guid id);
}

class OrderRepository : IOrderRepository
{
    private readonly ISqlExecutor sqlExecutor;
    public OrderRepository(ISqlExecutor sqlExecutor)
    {
        this.sqlExecutor = sqlExecutor;
    }

    public Order GetOrderByID(Guid id)
    {
        var sql = "SELECT blah, blah FROM Order WHERE OrderId = @p0";
        var dataset = this.sqlExecutor.Execute(sql, p0 = id);
        var result = this.orderFromDataset(dataset);
        return result;
    }
}

Затем при тестировании OrderRepositoryпередайте ISqlExecutorпроверенный объект и убедитесь, что тестируемый объект передает правильный SQL (это его работа) и возвращает надлежащий Orderобъект с заданным набором данных результата (также проверенный). Вероятно, единственный способ протестировать конкретный SqlExecutorкласс - это интеграционные тесты, достаточно справедливые, но это тонкий класс-обертка, и он редко когда-либо изменится, поэтому большое дело.

Вы все еще должны провести модульное тестирование своих хранимых процедур.

Скотт Уитлок
источник
0

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

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

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

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

CL22
источник