Wie sollten Sie Unit-Tests mit Entity Framework 6 durchführen?


170

Ich fange gerade mit Unit-Tests und TDD im Allgemeinen an. Ich habe mich schon einmal versucht, aber jetzt bin ich entschlossen, es meinem Workflow hinzuzufügen und bessere Software zu schreiben.

Ich habe gestern eine Frage gestellt, die dies beinhaltete, aber es scheint eine Frage für sich zu sein. Ich habe mich hingesetzt, um mit der Implementierung einer Serviceklasse zu beginnen, mit der ich die Geschäftslogik von den Controllern abstrahieren und mithilfe von EF6 bestimmten Modellen und Dateninteraktionen zuordnen kann.

Das Problem ist, dass ich mich bereits blockiert habe, weil ich EF nicht in einem Repository abstrahieren wollte (es wird weiterhin außerhalb der Dienste für bestimmte Abfragen usw. verfügbar sein) und meine Dienste testen möchte (EF-Kontext wird verwendet). .

Hier ist wohl die Frage, gibt es einen Grund, dies zu tun? Wenn ja, wie machen es die Leute in freier Wildbahn angesichts der undichten Abstraktionen, die von IQueryable verursacht werden, und der vielen großartigen Beiträge von Ladislav Mrnka zum Thema Unit-Tests, die aufgrund der Unterschiede bei den Linq-Anbietern bei der Arbeit mit einem In-Memory nicht einfach sind Implementierung gemäß einer bestimmten Datenbank.

Der Code, den ich testen möchte, scheint ziemlich einfach zu sein. (Dies ist nur Dummy-Code, um zu verstehen, was ich tue. Ich möchte die Erstellung mit TDD vorantreiben.)

Kontext

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

Bedienung

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

Derzeit bin ich in der Einstellung, ein paar Dinge zu tun:

  1. Verspotten des EF-Kontexts mit so etwas wie diesem Ansatz - Verspotten des EF beim Unit-Test oder direktes Verwenden eines Mocking-Frameworks auf der Schnittstelle wie moq - Nehmen Sie den Schmerz, den die Unit-Tests möglicherweise bestehen, aber nicht unbedingt durchgängig arbeiten, und sichern Sie sie mit Integrationstests?
  2. Vielleicht etwas wie Effort verwenden , um EF zu verspotten - ich habe es nie benutzt und bin mir nicht sicher, ob es jemand anderes in freier Wildbahn benutzt?
  3. Sie müssen nichts testen, was einfach zu EF zurückruft. Im Wesentlichen werden Servicemethoden, die EF direkt aufrufen (getAll usw.), nicht auf Einheiten getestet, sondern nur auf Integration getestet?

Jemand da draußen, der das tatsächlich ohne Repo macht und Erfolg hat?


Hey Modika, ich habe kürzlich darüber nachgedacht (wegen dieser Frage: stackoverflow.com/questions/25977388/… ). Darin versuche ich etwas formeller zu beschreiben, wie ich im Moment arbeite, aber ich würde gerne hören, wie du machst es
Samy

Hallo @samy, die Art und Weise, wie wir uns dazu entschieden haben, war kein Unit-Test, der EF direkt berührte. Abfragen wurden getestet, jedoch als Integrationstest, nicht als Komponententests. Das Verspotten von EF fühlt sich ein wenig schmutzig an, aber dieses Projekt war klein, so dass die Auswirkungen auf die Leistung, wenn viele Tests auf eine Datenbank treffen, nicht wirklich ein Problem waren, sodass wir etwas pragmatischer sein könnten. Ich bin mir immer noch nicht zu 100% sicher, was der beste Ansatz ist, um mit Ihnen völlig ehrlich zu sein. Irgendwann werden Sie EF (und Ihre DB) treffen, und Unit-Tests fühlen sich für mich hier nicht richtig an.
Modika

Antworten:


186

Dies ist ein Thema, das mich sehr interessiert. Es gibt viele Puristen, die sagen, dass Sie Technologien wie EF und NHibernate nicht testen sollten. Sie haben Recht, sie sind bereits sehr streng getestet und wie in einer früheren Antwort angegeben, ist es oft sinnlos, viel Zeit damit zu verbringen, zu testen, was Sie nicht besitzen.

Sie besitzen jedoch die Datenbank darunter! Hier bricht dieser Ansatz meiner Meinung nach zusammen. Sie müssen nicht testen, ob EF / NH ihre Arbeit korrekt ausführen. Sie müssen testen, ob Ihre Zuordnungen / Implementierungen mit Ihrer Datenbank funktionieren. Meiner Meinung nach ist dies einer der wichtigsten Teile eines Systems, das Sie testen können.

Genau genommen bewegen wir uns jedoch aus dem Bereich des Unit-Tests in den Bereich des Integrationstests, aber die Prinzipien bleiben dieselben.

Das erste, was Sie tun müssen, ist, Ihre DAL verspotten zu können, damit Ihre BLL unabhängig von EF und SQL getestet werden kann. Dies sind Ihre Unit-Tests. Als nächstes müssen Sie Ihre Integrationstests entwerfen , um Ihre DAL zu beweisen. Meiner Meinung nach sind diese genauso wichtig.

