2016-05-17 9 views
5

Ich habe eine N-Layer-Anwendung mit Entity Framework (Code-First-Ansatz). Jetzt möchte ich einige Tests automatisieren. Ich verwende Moq Rahmen. Ich finde ein Problem beim Schreiben der Tests. Vielleicht ist meine Architektur falsch? Mit falsch, ich meine, dass ich Komponenten geschrieben habe, die nicht gut isoliert sind und daher nicht testbar sind. Ich mag das nicht wirklich ... Oder vielleicht kann ich moq Framework einfach nicht richtig benutzen.Wie man Entity Framework in einer N-Layer-Architektur vortäuscht

Ich lasse Sie meine Architektur sehen:

enter image description here

Auf jeder Ebene ich meine context im Konstruktor der Klasse zu injizieren.

Die Fassade:

public class PublicAreaFacade : IPublicAreaFacade, IDisposable 
{ 
    private UnitOfWork _unitOfWork; 

    public PublicAreaFacade(IDataContext context) 
    { 
     _unitOfWork = new UnitOfWork(context); 
    } 
} 

Der BLL:

public abstract class BaseManager 
{ 
    protected IDataContext Context; 

    public BaseManager(IDataContext context) 
    { 
     this.Context = context; 
    } 
} 

Das Repository:

public class Repository<TEntity> 
    where TEntity : class 
{ 
    internal PublicAreaContext _context; 
    internal DbSet<TEntity> _dbSet; 

    public Repository(IDataContext context) 
    { 
     this._context = context as PublicAreaContext; 
    } 
} 

IDataContext ist eine Schnittstelle, die von meinem DbContext implementiert:

public partial class PublicAreaContext : DbContext, IDataContext 

Nun, wie ich verspotten EF und wie schreibe ich die Tests:

[TestInitialize] 
public void Init() 
{ 
    this._mockContext = ContextHelper.CreateCompleteContext(); 
} 

Wo ContextHelper.CreateCompleteContext() ist:

public static PublicAreaContext CreateCompleteContext() 
{ 
    //Here I mock my context 
    var mockContext = new Mock<PublicAreaContext>(); 

    //Here I mock my entities 
    List<Customer> customers = new List<Customer>() 
    { 
     new Customer() { Code = "123455" }, //Customer with no invoice 
     new Customer() { Code = "123456" } 
    }; 

    var mockSetCustomer = ContextHelper.SetList(customers); 
    mockContext.Setup(m => m.Set<Customer>()).Returns(mockSetCustomer); 

    ... 

    return mockContext.Object; 
} 

Und hier, wie ich schreibe meinen Test:

[TestMethod] 
public void Success() 
{ 
    #region Arrange 
    PrepareEasyPayPaymentRequest request = new PrepareEasyPayPaymentRequest(); 
    request.CodiceEasyPay = "128855248542874445877"; 
    request.Servizio = "MyService"; 
    #endregion 

    #region Act 
    PublicAreaFacade facade = new PublicAreaFacade(this._mockContext); 
    PrepareEasyPayPaymentResponse response = facade.PrepareEasyPayPayment(request); 
    #endregion 

    #region Assert 
    Assert.IsTrue(response.Result == it.MC.WebApi.Models.ResponseDTO.ResponseResult.Success); 
    #endregion 
} 

Hier scheint es funktioniert alles richtig !!! Und es sieht so aus, als wäre meine Architektur korrekt. Was aber, wenn ich eine Entity einfügen/aktualisieren möchte? Nichts funktioniert mehr! Ich erklären, warum:

Wie Sie gebe ich ein *Request Objekt sehen kann (es ist das DTO) an die Fassade, dann in meinem TOA ich mein Unternehmen aus der propertiess des DTO erzeugen:

private PaymentAttemptTrace CreatePaymentAttemptTraceEntity(string customerCode, int idInvoice, DateTime paymentDate) 
{ 
    PaymentAttemptTrace trace = new PaymentAttemptTrace(); 
    trace.customerCode = customerCode; 
    trace.InvoiceId = idInvoice; 
    trace.PaymentDate = paymentDate; 

    return trace; 
} 

