it-swarm.com.de

Wie werden People Unit-Tests mit Entity Framework 6 durchgeführt?

Ich beginne gerade mit Unit-Tests und TDD im Allgemeinen. Ich habe mich schon einmal damit beschäftigt, 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 an die Implementierung einer Serviceklasse gemacht, mit der ich die Geschäftslogik von den Controllern abstrahieren und mit EF6 bestimmten Modellen und Dateninteraktionen zuordnen kann.

Das Problem ist, dass ich mich bereits blockiert habe, weil ich EF nicht in einem Repository entfernen 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 der Natur angesichts der undichten Abstraktionen, die von IQueryable und den vielen großartigen Beiträgen von Ladislav Mrnka zum Thema Unit-Tests verursacht werden, die aufgrund der Unterschiede in Linq nicht einfach sind? Anbieter bei der Arbeit mit einer In-Memory-Implementierung, die einer bestimmten Datenbank zugeordnet ist.

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)
    {

    }
}

Service

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

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

  1. Mocking EF Kontext mit so etwas wie diesem Ansatz Mocking EF beim Unit-Testen oder direkt mit einem Mocking-Framework auf der Schnittstelle wie moq - unter Berücksichtigung des Schmerzes, den die Unit-Tests bestehen können, aber nicht unbedingt von Ende zu Ende funktionieren und Sichern Sie sie mit Integrationstests?
  2. Vielleicht mit etwas wie Anstrengung , um EF zu verspotten - Ich habe es nie benutzt und bin mir nicht sicher, ob jemand anderes es in freier Wildbahn benutzt?
  3. Machen Sie sich nicht die Mühe, irgendetwas zu testen, das einfach zu EF zurückruft - also sind im Wesentlichen Dienstmethoden, die EF direkt aufrufen (getAll usw.), nicht Unit-getestet, sondern nur Integration-getestet?

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

162
Modika

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 eine frühere Antwort sagte, ist es oft sinnlos, viel Zeit damit zu verbringen, zu testen, was Sie nicht besitzen.

Allerdings besitzen Sie die darunterliegende Datenbank! Hier schlägt sich meiner Meinung nach dieser Ansatz nieder, Sie müssen nicht testen, ob EF/NH vorhanden sind ihre Arbeit richtig machen. 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 nicht mehr im Bereich Unit-Tests, sondern im Bereich Integrationstests, aber die Prinzipien bleiben dieselben.

Als Erstes müssen Sie in der Lage sein, Ihre DAL zu verspotten, damit Ihre BLL unabhängig von EF und SQL getestet werden kann. Dies sind Ihre Komponententests. Als nächstes müssen Sie Ihre Integrationstests entwerfen. um deine DAL zu beweisen, sind diese meiner meinung nach 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 dazu entweder ein Backup oder erstellen 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. Auf diese Weise wird sichergestellt, dass sich Ihre Komponententestdatenbank zu Beginn jedes Tests immer im gleichen 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, bestimmte 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, um alle diese Setup-/Teardown-Skripte auszuführen.

Ein letzter Punkt: Es kann sehr schwierig sein, so viel SQL zu schreiben, um Ihren 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 eigenes 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. In der Praxis finde ich es jedoch VIEL einfacher zu handhaben (besonders wenn Sie mehrere tausend Tests haben), sonst erstellen Sie eine enorme Anzahl von Skripten. 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 Ihrer 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 Ansatz stehe, für den ich mich oben ausgesprochen habe, habe ich meinen Testmechanismus leicht aktualisiert. Ich neige jetzt dazu, die Entitäten im 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();
}

Dann testen Sie 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 granularer ( einzelne Aussagen sind gut )

Bearbeiten 03.05.2015

Eine weitere Überarbeitung dieses Ansatzes. Während Setups auf Klassenebene für Tests wie das Laden von Eigenschaften sehr hilfreich sind, sind sie dort weniger nützlich, wo die verschiedenen Setups erforderlich sind. In diesem Fall ist die Einrichtung einer neuen Klasse für jeden Fall zu viel des Guten.

Um dies zu unterstützen, habe ich jetzt zwei Basisklassen SetupPerTest und SingleSetup. Diese beiden Klassen legen das erforderliche Framework offen.

In der SingleSetup haben wir 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 Entitäten 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.

173
Liath