Es gibt ein paar Dinge zu beachten:

  1. Ihre Datenbank muss bei jedem Test in einem bekannten Zustand sein. Die meisten Systeme verwenden entweder ein Backup oder erstellen dafür Skripte.
  2. Jeder Test muss wiederholbar sein
  3. Jeder Test muss atomar sein

Es gibt zwei Hauptansätze zum Einrichten Ihrer Datenbank: Der erste besteht darin, ein UnitTest-Skript zum Erstellen einer Datenbank auszuführen. Dadurch wird sichergestellt, dass sich Ihre Komponententestdatenbank zu Beginn jedes Tests immer im selben Status befindet (Sie können dies entweder zurücksetzen oder jeden Test in einer Transaktion ausführen, um dies sicherzustellen).

Ihre andere Option ist, was ich tue, spezifische Setups für jeden einzelnen Test auszuführen. Ich glaube, dies ist aus zwei Hauptgründen der beste Ansatz:

  • Ihre Datenbank ist einfacher, Sie benötigen nicht für jeden Test ein vollständiges Schema
  • Jeder Test ist sicherer. Wenn Sie einen Wert in Ihrem Erstellungsskript ändern, werden Dutzende anderer Tests nicht ungültig.

Leider ist Ihr Kompromiss hier Geschwindigkeit. Es braucht Zeit, um alle diese Tests auszuführen und alle diese Setup- / Teardown-Skripte auszuführen.

Ein letzter Punkt: Es kann sehr schwierig sein, eine so große Menge an SQL zu schreiben, um Ihr ORM zu testen. Hier gehe ich sehr böse vor (die Puristen hier werden mir nicht zustimmen). Ich benutze mein ORM, um meinen Test zu erstellen! Anstatt für jeden DAL-Test in meinem System ein separates Skript zu haben, habe ich eine Test-Setup-Phase, in der die Objekte erstellt, an den Kontext angehängt und gespeichert werden. Ich führe dann meinen Test durch.

Dies ist alles andere als die ideale Lösung, aber in der Praxis finde ich, dass es viel einfacher zu verwalten ist (insbesondere wenn Sie mehrere tausend Tests haben), da Sie sonst eine große Anzahl von Skripten erstellen. Praktikabilität über Reinheit.

Ich werde ohne Zweifel in ein paar Jahren (Monaten / Tagen) auf diese Antwort zurückblicken und mit mir selbst nicht einverstanden sein, da sich meine Ansätze geändert haben - dies ist jedoch mein aktueller Ansatz.

Um alles zusammenzufassen, was ich oben gesagt habe, ist dies mein typischer DB-Integrationstest:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

Das Wichtigste dabei ist, dass die Sitzungen der beiden Schleifen völlig unabhängig sind. Bei der Implementierung von RunTest müssen Sie sicherstellen, dass der Kontext festgeschrieben und zerstört wird und Ihre Daten nur für den zweiten Teil aus Ihrer Datenbank stammen können.

Bearbeiten 13/10/2014

Ich habe gesagt, dass ich dieses Modell wahrscheinlich in den kommenden Monaten überarbeiten werde. Während ich weitgehend zu dem oben vertretenen Ansatz stehe, habe ich meinen Testmechanismus leicht aktualisiert. Ich neige jetzt dazu, die Entitäten in TestSetup und TestTearDown zu erstellen.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

Testen Sie dann jede Eigenschaft einzeln

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

Für diesen Ansatz gibt es mehrere Gründe:

  • Es gibt keine zusätzlichen Datenbankaufrufe (ein Setup, ein Teardown)
  • Die Tests sind weitaus detaillierter. Jeder Test überprüft eine Eigenschaft
  • Die Setup / TearDown-Logik wird aus den Testmethoden selbst entfernt

Ich denke, dies macht die Testklasse einfacher und die Tests detaillierter ( einzelne Aussagen sind gut )

Bearbeiten 03.05.2015

Eine weitere Überarbeitung dieses Ansatzes. Setups auf Klassenebene sind zwar sehr hilfreich für Tests wie das Laden von Eigenschaften, aber weniger nützlich, wenn die verschiedenen Setups erforderlich sind. In diesem Fall ist das Einrichten einer neuen Klasse für jeden Fall übertrieben.

Um dies zu unterstützen, habe ich jetzt tendenziell zwei Basisklassen SetupPerTestund SingleSetup. Diese beiden Klassen legen das Framework nach Bedarf offen.

In der haben SingleSetupwir einen sehr ähnlichen Mechanismus wie in meiner ersten Bearbeitung beschrieben. Ein Beispiel wäre

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

Referenzen, die sicherstellen, dass nur die richtigen Entites geladen werden, können jedoch einen SetupPerTest-Ansatz verwenden

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

Zusammenfassend funktionieren beide Ansätze je nachdem, was Sie testen möchten.


2
Hier ist ein anderer Ansatz für Integrationstests. TL; DR - Verwenden Sie die Anwendung selbst, um Testdaten einzurichten und eine Transaktion pro Test zurückzusetzen.
Gert Arnold

