Gör dina Entity Framework Repositories testbara
Denna artikel är relevant för EF 3.5 SP1. I och med EF 4 har vi tillgång till IObjectSet interfacet vilket gör saker enklare.
Entity Framework är väldigt enkelt att komma igång med. Sen, som i flera andra fall, möter man högre ställda krav på sin arkitektur när man försöker använda det i större, komplexare och kanske distribuerade sammanhang.
Med EF hör det idag till vanligheten att man implementerar Repository-mönstret för att hålla samman resursåtkomst för en viss typ av entitet. Dessvärre hålls ofta dessa Repositories hårt knutna till EF:s ObjectContext objekt, vilket gör dem svåra att enhetstesta. För att då kunna testa sina Repositories behöver man förbereda databasen med ett känt tillstånd för att kunna verifiera att uppdateringar görs på korrekt sätt samt att rätt entiteter hämtas vid en specifik fråga etc.
Som du säkert inser så blir detta problematiskt i samband med enhetstester, då man vill testa t ex en enskild metod på ett isolerat sätt.
Mitt förslag på lösning för detta problem är att implementera en abstraktionsnivå på EF:s ObjectContext, i stil med beskrivningen i Jimmy Nilssons utmärkta bok Applying Domain-Driven Design and Patterns.
Först definierar jag modellen för detta exempel. (Jag håller mig kvar vid Motorcykelaffären från en tidigare post)
Efter generering av schema och import av detta så har jag en EDM som ser ut på följande sätt. En motorcykel kan ha flera tillbehör.
Vad jag skall göra nu är att implementera en metod på mitt AccessoryRepository som returnerar alla tillbehör till hojar av märket Ducati.
Det skulle kunna se ut på detta vis.
public IQueryable<Accessory> ListAllAccessoriesForDucati() {
StoreContext context = new StoreContext();
var accessories = from accessory in context.AccessorySet
where accessory.FitsBike.Brand == "Ducati"
select accessory;
return accessories;
}
Men notera då att mitt ObjectContext (StoreContext) är fast i Repositoryt och min fråga skulle då vara beroende av databasen för att kunna testas. Att bara bryta ut ObjectContext objektet från Repositoryt och skicka med det som en referens i t ex constructorn skulle inte hjälpa mig alls med detta problem.
För att bli fri från ObjectContext låter jag istället mitt Repository vara beroende av ett Workspace. Med detta Workspace implementerar jag UnitOfWork-mönstret, vilket faktiskt ObjectContext också gör men, nu i form av ett extra lager ovan på ObjectContext så kan jag välja om jag vill använda EF:s ObjectContext eller inte.
public class AccessoryRepository {
private IWorkspace _workspace;
public AccessoryRepository(IWorkspace workspace) {
_workspace = workspace;
}
public IQueryable<Accessory> ListAllAccessoriesForDucati() {
var accessories = from accessory in _workspace.Accessories
where accessory.FitsBike.Brand == "Ducati"
select accessory;
return accessories;
}
}
Hur ser det då ut när man använder detta i sin tillämpning?
Nedan exempel visar hur ett enhetstest kan se ut för att verifiera att frågan i mitt Repository faktiskt returnerar vad den skall. Detta gör jag genom att tilldela mitt MockWorkspace (vilket implementerar interfacet IWorkspace) en lista med tillgängliga tillbehör. Notera användandet av IQueryable<T> och AsQuaryable().
Jag kan nu testa mitt EF Repository utan att behöva bry mig om hur databasen ser ut eller om jag har åtkomst till den.
[TestMethod]
public void CanListAllAccessoriesForDucati_TheIsloatedWay() {
IWorkspace workspace = new MockWorkspace() {
Accessories = new List<Accessory>() {
new Accessory(){
FitsBike = new Motorbike(){ Brand = "Ducati", Name = "Monster"},
Name = "Carbon Fairing"
},
new Accessory(){
FitsBike = new Motorbike(){ Brand = "Ducati", Name = "1198" },
Name = "Racing Exhaust System"
},
new Accessory(){
FitsBike = new Motorbike(){ Brand = "HD", Name = "Softail" },
Name = "Chrome Mirror"
}
}.AsQueryable()
};
AccessoryRepository repository = new AccessoryRepository(workspace);
var accessories = repository.ListAllAccessoriesForDucati();
int expected = 2;
int actual = accessories.Count();
Assert.AreEqual<int>(expected, actual, "Two accessories should be returned");
}
För att göra de verkliga slagningarna mot databasen använder jag istället mitt StoreWorkspace vilket i sin tur konsumerar EF:s ObjectContext.
[TestMethod]
public void CanListAllAccessoriesForDucati_Acceptance() {
IWorkspace workspace = new StoreWorkspace(new StoreContext());
AccessoryRepository repository = new AccessoryRepository(workspace);
var accessories = repository.ListAllAccessoriesForDucati();
int expected = 2;
int actual = accessories.Count();
Assert.AreEqual<int>(expected, actual, "Two accessories should be returned");
}
Om jag sedan lägger till Unity till ekvationen så får jag en fint, testbart och pluggbart dataåtkomstlager.
Håll med om att lite kod säger mer än tusen ord. ?
-c
Comments
- Anonymous
March 14, 2009
PingBack from http://www.clickandsolve.com/?p=23157