Выбор стратегии тестирования

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

Тестирование на реальный внешний ресурс вместо замены тестовым двойником может привести к следующим трудностям:

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

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

Тестирование базы данных может быть проще, чем кажется

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

  1. Большинство баз данных в настоящее время можно легко установить на компьютере разработчика. Технологии на основе контейнеров, такие как Docker, могут сделать это очень легко, и такие технологии, как Github Workspaces и Контейнер разработки, настраивают всю среду разработки для вас (включая базу данных). При использовании SQL Server также можно протестировать с помощью LocalDB в Windows или легко настроить образ Docker в Linux.
  2. Тестирование с локальной базой данных с разумным набором данных теста обычно очень быстро: обмен данными полностью локальный, а тестовые данные обычно буферизуются в памяти на стороне базы данных. Сам EF Core содержит более 30 000 тестов только для SQL Server; они выполняются надежно в течение нескольких минут, выполняются в CI для каждой отдельной фиксации и очень часто выполняются разработчиками локально. Некоторые разработчики обращаются к базе данных в памяти ("поддельные") в убеждении, что это необходимо для скорости - это почти никогда в действительности дело.
  3. Изоляция действительно является препятствием при выполнении тестов с реальной базой данных, так как тесты могут изменять данные и вмешиваться друг в друга. Однако существуют различные методы, обеспечивающие изоляцию в сценариях тестирования базы данных; мы сосредоточимся на них в тестировании в рабочей системе базы данных).

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

Разные типы тестов двойники

Тест двойники — это широкий термин, охватывающий очень разные подходы. В этом разделе рассматриваются некоторые распространенные методы, связанные с двойниками тестов для тестирования приложений EF Core:

  1. Используйте SQLite (в режиме памяти) в качестве поддельной базы данных, заменив рабочую систему базы данных.
  2. Используйте поставщик EF Core в памяти в качестве поддельных баз данных, заменив рабочую систему базы данных.
  3. Макет или заглушка из DbContext и DbSet.
  4. Введите уровень репозитория между EF Core и кодом приложения, и макет или заглушку этого слоя.

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

SQLite в качестве поддельных баз данных

Одним из возможных подходов к тестированию является замена рабочей базы данных (например, SQL Server) на SQLite, эффективно используя ее в качестве тестовой "поддельные". Помимо простоты установки SQLite имеет функцию базы данных в памяти, которая особенно полезна для тестирования: каждый тест естественно изолирован в собственной базе данных в памяти, и фактические файлы не нужно управлять.

Однако перед этим важно понимать, что в EF Core разные поставщики баз данных ведут себя по-разному. EF Core не пытается абстрагировать каждый аспект базовой системы баз данных. По сути, это означает, что тестирование на SQLite не гарантирует те же результаты, что и для SQL Server, или любой другой базы данных. Ниже приведены некоторые примеры возможных различий в поведении:

  • Один и тот же запрос LINQ может возвращать разные результаты для разных поставщиков. Например, SQL Server выполняет сравнение строк без учета регистра по умолчанию, в то время как SQLite учитывает регистр. Это может сделать тесты пройдены в SQLite, где они завершаются сбоем с SQL Server (или наоборот).
  • Некоторые запросы, которые работают на SQL Server, просто не поддерживаются в SQLite, так как точную поддержку SQL в этих двух базах данных отличается.
  • Если запрос выполняется с использованием метода, определенного поставщиком, например SQL Server EF.Functions.DateDiffDay, этот запрос завершится сбоем в SQLite и не может быть проверен.
  • Необработанный SQL может работать, или он может завершиться ошибкой или возвращать различные результаты в зависимости от того, что выполняется. Диалекты SQL различаются различными способами в разных базах данных.

По сравнению с выполнением тестов в рабочей системе базы данных, это относительно легко приступить к работе с SQLite, и так много пользователей делают. К сожалению, приведенные выше ограничения в конечном итоге становятся проблематичными при тестировании приложений EF Core, даже если они не кажутся в начале. В результате мы рекомендуем либо записывать тесты в реальную базу данных, либо при использовании тестового двойника является абсолютной необходимостью, принимая затраты на шаблон репозитория, как описано ниже.

Сведения об использовании SQLite для тестирования см. в этом разделе.

В памяти как поддельная база данных

В качестве альтернативы SQLite EF Core также поставляется с поставщиком в памяти. Хотя этот поставщик изначально был разработан для поддержки внутреннего тестирования EF Core, некоторые разработчики используют его в качестве поддельных баз данных при тестировании приложений EF Core. Это очень не рекомендуется: как поддельные базы данных в памяти имеют те же проблемы, что и SQLite (см. выше), но в дополнение имеет следующие дополнительные ограничения:

  • Поставщик в памяти обычно поддерживает меньше типов запросов, чем поставщик SQLite, так как он не является реляционной базой данных. Другие запросы завершаются сбоем или ведут себя по-разному по сравнению с рабочей базой данных.
  • Транзакции не поддерживаются.
  • Необработанный SQL полностью не поддерживается. Сравните это с SQLite, где можно использовать необработанный SQL, если этот SQL работает так же, как в SQLite и рабочей базе данных.
  • Поставщик в памяти не оптимизирован для производительности и обычно работает медленнее, чем SQLite в режиме в памяти (или даже в рабочей системе базы данных).