3
@ Liath, tolle Antwort. Sie haben meinen Verdacht bestätigt, EF zu testen. Meine Frage ist dies; Ihr Beispiel ist für einen sehr konkreten Fall, der in Ordnung ist. Wie Sie bereits bemerkt haben, müssen Sie möglicherweise Hunderte von Entitäten testen. Wie skalieren Sie Ihre Lösung gemäß dem DRY-Prinzip (Wiederholen Sie sich nicht), ohne jedes Mal das gleiche grundlegende Codemuster zu wiederholen?
Jeffrey A. Gochin

4
Ich muss damit nicht einverstanden sein, weil es das Problem völlig umgeht. Beim Unit-Test geht es darum, die Logik der Funktion zu testen. Im OP-Beispiel ist die Logik von einem Datenspeicher abhängig. Sie haben Recht, wenn Sie sagen, dass Sie EF nicht testen sollen, aber das ist nicht das Problem. Das Problem besteht darin, Ihren Code isoliert vom Datenspeicher zu testen. Das Testen Ihres Mappings ist imo ein völlig anderes Thema. Um zu testen, ob die Logik korrekt mit Daten interagiert, müssen Sie in der Lage sein, den Speicher zu steuern.
Sinaesthetic

7
Niemand ist auf dem Zaun darüber, ob Sie Entity Framework selbst testen sollten. Was passiert ist, dass Sie eine Methode testen müssen, die einige Dinge erledigt und auch einen EF-Aufruf an die Datenbank ausführt. Das Ziel ist es, EF zu verspotten, damit Sie diese Methode testen können, ohne eine Datenbank auf Ihrem Build-Server zu benötigen.
Der Muffin-Mann

4
Ich mag die Reise wirklich. Vielen Dank, dass Sie im Laufe der Zeit Änderungen hinzugefügt haben. Es ist, als würden Sie die Quellcodeverwaltung lesen und verstehen, wie sich Ihr Denken entwickelt hat. Ich schätze auch die Unterscheidung zwischen Funktion (mit EF) und Einheit (mit verspottetem EF) sehr.
Tom Leys

21

Effort Experience Feedback hier

Nach vielem Lesen habe ich Effort in meinen Tests verwendet: Während der Tests wird der Kontext von einer Factory erstellt, die eine In-Memory-Version zurückgibt, mit der ich jedes Mal gegen eine leere Tafel testen kann. Außerhalb der Tests wird die Factory in eine aufgelöst, die den gesamten Kontext zurückgibt.

Ich habe jedoch das Gefühl, dass das Testen gegen ein voll funktionsfähiges Modell der Datenbank dazu neigt, die Tests nach unten zu ziehen. Sie erkennen, dass Sie sich darum kümmern müssen, eine ganze Reihe von Abhängigkeiten einzurichten, um einen Teil des Systems zu testen. Sie tendieren auch dazu, Tests zusammenzustellen, die möglicherweise nicht miteinander zusammenhängen, nur weil es nur ein großes Objekt gibt, das alles handhabt. Wenn Sie nicht aufpassen, führen Sie möglicherweise Integrationstests anstelle von Komponententests durch

Ich hätte es vorgezogen, gegen etwas Abstrakteres als gegen einen riesigen DBContext zu testen, aber ich konnte den Sweet Spot zwischen aussagekräftigen Tests und Bare-Bone-Tests nicht finden. Kreide es bis zu meiner Unerfahrenheit.

Also finde ich Effort interessant; Wenn Sie sofort loslegen müssen, ist dies ein gutes Werkzeug, um schnell loszulegen und Ergebnisse zu erzielen. Ich denke jedoch, dass etwas eleganteres und abstrakteres der nächste Schritt sein sollte, und das werde ich als nächstes untersuchen. Diesen Beitrag favorisieren, um zu sehen, wohin er als nächstes geht :)

Bearbeiten, um hinzuzufügen : Das Aufwärmen dauert einige Zeit. 5 Sekunden beim Teststart. Dies kann ein Problem für Sie sein, wenn Ihre Testsuite sehr effizient sein soll.


Zur Verdeutlichung bearbeitet:

Ich habe Effort verwendet, um eine Webservice-App zu testen. Jede eingegebene Nachricht M wird IHandlerOf<M>über Windsor an a weitergeleitet. Castle.Windsor löst das auf IHandlerOf<M>, wodurch die Abhängigkeiten der Komponente aufgelöst werden. Eine dieser Abhängigkeiten ist die DataContextFactory, mit der der Handler nach der Factory fragen kann

In meinen Tests instanziiere ich die IHandlerOf-Komponente direkt, verspotte alle Unterkomponenten des SUT und verarbeite den Aufwand DataContextFactoryfür den Handler.

Dies bedeutet, dass ich keinen Unit-Test im engeren Sinne durchführe, da die DB von meinen Tests betroffen ist. Wie ich oben sagte, konnte ich jedoch sofort loslegen und einige Punkte in der Anwendung schnell testen


Vielen Dank für die Eingabe. Was ich tun kann, um dieses Projekt zum Laufen zu bringen, da es ein gut bezahlter Job ist, ist, mit einigen Repos zu beginnen und zu sehen, wie ich weiterkomme, aber die Anstrengung ist sehr interessant. Aus Interesse auf welcher Ebene haben Sie sich in Ihren Anwendungen bemüht?
Modika

