Chcę podzielić się podejściem, które zostało skomentowane i krótko omówione, ale pokażę rzeczywisty przykład, którego obecnie używam, aby pomóc w testach jednostkowych usług opartych na EF.
Po pierwsze, chciałbym użyć dostawcy w pamięci z EF Core, ale chodzi o EF 6. Ponadto w przypadku innych systemów magazynowania, takich jak RavenDB, byłbym również zwolennikiem testowania za pośrednictwem dostawcy bazy danych w pamięci. Ponownie - ma to na celu pomóc w testowaniu kodu opartego na EF bez zbytniej ceremonii .
Oto cele, które wyznaczyłem sobie, wymyślając wzór:
- To musi być łatwe do zrozumienia dla innych programistów w zespole
- Musi izolować kod EF na najniższym możliwym poziomie
- Nie może obejmować tworzenia dziwnych interfejsów z wieloma odpowiedzialnością (takich jak „ogólny” lub „typowy” wzorzec repozytorium)
- Musi być łatwy do skonfigurowania i ustawienia w teście jednostkowym
Zgadzam się z poprzednimi stwierdzeniami, że EF nadal jest szczegółem implementacyjnym i można czuć, że trzeba go wyabstrahować, aby wykonać „czysty” test jednostkowy. Zgadzam się też, że idealnie chciałbym upewnić się, że sam kod EF działa - ale dotyczy to bazy danych piaskownicy, dostawcy w pamięci itp. Moje podejście rozwiązuje oba problemy - można bezpiecznie testować jednostkowo kod zależny od EF i tworzyć testy integracji w celu przetestowania kodu EF w szczególności.
Sposób, w jaki to osiągnąłem, polegał na prostym hermetyzowaniu kodu EF w dedykowanych klasach Query i Command. Pomysł jest prosty: po prostu zawiń dowolny kod EF w klasę i polegaj na interfejsie w klasach, które pierwotnie go używały. Głównym problemem, który musiałem rozwiązać, było uniknięcie dodawania wielu zależności do klas i konfigurowania dużej ilości kodu w moich testach.
Tutaj pojawia się użyteczna, prosta biblioteka: Mediatr . Pozwala na proste przesyłanie komunikatów w trakcie procesu i robi to poprzez oddzielenie „żądań” od programów obsługi, które implementują kod. Ma to dodatkową zaletę w postaci oddzielenia „co” od „jak”. Na przykład, hermetyzując kod EF na małe fragmenty, umożliwia zastąpienie implementacji innym dostawcą lub zupełnie innym mechanizmem, ponieważ wszystko, co robisz, to wysyłanie żądania wykonania akcji.
Wykorzystując iniekcję zależności (z frameworkiem lub bez niego - preferencje użytkownika), możemy łatwo mockować mediatora i kontrolować mechanizmy żądania / odpowiedzi, aby umożliwić testowanie jednostkowe kodu EF.
Po pierwsze, powiedzmy, że mamy usługę, która ma logikę biznesową, którą musimy przetestować:
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
// ...
}
}
Czy zaczynasz dostrzegać korzyści płynące z tego podejścia? Nie tylko jawnie hermetyzujesz cały kod związany z EF w klasach opisowych, ale także pozwalasz na rozszerzalność, usuwając problem implementacji dotyczący sposobu obsługi tego żądania - ta klasa nie obchodzi, czy odpowiednie obiekty pochodzą z EF, MongoDB, lub plik tekstowy.
Teraz dla wnioskodawcy i osoby obsługującej, za pośrednictwem 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();
}
}
Jak widać, abstrakcja jest prosta i zamknięta. Jest to także absolutnie sprawdzalne , ponieważ w teście integracyjnym, to mógłby przetestować tę klasę indywidualnie - nie ma obawy roboczych mieszane tutaj.
Jak więc wygląda test jednostkowy naszej usługi funkcji? To bardzo proste. W tym przypadku używam Moq do robienia kpiny (używaj tego, co cię uszczęśliwia):
[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
}
}
Widzisz, że potrzebujemy tylko jednej konfiguracji i nie musimy nawet niczego konfigurować - to bardzo prosty test jednostkowy. Wyjaśnijmy: jest to całkowicie możliwe bez czegoś takiego jak Mediatr (po prostu zaimplementowałbyś interfejs i wyszywałbyś go np. Do testów IGetRelevantDbObjectsQuery
), ale w praktyce dla dużej bazy kodu z wieloma funkcjami i zapytaniami / poleceniami, uwielbiam hermetyzację i wrodzone wsparcie DI oferuje Mediatr.
Jeśli zastanawiasz się, jak organizuję te zajęcia, jest to całkiem proste:
- MyProject
- Features
- MyFeature
- Queries
- Commands
- Services
- DependencyConfig.cs (Ninject feature modules)
Organizowanie według wycinków funkcji nie ma znaczenia, ale pozwala to zachować cały odpowiedni / zależny kod razem i łatwo go znaleźć. Co najważniejsze, oddzielam zapytania od poleceń - kierując się zasadą separacji poleceń / zapytań .
To spełnia wszystkie moje kryteria: jest to niska ceremonia, łatwa do zrozumienia i są dodatkowe ukryte korzyści. Na przykład, jak radzisz sobie z zapisywaniem zmian? Teraz możesz uprościć kontekst Db, używając interfejsu roli (IUnitOfWork.SaveChangesAsync()
) i pozorowane wywołania interfejsu pojedynczej roli lub możesz hermetyzować zatwierdzanie / wycofywanie w ramach swoich RequestHandlers - jednak wolisz to robić, o ile jest to możliwe do utrzymania. Na przykład kusiło mnie, aby utworzyć jedno ogólne żądanie / procedurę obsługi, w którym po prostu przekazałbyś obiekt EF i zapisałby / zaktualizował / usunąłby go - ale musisz zapytać, jaki jest twój zamiar i pamiętaj, że jeśli chcesz zamienić moduł obsługi z innym dostawcą / implementacją pamięci masowej, prawdopodobnie powinieneś utworzyć jawne polecenia / zapytania, które reprezentują to, co zamierzasz zrobić. Najczęściej pojedyncza usługa lub funkcja będzie wymagać czegoś konkretnego - nie twórz ogólnych rzeczy, zanim będziesz ich potrzebować.
Istnieją oczywiście zastrzeżenia do tego wzorca - możesz posunąć się za daleko, korzystając z prostego mechanizmu pub / sub. Ograniczyłem swoją implementację tylko do abstrakcyjnego kodu związanego z EF, ale żądni przygód programiści mogli zacząć używać MediatR, aby przesadzić i wysyłać wiadomości - coś, co dobre praktyki przeglądu kodu i recenzje powinny złapać. Jest to problem związany z procesem, a nie problemem z MediatR, więc po prostu bądź świadomy tego, jak używasz tego wzorca.
Potrzebowałeś konkretnego przykładu tego, jak ludzie testują jednostkowe / kpią z EF i jest to podejście, które z powodzeniem sprawdza się w naszym projekcie - a zespół jest bardzo zadowolony z tego, jak łatwo je zastosować. Mam nadzieję, że to pomoże! Podobnie jak w przypadku wszystkich rzeczy w programowaniu, istnieje wiele podejść i wszystko zależy od tego, co chcesz osiągnąć. Cenię prostotę, łatwość obsługi, łatwość konserwacji i wykrywalność - a to rozwiązanie spełnia wszystkie te wymagania.