7

Ich habe Dutzende von Posts über PROs und CONs der Versuch zu mock \ gefälschte EF in der Geschäftslogik gelesen. Ich habe noch nicht entschieden, was zu tun ist - aber eine Sache, die ich weiß ist - ich muss die Abfragen von der Geschäftslogik trennen. In this post sah ich, dass Ladislav hat geantwortet, dass es 2 gute Möglichkeiten:Decouple EF-Anfragen von BL - Erweiterungsmethoden VS-Klasse-pro-Abfrage

  1. Lassen Sie sie, wo sie sind und benutzerdefinierte Erweiterungsmethoden verwenden, Query Views, kartiert Datenbankansichten oder benutzerdefinierte definieren Abfragen wiederverwendbare Teile zu definieren, .
  2. Legen Sie jede einzelne Abfrage als Methode für eine separate Klasse offen. Die Methode darf IQueryable nicht offen legen und darf Expression nicht als Parameter akzeptieren = Die gesamte Abfragelogik muss in die Methode eingebunden werden. Aber das macht Ihre Klasse für ähnliche Methoden wie Repository (die einzige , die verspottet oder gefälscht werden kann). Diese Implementierung befindet sich in der Nähe der Implementierung , die mit gespeicherten Prozeduren verwendet wird.
  1. Welche Methode ist Ihrer Meinung nach besser jede warum?
  2. Gibt es ANY Nachteile, um die Abfragen an ihren eigenen Ort zu setzen? (Vielleicht einige Funktionen von EF oder so etwas zu verlieren)
  3. Sehe ich wie auch die einfachsten Anfragen zu kapseln haben:

    using (MyDbContext entities = new MyDbContext) 
    { 
        User user = entities.Users.Find(userId); // ENCAPSULATE THIS ? 
    
        // Some BL Code here 
    } 
    

Antwort

7

Also ich denke, Ihr Hauptpunkt ist, Testbarkeit des Codes, isn oder? In diesem Fall sollten Sie zunächst die Zuständigkeiten der Methode, die Sie testen möchten, zählen und dann Ihren Code anhand des Single Responsibility-Schemas umgestalten.

Ihr Beispielcode hat mindestens drei Aufgaben:

  • ein Objekt zu schaffen, ist eine Verantwortung - Kontext ein Objekt ist. Außerdem ist es ein Objekt, das Sie nicht in Ihrem Komponententest verwenden möchten, also müssen Sie seine Erstellung woanders hin verschieben.
  • Ausführen von Abfrage ist eine Verantwortung. Außerdem ist es eine Verantwortung, die Sie in Ihrem Komponententest vermeiden möchten.
  • einige Business-Logik zu tun, ist eine Verantwortung

Testen zu vereinfachen Sie Ihren Code Refactoring sollte und diese Verantwortung auf verschiedene Methoden unterteilen.

public class MyBLClass() 
{ 
    public void MyBLMethod(int userId) 
    { 
     using (IMyContext entities = GetContext()) 
     { 
      User user = GetUserFromDb(entities, userId); 

      // Some BL Code here 
     } 
    } 

    protected virtual IMyContext GetContext() 
    { 
     return new MyDbContext(); 
    } 

    protected virtual User GetUserFromDb(IMyDbContext entities, int userId) 
    { 
     return entities.Users.Find(userId); 
    } 
} 

Jetzt Unit-Tests Business-Logik sollte Stück Kuchen sein, weil Ihr Gerät zu testen Sie Ihre Klasse und gefälschten Kontext Factory-Methode und Abfrageausführungs Methode erben kann und auf EF völlig unabhängig werden.

// NUnit unit test 
[TestFixture] 
public class MyBLClassTest : MyBLClass 
{ 
    private class FakeContext : IMyContext 
    { 
     // Create just empty implementation of context interface 
    } 

    private User _testUser; 

    [Test] 
    public void MyBLMethod_DoSomething() 
    { 
     // Test setup 
     int id = 10; 
     _testUser = new User 
      { 
       Id = id, 
       // rest is your expected test data - that is what faking is about 
       // faked method returns simply data your test method expects 
      }; 

     // Execution of method under test 
     MyBLMethod(id); 

     // Test validation 
     // Assert something you expect to happen on _testUser instance 
     // inside MyBLMethod 
    } 