2
nur wenn Effort Transaktionen ordnungsgemäß unterstützt hatte
Sedat Kapanoglu

und Anstrengung hat einen Fehler für Zeichenfolgen mit CSV-Loader, wenn wir in Zeichenfolgen '' anstelle von Null verwenden.
Sam

13

Wenn Sie Unit- Test-Code verwenden möchten, müssen Sie Ihren zu testenden Code (in diesem Fall Ihren Service) von externen Ressourcen (z. B. Datenbanken) isolieren. Sie könnten dies wahrscheinlich mit einer Art In-Memory-EF-Anbieter tun. Eine weitaus häufigere Methode besteht jedoch darin, Ihre EF-Implementierung zu abstrahieren, z. B. mit einer Art Repository-Muster. Ohne diese Isolation sind alle Tests, die Sie schreiben, Integrationstests, keine Komponententests.

Was das Testen von EF-Code betrifft - Ich schreibe automatisierte Integrationstests für meine Repositorys, die während ihrer Initialisierung verschiedene Zeilen in die Datenbank schreiben, und rufe dann meine Repository-Implementierungen auf, um sicherzustellen, dass sie sich wie erwartet verhalten (z. B. um sicherzustellen, dass die Ergebnisse korrekt gefiltert werden, oder dass sie in der richtigen Reihenfolge sortiert sind).

Hierbei handelt es sich um Integrationstests, nicht um Komponententests, da für die Tests eine Datenbankverbindung erforderlich ist und in der Zieldatenbank bereits das neueste aktuelle Schema installiert ist.


Danke @justin, ich weiß über das Repository-Muster Bescheid, aber das Lesen von Dingen wie ayende.com/blog/4784/… und lostechies.com/jimmybogard/2009/09/11/wither-the-repository hat mich unter anderem zu dem Gedanken gebracht, dass ich es nicht tue Ich möchte diese Abstraktionsschicht nicht, aber diese sprechen auch mehr über einen Abfrageansatz, der sehr verwirrend wird.
Modika

7
@Modika Ayende hat eine schlechte Implementierung des Repository-Musters zur Kritik ausgewählt und ist daher zu 100% richtig - es ist überarbeitet und bietet keine Vorteile. Eine gute Implementierung isoliert die Unit-testbaren Teile Ihres Codes von der DAL-Implementierung. Die direkte Verwendung von NHibernate und EF erschwert (wenn nicht unmöglich) den Unit-Test von Code und führt zu einer starren monolithischen Codebasis. Ich bin immer noch etwas skeptisch gegenüber dem Repository-Muster, bin jedoch zu 100% davon überzeugt, dass Sie Ihre DAL-Implementierung irgendwie isolieren müssen, und das Repository ist das Beste, was ich bisher gefunden habe.
Justin

2
@Modika Lies den zweiten Artikel noch einmal. "Ich will diese Abstraktionsschicht nicht", sagt er nicht. Lesen Sie außerdem das ursprüngliche Repository-Muster von Fowler ( martinfowler.com/eaaCatalog/repository.html ) oder DDD ( dddcommunity.org/resources/ddd_terms ). Glauben Sie Neinsagern nicht, ohne das ursprüngliche Konzept vollständig zu verstehen. Was sie wirklich kritisieren, ist ein kürzlich erfolgter Missbrauch des Musters, nicht des Musters selbst (obwohl sie dies wahrscheinlich nicht wissen).
Guillaume31

1
@ guillaume31 Ich bin nicht gegen das Repository-Muster (ich verstehe es) Ich versuche einfach herauszufinden, ob ich es brauche, um zu abstrahieren, was auf dieser Ebene bereits eine Abstraktion ist, und ob ich es weglassen und direkt durch Verspotten gegen EF testen kann und verwenden Sie es in meinen Tests auf einer Ebene höher in meiner Anwendung. Wenn ich kein Repo benutze, bekomme ich außerdem den Vorteil des erweiterten EF-Funktionsumfangs. Bei einem Repo bekomme ich das möglicherweise nicht.
Modika

Sobald ich die DAL mit einem Repository isoliert habe, muss ich die Datenbank (EF) irgendwie "verspotten". Bisher hat das Verspotten des Kontexts und verschiedener asynchroner Erweiterungen (ToListAsync (), FirstOrDefaultAsync () usw.) für mich zu Frustration geführt.
Kevin Burton

9

Hier ist also die Sache, Entity Framework ist eine Implementierung. Trotz der Tatsache, dass es die Komplexität der Datenbankinteraktion abstrahiert, ist die direkte Interaktion immer noch eine enge Kopplung, und deshalb ist das Testen verwirrend.

Beim Unit-Test geht es darum, die Logik einer Funktion und jedes ihrer potenziellen Ergebnisse isoliert von externen Abhängigkeiten zu testen, in diesem Fall dem Datenspeicher. Dazu müssen Sie das Verhalten des Datenspeichers steuern können. Wenn Sie beispielsweise behaupten möchten, dass Ihre Funktion false zurückgibt, wenn der abgerufene Benutzer bestimmte Kriterien nicht erfüllt, sollte Ihr [verspotteter] Datenspeicher so konfiguriert sein, dass immer ein Benutzer zurückgegeben wird, der die Kriterien nicht erfüllt, und umgekehrt umgekehrt für die gegenteilige Behauptung.