PaymentAttemptTrace ist die Entität Ich werde in Entity Framework einfügen .. Es ist nicht verspottet und ich kann es nicht injizieren. Also, selbst wenn ich meinen gespotteten Kontext (IDataContext) passiere, wenn ich versuche, eine Entität einzufügen, die nicht verspottet wird, schlägt mein Test fehl!

Hier, dass Zweifel über ich habe eine falsche Architektur hat sich erhoben!

Also, was ist los? Die Architektur oder die Art, wie ich moq benutze?

Vielen Dank für Hilfe

UPDATE

Hier, wie ich meinen Code testen .. Zum Beispiel habe ich die Spur einer Zahlung testen möchten ..

Hier ist der Test:

[TestMethod] 
public void NoPaymentDate() 
{ 
    TracePaymentAttemptRequest request = new TracePaymentAttemptRequest(); 
    request.AliasTerminale = "MyTerminal"; 
    //... 
    //I create my request object 

    //You can see how I create _mockContext above 
    PublicAreaFacade facade = new PublicAreaFacade(this._mockContext); 
    TracePaymentAttemptResponse response = facade.TracePaymentAttempt(request); 

    //My asserts 
} 

Hier ist die Fassade:

public TracePaymentAttemptResponse TracePaymentAttempt(TracePaymentAttemptRequest request) 
{ 
    TracePaymentAttemptResponse response = new TracePaymentAttemptResponse(); 

    try 
    { 
     ... 

     _unitOfWork.PaymentsManager.SavePaymentAttemptResult(
      easyPay.CustomerCode, 
      request.CodiceTransazione, 
      request.EsitoPagamento + " - " + request.DescrizioneEsitoPagamento, 
      request.Email, 
      request.AliasTerminale, 
      request.NumeroContratto, 
      easyPay.IdInvoice, 
      request.TotalePagamento, 
      paymentDate); 

     _unitOfWork.Commit(); 

     response.Result = ResponseResult.Success; 
    } 
    catch (Exception ex) 
    { 
     response.Result = ResponseResult.Fail; 
     response.ResultMessage = ex.Message; 
    } 

    return response; 
} 

Hier, wie ich die PaymentsManager entwickelt:

public PaymentAttemptTrace SavePaymentAttemptResult(string customerCode, string transactionCode, ...) 
{ 
    //here the problem... PaymentAttemptTrace is the entity of entity framework.. Here i do the NEW of the object.. It should be injected, but I think it would be a wrong solution 
    PaymentAttemptTrace trace = new PaymentAttemptTrace(); 
    trace.customerCode = customerCode; 
    trace.InvoiceId = idInvoice; 
    trace.PaymentDate = paymentDate; 
    trace.Result = result; 
    trace.Email = email; 
    trace.Terminal = terminal; 
    trace.EasypayCode = transactionCode; 
    trace.Amount = amount; 
    trace.creditCardId = idCreditCard; 
    trace.PaymentMethod = paymentMethod; 

    Repository<PaymentAttemptTrace> repository = new Repository<PaymentAttemptTrace>(base.Context); 
    repository.Insert(trace); 

    return trace; 
} 

Am Ende, wie ich das Repository geschrieben:

public class Repository<TEntity> 
    where TEntity : class 
{ 
    internal PublicAreaContext _context; 
    internal DbSet<TEntity> _dbSet; 

    public Repository(IDataContext context) 
    { 
     //the context is mocked.. Its type is {Castle.Proxies.PublicAreaContextProxy} 
     this._context = context as PublicAreaContext; 
     //the entity is not mocked. Its type is {PaymentAttemptTrace} but should be {Castle.Proxies.PaymentAttemptTraceProxy}... so _dbSet result NULL 
     this._dbSet = this._context.Set<TEntity>(); 
    } 

    public virtual void Insert(TEntity entity) 
    { 
     //_dbSet is NULL so "Object reference not set to an instance of an object" exception is raised 
     this._dbSet.Add(entity); 
    } 
} 
+1

