2016-05-17 108 views
5

Ho un'applicazione N-Layer con Entity Framework (approccio Code-First). Ora voglio automatizzare alcuni test. Sto usando il framework Moq. Sto riscontrando qualche problema nello scrivere i test. Forse la mia architettura è sbagliata? Con sbagliato, intendo che ho scritto componenti che non sono ben isolati e quindi non sono testabili. Non mi piace molto questo ... O forse, semplicemente non posso usare correttamente il framework moq.Come imitare Entity Framework in un'architettura N-Layer

mi permetterà di vedere la mia architettura:

enter image description here

Ad ogni livello che iniettare il mio context nel costruttore della classe.

facciata:

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

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

Il BLL:

public abstract class BaseManager 
{ 
    protected IDataContext Context; 

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

Il 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 è un'interfaccia che viene implementata da mia DbContext:

public partial class PublicAreaContext : DbContext, IDataContext 

Ora, come mi prendo gioco EF e come scrivo le prove:

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

Dove ContextHelper.CreateCompleteContext() è:

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

E qui come scrivo la mia prova:

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

Qui sembra che funzioni tutto correttamente !!! E sembra che la mia architettura sia corretta. Ma cosa succede se voglio inserire/aggiornare un'entità? Niente funziona più! Spiego perché:

Come potete vedere passo davanti a un oggetto *Request (è il DTO) per la facciata, poi nella mia TOA ho generare la mia entità dal propertiess del DTO:

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 è l'Entità che inserirò in Entity Framework. Non è deriso e non posso iniettarlo. Quindi, anche se passo il mio contesto deriso (IDataContext), quando provo ad inserire un'entità che non è derisa, il mio test fallisce!

Qui il dubbio sull'architettura sbagliata è aumentato!

Quindi, cosa c'è che non va? L'architettura o il modo in cui utilizzo moq?

Grazie per l'aiuto

UPDATE

Ecco come ho testare il mio codice .. Per esempio, voglio testare la traccia di un pagamento ..

Ecco il 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 
} 

Qui la facciata:

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

Ecco come ho sviluppato il PaymentsManager:

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

Alla fine, come ho scritto il repository:

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

Potrebbe per favore mostrarci il test sull'inserimento/aggiornamento delle entità e spiegare esattamente come fallisce? Anche il codice sotto test sarebbe utile. –

+0

Ho aggiornato la mia domanda con un esempio – Ciccio

risposta

2

La tua architettura sembra buona, ma l'implementazione è difettosa. È che perde l'astrazione.

nel diagramma della strato Facciata dipende solo dalla BLL, ma quando si guarda a costruttore 's il PublicAreaFacade si vedrà che in realtà ha una dipendenza diretta a un'interfaccia dal strato Repository:

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

Questo non dovrebbe essere.Dovrebbe prendere solo la sua dipendenza diretta in ingresso - la PaymentsManager o - meglio ancora - un'interfaccia di esso:

public PublicAreaFacade(IPaymentsManager paymentsManager) 
{ 
    ... 
} 

Il concequence è che il codice diventa modo più verificabili. Quando guardi i tuoi test ora vedi che devi prendere in giro il livello più interno del tuo sistema (es. IDataContext e persino i suoi accessori di entità Set<TEntity>) anche se stai testando uno dei livelli più esterni del tuo sistema (il PublicAreaFacade classe).

Questo è come un test di unità per il metodo TracePaymentAttempt potrebbe apparire come se l'PublicAreaFacade dipendesse solo da IPaymentsManager:

[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

'unitOfWork' contiene tutto il gestore necessario che è instanciato in modo pigro .. Informazioni su 'IDataContext' Sono d'accordo con te, non mi piace che lo passi all'interno della facciata, ma se Non lo passo Ho 2 problemi: 1. Come posso prendere in giro il contesto, se il contesto è "nascosto" dietro il 'BLL'? Se vedi i miei test, mi burlo del contesto e poi lo passo in input alla facciata; 2. 1 Facciata può chiamare diversi gestori. I manager usano il contesto. Se non passo il contesto in input alla facciata, come posso instanciare un solo contesto per ogni manager? – Ciccio

+1

1) Il trucco è che non è necessario prendere in giro il contesto durante il test di una facciata. Se testate un metodo di una facciata, volete solo testare ciò che la facciata stessa sta facendo, ma non ciò che sta accadendo implicitamente sullo sfondo - questa è la definizione stessa di derisione. –

+1

2) Quello che sto suggerendo di fare è chiamato * dipendenza da iniezione *. In DI di solito crei il grafico dell'oggetto completo nel punto di ingresso dell'applicazione. Nel tuo caso è l'endpoint del servizio REST. Ma puoi anche usare un framework DI come Micorsoft's Unity che rende molto più semplice la creazione del grafo degli oggetti. –

0

Passaggio IUnitOfWork nel costruttore di strati Facade o BLL, a seconda di quale si effettuino chiamate direttamente sull'unità di lavoro. Quindi puoi impostare cosa sta restituendo il Mock<IUnitOfWork> nei tuoi test. Non è necessario passare a IDataContext tutto tranne forse i costruttori di repository e l'unità di lavoro.

Ad esempio, se la facciata ha un metodo PrepareEasyPayPayment che effettua una chiamata di pronti contro termine tramite una chiamata UnitOfWork, impostare il finto in questo modo:

// 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); 

Allora hai preso in giro la chiamata dati e possono più facilmente prova il tuo codice nella facciata.

Per il test degli inserti, è necessario disporre di un metodo di facciata come CreatePayment che accetta uno PrepareEasyPayPaymentRequest. Dentro quel metodo CreatePayment, si dovrebbe fare riferimento al pronti contro termine, probabilmente attraverso l'unità di lavoro, come

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

Che cosa si vuole prendere in giro per il test delle unità è che questo creare/inserire chiamata repo restituisce true o false in modo da poter testare il codice si ramifica dopo che la chiamata repo è stata completata.

È inoltre possibile verificare che la chiamata di inserimento sia stata effettuata come previsto, ma di solito non è così utile a meno che i parametri per quella chiamata non siano coinvolti in modo logico nella creazione di tali chiamate.

+0

Capisco ... ma la tua soluzione non è risolta, non risolve il mio problema .. Ho già un metodo come 'CreatePayment' che affronta un 'PrepareEasyPayPaymentRequest'. Posso cambiare la mia facciata per passare 'IUnitOfWork' nel costruttore Facade .. ma il problema rimane! Il problema è che all'interno della facciata creo l'entità (di Entity Framework) da inserire all'interno del database. Instancia l'entità ... Faccio un 'nuovo' ... In questo modo l'entità non viene derisa ... quindi non posso testare l'inserto .. È come se avessi passato a Face l'entità derisoria, ma sicuramente questo è non è il modo corretto di risolvere il problema – Ciccio

0

sembra che sia necessario modificare leggermente il codice. Le cose nuove introducono le dipendenze hardcoded e le rendono non testabili, quindi cerca di astrarle. Forse puoi nascondere tutto ciò che ha a che fare con EF dietro un altro livello, quindi tutto ciò che devi fare è prendere in giro quel particolare layer e non toccare mai EF.

0

È possibile utilizzare questo framework open source per il test delle unità che è buono per entità finto quadro DbContext

https://effort.codeplex.com/

Prova questa vi aiuterà a deridere i vostri dati in modo efficiente.