02 October 2013 Entity Framework, TDD, Unit Testing, Unit Tests Robert Muehsig

Das Entity Framework ist zwar nicht unumstritten, aber wird oftmals in Verbindung mit der Datenabfrage aus dem SQL Server eingesetzt. Solche Lösungen neigen allerdings oftmals dazu das Thema Unit-Testing zu vernachlässigen.

Das Problem: Der SQL Server ist langsam und eine unangenehme Abhängigkeit bei Unit Tests

Unit Tests – so steht es schon im Namen – sollten nur eine Einheit testen. Durch den Zugriff auf die Datenbank wird dieses Unterfangen allerdings torpediert. Zudem sind Tests die gegen einen richtigen SQL Server laufen relativ langsam. Auch eine LocalDb ist nicht unbedingt schneller. SqlCe find ich auch nicht richtig vertrauenserweckend. Das Erstellen und die Sicherstellung der Datenkonsistenz ist auch nicht zu vernachlässigen.

Kurzum: Integrations Tests können und sollen gern gegen die “richtige” Datenbank gehen. Dies kann auch einige Zeit in Anspruch nehmen – für das Unit Testing ist dies aber nicht hilfreich.

Lösungsansatz 1: Repositories, Mocks und Fakes

Natürlich kann man das Entity Framework komplett hinter einer dicken Repository Sichicht kapseln und in Tests nur noch gegen Mocks oder Fakes laufen. Aber hier stelle ich einfach mal die Sinnhaftigkeit in Frage – in dem Sinne würde man das Entity Framework nur noch als SQL “Generator” nutzen. Das kann man so mache, ist aber doch sehr aufwändig.

Lösungsansatz 2: In-Memory Testing mit Effort

Eine wesentlich schnellere Methode wäre es die Daten nur In-Memory zu halten. Allerdings bietet das Entity Framework von Haus aus dies nicht an (zum Vergleich: RavenDB hat genau dieses Vorgehen von Anfang an implementiert).

Allerdings gibt es eine Open Source Bibliothek die sich als Entity Framework Provider ausgibt: Effort.

Was ist Effort?

Effort ist im Grunde ein ADO.NET Provider für das Entity Framework. Die Datenhaltung hier passiert aber In-Memory.

Der Vorteil von Effort:

Das Verhalten ist “fast” (mehr dazu weiter unten) wie bei einer richtign SQL Datenbank und es ist recht schnell und es ist recht schnell “konfiguriert”.

“Fast” das gleiche Verhalten?

Aktuell hat die Bibliothek noch Probleme wenn man SQL Server spezifische “Spalten-Typen” (also sowas wie “XML”-Spalte im SQL Server) nutzt – aber der Entwickler dahinter hat evtl. demnächst eine Lösung dafür. Bislang gibt es nur diesen Workaround. Desweitern verhalten sich Text-Vergleiche anders zwischen SQL Server und “.NET basierten In-Memory System”, aber wenn man auf diese beiden Sachen achtet hatte ich noch keine Probleme – selbst bei grösseren Models.

Hinweis für Entity Framework 6 Nutzer