Feedback zur Arbeitserfahrung hier

Nach vielem Lesen habe ich Anstrengung 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 mit einem vollständigen Modell der Datenbank dazu neigt, die Tests nach unten zu ziehen. Sie erkennen, dass Sie sich um das Einrichten einer ganzen Reihe von Abhängigkeiten kümmern müssen, um einen Teil des Systems zu testen. Sie tendieren auch dazu, Tests zu organisieren, die möglicherweise nicht miteinander in Beziehung stehen, nur weil es nur ein großes Objekt gibt, das sich um alles kümmert. Wenn Sie nicht aufpassen, werden Sie möglicherweise Integrationstests anstelle von Komponententests durchführen

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

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. Ich bevorzuge diesen Beitrag, um zu sehen, wohin er als nächstes führt :)

Bearbeiten zum Hinzufügen : Es kann einige Zeit dauern, bis das Gerät aufgewärmt ist. 5 Sekunden beim Teststart. Dies kann ein Problem für Sie sein, wenn Ihre Testsuite sehr effizient sein soll.


Zur Verdeutlichung bearbeitet:

Ich habe mit Effort eine Webservice-App getestet. Jede Nachricht M, die eingeht, wird über Windsor an einen IHandlerOf<M> Weitergeleitet. Castle.Windsor löst den IHandlerOf<M> Auf, der die Abhängigkeiten der Komponente auflöst. Eine dieser Abhängigkeiten ist das DataContextFactory, mit dem der Handler nach der Factory fragen kann

In meinen Tests instanziiere ich die IHandlerOf-Komponente direkt, verspotte alle Unterkomponenten des SUT und übergebe den Aufwand umschlossenen DataContextFactory an 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 schon sagte, habe ich es geschafft und konnte schnell einige Punkte in der Anwendung testen

20
samy

Wenn Sie den Code unit testen möchten, müssen Sie den Code, den Sie testen möchten (in diesem Fall Ihren Dienst), von externen Ressourcen (z. B. Datenbanken) isolieren. Sie könnten dies wahrscheinlich mit einer Art speicherinterner EF-Anbieter tun, jedoch ist es viel üblicher, Ihre EF-Implementierung zu abstrahieren, z. mit einer Art Repository-Muster. Ohne diese Isolierung sind alle Tests, die Sie schreiben, Integrationstests und keine Komponententests.

Zum Testen von EF-Code: 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 und nicht um Komponententests, da für diese Tests eine Datenbankverbindung erforderlich ist und in der Zieldatenbank bereits das aktuellste Schema installiert ist.

12
Justin

Hier ist die Sache, Entity Framework ist eine Implementierung, so dass trotz der Tatsache, dass es die Komplexität der Datenbankinteraktion abstrahiert, eine direkte Interaktion immer noch eine enge Kopplung darstellt und es deshalb verwirrend ist, sie zu testen.

Beim Unit-Testen geht es darum, die Logik einer Funktion und ihre potenziellen Ergebnisse unabhängig von externen Abhängigkeiten, in diesem Fall dem Datenspeicher, zu testen. 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 einige Kriterien nicht erfüllt, sollte Ihr [verspotteter] Datenspeicher so konfiguriert sein, dass er immer einen Benutzer zurückgibt, der die Kriterien nicht erfüllt, und umgekehrt umgekehrt für die gegenteilige Behauptung.

Angesichts dessen und der Tatsache, dass EF eine Implementierung ist, würde ich wahrscheinlich die Idee einer Zusammenfassung eines Repositorys befürworten. Scheinen Sie ein bisschen überflüssig? Dies ist jedoch nicht der Fall, da Sie ein Problem lösen, das Ihren Code von der Datenimplementierung isoliert.

In DDD geben die Repositorys immer nur aggregierte Roots zurück, nicht DAO. Auf diese Weise muss der Konsument 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 in Ihrer Anwendung ausgeblendet werden. Dies ist ein weiterer Vorteil des von Ihnen definierten Repositorys. Sie können ein Geschäftsobjekt als Rückgabetyp anstelle des EF-Objekts definieren. Das Repo verbirgt nun die Aufrufe von EF und ordnet die EF-Antwort dem in der Repos-Signatur 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 diese Schnittstelle verspotten, um Ihnen das Steuerelement zu geben, das Sie zum isolierten Testen Ihres Codes benötigen.

