Комментарии 40
Так я не понял, как вы все таки предлагаете решить проблему разводов?
Статья очень тяжело читается.
Начиная с цели этой статьи: почему именно MSTest? Потому что есть драйвер в VS? Так и для xUnit есть, и более того, MS уже давно его адаптировал в своих проектах.
Зачем в статье затрагивается Identity и MVC? Если цель показать просто тестирование DbContext вместе с миграциями, то и обычного консольного приложения хватило бы. Здесь же это добавляет информационного шума на полстатьи.
Если вам для тестов приходится удалять таблицу, то вы делаете что-то не так.
В 95% случаев используется Fluent api, и этот код работать не будет.
Начиная с цели этой статьи: почему именно MSTest? Потому что есть драйвер в VS? Так и для xUnit есть, и более того, MS уже давно его адаптировал в своих проектах.
Зачем в статье затрагивается Identity и MVC? Если цель показать просто тестирование DbContext вместе с миграциями, то и обычного консольного приложения хватило бы. Здесь же это добавляет информационного шума на полстатьи.
И третье: возможно, за один тест нам понадобится несколько раз удалить какую-то таблицу или несколько и создать заново, при этом оставив другие таблицы нетронутыми.
Если вам для тестов приходится удалять таблицу, то вы делаете что-то не так.
var tableNameAnnotation = entityType.GetAnnotation("Relational:TableName");
var tableSchemaAnnotation = entityType.GetAnnotation("Relational:Schema");
В 95% случаев используется Fluent api, и этот код работать не будет.
в больших проектах у меня есть ещё перегруженная функция EnsureDeleted(String,String) которая в этих случаях удаляет таблицу просто по имени и схеме, напрямую
иногда бывают проекты где таблицы создаются по нескольку штук для отдельного пользователя или подсистемы, ничего в этом нет необычного. есть задача — есть решение
Identity и MVC я затронул потому что если мы пишем просто базу данных для простого проекта то у неё нет таких архитектурных сложностей при которых нужен устойчивый проект с автоматическим тестированием в долгосрочной перспективе, это как раз для сложных случаев. тут архитектурные вопросы — часть общей проблемы
MSTest можно заменить любым другим тестом — это ничего не изменит, всё равно нужно будет контролируемо иметь тестовый контекст, возможность лёгкого применения миграций, добавления/удаления таблиц. MSTest просто потому что я с ним работаю обычно
обратите внимание на тип статьи — tutorial, это для тех кто возможно вобще не знает что такое тестирование. хабр же не только для самых опытных
а насчёт FluentAPI поясните пожалуйста, почему не будет работать?
иногда бывают проекты где таблицы создаются по нескольку штук для отдельного пользователя или подсистемы, ничего в этом нет необычного. есть задача — есть решение
Identity и MVC я затронул потому что если мы пишем просто базу данных для простого проекта то у неё нет таких архитектурных сложностей при которых нужен устойчивый проект с автоматическим тестированием в долгосрочной перспективе, это как раз для сложных случаев. тут архитектурные вопросы — часть общей проблемы
MSTest можно заменить любым другим тестом — это ничего не изменит, всё равно нужно будет контролируемо иметь тестовый контекст, возможность лёгкого применения миграций, добавления/удаления таблиц. MSTest просто потому что я с ним работаю обычно
обратите внимание на тип статьи — tutorial, это для тех кто возможно вобще не знает что такое тестирование. хабр же не только для самых опытных
а насчёт FluentAPI поясните пожалуйста, почему не будет работать?
this.ObjectLinks.ToList().Where(...).SingleOrDefault()
Загружаем ВСЕ записи из таблицы, когда нам нужна только одна.
А если их там тысячи или миллионы?
До того как начали лабать код не увидел толкового объяснения, что же мы такое делаем. В статье какая-то сборная солянка между способом построения слоя доступа к данным и собственно его тестированием. Почему сделано именно так как сделано — непонятно. Какие проблемы мы решили таким подходом — тоже неясно. Шутки сомнительного свойства.
Согласен с автором на счет тестирования БД. Да, это будут уже совсем не unit-тесты, а интеграционные. Но, как правильно было замечено, попытка замокать приводит к копипасту кода EF. Кстати, нельзя просто так замокать DbContext или DbSet. Отсюда вытекают фразы «так есть же шаблон UnitOfWork + Repository». Опять начинаются «танцы с бубном», чтобы обернуть EF в шаблон репозиторий, в то время как сам EF уже реализует UnitOfWork+Repo.
Второе. Когда ж, закончиться этот ужас с #if DEBUG. Ну специально для этого в .Net Core при работе с файлами конфигураций добавили поддержка имени окружения. Не зря же пустом проекте можно найти appsettings.json и appsettings.development.json
Почему не рассматривается вариант с проектом интеграционных тестов в Core? В своей практике я всегда его использовал для работы unit-тестов, связанных с БД. Нужна минимальная «обвязка». К тому же, в некоторых случаях необходимо работать с in-memory database. В случае статического метода, класс контекста начинает «обрастать» новыми метода, в зависимости от сценариев использования, что никак не добавляет читабельности коду. Даже class helpers проблему не решат, а только спрячут ее
Второе. Когда ж, закончиться этот ужас с #if DEBUG. Ну специально для этого в .Net Core при работе с файлами конфигураций добавили поддержка имени окружения. Не зря же пустом проекте можно найти appsettings.json и appsettings.development.json
Почему не рассматривается вариант с проектом интеграционных тестов в Core? В своей практике я всегда его использовал для работы unit-тестов, связанных с БД. Нужна минимальная «обвязка». К тому же, в некоторых случаях необходимо работать с in-memory database. В случае статического метода, класс контекста начинает «обрастать» новыми метода, в зависимости от сценариев использования, что никак не добавляет читабельности коду. Даже class helpers проблему не решат, а только спрячут ее
Когда ж, закончиться этот ужас с #if DEBUG.он уже закончился когда ввели имя окружения, но оно не работает тогда когда нет окружения — это в другом проекте есть окружение, в тестовом его нет и оно там не нужно.
а главное, #if DEBUG визуально показывает вам какие строчки выполнятся сейчас, в текущей конфигурации, это удобно чтобы их не перепутать
Почему не рассматривается вариант с проектом интеграционных тестов в Core?это не совсем понял — у меня же всё в core 3.1? или вы что-то другое имеете в виду?
docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1
Вот и будет у вас окружение
Вот и будет у вас окружение
Как то очень сумбурно.
Если вы хотите автоматизировать тестирование БД, то тестировать нужно конкретно те методы, которые DAL предоставляет другим слоям. Причем порядок вызова тестов не должен иметь никакого значения. В каком порядке методы DAL будут вызывать другие слои — ни DAL, ни тесты совершенно не должно волновать. Если нужно вытащить юзера по емейлу — вот вам метод, а разбираться, возвращена ли Entity (если есть) или null (если нет юзера с таким емейлом) — это задача не DAL, а того слоя, который вызвал метод. И юнит тесты с моками с этим прекрасно справляются. Плюс, для тестирования DAL тащить SQL Server Express совершенно необязательно, когда есть SQLite, а в dotnet core давным давно появился InMemory провайдер специально для тестов.
Если вам нужна персистентность DAL в юнит тестах — у меня для вас плохие новости.
Юнит тесты вообще не должны зависеть от любого другого слоя, моки именно для этого и существуют.
Моки — не уродливы, не раздувают код, и, тем более, не являются дублированием кода или копи-пастой. Они имитируют зависимости так, как это нужно для конкретного теста. Если DAL может выдать разную реакцию на вызов его метода — пишем несколько тестов.
Мало того, если у вас успешность прохождения тестов зависит от порядка их вызова — это вообще ужас-ужас.
Интеграционные же тесты лучше проводить не на машинах девелоперов (хотя это не запрещено), а, например, дерганьем реальных апи в специально подготовленном для этого окружении с реальной БД, настройками, связями сервисов, DI и т.д.
А так получается раздутый юнит тест, натянутый на глобус и названный интеграционнным
Если вы хотите автоматизировать тестирование БД, то тестировать нужно конкретно те методы, которые DAL предоставляет другим слоям. Причем порядок вызова тестов не должен иметь никакого значения. В каком порядке методы DAL будут вызывать другие слои — ни DAL, ни тесты совершенно не должно волновать. Если нужно вытащить юзера по емейлу — вот вам метод, а разбираться, возвращена ли Entity (если есть) или null (если нет юзера с таким емейлом) — это задача не DAL, а того слоя, который вызвал метод. И юнит тесты с моками с этим прекрасно справляются. Плюс, для тестирования DAL тащить SQL Server Express совершенно необязательно, когда есть SQLite, а в dotnet core давным давно появился InMemory провайдер специально для тестов.
Если вам нужна персистентность DAL в юнит тестах — у меня для вас плохие новости.
Юнит тесты вообще не должны зависеть от любого другого слоя, моки именно для этого и существуют.
Моки — не уродливы, не раздувают код, и, тем более, не являются дублированием кода или копи-пастой. Они имитируют зависимости так, как это нужно для конкретного теста. Если DAL может выдать разную реакцию на вызов его метода — пишем несколько тестов.
Мало того, если у вас успешность прохождения тестов зависит от порядка их вызова — это вообще ужас-ужас.
Интеграционные же тесты лучше проводить не на машинах девелоперов (хотя это не запрещено), а, например, дерганьем реальных апи в специально подготовленном для этого окружении с реальной БД, настройками, связями сервисов, DI и т.д.
А так получается раздутый юнит тест, натянутый на глобус и названный интеграционнным
Очень интересно. Я нашёл вот такой пример: docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory
И сразу возник вопрос: в примере сервисы проектируются так, что DbContext является зависимостью, похоже её можно инжектить через DI. Но у нас, ради производительности, сервисы проектируются так, что один созданный экземпляр со всеми зависимостями можно использовать многократно, и даже параллельно — у сервисов нет состояния, результат зависит только от параметров. Здесь же придётся создавать на каждый запрос свой DbContext и зависимые от него сервисы. Вопрос в том, хорошая ли это практика.
И сразу возник вопрос: в примере сервисы проектируются так, что DbContext является зависимостью, похоже её можно инжектить через DI. Но у нас, ради производительности, сервисы проектируются так, что один созданный экземпляр со всеми зависимостями можно использовать многократно, и даже параллельно — у сервисов нет состояния, результат зависит только от параметров. Здесь же придётся создавать на каждый запрос свой DbContext и зависимые от него сервисы. Вопрос в том, хорошая ли это практика.
Не совсем так
DbContext желательно передавать как зависимость, причем отдельный экземпляр желательно создавать на каждый LifetimeScope, говоря терминами IoC контейнеров. А еще лучше брать из пула подключений, но этим всем обычно занимается asp net core и разработчик не забивает себе этим голову. Причин несколько, главная из которых — нельзя шарить один контекст на несколько, скажем, api запросов, потому что контекст один, а реальное состояние могло поменяться. Плюс EntityFramework Core не допускает параллельного выполнения запросов в рамках одного DbContext (читай — подключения к БД).
Нужно понимать, что репозиторий (как паттерн), хоть программно и является обычным сервисом, но все таки имеет свои особенности, связанные именно с тем, что под капотом у него есть конкретное состояние и взаимодействие с «внешним миром» (БД), который может изменится в любой момент времени, вплоть до недоступности.
Исходя из этого, Persistence, как слой архитектуры, зачастую выносится отдельно и является подвидом инфраструктурного слоя.
По этому DbContext обычно создается на весь LifetimeScope.
К примеру, в рамках WebApi — это http запрос. Создается один DbContext, который инжектируется во все (требующие DbContext) зависимости этого запроса и используется на столько запросов к БД, сколько нужно. После завершения http запроса DbContext закрывается или передается обратно в пул.
А юнит тестах с InMemory провайдером — на каждый тест создается новый, чистый DbContext, который уже в тесте конфигурируется под потребности теста. Это очень быстро и вполне нормально
DbContext желательно передавать как зависимость, причем отдельный экземпляр желательно создавать на каждый LifetimeScope, говоря терминами IoC контейнеров. А еще лучше брать из пула подключений, но этим всем обычно занимается asp net core и разработчик не забивает себе этим голову. Причин несколько, главная из которых — нельзя шарить один контекст на несколько, скажем, api запросов, потому что контекст один, а реальное состояние могло поменяться. Плюс EntityFramework Core не допускает параллельного выполнения запросов в рамках одного DbContext (читай — подключения к БД).
Нужно понимать, что репозиторий (как паттерн), хоть программно и является обычным сервисом, но все таки имеет свои особенности, связанные именно с тем, что под капотом у него есть конкретное состояние и взаимодействие с «внешним миром» (БД), который может изменится в любой момент времени, вплоть до недоступности.
Исходя из этого, Persistence, как слой архитектуры, зачастую выносится отдельно и является подвидом инфраструктурного слоя.
По этому DbContext обычно создается на весь LifetimeScope.
К примеру, в рамках WebApi — это http запрос. Создается один DbContext, который инжектируется во все (требующие DbContext) зависимости этого запроса и используется на столько запросов к БД, сколько нужно. После завершения http запроса DbContext закрывается или передается обратно в пул.
А юнит тестах с InMemory провайдером — на каждый тест создается новый, чистый DbContext, который уже в тесте конфигурируется под потребности теста. Это очень быстро и вполне нормально
Понятно, .net развивается. Уже можно отдать фреймворку asp.net создание и сервисов, и DbContext-ов, при этом можно добиться переиспользования как сервисов, так и контекстов между запросами.
Возможно, в следующих проектах надо будет присмотреться к такой практике. А сейчас переписать всё так, чтобы DbContext-ы не создавались сервисами, а передавались как зависимость, малореально.
Возможно, в следующих проектах надо будет присмотреться к такой практике. А сейчас переписать всё так, чтобы DbContext-ы не создавались сервисами, а передавались как зависимость, малореально.
у меня написано именно так чтобы не надо было ничего переписывать — просто добавьте ещё один контекст через
в Startup.cs и передавайте его куда хотите как завимость — у него для этого есть OnConfiguring, в статье на этом специально внимание заострено
services.AddDbContext<ваш_контекст>(options => options.UseSqlServer(Configuration.GetConnectionString(ConnStr)));
в Startup.cs и передавайте его куда хотите как завимость — у него для этого есть OnConfiguring, в статье на этом специально внимание заострено
В проекте на гитхабе не увидел ни одного сервиса, HomeController — пустой.
Возможно, в вашем рабочем проекте это и так, но по статье не скажешь.
Также меня сильно озадачили тесты. Их внешняя зависимость — класс HabrDBContext. То есть, вы тестируете не сервис, а DbContext? То есть, вы почему-то думаете, что код типа
upd. Ага, тут вся бизнес-логика лежит в HabrDBContext. Ну, не стоит так делать, наверное — тянет на нарушение SRP ))
Возможно, в вашем рабочем проекте это и так, но по статье не скажешь.
Также меня сильно озадачили тесты. Их внешняя зависимость — класс HabrDBContext. То есть, вы тестируете не сервис, а DbContext? То есть, вы почему-то думаете, что код типа
db.Phones.Add(new Phone { ... });
может работать не так, как написан? Или вы выполняете роль DBA и тестируете миграции?upd. Ага, тут вся бизнес-логика лежит в HabrDBContext. Ну, не стоит так делать, наверное — тянет на нарушение SRP ))
да, это тестирование только DAL. функций DBContext.
нарушение SRP это философский вопрос — метод контроллера «GetUserAndAllConnectionData» нарушает SRP если внутри себя вызывает GetUser, GetUserData, GetUserLogins и прочие связанные вещи? наверное нарушает, но так же и надо — ведь этот метод должен возвращать полную модель. DAL точно так же можно разделить на слои, где в одном будут простейшие функции, дальше из них будет уже собираться более сложные (тут есть вопросы к производительности, возможно лучше сделать одну большую функцию чем 2 маленькие потому что база реже будет дёргаться)
да, это основная цель такого метода тестирования.
исходя из практики, я не видел ни одного проекта, в котором бы не было постоянных изменений модели данных базы. заказчик постоянно что-то просит, вносит какие-то новые предложения, требования, это всё требует изменения модели базы, изменения самих данных, чтобы они соответствовали новым требованиям, плюс данные должны оставаться консистентными и прочие требования. и вот я выяснил, что простейший метод, который работал вчера, например AddApparat, где запись из трёх полей, вдруг может перестать работать, потому что раньше аппарат был уникальным для места где он находится, а потом оказалось что их там может быть два одинаковых, но их нужно отличать, потому что на них нужно повесить оборудование которое отличается на каждом из них… и таких примеров — куча. и вот я для себя проработал такую стратегию тестирования, при которой я не лезу в mock вобще — у меня его нет, потому что там очень легко ошибиться при такой скорости изменений. и получается что тестировать контроллеры тоже не нужно — тут я доверяю Microsoft, если у них написано что return View(моя модель) то это будет работать. а вот как в моей модели данные появятся, если функции DAL которая их формирует, передать что-то отличное, от того что было раньше (а это возвращает другая DAL функция в 90% случаев) то вот что тогда будет?
вот та система которую я предлагаю и позволяет ответить на этот вопрос — что будет с остальными частями базы данных, если мы изменим эту таблицу и сделаем это вот так и так.
бывают такие изменения, которые EF не может отследить, потому что например constraint какой-то сменился в новой модели, и при компиляции ошибок не будет. а SQL выдаст исключение
нарушение SRP это философский вопрос — метод контроллера «GetUserAndAllConnectionData» нарушает SRP если внутри себя вызывает GetUser, GetUserData, GetUserLogins и прочие связанные вещи? наверное нарушает, но так же и надо — ведь этот метод должен возвращать полную модель. DAL точно так же можно разделить на слои, где в одном будут простейшие функции, дальше из них будет уже собираться более сложные (тут есть вопросы к производительности, возможно лучше сделать одну большую функцию чем 2 маленькие потому что база реже будет дёргаться)
То есть, вы почему-то думаете, что код типа db.Phones.Add(new Phone {… }); может работать не так, как написан?
да, это основная цель такого метода тестирования.
исходя из практики, я не видел ни одного проекта, в котором бы не было постоянных изменений модели данных базы. заказчик постоянно что-то просит, вносит какие-то новые предложения, требования, это всё требует изменения модели базы, изменения самих данных, чтобы они соответствовали новым требованиям, плюс данные должны оставаться консистентными и прочие требования. и вот я выяснил, что простейший метод, который работал вчера, например AddApparat, где запись из трёх полей, вдруг может перестать работать, потому что раньше аппарат был уникальным для места где он находится, а потом оказалось что их там может быть два одинаковых, но их нужно отличать, потому что на них нужно повесить оборудование которое отличается на каждом из них… и таких примеров — куча. и вот я для себя проработал такую стратегию тестирования, при которой я не лезу в mock вобще — у меня его нет, потому что там очень легко ошибиться при такой скорости изменений. и получается что тестировать контроллеры тоже не нужно — тут я доверяю Microsoft, если у них написано что return View(моя модель) то это будет работать. а вот как в моей модели данные появятся, если функции DAL которая их формирует, передать что-то отличное, от того что было раньше (а это возвращает другая DAL функция в 90% случаев) то вот что тогда будет?
вот та система которую я предлагаю и позволяет ответить на этот вопрос — что будет с остальными частями базы данных, если мы изменим эту таблицу и сделаем это вот так и так.
бывают такие изменения, которые EF не может отследить, потому что например constraint какой-то сменился в новой модели, и при компиляции ошибок не будет. а SQL выдаст исключение
в основном проекте так и есть. а в тестовом не получится, потому что данные для тестирования нужно сначала положить в базу, потом вызвать тестовый метод, потом удалить данные, потом вызвать другой тестовый метод. и база должна шариться между этими методами, потому что иначе её придётся избыточно создавать в каждом. это не unit тесты, об этом достаточно ясно сказано в статье. это интеграционный тест для которого мне лично удобнее использовать встроенную систему тестирования.
тут для внедрения зависимостей всё есть, но инжектирование придётся настраивать в самих тестах, а по сути это будет имитация инжектирования ради самого процесса — контекст то один, и конфигурируется он в самом тесте. ещё обратите внимание, InMemory имеет ограничения, от которых я как раз и пытаюсь избавиться
тут для внедрения зависимостей всё есть, но инжектирование придётся настраивать в самих тестах, а по сути это будет имитация инжектирования ради самого процесса — контекст то один, и конфигурируется он в самом тесте. ещё обратите внимание, InMemory имеет ограничения, от которых я как раз и пытаюсь избавиться
инжектирование придётся настраивать в самих тестах, а по сути это будет имитация инжектирования ради самого процесса — контекст то один, и конфигурируется он в самом тесте
Именно это и есть самый настоящий юнит тест, в котором зависимости настроены на поведение, тестируемое в конкретном кейсе. Несколько вариаций поведения зависимостей (сущность найдена или не найдена) — пишем еще юнит тесты с измененной логикой зависимостей
я использую механизм MSTest но там нигде не сказано что я могу его использовать только как unit тест. это по факту интеграционное тестирование, и в статье об этом написано. базу данных не целесообразно тестировать в юнит-форме, потму что подготовка данных — это одна функция, а использование их — другая. и мне удобно видеть за какое время выполняется каждый отдельный тест, проще говоря отдельно положить, отдельно взять — так сразу ясно какой метод отнимает много времени. и если у вас пару тысяч тестов то класть лишнее будет слишком много времени отнимать. поэтому я уже наученный практическим опытом, решил немного отойти от догм и канонов «юнит» подхода и сделать просто удобно — чтобы всё работало быстро, как надо, без дополнительной работы и сразу давало знать об ошибках
Использовать MsTest для интеграционного тестирования с привлечением реальной СУБД конечно можно, но напоминает троллейбус из буханки хлеба.
Это отлично вписывается в паттерн AAA (Arrange-Act-Assert) для юнит тестов, где подготовка данных — arrange, использование — act, и, наконец, проверка валидности запроса — assert
базу данных не целесообразно тестировать в юнит-форме, потму что подготовка данных — это одна функция, а использование их — другая
Это отлично вписывается в паттерн AAA (Arrange-Act-Assert) для юнит тестов, где подготовка данных — arrange, использование — act, и, наконец, проверка валидности запроса — assert
многократно он используется только в тестировании, во всех остальных случаях идёт как обычная зависимость — внедряйте его сколько хотите как сервис, у меня это в моих проектах именно так работает. не хотелось бы DI вставлять туда, где я его не планирую использовать — в тестировании он не нужен. но сделать это можно, создав провайдер сервисов и засунув туда, но потом придётся его оттуда вытаскивать, а смысл это делать если я его планирую использовать в тестах только напрямую?
DI я делаю в тестах для тестирования Identity, но там смысл имитировать поведение служб именно в интеграционной схеме (при проверке ролей)
а в той статье которую вы привели описаны ограничения которые я как раз пытаюсь обойти
DI я делаю в тестах для тестирования Identity, но там смысл имитировать поведение служб именно в интеграционной схеме (при проверке ролей)
а в той статье которую вы привели описаны ограничения которые я как раз пытаюсь обойти
так у меня конкретно те методы которые надо и тестируются. просто проблема данных в том, что они связаны изначально, а тесты не должны быть. как вы предлагаете решить эти противоречия без mock? у которых на самом деле те же самые проблемы?
про InMemory: у них есть ограничения, в той же самое статье, которую приводит qw1, написано предупреждение, что многое не работает. и зачем мне об этом думать если цель статьи была чтобы работало всё абсолютно точно так же как на реальной базе?
про DI — специально сделал проект где в Setup.exe даются все DI в рекомендованном Microsoft стиле. просто в одном из последних проектов я столкнулся с такой базой, в которой логику, написанную на EF нужно было анализировать вдвоём по паре часов с профильным специалистом, чтобы просто понять что будет если изменить вот эту вот одну строку. это для сложных проектов, в которых логика базы данных с многими зависимостями таблиц и данных друг от друга. а как вы гарантируете что написали mock правильно?
про InMemory: у них есть ограничения, в той же самое статье, которую приводит qw1, написано предупреждение, что многое не работает. и зачем мне об этом думать если цель статьи была чтобы работало всё абсолютно точно так же как на реальной базе?
про DI — специально сделал проект где в Setup.exe даются все DI в рекомендованном Microsoft стиле. просто в одном из последних проектов я столкнулся с такой базой, в которой логику, написанную на EF нужно было анализировать вдвоём по паре часов с профильным специалистом, чтобы просто понять что будет если изменить вот эту вот одну строку. это для сложных проектов, в которых логика базы данных с многими зависимостями таблиц и данных друг от друга. а как вы гарантируете что написали mock правильно?
Эти вопросы у вас возникают потому, что вы «из контроллера» пытаетесь «смотреть на логику» в DAL.
Вот в этом и кроется ошибка.
Контроллеру должно быть совершенно фиолетово, насколько сложен реальный запрос и сколько задействуется таблиц в методе _repository.GetSomeEntity(...). Пишется простейший мок вида:
и забывается вся сложная логика из множества таблиц, связей и т.д на десятки строк в DAL.
Если в контроллере вызывается несколько методов репозитория — создаем несколько настроек на этот мок.
Если логика контроллера зависит от результатов вызова репозитория — пишем несколько юнит тестов.
Кто сказал что будет легко? Юнит тесты должны покрывать все возможные сценарии тестируемого метода, включая ошибки, разные результаты вызовов репозиторя, в том числе и цепочки вызовов. И при этом очень важно, чтоб они были изолированы друг от друга и не зависели от неких внешних БД, последовательности вызова тестов и т.д. Если делать только один тест на ожидаемый сценарий — то лучше вообще ничего не делать.
А вот для таких сложных и запутанных DAL методов, как вы привели в примере (пара часов и сложные зависимости таблиц) и пишутся отдельные юнит тесты, тестирующие конкретно эти методы, со своей настройкой подключения, заполнением таблиц, всеми возможными состояниями и т.д. Если в них что то нужно будет поменять — то изменятся только юнит тесты, покрывающие DAL. Все остальные потребители (контроллеры) остануться неизменными, потому что им все равно, что там происходит в DAL — им нужна SomeEntity, которая в тестах получается настройкой мока.
Запомните, логика DAL ни в коем случае не должна проникать в другие слои, в том числе и в тестах.
InMemory — это как пример очень быстрой конфигурации, работающий практически из коробки. Есть еще отличный SQLite. Хотите реальную БД — создавайте полноценное окружние с СУБД, эквивалентной боевой и там настраивайте полноценные интеграционные тесты.
Вот в этом и кроется ошибка.
Контроллеру должно быть совершенно фиолетово, насколько сложен реальный запрос и сколько задействуется таблиц в методе _repository.GetSomeEntity(...). Пишется простейший мок вида:
var repositoryMock = new Mock<IRepository>();
repositoryMock
.Setup(e => e.GetSomeEntity(...))
.Returns(someEntity);
и забывается вся сложная логика из множества таблиц, связей и т.д на десятки строк в DAL.
Если в контроллере вызывается несколько методов репозитория — создаем несколько настроек на этот мок.
Если логика контроллера зависит от результатов вызова репозитория — пишем несколько юнит тестов.
Кто сказал что будет легко? Юнит тесты должны покрывать все возможные сценарии тестируемого метода, включая ошибки, разные результаты вызовов репозиторя, в том числе и цепочки вызовов. И при этом очень важно, чтоб они были изолированы друг от друга и не зависели от неких внешних БД, последовательности вызова тестов и т.д. Если делать только один тест на ожидаемый сценарий — то лучше вообще ничего не делать.
А вот для таких сложных и запутанных DAL методов, как вы привели в примере (пара часов и сложные зависимости таблиц) и пишутся отдельные юнит тесты, тестирующие конкретно эти методы, со своей настройкой подключения, заполнением таблиц, всеми возможными состояниями и т.д. Если в них что то нужно будет поменять — то изменятся только юнит тесты, покрывающие DAL. Все остальные потребители (контроллеры) остануться неизменными, потому что им все равно, что там происходит в DAL — им нужна SomeEntity, которая в тестах получается настройкой мока.
Запомните, логика DAL ни в коем случае не должна проникать в другие слои, в том числе и в тестах.
InMemory — это как пример очень быстрой конфигурации, работающий практически из коробки. Есть еще отличный SQLite. Хотите реальную БД — создавайте полноценное окружние с СУБД, эквивалентной боевой и там настраивайте полноценные интеграционные тесты.
вот! золотые слова!
только проникновения слоёв там не было — эти все сложности были в самом DAL слое, потому что очень сложная логика самих данных — куча связанных таблиц, и от выбора пользователя в одном месте зависит поиск данных в другом, и в итоге это всё собирается в гигантскую структуру данных, которую надо проверить не просто на «есть там такие данные или нет», а на то, что выбор пользователя правильно повлиял на дальнейшие действия системы. и написать mock для этого — я просто посчитал время, которое я на это потрачу, а потом прикинул чего будет стоить одна ошибка в написании mock, и понял что надо использовать другой подход — вот как раз тот, который использован в этой статье
только проникновения слоёв там не было — эти все сложности были в самом DAL слое, потому что очень сложная логика самих данных — куча связанных таблиц, и от выбора пользователя в одном месте зависит поиск данных в другом, и в итоге это всё собирается в гигантскую структуру данных, которую надо проверить не просто на «есть там такие данные или нет», а на то, что выбор пользователя правильно повлиял на дальнейшие действия системы. и написать mock для этого — я просто посчитал время, которое я на это потрачу, а потом прикинул чего будет стоить одна ошибка в написании mock, и понял что надо использовать другой подход — вот как раз тот, который использован в этой статье
Я вам вот что пытаюсь донести:
- Если у вас сложная логика в методе из DAL — то создайте тестовое окружение конкретно для тестов этого метода, хотя бы даже так, как вы написали в статье. Не перекладывайте настройку окружения на тест контроллера, который использует этот метод. Что, если этот метод будут дергать несколько контроллеров? на каждый будете копипастить настройку?
- Если у вас сложная логика в методе из контроллера — то здесь нужно использовать только моки. Если вы всунете реальную БД в тест контроллера — то вот тут то и произойдет проникновение слоя DAL в тест и появится прямая зависимость теста от некоего внешнего состояния. А потом новый сотрудник что то изменит, и все-все тесты упали — будете очень долго разгребать, что и почему. А глядя на настроенные моки — все сразу станет понятно, какой тест упал и на каких настройках.
Я уже писал, что тесты должны быть произведены на все сценарии. Если все настолько сложно и запутано — сколько вы делаете таких интеграционных тестов? Один с успешным сценарием? А ведь надо проверить все сценарии, будете для этого создавать множество заполнений таблиц? Поверьте, с моками все гораздо проще
у меня нет контроллера, только DAL.
особенность моего подхода в том, что:
1. он позволяет создать персистентный объект базы данных один на все тесты
2. использовать результат прохождения одного теста в других (это удобно, потому что иначе один и тот же код просто будет выполняться много раз, зачем это?
3. сценарии проверяются в конкретном тесте
мы добровольно отказываемся от изоляции тестов чтобы получить дополнительные преимущества — последовательности использования методов (в самом деле, чтобы проверить выборку товара его нужно сначала положить — вот в таком порядке и тестируем:
тест 1 — положить товар в базу;
тест 2 — взять товар из базы.
если делать как вы предлагаете, то нужно будет делать так:
тест 1 — положить товар в базу;
удалить товар из базы
тест 2 — положить товар в базу
взять товар из базы
и так далее — в итоге вам придётся делать по 10-20 однотипных действий, которые уже прошли в предыдущих тестах, только для того чтобы назвать это «юнит» тестами. смысл? не проще ли учесть то что уже сделано до того и использовать это?
особенность моего подхода в том, что:
1. он позволяет создать персистентный объект базы данных один на все тесты
2. использовать результат прохождения одного теста в других (это удобно, потому что иначе один и тот же код просто будет выполняться много раз, зачем это?
3. сценарии проверяются в конкретном тесте
мы добровольно отказываемся от изоляции тестов чтобы получить дополнительные преимущества — последовательности использования методов (в самом деле, чтобы проверить выборку товара его нужно сначала положить — вот в таком порядке и тестируем:
тест 1 — положить товар в базу;
тест 2 — взять товар из базы.
если делать как вы предлагаете, то нужно будет делать так:
тест 1 — положить товар в базу;
удалить товар из базы
тест 2 — положить товар в базу
взять товар из базы
и так далее — в итоге вам придётся делать по 10-20 однотипных действий, которые уже прошли в предыдущих тестах, только для того чтобы назвать это «юнит» тестами. смысл? не проще ли учесть то что уже сделано до того и использовать это?
Вы нарушаете одно из правил тестов — они должны быть независимы друг от друга.
Ок, если у вас в DAL 2 метода — «Добавить товар» и «Получить товар», то это 2 совершенно независимых метода, на которые пишуться совершенно разные и независимые тесты.
Первый тест — на метод «Добавить товар». Проверяется только то, что тавар добавился. Все.
Вторая группа тестов — на «Получить товар». Тут количество тестов зависит только от количества сценариев этого метода.
Тесты из этой группы не должны зависеть от предыдущего выполнения метода «Добавить товар». Да, вам придется ручками в каждом тесте настаивать заполненность таблиц (почему бы это просто не вынести в отдельным метод и вызывать в каждом тесте в начале). И протестировать вам придется несколько ситуаций, одна из которых — есть ли товар в таблице. Товара нет — другой тест.
Метод «Получить товар» не должен опираться на то, был ли до этого где-то в бизнес логике вызван метод «Добавить товар». Это самостоятельная единица. Тестируйте бизнес-логику на то, что должно быть перед чем вызвано, отдельно.
Вы делаете критическую ошибку в том, что понадеялись на особенность фреймворка, который выполняет тесты «по алфавиту». Прийдет на проект синьер и совершенно обоснованно выкинет MsTest в помойку и прикрутит современный xUnit — а вот тут то и все упадет. Потому что тесты должны быть независимы друг от друга, поймите это. И настраиваться должны все по отдельности. А еще может прийти джун, даст новом тесту «неправильное» имя, из-за чего рухнет вся ваша цепочка вызовов. А еще есть всякие умные утилиты типа NCrunch, которые запускают только изменившиеся тесты, и в вашем случае они упадут даже будучи корректными, потому что перед ними не был вызван другой тест. Перезапускать весь проект каждый раз?
Юнит тесты — далеко не самая простая штука, и, зачастую, времени на их написание уходит порядочно. В том числе и выполнять однотипную настройку. По этому аргумент — много времени уходит на настройку, по этому давайте переиспользуем существующие тесты — не аргумент. Вы можете общий код вынести в общие методы, но переиспользовать результат других тестов, а тем более, использовать некое внешнее состояние — это табу.
Кстати, если вы просто тестируете метод из DAL — это классический юнит тест. То, что вы прикрутили туда реальную СУБД и состояние — не позволяет назвать этот тест интеграционным.
Интеграционный тест — это когда некий бизнес-процесс из конечного множества действий тестируется с начала и до конца в реальных условиях. Вызов метода DAL — не бизнес-процесс, сколько бы там сложных запросов и связей ни было.
Ок, если у вас в DAL 2 метода — «Добавить товар» и «Получить товар», то это 2 совершенно независимых метода, на которые пишуться совершенно разные и независимые тесты.
Первый тест — на метод «Добавить товар». Проверяется только то, что тавар добавился. Все.
Вторая группа тестов — на «Получить товар». Тут количество тестов зависит только от количества сценариев этого метода.
Тесты из этой группы не должны зависеть от предыдущего выполнения метода «Добавить товар». Да, вам придется ручками в каждом тесте настаивать заполненность таблиц (почему бы это просто не вынести в отдельным метод и вызывать в каждом тесте в начале). И протестировать вам придется несколько ситуаций, одна из которых — есть ли товар в таблице. Товара нет — другой тест.
Метод «Получить товар» не должен опираться на то, был ли до этого где-то в бизнес логике вызван метод «Добавить товар». Это самостоятельная единица. Тестируйте бизнес-логику на то, что должно быть перед чем вызвано, отдельно.
Вы делаете критическую ошибку в том, что понадеялись на особенность фреймворка, который выполняет тесты «по алфавиту». Прийдет на проект синьер и совершенно обоснованно выкинет MsTest в помойку и прикрутит современный xUnit — а вот тут то и все упадет. Потому что тесты должны быть независимы друг от друга, поймите это. И настраиваться должны все по отдельности. А еще может прийти джун, даст новом тесту «неправильное» имя, из-за чего рухнет вся ваша цепочка вызовов. А еще есть всякие умные утилиты типа NCrunch, которые запускают только изменившиеся тесты, и в вашем случае они упадут даже будучи корректными, потому что перед ними не был вызван другой тест. Перезапускать весь проект каждый раз?
Юнит тесты — далеко не самая простая штука, и, зачастую, времени на их написание уходит порядочно. В том числе и выполнять однотипную настройку. По этому аргумент — много времени уходит на настройку, по этому давайте переиспользуем существующие тесты — не аргумент. Вы можете общий код вынести в общие методы, но переиспользовать результат других тестов, а тем более, использовать некое внешнее состояние — это табу.
Кстати, если вы просто тестируете метод из DAL — это классический юнит тест. То, что вы прикрутили туда реальную СУБД и состояние — не позволяет назвать этот тест интеграционным.
Интеграционный тест — это когда некий бизнес-процесс из конечного множества действий тестируется с начала и до конца в реальных условиях. Вызов метода DAL — не бизнес-процесс, сколько бы там сложных запросов и связей ни было.
я строю тесты последовательно, и сразу вижу какой упал — если он упал, то в нём и проблема.
контроллеров там нет вобще, это только тестирование DAL изолированно
прямая зависимость теста от состояния — это преимущество, а не недостаток.
потому что реальная база тоже зависит от состояния — например, как вы добавите права несуществующему пользователю? или как осуществить продажу, если нет контрагента? это же тоже всё надо учесть, поэтому вызываем функции по очереди и становится понятна иерархия вызовов, что должно быть к тому моменту как оно будет использоваться
я согласен с вами в том что да, это не совсем привычное использование тестов, но они понятны и сразу виден способ взаимодействия между различными механизмами.
вам уже не нужно заходить на какую-то страницу, смотреть что будет если пользователь введёт те или иные данные — просто добавьте в метод тестирования добавления этих данных и посмотрите что будет, и добавьте новый сценарий
и это не замена полному интеграционному тестированию, это именно тест на консистентность базы данных при необходимости быстрых изменений
при этой методике в одном тестовом методе я делаю все сценарии
контроллеров там нет вобще, это только тестирование DAL изолированно
прямая зависимость теста от состояния — это преимущество, а не недостаток.
потому что реальная база тоже зависит от состояния — например, как вы добавите права несуществующему пользователю? или как осуществить продажу, если нет контрагента? это же тоже всё надо учесть, поэтому вызываем функции по очереди и становится понятна иерархия вызовов, что должно быть к тому моменту как оно будет использоваться
я согласен с вами в том что да, это не совсем привычное использование тестов, но они понятны и сразу виден способ взаимодействия между различными механизмами.
вам уже не нужно заходить на какую-то страницу, смотреть что будет если пользователь введёт те или иные данные — просто добавьте в метод тестирования добавления этих данных и посмотрите что будет, и добавьте новый сценарий
и это не замена полному интеграционному тестированию, это именно тест на консистентность базы данных при необходимости быстрых изменений
при этой методике в одном тестовом методе я делаю все сценарии
я строю тесты последовательно, и сразу вижу какой упал — если он упалА потом нужно модифицировать логику, и вы полдня будете искать, между какими тестами нужно вставить новый тест. Так нельзя.
прямая зависимость теста от состояния — это преимущество, а не недостаток.Это недостаток, а не преймущество. Окружение теста должно настраиваться в самом тесте, а не «где-то там». Только так видна полная картина теста и что именно он тестирует.
как вы добавите права несуществующему пользователю?Это бизнес-логика, и ей не место в DAL
как осуществить продажу, если нет контрагента?Это бизнес-логика, и ей не место в DAL
вызываем функции по очереди и становится понятна иерархия вызововИ это тоже бизнес-логика, которой не место в DAL.
смотреть что будет если пользователь введёт те или иные данныевалидация данных — это вообще Presentation Layer, а проверка бизнес-требований — это задача для слоя бизнес-логики
тест на консистентность базы данныхВас вообще это не должно волновать. В случае RDB и правильной схемы консистентность вам гарантируется. А если приложение падает из-за того, что в какой то момент не был найден контрагент — то это явный баг в бизнес-логике. Тесты консистентности БД тут не помогут.
В общем, вы запихнули кучу логики в DAL, которой там не должно быть в принципе, пытаетесь это как то проверить и городите костыли. В целом, хоть какое то тестирование — это хорошо, но вы натягиваете свои тесты на явный архитектурный просчет.
Задача DAL проста — обеспечить персистентность данных. Это не место для логики.
Моки зачастую зло, тесты с ними не говорят о работоспособности приложения ну вообще, они говорят лишь о тестируемом куске. Моки можно использовать если интеграция компонентов сделана идеально или есть функциональные тесты.
я так понял по отзывам что это очень индивидуальный момент, какие технологии тестирования подходят для какой ситуации. если база сложная — один подход, если часто меняется — другой, это зависит ещё и от того стиля в котором вы пишете.
вот TimurNes в своей статье про DDD вобще другой стиль работы использует, хотя там тоже можно использовать то что я предлагаю. не вижу противоречия. попробую завтра написать пример, может я как-то не так выразился...
В своем проекте, в котором я стараюсь использовать DDD и Clean Architecture на полную катушку, и с опыта которого я написал статью про ValueObjects в EF Core, слой Persistence выполняет строго отведенную ему роль — обеспечивает персистентность сущностей. Другими словами — сохраняет и загружает требуемые обьекты/коллекции.
Никакой бизнес-логики, манипуляций данными и т.д. там нет — все это в отдельном слое.
Если я хочу, условно, сделать заказ, то бизнес логика сначала загружает контрагента через var partner = _partners.WithId(id), если не найден — кидает специальное исключение, потом формирует заказ примерно как var order = new Order(partner), потом сохраняет его _orders.Add(order).
В итоге репозитории — обычно простые, как угол дома, потому что методы выполняют строго отведенную им задачу.
Бизнес-логика в RequestHandler'ах точно так же наглядна и понятна.
И то, и то просто и понятно тестируется — бизнес-логика мокает методы репозиториев и других зависимостей, а сами репозитории тестируются уже с помощью InMemory провайдера. Если бы мне были критичны ограничения InMemory — я бы использовал SQLite
Это буква S из аббревиатуры SOLID — Single responsibility. Не должен репозиторий что то там проверять и вычислять — не его это задача.
Никакой бизнес-логики, манипуляций данными и т.д. там нет — все это в отдельном слое.
Если я хочу, условно, сделать заказ, то бизнес логика сначала загружает контрагента через var partner = _partners.WithId(id), если не найден — кидает специальное исключение, потом формирует заказ примерно как var order = new Order(partner), потом сохраняет его _orders.Add(order).
В итоге репозитории — обычно простые, как угол дома, потому что методы выполняют строго отведенную им задачу.
Бизнес-логика в RequestHandler'ах точно так же наглядна и понятна.
И то, и то просто и понятно тестируется — бизнес-логика мокает методы репозиториев и других зависимостей, а сами репозитории тестируются уже с помощью InMemory провайдера. Если бы мне были критичны ограничения InMemory — я бы использовал SQLite
Это буква S из аббревиатуры SOLID — Single responsibility. Не должен репозиторий что то там проверять и вычислять — не его это задача.
ну да, я заметил что у нас немного разные архитектурные подходы.
у меня DAL делится на 2 слоя — один это простейшие функции доступа к данным, другой слой, который эти функции вызывает уже — это более сложная бизнес-логика, которую я убираю из контроллеров. таким способом я могу потом этот класс отдать инженеру баз данных на оптимизацию, и он может без проблем какие-то сложные вызовы переписать на хранимые процедуры. это развязывает его от серверного программиста и от дизайнера, потому что он должен только своим слоем заниматься — тем что в функцию приходит и то что из неё уходит
у меня DAL делится на 2 слоя — один это простейшие функции доступа к данным, другой слой, который эти функции вызывает уже — это более сложная бизнес-логика, которую я убираю из контроллеров. таким способом я могу потом этот класс отдать инженеру баз данных на оптимизацию, и он может без проблем какие-то сложные вызовы переписать на хранимые процедуры. это развязывает его от серверного программиста и от дизайнера, потому что он должен только своим слоем заниматься — тем что в функцию приходит и то что из неё уходит
С юмором у автора все в порядке.
Для серьезных же тестов использовать MS Visual Studio??
Особенно, 2019, которая еще только в стадии «допиливания»?
Ну это скорее от безысходности и отсутствия нормальных инструментов для тестирования.
Для серьезных же тестов использовать MS Visual Studio??
Особенно, 2019, которая еще только в стадии «допиливания»?
Ну это скорее от безысходности и отсутствия нормальных инструментов для тестирования.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Мы из другого теста — тестируем базу данных на MSTest