Wer das Entity Framework 6 nutzt (welches gerade erst den RC1 Status bekommen hat) der sollte den Nightly-NuGet Feed nutzen. (install-package effort.ef6 -pre -source http://development.flamich.net/oss-nightly/nuget)

Code:

Die Definition des Entity Framework Models:

   1: public class DemoContext : DbContext
   2:     {
   3:  
   4:         public DemoContext()
   5:         {
   6:             this.Configuration.LazyLoadingEnabled = false;
   7:             this.Configuration.ProxyCreationEnabled = false;
   8:         }
   9:  
  10:         public DemoContext(DbConnection connection)
  11:             : base(connection, true)
  12:         {
  13:             this.Configuration.LazyLoadingEnabled = false;
  14:             this.Configuration.ProxyCreationEnabled = false;
  15:         }
  16:  
  17:         public DbSet<Blog> Blogs { get; set; }
  18:         public DbSet<Post> Posts { get; set; }
  19:  
  20:         protected override void OnModelCreating(DbModelBuilder modelBuilder)
  21:         {
  22:         }
  23:     }
  24:  
  25:     public class Blog
  26:     {
  27:         public int BlogId { get; set; }
  28:         public string Name { get; set; }
  29:         public string Abstract { get; set; }
  30:  
  31:         public virtual ICollection<Post> Posts { get; set; }
  32:     }
  33:  
  34:     public class Post
  35:     {
  36:         public int PostId { get; set; }
  37:         public string Title { get; set; }
  38:         public string Content { get; set; }
  39:         public byte[] Photo { get; set; }
  40:         public virtual Blog Blog { get; set; }
  41:     }

Ein sehr einfaches Model – Blogs und Posts dazu. Hierbei habe ich noch LazyLoading und die ProxyCreation ausgeschaltet – wobei dies für Effort keine Rolle spielt.

Der Test-Code:

   1: [TestClass]
   2:     public class SimpleTest
   3:     {
   4:         public DemoContext Context { get; set; }
   5:  
   6:         public DbConnection DbConnection { get; set; }
   7:  
   8:         public void FixEfProviderServicesProblem()
   9:         {
  10:             //The Entity Framework provider type 'System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer'
  11:             //for the 'System.Data.SqlClient' ADO.NET provider could not be loaded. 
  12:             //Make sure the provider assembly is available to the running application. 
  13:             //See http://go.microsoft.com/fwlink/?LinkId=260882 for more information.
  14:  
  15:             var instance = System.Data.Entity.SqlServer.SqlProviderServices.Instance;
  16:         }
  17:  
  18:         public bool IsInMemory { get; private set; }
  19:  
  20:         public void CreateInMemoryDb()
  21:         {
  22:             DbConnection = Effort.DbConnectionFactory.CreatePersistent(Guid.NewGuid().ToString());
  23:             this.Context = new DemoContext(DbConnection);
  24:             this.IsInMemory = true;
  25:         }
  26:  
  27:         [TestMethod]
  28:         public void Context_Created()
  29:         {
  30:             this.CreateInMemoryDb();
  31:             Assert.IsNotNull(Context.Database);
  32:         }
  33:  
  34:  
  35:         [TestMethod]
  36:         public void Invoke_Works()
  37:         {
  38:  
  39:             this.CreateInMemoryDb();
  40:             var test = this.Context.Blogs.ToList();
  41:  
  42:             Assert.IsTrue(true);
  43:         }
  44:  
  45:         [TestMethod]
  46:         public void Save_Works()
  47:         {
  48:  
  49:             this.CreateInMemoryDb();
  50:             this.Context.Blogs.Add(Builder<Blog>.CreateNew().Build());
  51:             this.Context.Blogs.Add(Builder<Blog>.CreateNew().Build());
  52:             this.Context.Blogs.Add(Builder<Blog>.CreateNew().Build());
  53:             this.Context.SaveChanges();
  54:  
  55:             using (new DemoContext(this.DbConnection))
  56:             {
  57:                 Assert.IsTrue(this.Context.Blogs.Count() == 3);    
  58:             }
  59:         }
  60:     }

Wobei hier die wichtigste Methode die “CreateInMemoryDb” ist. Diese DbConnection nutze ich um meine Test-Datensätze vorzubereiten (“Arrange”) und hinther die Daten abzufragen. Den Test kann ich beliebig oft wiederholen und es kommt zu keinen bösen Seiteneffekten.

 

image

Fazit

Ich habe Effort jetzt in einem größeren Projekt eingesetzt und selbst bei einem komplexen Model, was bereits zwei Jahre “alt” ist, hat Effort funktioniert. Ich war kein Freund der vielen Mocks – mit Effort kann man sich die Arbeit sparen. Dies entbindet einem natürlich trotzdem nicht genau zu überlegen wie man das Entity Framework einsetzt – bunt verstreut im ganzen Projekt ist keine Strategie – aber das ist ein anderes Thema.

Wer auf dem Laufenden bleiben möchte sollte dem Entwickler, Tamas Flamich, hinter Effort auf Twitter folgen oder seinen Blog im Auge behalten.

Den gesamten Code gibt es natürlich auch auf GitHub.

Falls jemand ein anderes Vorgehen hat oder kennt würde ich mich natürlich über einen Kommentar freuen.


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!