Vor diesem Hintergrund und angesichts der Tatsache, dass EF eine Implementierung ist, würde ich wahrscheinlich die Idee der Abstraktion eines Repositorys befürworten. Scheint ein bisschen überflüssig? Dies ist nicht der Fall, da Sie ein Problem lösen, bei dem Ihr Code von der Datenimplementierung isoliert wird.

In DDD geben die Repositorys immer nur aggregierte Roots zurück, nicht DAO. Auf diese Weise muss der Verbraucher des Repositorys nie über die Datenimplementierung Bescheid wissen (wie es nicht sein sollte), und wir können dies als Beispiel für die Lösung dieses Problems verwenden. In diesem Fall ist das von EF generierte Objekt ein DAO und sollte daher vor Ihrer Anwendung ausgeblendet werden. Dies ist ein weiterer Vorteil des von Ihnen definierten Repositorys. Sie können ein Geschäftsobjekt anstelle des EF-Objekts als Rückgabetyp definieren. Das Repo versteckt nun die Aufrufe von EF und ordnet die EF-Antwort dem in der Reposignatur definierten Geschäftsobjekt zu. Jetzt können Sie dieses Repo anstelle der DbContext-Abhängigkeit verwenden, die Sie in Ihre Klassen einfügen, und folglich können Sie diese Schnittstelle verspotten, um Ihnen die Kontrolle zu geben, die Sie benötigen, um Ihren Code isoliert zu testen.

Es ist ein bisschen mehr Arbeit und viele schnüffeln mit der Nase daran, aber es löst ein echtes Problem. Es gibt einen In-Memory-Anbieter, der in einer anderen Antwort erwähnt wurde, die eine Option sein könnte (ich habe es nicht ausprobiert), und seine Existenz ist ein Beweis für die Notwendigkeit der Praxis.

Ich bin mit der Top-Antwort überhaupt nicht einverstanden, da sie das eigentliche Problem, das darin besteht, Ihren Code zu isolieren, umgeht und sich dann mit dem Testen Ihrer Zuordnung befasst. Testen Sie auf jeden Fall Ihr Mapping, wenn Sie möchten, aber gehen Sie hier auf das eigentliche Problem ein und erhalten Sie eine echte Codeabdeckung.


8

Ich würde keinen Unit-Test-Code verwenden, den ich nicht besitze. Was testen Sie hier, damit der MSFT-Compiler funktioniert?

Um diesen Code testbar zu machen, MÜSSEN Sie jedoch Ihre Datenzugriffsschicht fast von Ihrem Geschäftslogikcode trennen. Ich nehme alle meine EF-Sachen und stelle sie in eine (oder mehrere) DAO- oder DAL-Klasse, die auch eine entsprechende Schnittstelle hat. Dann schreibe ich meinen Dienst, in den das DAO- oder DAL-Objekt als Abhängigkeit (vorzugsweise Konstruktorinjektion) als Schnittstelle eingefügt wird. Jetzt kann der zu testende Teil (Ihr Code) einfach getestet werden, indem Sie die DAO-Schnittstelle verspotten und diese in Ihre Service-Instanz innerhalb Ihres Unit-Tests einfügen.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

Ich würde Live-Datenzugriffsschichten als Teil des Integrationstests betrachten, nicht als Komponententest. Ich habe gesehen, wie Leute zuvor überprüft haben, wie viele Fahrten in den Datenbank-Ruhezustand unternommen wurden, aber sie waren an einem Projekt beteiligt, bei dem Milliarden von Datensätzen in ihrem Datenspeicher enthalten waren, und diese zusätzlichen Fahrten waren wirklich wichtig.


1
Vielen Dank für die Antwort, aber was wäre der Unterschied dazu, ein Repository zu sagen, in dem Sie die Interna von EF auf dieser Ebene dahinter verstecken? Ich möchte EF nicht wirklich abstrahieren, obwohl ich das vielleicht immer noch mit der IContext-Schnittstelle mache? Ich bin neu in diesem, sei sanft :)
Modika

3
@Modika Ein Repo ist auch in Ordnung. Welches Muster auch immer Sie wollen. "Ich möchte EF nicht wirklich abstrahieren" Möchten Sie testbaren Code oder nicht?
Jonathan Henson

1
@Modika Mein Punkt ist, dass Sie keinen testbaren Code haben, wenn Sie Ihre Bedenken nicht trennen. Datenzugriff und Geschäftslogik MÜSSEN sich in getrennten Schichten befinden, um gute wartbare Tests durchzuführen.
Jonathan Henson

