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

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

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

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

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

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

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

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

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

Различные типы тестов doubles

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

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

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

SQLite как поддельная база данных

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

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

  • Один и тот же запрос LINQ может возвращать разные результаты для разных поставщиков. Например, SQL Server по умолчанию выполняет сравнение строк без учета регистра, тогда как SQLite учитывает регистр. Это может сделать тесты пройдены в SQLite, где они не смогут SQL Server (или наоборот).
  • Некоторые запросы, работающие с SQL Server, просто не поддерживаются в SQLite, так как точную поддержку SQL в этих двух базах данных отличается.
  • Если ваш запрос использует метод, зависящий от поставщика, например SQL ServerEF.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-returning, он накладывает дополнительный архитектурный уровень в приложении и может нести значительные затраты на реализацию и обслуживание. Эта стоимость не должна быть снижена при выборе способа тестирования приложения, особенно учитывая, что тесты с реальной базой данных по-прежнему могут потребоваться для запросов, предоставляемых репозиторием.

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

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

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

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

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

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

Сводка

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