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

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

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

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

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

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

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

  1. Большинство баз данных в настоящее время можно легко установить на компьютере разработчика. Технологии на основе контейнеров, такие как Docker, могут сделать это очень легко, и библиотеки, такие как Testcontainers , могут автоматизировать жизненный цикл контейнерных баз данных в тестах. Такие технологии, как 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 в качестве поддельных баз данных

Один из возможных подходов к тестированию — переключение рабочей базы данных (e.g. 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) с шаблоном репозитория:

Сравнение поддельных поставщиков с шаблоном репозитория

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

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

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

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

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

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

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

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

Итоги

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