2
Ich hielt es einfach nicht für notwendig, EF in eine Repository-Abstraktion einzuschließen, da im Wesentlichen die IDbSets Repos und der Kontext die UOW sind. Ich werde meine Frage ein wenig aktualisieren, da dies irreführend sein kann. Das Problem tritt bei jeder Abstraktion auf, und der Hauptpunkt ist, was genau ich teste, da meine Queiries nicht an denselben Grenzen ausgeführt werden (Linq-to-Entities vs. Linq-to-Objects). Wenn ich also nur teste, dass mein Service a Anruf, der ein bisschen verschwenderisch erscheint, oder geht es mir hier gut?
Modika

1
Obwohl ich Ihren allgemeinen Punkten zustimme, ist DbContext eine Arbeitseinheit und IDbSets sind definitiv einige für die Repository-Implementierung, und ich bin nicht der einzige, der das denkt. Ich kann EF verspotten, und auf einer bestimmten Ebene muss ich Integrationstests ausführen. Ist das wirklich wichtig, wenn ich es in einem Repository oder weiter oben in einem Service mache? Eine enge Verbindung zu einer Datenbank ist kein wirkliches Problem. Ich bin mir sicher, dass dies geschieht, aber ich werde nicht planen, was möglicherweise nicht geschieht.
Modika

8

Ich habe irgendwann herumgefummelt, um diese Überlegungen zu erreichen:

1- Wenn meine Anwendung auf die Datenbank zugreift, warum sollte der Test nicht? Was ist, wenn beim Datenzugriff etwas nicht stimmt? Die Tests müssen es vorher wissen und mich auf das Problem aufmerksam machen.

2- Das Repository-Muster ist etwas schwierig und zeitaufwändig.

Also habe ich mir diesen Ansatz ausgedacht, den ich nicht für den besten halte, der aber meine Erwartungen erfüllt hat:

Use TransactionScope in the tests methods to avoid changes in the database.

Dazu ist es notwendig:

1- Installieren Sie das EntityFramework im Testprojekt. 2- Fügen Sie die Verbindungszeichenfolge in die Datei app.config von Test Project ein. 3- Verweisen Sie auf die DLL System.Transactions in Test Project.

Der einzigartige Nebeneffekt besteht darin, dass der Identitätsstart beim Einfügen erhöht wird, selbst wenn die Transaktion abgebrochen wird. Da die Tests jedoch anhand einer Entwicklungsdatenbank durchgeführt werden, sollte dies kein Problem sein.

Beispielcode:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

1
Eigentlich mag ich diese Lösung sehr. Super einfach zu implementierende und realistischere Testszenarien. Vielen Dank!
Slopapa

1
Mit EF 6 würden Sie DbContext.Database.BeginTransaction verwenden, nicht wahr?
SwissCoder

5

Kurz gesagt, ich würde nein sagen, der Saft ist es nicht wert, eine Servicemethode mit einer einzelnen Zeile zu testen, die Modelldaten abruft. Nach meiner Erfahrung wollen TDD-Neulinge absolut alles testen. Die alte Kastanie, eine Fassade zu einem Framework eines Drittanbieters zu abstrahieren, nur damit Sie ein Modell dieser Framework-API erstellen können, mit der Sie bastardisieren / erweitern, damit Sie Dummy-Daten einfügen können, ist für mich von geringem Wert. Jeder hat eine andere Ansicht darüber, wie viel Unit-Test am besten ist. Ich bin heutzutage eher pragmatisch und frage mich, ob und zu welchem ​​Preis mein Test wirklich einen Mehrwert für das Endprodukt darstellt.


1
Ja zum Pragmatismus. Ich behaupte immer noch, dass die Qualität Ihrer Komponententests der Qualität Ihres ursprünglichen Codes unterlegen ist. Natürlich ist es sinnvoll, TDD zu verwenden, um Ihre Codierungspraxis zu verbessern und die Wartbarkeit zu verbessern, aber TDD kann einen abnehmenden Wert haben. Wir führen alle unsere Tests für die Datenbank durch, da wir dadurch sicher sein können, dass EF und die Tabellen selbst einwandfrei verwendet werden. Die Tests dauern zwar länger, sind aber zuverlässiger.
Savage

3

Ich möchte einen kommentierten und kurz diskutierten Ansatz teilen, aber ein aktuelles Beispiel zeigen, das ich derzeit verwende, um beim Testen von EF-basierten Diensten zu helfen .

Zunächst würde ich gerne den In-Memory-Anbieter von EF Core verwenden, aber hier geht es um EF 6. Darüber hinaus wäre ich für andere Speichersysteme wie RavenDB auch ein Befürworter des Testens über den In-Memory-Datenbankanbieter. Auch dies dient speziell dazu, EF-basierten Code ohne viel Zeremonie zu testen .

Hier sind die Ziele, die ich hatte, als ich ein Muster entwickelte:

  • Für andere Entwickler im Team muss es einfach zu verstehen sein
  • Es muss den EF-Code auf der geringstmöglichen Ebene isolieren
  • Es darf keine seltsamen Schnittstellen mit mehreren Verantwortlichkeiten erstellen (z. B. ein "generisches" oder "typisches" Repository-Muster).
  • Die Konfiguration und Einrichtung in einem Komponententest muss einfach sein

