Я хочу поділитися прокоментованим та коротко обговореним підходом, але показати фактичний приклад, який я зараз використовую для тестування одиниць послуг на базі EF.
По-перше, я хотів би використовувати провайдера пам'яті від EF Core, але це приблизно для EF 6. Крім того, для інших систем зберігання даних, таких як RavenDB, я також був би прихильником тестування через постачальника баз даних в пам'яті. Знову ж таки - це спеціально для тестування коду на основі EF без великої кількості церемоній .
Ось цілі, які я мав, коли я придумав схему:
- Інші розробники в команді повинні бути зрозумілі
- Він повинен ізолювати код EF на найменшому можливому рівні
- Він не повинен включати створення дивних інтерфейсів з багатовідповідальністю (наприклад, "загальний" або "типовий" шаблон сховища)
- Конфігурувати та налаштовувати під час тестування пристрою слід легко
Я погоджуюся з попередніми твердженнями, що EF все ще є детальною інформацією про реалізацію, і непогано відчувати, що вам потрібно її абстрагувати, щоб зробити "чистий" одиничний тест. Я також погоджуюся, що в ідеалі я хотів би переконатись, що сам код EF працює - але це включає базу даних пісочниці, постачальника пам'яті тощо. Мій підхід вирішує обидві проблеми - можна сміливо використовувати тестовий код, залежний від EF, і створювати інтеграційні тести, щоб спеціально перевірити код EF.
Я домігся цього шляхом просто інкапсуляції коду EF у спеціальні класи Query and Command. Ідея проста: просто загорніть будь-який код 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, використовуючи рольовий інтерфейс (IUnitOfWork.SaveChangesAsync()) та знущатися над дзвінками до інтерфейсу однієї ролі, або ти можеш інкапсулювати здійснення чи відкат всередині своїх RequestHandlers - однак ти вважаєш за краще робити це залежно від тебе, поки це неможливо. Наприклад, я спокусився створити єдиний загальний запит / обробник, де ви просто передатимете об'єкт EF, і він збереже / оновить / видалить його - але ви повинні запитати, який ваш намір, і пам’ятати, що якщо ви хочете поміняйте обробник з іншим постачальником / реалізацією пам’яті, ви, ймовірно, повинні створити явні команди / запити, які відображають те, що ви маєте намір робити. Частіше всього для одного сервісу чи функції знадобиться щось конкретне - не створюйте загальних речей, перш ніж у вас виникне потреба.
Є , звичайно , застережень до цієї моделі - ви можете зайти надто далеко з простим механізмом паб / к південь. Я обмежив свою реалізацію лише абстрагуванням коду, пов'язаного з EF, але авантюрні розробники могли почати використовувати MediatR для того, щоб перебирати за борт і повідомляти про все - щось хороша практика перегляду коду та експертні огляди повинні вловлювати. Це проблема процесу, а не проблема з MediatR, тому просто усвідомлюйте, як ви використовуєте цей шаблон.
Ви хотіли конкретного прикладу того, як люди проходять тестування / глузування з EF, і це такий підхід, який успішно працює для нас над нашим проектом - і команда дуже задоволена тим, як легко їх прийняти. Я сподіваюся, що це допомагає! Як і у всіх речах програмування, існує кілька підходів, і все залежить від того, чого ви хочете досягти. Я ціную простоту, зручність у використанні, ремонтопридатність та відкритість - і це рішення відповідає всім цим вимогам.