Před pár dny se kdosi na ASP.NET fóru ptal, jak testovat kód, který používá Entity Framework. Tazatel psal webovou aplikaci, connection string do databáze měl ve web.configu, a kdekoliv chtěl pracovat s databází, měl tenhle kus kódu:
using (var dc = new DbEntities())
{
}
Potíž je v tom new DbEntities. Pokud se bude vyskytovat v projektu na 1000 místech, je to nepříjemné. Kdykoliv totiž budeme chtít po inicializaci objektu něco nastavit, máme problém – budeme to muset měnit na 1000 místech.
Jak tohle souvisí s testováním? Tazatel měl několik databází s předpřipravenými daty, nad kterými chtěl testovaný kód spouštět. Connection string k databázi se, pokud není v konstruktoru DbEntities zadán, bere z konfiguračního souboru. Pokud tedy testujete proti jedné databázi, řešením by bylo do testovacího projektu přidat soubor app.config a v něm v sekci connectionStrings říct, kam se má aplikace připojovat, když běží v režimu testování.
Pokud máte testovacích databází více, museli byste mít pro každou samostatný testovací projekt a samostatné app.config soubory, nebo to řešit nějakou jinou škaredou obezličkou, což není právě elegantní.
Ale i pokud nechcete testovat, hodí se mít vytváření “důležitých” objektů v nějaké factory metodě, která může v nejhorším případě vypadat i takto:
public static DbEntities CreateEntities()
{
return new DbEntities();
}
Náš problém s testováním to nevyřeší, ale v případě, že budeme chtít při vytváření DbEntitites ještě tomuto objektu něco nastavit, pomůže i tohle.
Naivní (ale funkční) řešení by bylo dát do této factory metody podmínku if (IsRunningInTest()) a podle toho vytvářet instance se správným connection stringem, ale obecně není dobré prasit aplikační kód výhybkami pro testy.
Na řešení problému u testování je vhodné vytvořit trochu lepší infrastrukturu. Co potřebujeme? Mechanismus, který nám bude poskytovat správně nastavené instance třídy DbEntities. Budeme chtít různé implementace pro reálné použití a pro testy.
Začneme tedy abstraktní třídou (dalo by se použít i rozhraní, ale za chvíli uvidíme, proč tady mám třídu).
public abstract class DbEntitiesFactory
{
public abstract DbEntities CreateEntities();
}
/// <summary>
/// Factory pro použití v reálné aplikaci
/// </summary>
public class RealDbEntitiesFactory : DbEntitiesFactory
{
public override DbEntities CreateEntities()
{
return new DbEntities();
}
}
/// <summary>
/// Factory pro použití v testech
/// </summary>
public class TestDbEntitiesFactory : DbEntitiesFactory
{
public string ConnectionString { get; set; }
public TestDbEntitiesFactory(string connectionString)
{
ConnectionString = connectionString;
}
public override DbEntities CreateEntities()
{
// nainicializovat connectionStringem z konstruktoru
return new DbEntities(ConnectionString);
}
}
Máme tedy abstraktní třídu a dvě třídy, které implementují chování v reálném provozu a chování při testech. Factory pro testy bere v konstruktoru connection string, který pak používá při vytváření instancí.
Jak ale zajistit, že v reálné aplikaci budeme používat jednu třídu a v testech druhou? Řešení je několik, možné bude například přidání statické vlastnosti do třídy DbEntitiesFactory, která bude obsahovat ten správný objekt. Ve výchozím nastavení bude obsahovat instanci třídy RealDbEntitiesFactory, a v testovacím projektu do této vlastnosti přiřadíme instanci třídy TestDbEntitiesFactory.
private static DbEntitiesFactory factory = new RealDbEntitiesFactory();
public static DbEntitiesFactory Default
{
get { return factory; }
internal set { factory = value; }
}
Pokud budeme chtít v aplikaci vytvořit instanci DbEntities, zavoláme jednoduše:
DbEntitiesFactory.Default.CreateEntities()
Před každým testem přiřadíme do DbEntitiesFactory.Default instanci TestDbEntitiesFactory se správným connection stringem. Pokud máme pro každou testovací databázi samostatnou testovací třídu, můžeme využít metody ClassInitialize. Tím zajistíme, že se tato metoda spustí před prvním spuštěným testem z té testovací třídy.
[ClassInitialize()]
public static void ClassInit()
{
DbEntitiesFactory.Default = new TestDbEntitiesFactory("test connection string");
}
Pokud ale v nějaké testovací metodě používáme jinou databázi, můžeme tuto vlastnost nastavit kdykoliv jindy, klidně i v každém jednotlivém testu.
Vzhledem k tomu, že z testovacího projektu voláme setter, který je internal a tedy není vidět, budeme ještě v assembly aplikace muset pomocí atributu InternalsVisibleTo povolit testovacímu projektu, aby viděl internal metody. Stačí v projektu aplikace rozbalit položku Properties a do souboru AssemblyInfo.cs připsat tento řádek:
[assembly: InternalsVisibleTo("název assembly testovacího projektu")]
Pokud jste se prokousali až sem, tak vězte, že ještě není konec. Řešení, ke kterému jsme došli, není rozhodně dokonalé, mimo jiné:
1) Instance RealDbEntitiesFactory se vytváří i v testech, i když není potřeba a hned je přepsána instancí TestDbEntitiesFactory. Čert to vem, je to jeden objekt bez dat, ale i tak.
2) Není kontrola nad tím, kdo kdy onu instanci té factory mění. Může to udělat kdokoliv a kdykoliv, což v testovacím projektu můžeme uvítat (v rámci jednoho testu něco provedeme na jedné a pak na druhé databázi a porovnáváme, jak se výsledky liší), ale v reálném prostředí to nemusí být vhodné.
3) Obdobnou věc budete pravděpodobně potřebovat pro jiné objekty, např. logování, rozesílání e-mailů a spoustu dalších situací. V testech můžete často chtít některé věci dělat jinak než v reálu. Pokud byste šli touto cestou, budete mít 20 abstraktních tříd a v každé bude statická vlastnost, kam budete podstrkovat správnou instanci.
4) Tento mechanismus neřeší dobu života instancí. Občas by se nám hodilo (ve webových aplikacích obzvlášť), aby existovala jen jedna instance DbEntities pro každý HTTP požadavek, a po jeho skončení se na ni automaticky zavolalo Dispose. Pro jiné účely, např. určité typy logování, je například zase vhodné, aby existovala jedna sdílená instance pro všechny (tedy singleton).
Pokud celý tento mechanismus zobecníme a doplníme ještě o pár věcí, dostaneme buzzword dnešní doby – IoC/DI.
Když jsem o tom četl poprvé, zjistil jsem (podobně jako u většiny návrhových vzorů), že něco takového používám už dávno, aniž bych tušil, že se to tak jmenuje a že se kolem toho nadělá takový kopec řečí.
A cože vlastně ten IoC/DI kontejner dělá? Je to objekt, který při spuštění aplikace dostane sadu pravidel, tzv. “setup”. Pravidlo vypadá například tak, že “kdykoliv v aplikaci někdo bude potřebovat implementaci rozhraní / abstraktní třídy DbEntitiesFactory, tak mu podstrč objekt RealDbEntitiesFactory”.
Když aplikace potřebuje instanci, nezavolá si new, ale řekne si o ní kontejneru. Například zavolá
container.Resolve<DbEntitiesFactory>().
Kontejner se podívá do svých vnitřních pravidel a vyplivne požadovanou instanci.
Tady vidíme, že stačí mít v aplikaci a v testech různý setup a dělá to přesně to, co jsme potřebovali.
To vytváření instancí v IoC/DI kontejnerech je navíc poměrně inteligentní. Pokud se při vytváření instance třídy A zjistí, že třída A v konstruktoru vyžaduje instanci rozhraní IB, zavolá se container.Resolve<IB>() a výsledný objekt se dosadí do toho konstruktoru.
V praxi si takto můžete nechávat vytvářet třeba instanci pro rozhraní ILogger, což bude třída DatabaseLogger. Tato třída potřebuje v konstruktoru nějakou implementaci rozhraní IDatabaseAccess, aby mohla chybové hlášky zapisovat do databáze. Jinak řečeno DatabaseLogger má závislost na rozhraní IDatabaseAccess, kterou naznačí tím, že chce objekt implementující IDatabaseAccess v konstruktoru. Kontejner tento objekt podle setupu sám vytvoří a o všechno se postará.
Často se Resolve volá jen při startu aplikace, kdy poskládáme základní aplikační objekty a všechny jejich závislosti naznačené v konstruktorech kontejner dosadí sám. Ve třídě, kde potřebujeme instanci DbEntitiesFactory, ji již máme připravenou, jelikož jsme si ji vyžádali v konstruktoru.
Kromě toho kontejnery řeší lifetime instancí – typicky máte u pravidel k dispozici režimy Transient (vždy dostanete novou instanci požadované třídy), Singleton (jedna instance je sdílená pro všechny) a PerWebRequest (každý HTTP požadavek má svou).
V .NETu existuje spousta kontejnerů, nejpoužívanější jsou asi Unity, Windsor a nInject.
Velmi pěkný článek o IoC/DI napsal Michal Augustýn.