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