В целом, в памяти есть все недостатки SQLite, а также несколько других - и не предлагает никаких преимуществ в ответ. Если вы ищете простую фиктивную базу данных в памяти, используйте SQLite вместо поставщика в памяти; но рекомендуется использовать шаблон репозитория, как описано ниже.

Сведения об использовании в памяти для тестирования см. в этом разделе.

Макетирование или разбиение DbContext и DbSet

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

Однако правильное макетирование DbSetфункциональных возможностей запросов невозможно, так как запросы выражаются с помощью операторов LINQ, которые являются вызовами IQueryableстатических методов расширения. В результате, когда некоторые люди говорят о "насмешках DbSet", то, что они действительно означают, что они создают DbSet поддерживаемую коллекцию в памяти, а затем оценивают операторы запросов против этой коллекции в памяти, как простой IEnumerable. Вместо макета, это на самом деле своего рода поддельный, где коллекция в памяти заменяет реальную базу данных.

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

Шаблон репозитория

Приведенные выше подходы попытались либо заменить поставщик рабочей базы данных EF Core поддельным поставщиком тестирования, либо создать DbSet резервную копию коллекции в памяти. Эти методы аналогичны тем, что они по-прежнему оценивают запросы LINQ программы ( в SQLite или в памяти), и это в конечном счете источник трудностей, описанных выше: запрос, предназначенный для выполнения в конкретной рабочей базе данных, не может надежно выполняться в другом месте без проблем.

Для правильного надежного теста рекомендуется представить уровень репозитория, который медиатирует код приложения и EF Core. Рабочая реализация репозитория содержит фактические запросы LINQ и выполняет их с помощью EF Core. При тестировании абстракция репозитория напрямую кусается или издевается без необходимости выполнять фактические запросы LINQ, эффективно удаляя EF Core из стека тестирования и позволяя тестам сосредоточиться только на коде приложения.

На следующей схеме сравнивается поддельный подход базы данных (SQLite/in-memory) с шаблоном репозитория:

Comparison of fake provider with repository pattern

Так как запросы LINQ больше не являются частью тестирования, вы можете напрямую предоставить результаты запроса приложению. Поместите другой способ, предыдущие подходы примерно позволяют выполнять вставки входных данных запроса (например, заменяя таблицы SQL Server в памяти), но затем по-прежнему выполняют фактические операторы запросов в памяти. В отличие от шаблона репозитория, можно напрямую заглушить выходные данные запросов, что позволяет выполнять гораздо более мощное и ориентированное модульное тестирование. Обратите внимание, что для этого репозиторий не может предоставлять какие-либо методы возврата IQueryable, так как эти методы еще раз не могут быть перемечены; Вместо этого следует вернуть IEnumerable.

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

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

Пример тестирования с помощью репозитория см. в этом разделе.

Общее сравнение

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

Компонент В памяти SQLite в памяти Mock DbContext Шаблон репозитория Тестирование базы данных
Тест двойного типа Поддельные Поддельные Поддельные Мок/заглушка Реальный, нет двойного
Необработанный SQL? No Зависит No Да Да
Операций? Нет (игнорируется) Да Да Да Да
Переводы, относящиеся к поставщику? No Да Нет Да Да
Точное поведение запроса? Зависит Зависит Зависит Да Да
Можно ли использовать LINQ в любом месте приложения? Да Да Да Нет* Да

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

Итоги

  • Рекомендуется, чтобы разработчики имели хорошее тестовое покрытие приложения, работающего в своей рабочей системе базы данных. Это обеспечивает уверенность в том, что приложение на самом деле работает в рабочей среде и с правильной структурой, тесты могут выполняться надежно и быстро. Так как эти тесты требуются в любом случае, рекомендуется начать там, и при необходимости добавить тесты с помощью тестов двойных тестов по мере необходимости.
  • Если вы решили использовать тестовый двойник, рекомендуется реализовать шаблон репозитория, который позволяет заглушить или вымехать уровень доступа к данным выше EF Core, а не использовать поддельный поставщик EF Core (Sqlite/in-memory) или издеваясь DbSet.
  • Если шаблон репозитория не является жизнеспособным по какой-то причине, рассмотрите возможность использования баз данных SQLite в памяти.
  • Избегайте поставщика в памяти для тестирования. Это не рекомендуется и поддерживается только для устаревших приложений.
  • Избегайте насмехания DbSet в целях запроса.