Wie ich aktuell test-getrieben entwickle habe ich kürzlich in diesem Post beschrieben. Was ich darin verschwiegen habe ist, wie ich meinen Datenzugriff teste.
Den Zugriff auf die Daten einer Anwendung kapsele ich in Repositories. Dies verhindert Abhängigkeiten zu und von anderen Teilen der Anwendung und sorgt für eine 100% einwandfreie Trennung, da alle anderen Komponenten nur gegen ein Interface arbeiten:
1: public interface ICompanyRepository
2: {
3: void CreateCompany(Company company);
4: Company GetCompanyByName(Guid clientID, string name);
5: }
Für die konkrete Implementierung bevorzuge ich seit Längerem das von vielen belächelte Linq to SQL. Da ich aktuell fast immer einen SQL Server als Datenbank verwende deckt das meine Bedürfnisse ganz gut ab.
Beispiel für eine Implementierung:
1: public class CompanyRepository : RepositoryBase, ICompanyRepository
2: {
3:
4: public CompanyRepository(IConfigurationService config) : base(config)
5: {
6: }
7:
8: public void CreateCompany(Model.Company company)
9: {
10: var newCompany = new Company();
11: newCompany.ID = Guid.NewGuid();
12: newCompany.Created = DateTime.Now;
13: newCompany.FKClientID = company.ClientID;
14: newCompany.Language = (int)company.Language;
15: newCompany.Name = company.Name;
16: newCompany.TimeZone = company.TimeZoneID;
17: Database.Companies.InsertOnSubmit(newCompany);
18: Database.SubmitChanges();
19: }
20:
21: public Model.Company GetCompanyByName(Guid clientID, string name)
22: {
23: return MapCompany(Database.Companies.SingleOrDefault(c => c.FKClientID ==
24: clientID && c.Name.Trim().ToLower() == name.Trim().ToLower()));
25: }
26:
27: private static Model.Company MapCompany(Company source)
28: {
29:
30: if (source == null)
31: return null;
32:
33: var company = new Model.Company();
34: company.ClientID = source.FKClientID;
35: company.Created = source.Created;
36: company.ID = source.ID;
37: company.Language = (Model.Language) source.Language;
38: company.Name = source.Name;
39: company.TimeZoneID = source.TimeZone;
40:
41: return company;
42:
43: }
44:
45: }
Wie leicht zu sehen ist, passiert hier außer ein bisschen Mapping nicht viel. Dennoch treibt mich seit Längerem die Frage um, ob ich nicht auch meine Repositories testen sollte. Also testen im Sinne von Test-first und test-getriebener Entwicklung. Ich tue mich mit dieser Frage ausgesprochen schwer.
Dafür spräche, dass es hin und wieder vorkommt, dass in der Anwendung, die, wenn man den Testrunner laufen lässt, 100% grün ist, später im Browser Fehler auftreten. Weil z.B. irgendwo ein Feld verwendet wird, welches im Test des Service, in dem das Repository gemockt ist, einen Wert aufweist, in der Praxis aber nicht, weil schlicht das Mapping fehlt (vergessen wurde). Oder weil Inserts plötzlich nicht mehr klappen, weil die ID nicht gesetzt wurde und so versucht wird mehrfach 0 (Guid.Empty) hinzuzufügen.
Dagegen spricht der enorme Aufwand. Denn eines ist ja mal klar: 100% Test-Abdeckung kann nicht das Ziel sein, wenn man nicht seine Produktivität völlig opfern möchte. Denn wenn man jeden Schnipsel test-first entwickelt, anschließend noch Integrations- und Akzeptanztests durchführen soll, wird man schlicht nie fertig. Und wenn man in diesem Fall sinnvollerweise den ORM nicht wegmockt sondern eine echte Datenbank mit echten Testdatensätzen verwendet, muss man auch noch das Schema (und die Daten!) dieser Test-Datenbank ständig aktuell halten, was bei einer iterativen Entwicklung äußerst schwer fällt (und auf die Nerven geht).
Ich habe mich bei dieser Gelegenheit an eine Diskussion und einen Blog-Beitrag von Steven Sanderson vom letzten Jahr erinnert, in dem er ein ganz treffendes Schaubild brachte, wie ich finde:
Meine Meinung: Code wie oben gehört für mich in die rechte untere Ecke. Im Prinzip koordiniert das Repository nur das Mapping der Daten vom ORM zu meinen Objekten (Entities, Value Objects) und umgekehrt. Man kann (in den allermeisten Fällen) auf einen Blick erkennen, was eine Methode tut und es würde keinen wirklichen Nutzen bringen, diese Einzeiler zu testen.
Daher mein (vorläufiges) Fazit, auch geklaut bei Steven:
Your time is finite; spend it more effectively elsewhere.
Disclaimer:
Dies gilt ausdrücklich für die Kombination Repository + Linq to SQL/ORM und ist natürlich nicht allgemein gültig. Ich habe z.B. einen Fall einer Anwendung, in der ich innerhalb der Repositories auf ein REST-API zum Lesen und Schreiben zugreife. Hier war die test-getriebene Entwicklung mehr als hilfreich, gerade auch für die Wartung (als sich das API z.B. einmal ohne Vorankündigung änderte war die Ursache dank der Integrationstests schnell ausgemacht).