    protected override IMyContext GetContext() 
    { 
     return new FakeContext(); 
    } 

    protected override User GetUserFromDb(IMyContext context, int userId) 
    { 
     return _testUser.Id == userId ? _testUser : null; 
    } 
} 

Wie fügen Sie weitere Methoden und Ihre Anwendung wächst Sie diese Abfrage-Ausführungsmethoden und Kontext Factory-Methode zu getrennten Klassen Refactoring sowie einzelne Verantwortung auf Klassen zu folgen - Sie Kontext Fabrik erhalten und entweder ein Abfrage-Provider oder In einigen Fällen Repository (aber dieses Repository wird nie IQueryable zurückgeben oder Expression als Parameter in einer seiner Methoden erhalten). Auf diese Weise können Sie auch dem DRY-Prinzip folgen, bei dem Ihre Kontexterstellung und die am häufigsten verwendeten Abfragen nur einmal an einem zentralen Ort definiert werden.

So am Ende können Sie so etwas wie dieses:

public class MyBLClass() 
{ 
    private IContextFactory _contextFactory; 
    private IUserQueryProvider _userProvider; 

    public MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider) 
    { 
     _contextFactory = contextFactory; 
     _userProvider = userProvider; 
    } 

    public void MyBLMethod(int userId) 
    { 
     using (IMyContext entities = _contextFactory.GetContext()) 
     { 
      User user = _userProvider.GetSingle(entities, userId); 

      // Some BL Code here 
     } 
    } 
} 

Sind diese Schnittstellen wie aussehen:

public interface IContextFactory 
{ 
    IMyContext GetContext(); 
} 

public class MyContextFactory : IContextFactory 
{ 
    public IMyContext GetContext() 
    { 
     // Here belongs any logic necessary to create context 
     // If you for example want to cache context per HTTP request 
     // you can implement logic here. 
     return new MyDbContext(); 
    } 
} 

und

public interface IUserQueryProvider 
{ 
    User GetUser(int userId); 

    // Any other reusable queries for user entities 
    // Non of queries returns IQueryable or accepts Expression as parameter 
    // For example: IEnumerable<User> GetActiveUsers(); 
} 

public class MyUserQueryProvider : IUserQueryProvider 
{ 
    public User GetUser(IMyContext context, int userId) 
    { 
     return context.Users.Find(userId); 
    } 

    // Implementation of other queries 

    // Only inside query implementations you can use extension methods on IQueryable 
} 

Ihr Test wird jetzt nur Gebrauch Fälschungen für Kontext Factory und Abfrageanbieter.

// NUnit + Moq unit test 
[TestFixture] 
public class MyBLClassTest 
{ 
    private class FakeContext : IMyContext 
    { 
     // Create just empty implementation of context interface 
    } 

    [Test] 
    public void MyBLMethod_DoSomething() 
    { 
     // Test setup 
     int id = 10; 
     var user = new User 
      { 
       Id = id, 
       // rest is your expected test data - that is what faking is about 
       // faked method returns simply data your test method expects 
      }; 

     var contextFactory = new Mock<IContextFactory>(); 
     contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext()); 

     var queryProvider = new Mock<IUserQueryProvider>(); 
     queryProvider.Setup(f => f.GetUser(It.IsAny<IContextFactory>(), id)).Returns(user); 

     // Execution of method under test 
     var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object); 
     myBLClass.MyBLMethod(id); 

     // Test validation 
     // Assert something you expect to happen on user instance 
     // inside MyBLMethod 
    } 
} 

Es wäre wenig anders bei Repository, die sich auf Kontext an den Konstruktor übergeben haben sollte, bevor es zu einem Business-Klasse zu injizieren. Ihre Business-Klasse kann immer noch einige Abfragen definieren, die nie in anderen Klassen verwendet werden - diese Abfragen sind höchstwahrscheinlich Teil ihrer Logik. Sie können auch Erweiterungsmethoden verwenden, um einige wiederverwendbare Teile von Abfragen zu definieren. Sie müssen jedoch immer diese Erweiterungsmethoden außerhalb Ihrer Kerngeschäftslogik verwenden, die Sie testen möchten (entweder in Abfrageausführungsmethoden oder in Abfrageanbieter/Repository). Das ermöglicht Ihnen, die Ausführung von Abfrageprovidern oder Abfrageausführungen einfach zu fälschen.