Ich stimme früheren Aussagen zu, dass EF immer noch ein Implementierungsdetail ist und es in Ordnung ist, das Gefühl zu haben, dass Sie es abstrahieren müssen, um einen "reinen" Komponententest durchzuführen. Ich stimme auch zu, dass ich im Idealfall sicherstellen möchte, dass der EF-Code selbst funktioniert - dies beinhaltet jedoch eine Sandbox-Datenbank, einen In-Memory-Anbieter usw. Mein Ansatz löst beide Probleme - Sie können EF-abhängigen Code sicher testen und erstellen Integrationstests zum spezifischen Testen Ihres EF-Codes.

Dies wurde durch einfaches Einkapseln von EF-Code in dedizierte Abfrage- und Befehlsklassen erreicht. Die Idee ist einfach: Binden Sie einfach einen EF-Code in eine Klasse ein und hängen Sie von einer Schnittstelle in den Klassen ab, die ihn ursprünglich verwendet hätten. Das Hauptproblem, das ich lösen musste, bestand darin, zu vermeiden, dass Klassen zahlreiche Abhängigkeiten hinzugefügt und in meinen Tests viel Code eingerichtet wurden.

Hier kommt eine nützliche, einfache Bibliothek ins Spiel : Mediatr . Es ermöglicht einfaches In-Process-Messaging und entkoppelt "Anforderungen" von den Handlern, die den Code implementieren. Dies hat den zusätzlichen Vorteil, dass das "Was" vom "Wie" entkoppelt wird. Wenn Sie beispielsweise den EF-Code in kleine Blöcke kapseln, können Sie die Implementierungen durch einen anderen Anbieter oder einen völlig anderen Mechanismus ersetzen, da Sie lediglich eine Anforderung zum Ausführen einer Aktion senden.

Mithilfe der Abhängigkeitsinjektion (mit oder ohne Framework - Ihre Präferenz) können wir den Mediator leicht verspotten und die Anforderungs- / Antwortmechanismen steuern, um das Testen von EF-Code zu ermöglichen.

Nehmen wir zunächst an, wir haben einen Service mit Geschäftslogik, die wir testen müssen:

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
    // ...    
  }
}

Beginnen Sie, den Nutzen dieses Ansatzes zu erkennen? Sie kapseln nicht nur den gesamten EF-bezogenen Code explizit in beschreibende Klassen, sondern ermöglichen auch die Erweiterbarkeit, indem Sie die Bedenken hinsichtlich der Implementierung dieser Anforderung beseitigen. Dieser Klasse ist es egal, ob die relevanten Objekte von EF, MongoDB, stammen. oder eine Textdatei.

Nun zur Anfrage und Bearbeitung über 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();
  }
}

Wie Sie sehen können, ist die Abstraktion einfach und gekapselt. Es ist auch absolut prüfbar , weil in einem Integrationstest, Sie könnten diese Klasse einzeln testen - es gibt keine Geschäftsinteressen in hier gemischt.

Wie sieht ein Unit-Test unseres Feature-Service aus? Es ist ganz einfach. In diesem Fall benutze ich Moq, um mich zu verspotten (benutze, was dich glücklich macht):

[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
  }

}

Sie können sehen, dass wir nur ein einziges Setup benötigen und nicht einmal zusätzliche Einstellungen vornehmen müssen - es ist ein sehr einfacher Komponententest. Lassen Sie uns klar sein: Dies ist völlig möglich ohne etwas wie Mediatr (Sie würden einfach eine Schnittstelle implementieren und sie für Tests verspotten, z. B. IGetRelevantDbObjectsQuery), aber in der Praxis für eine große Codebasis mit vielen Funktionen und Abfragen / Befehlen liebe ich die Kapselung und angeborene DI-Unterstützung, die Mediatr anbietet.

Wenn Sie sich fragen, wie ich diese Kurse organisiere, ist das ziemlich einfach:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

Das Organisieren nach Feature-Slices ist nebensächlich, aber dies hält den gesamten relevanten / abhängigen Code zusammen und ist leicht erkennbar. Am wichtigsten ist, dass ich die Abfragen gegen Befehle trenne - nach dem Prinzip der Befehls- / Abfragetrennung .

Dies erfüllt alle meine Kriterien: Es ist eine niedrige Zeremonie, es ist leicht zu verstehen und es gibt zusätzliche versteckte Vorteile. Wie gehen Sie beispielsweise mit dem Speichern von Änderungen um? Jetzt können Sie Ihren Datenbankkontext mithilfe einer Rollenschnittstelle vereinfachen (IUnitOfWork.SaveChangesAsync()) und Scheinaufrufe an die Einzelrollenschnittstelle oder Sie können das Festschreiben / Zurücksetzen in Ihren RequestHandlern kapseln - Sie möchten dies jedoch lieber tun, solange es wartbar ist. Zum Beispiel war ich versucht, einen einzelnen generischen Anforderungs- / Handler zu erstellen, bei dem Sie nur ein EF-Objekt übergeben und es speichern / aktualisieren / entfernen würden - aber Sie müssen fragen, was Ihre Absicht ist, und sich daran erinnern, wenn Sie möchten Wenn Sie den Handler gegen einen anderen Speicheranbieter / eine andere Implementierung austauschen, sollten Sie wahrscheinlich explizite Befehle / Abfragen erstellen, die das darstellen, was Sie beabsichtigen. In den meisten Fällen benötigt ein einzelner Dienst oder eine einzelne Funktion etwas Bestimmtes. Erstellen Sie keine allgemeinen Inhalte, bevor Sie sie benötigen.