Es ist ein bisschen mehr Arbeit, und viele stöbern damit herum, 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 völlig anderer Meinung als die beste Antwort, da sie das eigentliche Problem umgeht, das darin besteht, Ihren Code zu isolieren, und sich dann mit dem Testen Ihrer Zuordnung befasst. Testen Sie auf jeden Fall Ihre Zuordnung, wenn Sie möchten, aber beheben Sie das eigentliche Problem hier und erhalten Sie eine echte Codeabdeckung.

8
Sinaesthetic

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

Um diesen Code testbar zu machen, MÜSSEN Sie Ihre Datenzugriffsschicht jedoch fast von Ihrem Geschäftslogikcode trennen. Was ich tue, ist, all meine EF-Sachen zu nehmen und sie in eine (oder mehrere) DAO- oder DAL-Klasse zu stecken, die auch eine entsprechende Schnittstelle hat. Dann schreibe ich meinen Dienst, bei dem das DAO- oder DAL-Objekt als Abhängigkeit (vorzugsweise Konstruktorinjektion) eingefügt wird, auf die als Schnittstelle verwiesen wird. Nun 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 einspeisen.

//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 und nicht als Komponententests betrachten. Ich habe gesehen, wie Jungs überprüft haben, wie viele Fahrten in den Ruhezustand der Datenbank zuvor unternommen wurden, aber sie waren an einem Projekt beteiligt, das Milliarden von Datensätzen in ihrem Datenspeicher beinhaltete, und diese zusätzlichen Fahrten waren wirklich wichtig.

7
Jonathan Henson

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

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

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 ist, dass der Identitätsstartwert erhöht wird, wenn versucht wird, Daten einzufügen, selbst wenn die Transaktion abgebrochen wird. Da die Tests jedoch mit einer Entwicklungsdatenbank durchgeführt werden, sollte dies kein Problem darstellen.

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();
        }
    }
}
7
Marquinho Peli

Kurz gesagt würde ich 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 Gewohnheit, eine Fassade an ein Framework eines Drittanbieters zu abstrahieren, nur damit Sie eine Kopie dieser Framework-API erstellen können, mit der Sie Dummy-Daten bastardieren/erweitern können, ist für mich von geringem Wert. Jeder hat eine andere Meinung darüber, wie viel Unit-Testing am besten ist. Ich bin heutzutage eher pragmatisch und frage mich, ob mein Test wirklich einen Mehrwert für das Endprodukt darstellt und zu welchem ​​Preis.

5
ComeIn

Es gibt Effort, einen In-Memory-Entity-Framework-Datenbankanbieter. Ich habe es nicht wirklich ausprobiert ... Haa hat gerade bemerkt, dass dies in der Frage erwähnt wurde!

Alternativ können Sie zu EntityFrameworkCore wechseln, in das ein Anbieter für die Speicherdatenbank 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 abzurufen, 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.

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

Ich möchte einen kommentierten und kurz diskutierten Ansatz teilen, aber ein aktuelles Beispiel zeigen, mit dem ich Komponententest EF-basierte Dienste unterstütze.

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