Ich sah your previous question und dachte über das Schreiben eines Blogposts zu diesem Thema, aber der Kern meiner Meinung über das Testen mit EF ist in dieser Antwort.

Edit:

Repository ist anderes Thema, die auf Ihre ursprüngliche Frage bezieht sich nicht. Das spezifische Repository ist immer noch gültig. Wir sind nicht gegen Repositories, we are against generic repositories, weil sie keine zusätzlichen Funktionen bieten und kein Problem lösen.

Das Problem ist, dass Repository allein nichts löst. Es gibt drei Muster, die zusammen verwendet werden müssen, um eine richtige Abstraktion zu bilden: Repository, Arbeitseinheit und Spezifikationen. Alle drei sind bereits in EF verfügbar: DbSet/ObjectSet als Repositories, DbContext/ObjectContext als Unit of Works und Linq to Entities als Spezifikationen. Das Hauptproblem bei der allgemeinen Implementierung von generischen Repositories ist, dass sie nur das Repository und die Arbeitseinheit durch eine benutzerdefinierte Implementierung ersetzen, aber immer noch von den ursprünglichen Spezifikationen abhängen => Abstraktion ist unvollständig und in Tests, in denen das gefälschte Repository sich genauso verhält wie gefälschter Satz/Kontext.

Der Hauptnachteil meines Abfrageanbieters ist die explizite Methode für jede Abfrage, die Sie ausführen müssen. Im Falle eines Repositories werden Sie solche Methoden nicht haben, Sie werden nur wenige Methoden haben, die die Spezifikation akzeptieren (aber wiederum sollten diese Spezifikationen im DRY-Prinzip definiert sein), die Abfrage Filterbedingungen, Bestellung usw. erstellen werden.

Die Diskussion dieses Themas geht weit über den Rahmen dieser Frage hinaus und es erfordert, dass Sie etwas Selbststudium machen.

Btw. Spott und Fälschung hat einen anderen Zweck - Sie fälschen einen Aufruf, wenn Sie Testdaten von der Methode in der Abhängigkeit abrufen müssen, und Sie verspotten den Aufruf, wenn Sie bestätigen müssen, dass die Methode für Abhängigkeit mit erwarteten Argumenten aufgerufen wurde.

+0

Ich bin so froh, dass du derjenige bist, der geantwortet hat, denn du scheinst der 'Go-to-Guy' in Bezug auf EF Testability zu sein. Ich habe ein paar Fragen: Sie sagten ** Sie erhalten Kontext Factory und entweder ein Abfrage-Provider oder in einigen Fällen Repository (aber dieses Repository wird nie IQueryable zurückgeben oder Expression als Parameter in einer seiner Methoden abrufen). Auf diese Weise können Sie auch dem DRY-Prinzip folgen, bei dem Ihre Kontexterstellung und die am häufigsten verwendeten Abfragen nur einmal an einem zentralen Ort definiert werden. ** - Ich dachte, Sie wären total gegen die Verwendung des Repositories. Können Sie das bitte mit einem Beispiel verdeutlichen? –

+0

Kannst du bitte auch diesen Satz verdeutlichen: ** 'Es wäre etwas anders im Falle eines Repositorys, das einen Verweis auf einen Kontext haben sollte, der an seinen Konstruktor übergeben wurde, bevor es in deine Business-Klasse eingefügt wurde. ** (ein Beispiel für ein Repository von dieser Art und wenn man es verwenden sollte, wäre so sehr geschätzt) –

+0

und schließlich - der letzte Block, den Sie geschrieben: ** Ihre Business-Klasse kann noch einige Abfragen definieren, die nie in anderen Klassen verwendet werden - diese Abfragen sind höchstwahrscheinlich Teil seiner Logik. ** - Ihr erstes Codebeispiel verwendet diese Technik, oder? Wann würde ich das verwenden und wann würde ich Erweiterungsmethoden verwenden? Wohin gehen diese Erweiterungsmethoden? –