Dieses Muster weist natürlich einige Einschränkungen auf - Sie können mit einem einfachen Pub / Sub-Mechanismus zu weit gehen. Ich habe meine Implementierung darauf beschränkt, nur EF-bezogenen Code zu abstrahieren, aber abenteuerlustige Entwickler könnten MediatR verwenden, um über Bord zu gehen und alles mit Nachrichten zu versehen - etwas, das gute Codeüberprüfungspraktiken und Peer-Reviews erreichen sollten. Dies ist ein Prozessproblem, kein Problem mit MediatR. Seien Sie sich also bewusst, wie Sie dieses Muster verwenden.

Sie wollten ein konkretes Beispiel dafür, wie Menschen EF testen / verspotten, und dies ist ein Ansatz, der für uns bei unserem Projekt erfolgreich funktioniert - und das Team ist sehr zufrieden damit, wie einfach es zu übernehmen ist. Ich hoffe das hilft! Wie bei allen Dingen in der Programmierung gibt es mehrere Ansätze, und alles hängt davon ab, was Sie erreichen möchten. Ich schätze Einfachheit, Benutzerfreundlichkeit, Wartbarkeit und Auffindbarkeit - und diese Lösung erfüllt all diese Anforderungen.


Vielen Dank für die Antwort. Es ist eine großartige Beschreibung des QueryObject-Musters mit einem Mediator und etwas, das ich auch in meinen Projekten vorantreibe. Möglicherweise muss ich die Frage aktualisieren, aber ich teste EF nicht mehr, die Abstraktionen sind zu undicht (SqlLite ist jedoch möglicherweise in Ordnung), sodass ich nur meine Integrationstests durchführe, die die Geschäftsregeln für Datenbank- und Komponententests und andere Logik abfragen.
Modika

3

Es gibt Effort, einen Datenbankanbieter für In-Memory-Entity-Frameworks. Ich habe es nicht wirklich versucht ... Haa hat gerade entdeckt, dass dies in der Frage erwähnt wurde!

Alternativ können Sie zu EntityFrameworkCore wechseln, in dem ein In-Memory-Datenbankanbieter integriert ist.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

Ich habe eine Factory verwendet, um einen Kontext zu erhalten, damit ich den Kontext in der Nähe seiner Verwendung erstellen kann. Dies scheint lokal in Visual Studio zu funktionieren, aber nicht auf meinem TeamCity-Build-Server. Ich weiß noch nicht warum.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

Hallo Andrew, das Problem war, nie den Kontext zu bekommen. Sie können den Kontext, den wir gemacht haben, herausarbeiten, den Kontext abstrahieren und ihn von der Fabrik erstellen lassen. Das größte Problem war die Konsistenz zwischen dem, was sich im Speicher befand, und dem, was Linq4Entities tut. Sie sind nicht dieselben, was zu irreführenden Tests führen kann. Derzeit haben wir nur Integrationstest-Datenbankmaterial, möglicherweise nicht der beste Prozess für alle, wohlgemerkt.
Modika

Dieser Moq-Helfer funktioniert ( codeproject.com/Tips/1045590/… ), wenn Sie einen Kontext zum Verspotten haben. Wenn Sie den verspotteten Kontext mit einer Liste sichern, verhält er sich möglicherweise nicht wie ein Kontext, der von einer SQL-Datenbank unterstützt wird.
Andrew Pate

2

Ich möchte meine Filter von anderen Teilen des Codes trennen und diese testen, wie ich in meinem Blog hier skizziere: http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Allerdings ist die getestete Filterlogik aufgrund der Übersetzung zwischen dem LINQ-Ausdruck und der zugrunde liegenden Abfragesprache wie T-SQL nicht identisch mit der Filterlogik, die beim Ausführen des Programms ausgeführt wird. Auf diese Weise kann ich die Logik des Filters überprüfen. Ich mache mir keine allzu großen Sorgen über die Übersetzungen und Dinge wie Groß- und Kleinschreibung und Nullbehandlung, bis ich die Integration zwischen den Ebenen teste.


0

Es ist wichtig zu testen, was das Entity Framework erwartet (dh Ihre Erwartungen zu validieren). Eine Möglichkeit, dies erfolgreich zu tun, ist die Verwendung von moq wie in diesem Beispiel gezeigt (zu lang, um in diese Antwort zu kopieren):

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

Seien Sie jedoch vorsichtig ... Es wird nicht garantiert, dass ein SQL-Kontext Dinge in einer bestimmten Reihenfolge zurückgibt, es sei denn, Sie haben ein geeignetes "OrderBy" in Ihrer Linq-Abfrage, sodass es möglich ist, Dinge zu schreiben, die beim Testen mit einer In-Memory-Liste erfolgreich sind ( linq-to-entity), schlägt jedoch in Ihrer uat / live-Umgebung fehl, wenn (linq-to-sql) verwendet wird.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.