Hier sind die Ziele, die ich bei der Ausarbeitung eines Musters hatte:

  • Für andere Entwickler im Team muss es einfach zu verstehen sein
  • Es muss den EF-Code so weit wie möglich isolieren
  • Es darf nicht das Erstellen seltsamer Schnittstellen mit mehreren Verantwortlichkeiten beinhalten (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 bin auch damit einverstanden, 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 Unit-Test und Integrationstests erstellen, um Ihren EF-Code speziell zu testen.

Dies gelang mir, indem ich einfach EF-Code einkapselte in dedizierte Abfrage- und Befehlsklassen. Die Idee ist einfach: Binden Sie jeden EF-Code in eine Klasse ein und hängen Sie von einer Schnittstelle in den Klassen ab, die sie ursprünglich verwendet hätten. Das Hauptproblem, das ich lösen musste, bestand darin, das Hinzufügen zahlreicher Abhängigkeiten zu Klassen und das Einrichten von viel Code in meinen Tests zu vermeiden.

Hier kommt eine nützliche, einfache Bibliothek ins Spiel: Mediatr . Es ermöglicht einfaches In-Process-Messaging und entkoppelt "Requests" von den Handlern, die den Code implementieren. Dies hat den zusätzlichen Vorteil, dass das "Was" vom "Wie" abgekoppelt wird. Wenn Sie beispielsweise den EF-Code in kleine Blöcke einkapseln, 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 den EF-Code für Komponententests zu ermöglichen.

Nehmen wir zunächst an, wir haben einen Service mit einer 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 explizit den gesamten EF-bezogenen Code in beschreibende Klassen, Sie ermöglichen auch die Erweiterbarkeit, indem Sie die Implementierungsbedenken hinsichtlich der Behandlung dieser Anforderung beseitigen --dieser Klasse ist es egal, ob die relevanten Objekte aus EF, MongoDB oder einer Textdatei stammen.

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, ist die Abstraktion einfach und gekapselt. Es ist auch absolut testbar , da Sie in einem Integrationstest könnten diese Klasse einzeln testen - es gibt keine geschäftlichen Bedenken 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 auch immer 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 mehr etwas konfigurieren müssen - es ist ein sehr einfacher Unit-Test. Lassen Sie uns klar sein: Dies ist völlig möglich ohne so etwas wie Mediatr (Sie würden einfach eine Schnittstelle implementieren und verspotten es für Tests, zB IGetRelevantDbObjectsQuery), aber in der Praxis für eine große Codebasis mit vielen Funktionen und Abfragen/Befehlen, liebe ich die Kapselung und die angeborene DI-Unterstützung, die Mediatr bietet.

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

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

Organisieren nach Feature-Slices ist nicht wichtig, aber dies hält den gesamten relevanten/abhängigen Code zusammen und ist leicht auffindbar. Am wichtigsten ist, ich trenne die Queries vs. Commands - nach dem Command/Query Separation Prinzip.

Dies erfüllt alle meine Kriterien: Es ist einfach, 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 Db-Kontext vereinfachen, indem Sie eine Rollenschnittstelle (IUnitOfWork.SaveChangesAsync()) verwenden und Aufrufe an die Einzelrollenschnittstelle verspotten oder das Festschreiben/Zurücksetzen in Ihren RequestHandlern kapseln für Sie, solange es wartbar ist. Ich war beispielsweise versucht, eine einzelne generische Anforderung/einen einzelnen generischen Handler zu erstellen, bei dem/dem Sie nur ein EF-Objekt übergeben und es speichern/aktualisieren/entfernen würden - aber Sie müssen nach Ihrer Absicht fragen und sich daran erinnern, wenn Sie möchten Tauschen Sie den Handler mit einem anderen Speicheranbieter/einer anderen Speicherimplementierung aus. Sie sollten wahrscheinlich explizite Befehle/Abfragen erstellen, die Ihre Absicht widerspiegeln. In den meisten Fällen ist für einen einzelnen Dienst oder eine einzelne Funktion etwas Bestimmtes erforderlich. Erstellen Sie keine allgemeinen Informationen, bevor Sie diese benötigen.

Es gibt natürlich Vorbehalte zu diesem Muster - 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. Dies ist ein Prozessproblem, kein Problem mit MediatR. Achten Sie also darauf, wie Sie dieses Muster verwenden.

Sie wollten ein konkretes Beispiel dafür, wie Leute EF testen/verspotten, und dies ist ein Ansatz, der für uns bei unserem Projekt erfolgreich funktioniert - und das Team ist super glücklich darüber, wie einfach es ist, EF zu übernehmen. Ich hoffe das hilft! Wie bei allen Programmieraufgaben 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.

3
kamranicus

Ich trenne meine Filter gerne von anderen Teilen des Codes und teste diese, wie ich in meinem Blog hier skizziere http://coding.grax.com/2013/08/testing-custom-linq-filter-operators). html

Abgesehen davon ist die zu testende Filterlogik aufgrund der Übersetzung zwischen dem LINQ-Ausdruck und der zugrunde liegenden Abfragesprache wie T-SQL nicht mit der Filterlogik identisch, die beim Ausführen des Programms ausgeführt wird. Dies ermöglicht mir jedoch, die Logik des Filters zu validieren. Ich mache mir keine allzu großen Sorgen um die Übersetzungen und Dinge wie Groß- und Kleinschreibung und Null-Behandlung, bis ich die Integration zwischen den Ebenen teste.

2
Grax