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