Könnten Sie uns bitte den Test zum Einfügen/Aktualisieren von Entitäten zeigen und erklären, wie genau dies fehlschlägt? Auch der zu testende Code wäre hilfreich. –

+0

Ich habe meine Frage mit einem Beispiel aktualisiert – Ciccio

Antwort

2

Ihre Architektur sieht gut aus, aber die Implementierung ist fehlerhaft. Es ist undichte Abstraktion.

In Ihrem Diagramm der Fassade Schicht hängt nur von den BLL aber wenn man sich die PublicAreaFacade ‚s Konstruktor schauen Sie in Wirklichkeit hat es eine direkte Abhängigkeit zu einer Schnittstelle von Repository Schicht sehen, dass:

public PublicAreaFacade(IDataContext context) 
{ 
    _unitOfWork = new UnitOfWork(context); 
} 

Dies sollte nicht sein.Es sollte nur seine direkte Abhängigkeit als Eingabe - die PaymentsManager oder - noch besser - eine Schnittstelle davon:

public PublicAreaFacade(IPaymentsManager paymentsManager) 
{ 
    ... 
} 

Der concequence ist, dass Ihr Code Weg mehr testbar wird. Wenn Sie jetzt Ihre Tests betrachten, sehen Sie, dass Sie die innerste Schicht Ihres Systems (dh die und sogar ihre Entity Accessoren Set<TEntity>) verspotten müssen, obwohl Sie eine der äußersten Schichten Ihres Systems testen (die PublicAreaFacade Klasse).

Dies ist, wie ein Unit-Test für die TracePaymentAttempt Methode aussehen würde, wenn die PublicAreaFacade nur auf IPaymentsManager abhing:

[TestMethod] 
public void CallsPaymentManagerWithRequestDataWhenTracingPaymentAttempts() 
{ 
    // Arrange 
    var pm = new Mock<IPaymentsManager>(); 
    var pa = new PulicAreaFacade(pm.Object); 
    var payment = new TracePaymentAttemptRequest 
     { 
      ... 
     } 

    // Act 
    pa.TracePaymentAttempt(payment); 

    // Assert that we call the correct method of the PaymentsManager with the data from 
    // the request. 
    pm.Verify(pm => pm.SavePaymentAttemptResult(
     It.IsAny<string>(), 
     payment.CodiceTransazione, 
     payment.EsitoPagamento + " - " + payment.DescrizioneEsitoPagamento, 
     payment.Email, 
     payment.AliasTerminale, 
     payment.NumeroContratto, 
     It.IsAny<int>(), 
     payment.TotalePagamento, 
     It.IsAny<DateTime>())) 
} 
+0

Das 'UnitOfWork' enthält alle notwendigen Manager, die auf eine faule Art und Weise instanziate sind. Über den' IDataContext' stimme ich Ihnen zu, ich mag es nicht, dass ich es innerhalb der Fassade, aber wenn Ich überhole es nicht Ich habe 2 Probleme: 1. Wie kann ich den Kontext verspotten, wenn der Kontext hinter dem "BLL" versteckt ist? Wenn Sie meine Tests sehen, spotte ich den Kontext und gebe ihn dann an die Fassade weiter; 2. 1 Fassade kann verschiedene Manager aufrufen. Manager verwenden den Kontext. Wenn ich den Kontext der Eingabe nicht an die Fassade übergebe, wie kann ich nur einen Kontext für jeden Manager einrichten? – Ciccio

+1

1) Der Trick ist, dass Sie beim Testen einer Fassade nicht den Kontext überspielen müssen. Wenn man eine Methode einer Fassade testet, will man nur testen, was die Fassade selbst macht, aber nicht was implizit im Hintergrund passiert - das ist die Definition von Spott. –

+1

