Как люди могут тестировать модули с Entity Framework 6, стоит ли беспокоиться?
Я только начинаю с юнит-тестов и TDD в целом. Раньше я баловался, но теперь я полон решимости добавить его в свой рабочий процесс и написать лучшее программное обеспечение.
Я задал вопрос вчера, что-то вроде этого, но, похоже, это вопрос сам по себе. Я сел приступить к реализации класса обслуживания, который буду использовать для отвлечения бизнес-логики от контроллеров и сопоставления с конкретными моделями и взаимодействиями данных с использованием EF6.
Проблема в том, что я уже заблокировал себя, потому что я не хотел абстрагировать EF в репозитории (он все еще будет доступен за пределами сервисов для определенных запросов и т. Д.) И хотел бы протестировать мои сервисы (будет использоваться контекст EF),
Здесь, наверное, вопрос, есть ли смысл делать это? Если да, то как люди делают это в дикой природе в свете утечек абстракций, вызванных IQueryable, и многих замечательных постов Ladislav Mrnka, посвященных модульному тестированию, которые не являются прямыми из-за различий в поставщиках Linq при работе с памятью реализация в применении к конкретной базе данных.
Код, который я хочу проверить, кажется довольно простым. (это просто фиктивный код, чтобы попытаться понять, что я делаю, я хочу управлять созданием, используя TDD)
контекст
public interface IContext
{
IDbSet<Product> Products { get; set; }
IDbSet<Category> Categories { get; set; }
int SaveChanges();
}
public class DataContext : DbContext, IContext
{
public IDbSet<Product> Products { get; set; }
public IDbSet<Category> Categories { get; set; }
public DataContext(string connectionString)
: base(connectionString)
{
}
}
обслуживание
public class ProductService : IProductService
{
private IContext _context;
public ProductService(IContext dbContext)
{
_context = dbContext;
}
public IEnumerable<Product> GetAll()
{
var query = from p in _context.Products
select p;
return query;
}
}
В настоящее время я собираюсь сделать несколько вещей:
- Mocking EF Контекст с чем-то вроде этого подхода - Mocking EF При модульном тестировании или непосредственное использование среды моделирования на интерфейсе, таком как moq - вы испытываете боль, которую могут пройти модульные тесты, но не обязательно работают из конца в конец, и резервируйте их с помощью интеграционных тестов?
- Может быть, использовать что-то вроде Effort, чтобы издеваться над EF - я никогда не использовал его и не уверен, что кто-то еще использует его в дикой природе?
- Не беспокойтесь о тестировании чего-либо, что просто вызывает EF - так что, по сути, сервисные методы, которые вызывают EF напрямую (getAll и т. Д.), Не тестируются модулем, а просто проверяются на интеграцию?
Кто-нибудь на самом деле делает это без репо и имеет успех?
12 ответов
Это тема, которая меня очень интересует. Многие пуристы говорят, что не стоит тестировать такие технологии, как EF и NHibernate. Они правы, они уже очень строго проверены и, как говорилось в предыдущем ответе, часто бессмысленно тратить огромное количество времени на проверку того, чем вы не владеете.
Тем не менее, у вас есть база данных внизу! Вот где этот подход, по моему мнению, выходит из строя, вам не нужно проверять, что EF/NH выполняют свою работу правильно. Вы должны проверить, что ваши отображения / реализации работают с вашей базой данных. На мой взгляд, это одна из самых важных частей системы, которую вы можете протестировать.
Строго говоря, мы выходим из области модульного тестирования и в интеграционное тестирование, но принципы остаются неизменными.
Первое, что вам нужно сделать, это уметь имитировать ваш DAL, чтобы ваш BLL мог быть протестирован независимо от EF и SQL. Это ваши юнит-тесты. Затем вам нужно разработать свои интеграционные тесты, чтобы подтвердить свой DAL, на мой взгляд, они так же важны.
Есть несколько вещей для рассмотрения:
- Ваша база данных должна быть в известном состоянии с каждым тестом. Большинство систем используют для этого либо резервную копию, либо создают сценарии.
- Каждый тест должен повторяться
- Каждый тест должен быть атомным
Существует два основных подхода к настройке вашей базы данных, первый - запустить скрипт создания базы данных UnitTest. Это гарантирует, что ваша база данных модульных тестов всегда будет в одном и том же состоянии в начале каждого теста (вы можете сбросить это или запустить каждый тест в транзакции, чтобы убедиться в этом).
Другой вариант - то, что я делаю, - запускаю конкретные настройки для каждого отдельного теста. Я считаю, что это лучший подход по двум основным причинам:
- Ваша база данных проще, вам не нужна полная схема для каждого теста
- Каждый тест безопаснее, если вы измените одно значение в вашем скрипте создания, это не лишит законной силы десятки других тестов.
К сожалению, ваш компромисс здесь - скорость. Требуется время, чтобы выполнить все эти тесты, чтобы запустить все эти сценарии установки / демонтажа.
И наконец, написать такой большой объем SQL для тестирования ORM может быть очень тяжелой работой. Вот где я применяю очень противный подход (пуристы здесь не согласятся со мной). Я использую свой ORM для создания своего теста! Вместо того, чтобы иметь отдельный сценарий для каждого теста DAL в моей системе, у меня есть фаза настройки теста, которая создает объекты, присоединяет их к контексту и сохраняет их. Затем я запускаю свой тест.
Это далеко не идеальное решение, однако на практике я считаю, что им ОЧЕНЬ проще управлять (особенно если у вас несколько тысяч тестов), в противном случае вы создаете огромное количество скриптов. Практичность за чистотой.
Я, без сомнения, посмотрю на этот ответ через несколько лет (месяцев / дней) и не соглашусь с самим собой, поскольку мои подходы изменились - однако это мой нынешний подход.
Чтобы подытожить все, что я сказал выше, это мой типичный тест интеграции БД:
[Test]
public void LoadUser()
{
this.RunTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
return user.UserID;
}, id => // the ID of the entity we need to load
{
var user = LoadMyUser(id); // load the entity
Assert.AreEqual("Mr", user.Title); // test your properties
Assert.AreEqual("Joe", user.Firstname);
Assert.AreEqual("Bloggs", user.Lastname);
}
}
Ключевым моментом, на который следует обратить внимание, является то, что сеансы двух циклов полностью независимы. В вашей реализации RunTest вы должны убедиться, что контекст зафиксирован и уничтожен, и ваши данные могут поступать только из вашей базы данных для второй части.
Изменить 13/10/2014
Я сказал, что, вероятно, пересмотрю эту модель в ближайшие месяцы. Несмотря на то, что я в основном придерживаюсь подхода, который я защищал выше, я немного обновил свой механизм тестирования. Теперь я склонен создавать сущности в TestSetup и TestTearDown.
[SetUp]
public void Setup()
{
this.SetupTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
});
}
[TearDown]
public void TearDown()
{
this.TearDownDatabase();
}
Затем проверьте каждое свойство в отдельности
[Test]
public void TestTitle()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Mr", user.Title);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Bloggs", user.Lastname);
}
Есть несколько причин для такого подхода:
- Нет дополнительных вызовов базы данных (одна настройка, одна разборка)
- Тесты гораздо более детализированы, каждый тест проверяет одно свойство
- Логика Setup/TearDown удалена из самих методов тестирования
Я чувствую, что это делает тестовый класс проще, а тесты более детальными ( отдельные утверждения хороши)
Изменить 3/5/2015
Еще один пересмотр этого подхода. Хотя настройки уровня класса очень полезны для таких тестов, как загрузка свойств, они менее полезны, когда требуются разные настройки. В этом случае установка нового класса для каждого случая является излишним.
Чтобы помочь с этим, у меня теперь есть два базовых класса. SetupPerTest
а также SingleSetup
, Эти два класса предоставляют структуру по мере необходимости.
в SingleSetup
у нас очень похожий механизм, как описано в моем первом редактировании. Примером будет
public TestProperties : SingleSetup
{
public int UserID {get;set;}
public override DoSetup(ISession session)
{
var user = new User("Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Bloggs", user.Lastname);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
}
Однако ссылки, которые гарантируют, что загружены только правильные объекты, могут использовать подход SetupPerTest
public TestProperties : SetupPerTest
{
[Test]
public void EnsureCorrectReferenceIsLoaded()
{
int friendID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriend();
session.Save(user);
friendID = user.Friends.Single().FriendID;
} () =>
{
var user = GetUser();
Assert.AreEqual(friendID, user.Friends.Single().FriendID);
});
}
[Test]
public void EnsureOnlyCorrectFriendsAreLoaded()
{
int userID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriends(2);
var user2 = CreateUserWithFriends(5);
session.Save(user);
session.Save(user2);
userID = user.UserID;
} () =>
{
var user = GetUser(userID);
Assert.AreEqual(2, user.Friends.Count());
});
}
}
В итоге оба подхода работают в зависимости от того, что вы пытаетесь проверить.
Усилие Опыт Обратная связь здесь
После долгих чтений я использовал Effort в своих тестах: во время тестов Context создается фабрикой, которая возвращает версию в памяти, что позволяет мне каждый раз тестировать с чистого листа. Вне тестов фабрика преобразуется в ту, которая возвращает весь контекст.
Однако у меня есть ощущение, что тестирование на основе полнофункционального макета базы данных приводит к затягиванию тестов; вы понимаете, что вам нужно позаботиться о настройке целого ряда зависимостей, чтобы протестировать одну часть системы. Вы также склоняетесь к организации вместе тестов, которые могут быть не связаны, просто потому, что есть только один огромный объект, который обрабатывает все. Если вы не обращаете внимания, вы можете провести интеграционное тестирование вместо юнит-тестирования
Я бы предпочел тестирование против чего-то более абстрактного, а не огромного DBContext, но я не мог найти сладкое место между значимыми тестами и тестами "без костей". Мел, до моей неопытности.
Так что я нахожу усилия интересными; если вам нужно быстро начать работу, это хороший инструмент для быстрого начала работы и получения результатов. Однако я думаю, что следующим шагом должно быть что-то более элегантное и абстрактное, и это то, что я собираюсь исследовать дальше. Любим этот пост, чтобы увидеть, куда он пойдет дальше:)
Редактировать, чтобы добавить: Усилие действительно занимает некоторое время, чтобы согреться, так что вы смотрите на ок. 5 секунд при запуске теста. Это может быть проблемой для вас, если вы хотите, чтобы ваш набор тестов был очень эффективным.
Отредактировано для уточнения:
Я использовал Effort для тестирования приложения веб-сервиса. Каждое входящее сообщение M направляется на IHandlerOf<M>
через Виндзор. Castle.Windsor разрешает IHandlerOf<M>
который восстанавливает зависимости компонента. Одной из этих зависимостей является DataContextFactory
что позволяет обработчику запрашивать фабрику
В своих тестах я непосредственно создаю экземпляр компонента IHandlerOf, макетирую все подкомпоненты SUT и обрабатывает Effort-wrapped DataContextFactory
к обработчику.
Это означает, что я не занимаюсь модульным тестом в строгом смысле, поскольку БД поражен моими тестами. Однако, как я уже сказал выше, это позволило мне взяться за дело, и я мог быстро проверить некоторые точки в приложении
Если вы хотите выполнить модульное тестирование кода, то вам необходимо изолировать код, который вы хотите протестировать (в данном случае ваш сервис), от внешних ресурсов (например, баз данных). Возможно, вы могли бы сделать это с каким-либо провайдером EF в памяти, однако гораздо более распространенным способом является абстрагирование вашей реализации EF, например, с помощью некоторого шаблона репозитория. Без этой изоляции любые написанные вами тесты будут интеграционными, а не модульными.
Что касается тестирования кода EF - я пишу автоматические интеграционные тесты для моих репозиториев, которые записывают различные строки в базу данных во время их инициализации, а затем вызываю мои реализации репозитория, чтобы убедиться, что они ведут себя так, как ожидалось (например, чтобы убедиться, что результаты отфильтрованы правильно, или что они отсортированы в правильном порядке).
Это интеграционные тесты, а не модульные тесты, поскольку тесты основаны на наличии соединения с базой данных и на том, что в целевой базе данных уже установлена самая последняя обновленная схема.
Я как-то возился где-то, чтобы прийти к следующим соображениям:
1- Если мое приложение обращается к базе данных, почему тест не должен? Что если что-то не так с доступом к данным? Тесты должны знать это заранее и предупредить себя о проблеме.
2- Шаблон репозитория довольно сложен и требует много времени.
Поэтому я пришел к такому подходу, который я не считаю лучшим, но оправдал мои ожидания:
Use TransactionScope in the tests methods to avoid changes in the database.
Для этого необходимо:
1- Установите EntityFramework в тестовый проект. 2- Поместите строку подключения в файл app.config тестового проекта. 3- Ссылка на dll System.Transaction в тестовом проекте.
Уникальный побочный эффект заключается в том, что начальное значение идентификатора будет увеличиваться при попытке вставить, даже когда транзакция будет прервана. Но поскольку тесты проводятся на базе данных разработки, это не должно быть проблемой.
Образец кода:
[TestClass]
public class NameValueTest
{
[TestMethod]
public void Edit()
{
NameValueController controller = new NameValueController();
using(var ts = new TransactionScope()) {
Assert.IsNotNull(controller.Edit(new Models.NameValue()
{
NameValueId = 1,
name1 = "1",
name2 = "2",
name3 = "3",
name4 = "4"
}));
//no complete, automatically abort
//ts.Complete();
}
}
[TestMethod]
public void Create()
{
NameValueController controller = new NameValueController();
using (var ts = new TransactionScope())
{
Assert.IsNotNull(controller.Create(new Models.NameValue()
{
name1 = "1",
name2 = "2",
name3 = "3",
name4 = "4"
}));
//no complete, automatically abort
//ts.Complete();
}
}
}
Итак, вот в чем дело, Entity Framework - это реализация, поэтому, несмотря на то, что она абстрагирует сложность взаимодействия с базой данных, непосредственное взаимодействие по-прежнему тесно связано, и поэтому тестирование сложно.
Модульное тестирование - это тестирование логики функции и каждого из ее потенциальных результатов в отрыве от любых внешних зависимостей, которые в данном случае являются хранилищем данных. Для этого вам необходимо иметь возможность контролировать поведение хранилища данных. Например, если вы хотите утверждать, что ваша функция возвращает false, если выбранный пользователь не удовлетворяет некоторому набору критериев, тогда ваше хранилище данных [mocked] должно быть настроено так, чтобы всегда возвращать пользователя, который не соответствует критериям, и наоборот. наоборот для противоположного утверждения.
С учетом сказанного и принимая во внимание тот факт, что EF является реализацией, я, скорее всего, предпочел бы идею абстрагирования хранилища. Кажется, немного излишним? Это не так, потому что вы решаете проблему, которая изолирует ваш код от реализации данных.
В DDD репозитории всегда возвращают только совокупные корни, а не DAO. Таким образом, потребитель хранилища никогда не должен знать о реализации данных (как это не должно быть), и мы можем использовать это как пример того, как решить эту проблему. В этом случае объект, сгенерированный EF, является DAO и поэтому должен быть скрыт от вашего приложения. Это еще одно преимущество репозитория, который вы определяете. Вы можете определить бизнес-объект как тип его возврата вместо объекта EF. Теперь репо скрывает вызовы EF и сопоставляет ответ EF с этим бизнес-объектом, определенным в подписи репо. Теперь вы можете использовать этот репозиторий вместо зависимости DbContext, которую вы внедряете в свои классы, и, следовательно, теперь вы можете смоделировать этот интерфейс, чтобы дать вам контроль, который вам нужен для тестирования вашего кода изолированно.
Это немного больше работы, и многие суют свой нос в это, но это решает реальную проблему. Есть провайдер в памяти, который был упомянут в другом ответе, который мог бы быть вариантом (я не пробовал), и само его существование является доказательством необходимости практики.
Я полностью не согласен с основным ответом, потому что он обходит реальную проблему, которая изолирует ваш код, а затем идет по пути тестирования вашего отображения. Обязательно протестируйте свое отображение, если хотите, но решите настоящую проблему здесь и получите реальное покрытие кода.
Я бы не стал тестировать код, который мне не принадлежит. Что вы тестируете здесь, что компилятор MSFT работает?
Тем не менее, чтобы сделать этот код тестируемым, вы почти ДОЛЖНЫ отделить свой уровень доступа к данным от кода бизнес-логики. Что я делаю, так это беру все свои EF и помещаю их в (или несколько) классов DAO или DAL, которые также имеют соответствующий интерфейс. Затем я пишу свой сервис, в который будет вставлен объект DAO или DAL в качестве зависимости (предпочтительно для конструктора), на которую ссылается интерфейс. Теперь ту часть, которую необходимо протестировать (ваш код), можно легко протестировать, смоделировав интерфейс DAO и вставив его в экземпляр службы в модульном тесте.
//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
private IProductDAO _productDAO;
public ProductService(IProductDAO productDAO)
{
_productDAO = productDAO;
}
public List<Product> GetAllProducts()
{
return _productDAO.GetAll();
}
...
}
Я считаю, что живые уровни доступа к данным являются частью интеграционного тестирования, а не модульного тестирования. Я видел, как парни проверяли, сколько поездок в спящий режим базы данных совершал раньше, но они были в проекте, который включал миллиарды записей в своем хранилище данных, и эти дополнительные поездки действительно имели значение.
Короче говоря, я бы сказал, нет, сок не стоит того, чтобы тестировать метод обслуживания с помощью одной строки, которая извлекает данные модели. По моему опыту, люди, которые плохо знакомы с TDD, хотят протестировать абсолютно все. Старый каштан абстрагирования фасада от сторонних фреймворков просто для того, чтобы вы могли создать макет API этих фреймворков, с помощью которого вы убираете / расширяете, так что вы можете вводить фиктивные данные, на мой взгляд, мало что значит. У каждого свое мнение о том, сколько юнит-тестирования лучше. Сейчас я склонен быть более прагматичным и спрашиваю себя, действительно ли мой тест добавляет ценность конечному продукту и по какой цене.
Я хочу поделиться подходом, который прокомментирован и кратко обсужден, но покажу реальный пример, который я сейчас использую, чтобы помочь юнит-тестированию сервисов на основе EF.
Во-первых, я хотел бы использовать провайдера в памяти от EF Core, но это касается EF 6. Кроме того, для других систем хранения, таких как RavenDB, я также был бы сторонником тестирования через провайдера базы данных в памяти. Опять же - это специально для того, чтобы помочь тестировать код на основе EF без особых церемоний.
Вот цели, которые я ставил, когда придумывал шаблон:
- Для других разработчиков в команде должно быть просто понять
- Он должен изолировать код EF на минимально возможном уровне
- Он не должен включать создание странных интерфейсов с множественной ответственностью (таких как "общий" или "типичный" шаблон хранилища)
- Это должно быть легко настроить и настроить в модульном тесте
Я согласен с предыдущими утверждениями о том, что EF по-прежнему является деталью реализации, и вполне нормально чувствовать, что вам необходимо абстрагировать его для проведения "чистого" модульного теста. Я также согласен с тем, что в идеале я хотел бы убедиться, что сам код EF работает, но это включает в себя базу данных песочницы, провайдера в памяти и т. Д. Мой подход решает обе проблемы - вы можете безопасно выполнить модульный тест EF-зависимого кода и создать Интеграционные тесты для тестирования вашего кода EF.
Я достиг этого путем простой инкапсуляции кода EF в выделенные классы запросов и команд. Идея проста: просто обернуть любой код EF в класс и зависеть от интерфейса в классах, который первоначально использовал бы его. Основной проблемой, которую мне нужно было решить, было избегать добавления многочисленных зависимостей к классам и настройки большого количества кода в моих тестах.
Здесь полезная и простая библиотека: Mediatr. Он допускает простой обмен сообщениями в процессе и делает это путем отделения "запросов" от обработчиков, которые реализуют код. Это имеет дополнительное преимущество, заключающееся в том, чтобы отделить "что" от "как". Например, инкапсулируя код EF в небольшие порции, это позволяет вам заменить реализации другим провайдером или совершенно другим механизмом, потому что все, что вы делаете, это отправляете запрос на выполнение действия.
Используя внедрение зависимостей (с или без фреймворка - ваше предпочтение), мы можем легко смоделировать посредник и управлять механизмами запроса / ответа, чтобы включить модульное тестирование кода EF.
Во-первых, допустим, у нас есть сервис с бизнес-логикой, который мы должны протестировать:
public class FeatureService {
private readonly IMediator _mediator;
public FeatureService(IMediator mediator) {
_mediator = mediator;
}
public async Task ComplexBusinessLogic() {
// retrieve relevant objects
var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
// normally, this would have looked like...
// var results = _myDbContext.DbObjects.Where(x => foo).ToList();
// perform business logic
// ...
}
}
Вы начинаете видеть преимущества этого подхода? Вы не только явно инкапсулируете весь код, связанный с EF, в описательные классы, но и разрешаете расширяемость, устраняя проблему реализации того, "как" обрабатывается этот запрос - этому классу не важно, приходят ли соответствующие объекты из EF, MongoDB, или текстовый файл.
Теперь для запроса и обработчика через MediatR:
public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
// no input needed for this particular request,
// but you would simply add plain properties here if needed
}
public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
private readonly IDbContext _db;
public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
_db = db;
}
public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
return _db.DbObjects.Where(foo => bar).ToList();
}
}
Как видите, абстракция проста и инкапсулирована. Это также абсолютно пригодно для тестирования, потому что в интеграционном тесте вы можете протестировать этот класс индивидуально - здесь нет никаких проблем для бизнеса.
Так как же выглядит юнит-тест нашего сервиса объектов? Это очень просто. В этом случае я использую Moq для насмешек (используйте все, что вас радует):
[TestClass]
public class FeatureServiceTests {
// mock of Mediator to handle request/responses
private Mock<IMediator> _mediator;
// subject under test
private FeatureService _sut;
[TestInitialize]
public void Setup() {
// set up Mediator mock
_mediator = new Mock<IMediator>(MockBehavior.Strict);
// inject mock as dependency
_sut = new FeatureService(_mediator.Object);
}
[TestCleanup]
public void Teardown() {
// ensure we have called or expected all calls to Mediator
_mediator.VerifyAll();
}
[TestMethod]
public void ComplexBusinessLogic_Does_What_I_Expect() {
var dbObjects = new List<DbObject>() {
// set up any test objects
new DbObject() { }
};
// arrange
// setup Mediator to return our fake objects when it receives a message to perform our query
// in practice, I find it better to create an extension method that encapsulates this setup here
_mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
(GetRelevantDbObjectsQuery message, CancellationToken token) => {
// using Moq Callback functionality, you can make assertions
// on expected request being passed in
Assert.IsNotNull(message);
});
// act
_sut.ComplexBusinessLogic();
// assertions
}
}
Вы можете видеть, что все, что нам нужно, - это одна настройка, и нам даже не нужно ничего настраивать - это очень простой модульный тест. Давайте будем ясны: это абсолютно возможно обойтись без чего-то вроде Mediatr (вы просто реализуете интерфейс и смоделируете его для тестов, например, IGetRelevantDbObjectsQuery
), но на практике для большой кодовой базы со многими функциями и запросами / командами я люблю инкапсуляцию и врожденную поддержку DI, которую предлагает Mediatr.
Если вам интересно, как я организую эти классы, это довольно просто:
- MyProject
- Features
- MyFeature
- Queries
- Commands
- Services
- DependencyConfig.cs (Ninject feature modules)
Организация по частям функций не имеет смысла, но это сохраняет весь релевантный / зависимый код вместе и легко обнаруживается. Самое главное, я отделяю запросы от команд - следуя принципу разделения команд / запросов.
Это соответствует всем моим критериям: это непритязательная церемония, ее легко понять и есть дополнительные скрытые преимущества. Например, как вы справляетесь с сохранением изменений? Теперь вы можете упростить ваш Db Context, используя интерфейс ролей (IUnitOfWork.SaveChangesAsync()
) и имитировать вызовы к интерфейсу с одной ролью, или вы можете инкапсулировать фиксацию / откат внутри вашего RequestHandlers - однако вы предпочитаете делать это сами, если это поддерживается. Например, у меня возник соблазн создать один общий запрос / обработчик, в котором вы просто передадите объект EF, и он сохранит / обновит / удалит его - но вы должны спросить, каково ваше намерение, и помнить, что если вы хотите поменяйте обработчик с другим поставщиком / реализацией хранилища, вам, вероятно, следует создать явные команды / запросы, которые представляют то, что вы намерены делать. Чаще всего одному сервису или функции нужно что-то конкретное - не создавайте общие вещи, пока они вам не понадобятся.
Конечно, есть некоторые предостережения в отношении этого паттерна - вы можете зайти слишком далеко с помощью простого механизма pub / sub. Я ограничил свою реализацию только абстрагированием кода, связанного с EF, но авантюрные разработчики могли бы начать использовать MediatR для того, чтобы идти за борт и обмениваться сообщениями - что-то хорошее, что нужно изучить при проверке кода и проверке со стороны коллег. Это проблема процесса, а не проблема с MediatR, так что просто знайте, как вы используете этот шаблон.
Вы хотели конкретный пример того, как люди тестируют / насмехаются над EF, и этот подход успешно работает для нас в нашем проекте - и команда очень довольна тем, насколько легко его принять. Надеюсь, это поможет! Как и во всех вещах в программировании, существует несколько подходов, и все зависит от того, чего вы хотите достичь. Я ценю простоту, простоту использования, ремонтопригодность и открываемость - и это решение отвечает всем этим требованиям.
Чтобы создать код модульного тестирования, который полагается на вашу базу данных, вам необходимо настроить базу данных или макет для каждого теста.
- Наличие базы данных (реальной или имитации) с единым состоянием для всех ваших тестов быстро вас укусит; вы не можете проверить, что все записи действительны, а некоторые не из одних и тех же данных.
- При настройке базы данных в памяти в OneTimeSetup возникнут проблемы, когда старая база данных не будет очищена до запуска следующего теста. Будет показано, что тесты работают, когда вы запускаете их по отдельности, но не работают, когда вы запускаете их все.
- В идеале модульный тест должен устанавливать только то, что влияет на тест
Я работаю в приложении, в котором есть много таблиц с большим количеством подключений и несколько массивных блоков Linq. Это требует тестирования. Пропущенная простая группировка или соединение, в результате которого получается более одной строки, повлияют на результаты.
Чтобы справиться с этим, я установил тяжелый помощник по модульному тестированию, который требует много работы по настройке, но позволяет нам надежно имитировать базу данных в любом состоянии и запускать 48 тестов для 55 взаимосвязанных таблиц, при этом настройка всей базы данных занимает 48 раз. 4,7 секунды.
Вот как:
В классе контекста Db убедитесь, что каждый класс таблицы установлен как виртуальный
public virtual DbSet<Branch> Branches { get; set; } public virtual DbSet<Warehouse> Warehouses { get; set; }
В классе UnitTestHelper создайте метод для настройки вашей базы данных. Каждый класс таблицы является необязательным параметром. Если не указан, он будет создан с помощью метода Make.
internal static Db Bootstrap(bool onlyMockPassedTables = false, List<Branch> branches = null, List<Products> products = null, List<Warehouses> warehouses = null)
{
if (onlyMockPassedTables == false) {
branches ??= new List<Branch> { MakeBranch() };
warehouses ??= new List<Warehouse>{ MakeWarehouse() };
}
Для каждого класса таблицы каждый объект в нем сопоставлен с другими списками
branches?.ForEach(b => {
b.Warehouse = warehouses.FirstOrDefault(w => w.ID == b.WarehouseID);
});
warehouses?.ForEach(w => {
w.Branches = branches.Where(b => b.WarehouseID == w.ID);
});
И добавьте его в DbContext
var context = new Db(new DbContextOptionsBuilder<Db>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
context.Branches.AddRange(branches);
context.Warehouses.AddRange(warehouses);
context.SaveChanges();
return context;
}
Определите список идентификаторов, чтобы упростить их повторное использование и убедиться, что объединения действительны
internal const int BranchID = 1;
internal const int WarehouseID = 2;
Создайте Make для каждой таблицы, чтобы настроить самую простую, но связанную версию, которую можно
internal static Branch MakeBranch(int id = BranchID, string code = "The branch", int warehouseId = WarehouseID) => new Branch { ID = id, Code = code, WarehouseID = warehouseId };
internal static Warehouse MakeWarehouse(int id = WarehouseID, string code = "B", string name = "My Big Warehouse") => new Warehouse { ID = id, Code = code, Name = name };
Это большая работа, но ее нужно проделать только один раз, и тогда ваши тесты могут быть очень сфокусированными, потому что остальная часть базы данных будет настроена для этого.
[Test]
[TestCase(new string [] {"ABC", "DEF"}, "ABC", ExpectedResult = 1)]
[TestCase(new string [] {"ABC", "BCD"}, "BC", ExpectedResult = 2)]
[TestCase(new string [] {"ABC"}, "EF", ExpectedResult = 0)]
[TestCase(new string[] { "ABC", "DEF" }, "abc", ExpectedResult = 1)]
public int Given_SearchingForBranchByName_Then_ReturnCount(string[] codesInDatabase, string searchString)
{
// Arrange
var branches = codesInDatabase.Select(x => UnitTestHelpers.MakeBranch(code: $"qqqq{x}qqq")).ToList();
var db = UnitTestHelpers.Bootstrap(branches: branches);
var service = new BranchService(db);
// Act
var result = service.SearchByName(searchString);
// Assert
return result.Count();
}
Существует Effort, который является провайдером базы данных в памяти. Я на самом деле не пробовал... Хаа только что заметил это было упомянуто в вопросе!
В качестве альтернативы вы можете переключиться на EntityFrameworkCore, который имеет встроенный поставщик базы данных в памяти.
https://github.com/tamasflamich/effort
Я использовал фабрику, чтобы получить контекст, чтобы я мог создать контекст, близкий к его использованию. Кажется, это работает локально в visual studio, но не на моем сервере сборки TeamCity, пока не знаю почему.
return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");
Мне нравится отделять свои фильтры от других частей кода и тестировать их, как я обрисовал в общих чертах в моем блоге здесь http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html
При этом тестируемая логика фильтра не идентична логике фильтра, выполняемой при запуске программы из-за преобразования между выражением LINQ и базовым языком запросов, таким как T-SQL. Тем не менее, это позволяет мне проверить логику фильтра. Я не слишком беспокоюсь о переводах, которые происходят, и о таких вещах, как чувствительность к регистру и обработка нуля, пока не проверю интеграцию между слоями.
Важно проверить, чего вы ожидаете от entity framework (т.е. подтвердите ваши ожидания). Один из способов сделать это, который я успешно использовал, - использовать moq, как показано в этом примере (чтобы скопировать в этот ответ):
https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking
Однако будьте осторожны... Контекст SQL не гарантирует возврата вещей в определенном порядке, если у вас нет соответствующего "OrderBy" в вашем запросе linq, поэтому можно писать вещи, которые проходят, когда вы тестируете с использованием списка в памяти (linq-to-entity), но не работает в вашей среде uat / live при использовании (linq-to-sql).