2) Was ich Ihnen vorschlage, ist die * Abhängigkeitsinjektion *. In DI erstellen Sie normalerweise das vollständige Objektdiagramm im Einstiegspunkt der Anwendung. In Ihrem Fall ist das der REST-Dienstendpunkt. Sie können aber auch ein DI-Framework wie Micorsoft's Unity verwenden, das die Erstellung von Objektgraphen viel einfacher macht. –

0

Übergeben Sie IUnitOfWork in den Fassaden- oder BLL-Layer-Konstruktor, je nachdem, was direkt auf der Arbeitseinheit ausgeführt wird. Dann können Sie einstellen, was die Mock<IUnitOfWork> in Ihren Tests zurückgibt. Sie sollten nicht IDataContext an alles außer vielleicht die Repo-Konstruktoren und die Arbeitseinheit übergeben.

Zum Beispiel, wenn die Fassade hat eine Methode PrepareEasyPayPayment, die einen Repo-Anruf durch einen UnitOfWork Anruf tätigt, Setup das Mock wie folgt aus:

// Arrange 
var unitOfWork = new Mock<IUnitOfWork>(); 
unitOfWork.Setup(x => x.PrepareEasyPayPaymentRepoCall(request)).Returns(true); 
var paymentFacade = new PaymentFacade(unitOfWork.Object); 

// Act 
var result = paymentFacade.PrepareEasyPayPayment(request); 

Dann haben Sie den Datenanruf verspottet und mehr leicht Testen Sie Ihren Code in der Fassade.

Für den Einbautest sollten Sie eine Fassadenmethode wie CreatePayment haben, die eine PrepareEasyPayPaymentRequest dauert. Innerhalb dieses CreatePayment Methode, sollte es den Repo, wahrscheinlich durch die Einheit der Arbeit verweisen, wie

var result = _unitOfWork.CreatePaymentRepoCall(request); 
if (result == true) 
{ 
    // yes! 
} 
else 
{ 
    // oh no! 
} 

Was möchten Sie für Unit-Tests verspotten ist, dass diese erstellen/einfügen Repo-Aufruf gibt wahr oder falsch, so dass Sie testen können, der Code verzweigt, nachdem der Repo-Aufruf abgeschlossen wurde.

Sie können auch testen, dass der Insert-Aufruf wie erwartet durchgeführt wurde, aber das ist normalerweise nicht so wertvoll, es sei denn, die Parameter für diesen Aufruf sind sehr logisch.

+0

Ich verstehe ... aber unklug Ihre Lösung nicht mein Problem zu lösen .. Ich habe bereits eine Methode wie 'CreatePayment', die eine' PrepareEasyPayPaymentRequest' anheften. Ich kann meine Fassade ändern, um "IUnitOfWork" in den Fassadenkonstruktor zu übergeben. aber das Problem bleibt bestehen! Das Problem ist, dass ich innerhalb der Fassade die Entity (des Entity Framework) zum Einfügen in die Datenbank erstelle. Ich installiere die Entität ... Ich mache eine "neue" ... Auf diese Weise wird die Entität nicht verspottet ... also kann ich die Insert nicht testen. Es ist so, als würde ich die verspottete Entität nach Facade weiterleiten, aber das ist es sicherlich nicht der richtige Weg, um das Problem zu lösen – Ciccio

0

Es klingt wie Sie müssen den Code ein wenig ändern. Das Neueingeben von Dingen führt zu fest codierten Abhängigkeiten und macht sie untestabel, also versuche sie wegzuspalten. Vielleicht kannst du alles, was mit EF zu tun hat hinter einer anderen Ebene verstecken, dann musst du nur diese bestimmte Ebenenebene vortäuschen und niemals EF berühren.

0

Sie diesen Rahmen Open Source für Unit-Tests verwenden können, die Rahmen DbContext Mock Unternehmen gut ist

https://effort.codeplex.com/

Versuchen Sie, diese Sie Ihre Daten effizient verspotten helfen.