In che modo le persone test unitarie con Entity Framework 6, dovresti preoccuparti?


170

Ho appena iniziato con i test unitari e TDD in generale. Mi sono dilettato prima, ma ora sono deciso ad aggiungerlo al mio flusso di lavoro e scrivere software migliore.

Ho fatto una domanda ieri che includeva questo, ma sembra essere una domanda a sé stante. Mi sono seduto per iniziare a implementare una classe di servizio che userò per sottrarre la logica di business dai controller e mappare a modelli specifici e interazioni di dati usando EF6.

Il problema è che mi sono già bloccato perché non volevo estrarre EF in un repository (sarà ancora disponibile al di fuori dei servizi per query specifiche, ecc.) E vorrei testare i miei servizi (verrà utilizzato il contesto EF) .

Qui suppongo sia la domanda, c'è un motivo per farlo? In tal caso, come stanno facendo le persone allo stato brado alla luce delle astrazioni che perdono causate da IQueryable e dei molti grandi post di Ladislav Mrnka sull'argomento dei test unitari che non sono semplici a causa delle differenze tra i fornitori di Linq quando lavorano con un dispositivo in memoria implementazione come allegato a un database specifico.

Il codice che voglio testare sembra piuttosto semplice. (questo è solo un codice fittizio per cercare di capire cosa sto facendo, voglio guidare la creazione usando TDD)

Contesto

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

    }
}

Servizio

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

Attualmente sono nella mentalità di fare alcune cose:

  1. Deridere il contesto EF con qualcosa di simile a questo approccio - Deridere EF quando si testano le unità o si utilizza direttamente un framework beffardo sull'interfaccia come moq - sopportare il dolore che i test unitari possono superare ma non necessariamente lavorare da un capo all'altro e eseguirne il backup con i test di integrazione?
  2. Forse usando qualcosa come Effort per deridere EF - non l'ho mai usato e non sono sicuro se qualcun altro lo sta usando in natura?
  3. Non preoccuparti di provare qualcosa che semplicemente richiama EF - quindi essenzialmente i metodi di servizio che chiamano EF direttamente (getAll ecc.) Non sono test unitari ma solo test di integrazione?

Qualcuno là fuori lo sta davvero facendo senza Repo e avendo successo?


Ehi Modika, ci ho pensato di recente (a causa di questa domanda: stackoverflow.com/questions/25977388/… ) In esso provo a descrivere un po 'più formalmente come lavoro in questo momento, ma mi piacerebbe sentire come lo stai facendo.
samy

Ciao @samy, il modo in cui abbiamo deciso di farlo non è stato test unitario nulla che ha toccato direttamente EF. Le query sono state testate ma come test di integrazione, non test unitari. Prendere in giro EF sembra un po 'sporco, ma questo progetto è stato un po' scarso, quindi l'impatto sulle prestazioni di avere un sacco di test che colpiscono un database non era davvero un problema, quindi potremmo essere un po 'più pragmatici al riguardo. Non sono ancora sicuro al 100% di quale sia l'approccio migliore per essere completamente sincero con te, ad un certo punto colpirai EF (e il tuo DB) e i test delle unità non mi sembrano proprio qui.
Modika,

Risposte:


186

Questo è un argomento che mi interessa molto. Ci sono molti puristi che affermano che non dovresti testare tecnologie come EF e NHibernate. Hanno ragione, sono già sottoposti a test molto rigorosi e, come indicato in una risposta precedente, è spesso inutile dedicare una grande quantità di tempo a testare ciò che non si possiede.

Tuttavia, possiedi il database sottostante! È qui che secondo me questo approccio si interrompe, non è necessario verificare che EF / NH svolgano correttamente il proprio lavoro. È necessario verificare che le mappature / implementazioni funzionino con il database. Secondo me questa è una delle parti più importanti di un sistema che puoi testare.

A rigor di termini, tuttavia, ci stiamo spostando dal dominio dei test unitari ai test di integrazione, ma i principi rimangono gli stessi.

La prima cosa che devi fare è essere in grado di deridere il tuo DAL in modo che il tuo BLL possa essere testato indipendentemente da EF e SQL. Questi sono i test unitari. Successivamente devi progettare i tuoi test di integrazione per provare il tuo DAL, a mio avviso questi sono altrettanto importanti.

Ci sono un paio di cose da considerare:

  1. Il database deve essere in uno stato noto ad ogni test. La maggior parte dei sistemi utilizza un backup o crea script per questo.
  2. Ogni test deve essere ripetibile
  3. Ogni test deve essere atomico

Esistono due approcci principali per la configurazione del database, il primo è quello di eseguire uno script DB di creazione UnitTest. Ciò garantisce che il database di unit test sia sempre nello stesso stato all'inizio di ogni test (è possibile reimpostare questo o eseguire ciascun test in una transazione per garantire ciò).

L'altra opzione è quella che faccio, eseguo impostazioni specifiche per ogni singolo test. Credo che questo sia l'approccio migliore per due motivi principali:

  • Il tuo database è più semplice, non è necessario un intero schema per ogni test
  • Ogni test è più sicuro, se modifichi un valore nel tuo script di creazione non invalida decine di altri test.

Purtroppo il tuo compromesso qui è la velocità. Ci vuole tempo per eseguire tutti questi test, per eseguire tutti questi script di installazione / smontaggio.

Un ultimo punto, può essere molto difficile scrivere una quantità così grande di SQL per testare il tuo ORM. È qui che adotto un approccio molto brutto (i puristi qui non saranno d'accordo con me). Uso il mio ORM per creare il mio test! Invece di avere uno script separato per ogni test DAL nel mio sistema, ho una fase di configurazione del test che crea gli oggetti, li collega al contesto e li salva. Eseguo quindi il mio test.

Questa è lungi dall'essere la soluzione ideale, ma in pratica trovo che sia MOLTO più facile da gestire (specialmente quando hai diverse migliaia di test), altrimenti stai creando un numero enorme di script. Praticità sulla purezza.

Senza dubbio guarderò indietro a questa risposta tra qualche anno (mesi / giorni) e non sarò d'accordo con me stesso poiché i miei approcci sono cambiati, tuttavia questo è il mio approccio attuale.

Per provare a riassumere tutto ciò che ho detto sopra questo è il mio tipico test di integrazione DB:

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

La cosa fondamentale da notare qui è che le sessioni dei due loop sono completamente indipendenti. Nell'implementazione di RunTest è necessario assicurarsi che il contesto sia sottoposto a commit e distrutto e che i dati possano provenire dal database solo per la seconda parte.

Modifica 13/10/2014

Ho detto che probabilmente avrei rivisto questo modello nei prossimi mesi. Mentre sostengo ampiamente l'approccio che ho sostenuto sopra, ho leggermente aggiornato il mio meccanismo di test. Ora tendo a creare le entità in TestSetup e TestTearDown.

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

Quindi testare ogni proprietà singolarmente

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

Esistono diversi motivi per questo approccio:

  • Non ci sono chiamate al database aggiuntive (una configurazione, uno smontaggio)
  • I test sono molto più granulari, ogni test verifica una proprietà
  • La logica di Setup / TearDown viene rimossa dai metodi di test stessi

Ritengo che ciò renda la classe di test più semplice e i test più granulari (i singoli assert sono buoni )

Modifica il 03/05/2015

Un'altra revisione su questo approccio. Mentre le configurazioni a livello di classe sono molto utili per test come il caricamento delle proprietà, sono meno utili laddove sono richieste le diverse configurazioni. In questo caso, l'impostazione di una nuova classe per ciascun caso è eccessiva.

Per aiutare con questo, ora tendo ad avere due classi base SetupPerTeste SingleSetup. Queste due classi espongono il framework come richiesto.

Nel SingleSetupabbiamo un meccanismo molto simile come descritto nella mia prima modifica. Un esempio sarebbe

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

Tuttavia, i riferimenti che assicurano il caricamento solo delle entità corrette possono utilizzare un approccio SetupPerTest

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

In sintesi, entrambi gli approcci funzionano a seconda di ciò che si sta tentando di testare.


2
Ecco un approccio diverso ai test di integrazione. TL; DR: utilizzare l'applicazione stessa per impostare i dati di test, ripristinare una transazione per test.
Gert Arnold,

3
@Liath, ottima risposta. Hai confermato i miei sospetti sul test di EF. La mia domanda è questa; il tuo esempio è per un caso molto concreto, che va bene. Tuttavia, come hai notato, potrebbe essere necessario testare centinaia di entità. In linea con il principio DRY (Non ripetere te stesso) come ridimensionare la soluzione, senza ripetere lo stesso schema di codice di base ogni volta?
Jeffrey A. Gochin,

4
Non sono d'accordo con questo perché evita completamente il problema. Il test unitario riguarda il test della logica della funzione. Nell'esempio OP, la logica ha una dipendenza da un archivio dati. Hai ragione quando dici di non testare EF, ma non è questo il problema. Il problema è il test del codice in isolamento dall'archivio dati. Testare la tua mappatura è un argomento completamente diverso. Per verificare che la logica stia interagendo correttamente con i dati, è necessario essere in grado di controllare l'archivio.
Sinaesthetic

7
Nessuno è sul punto di stabilire se dovresti testare l'unità Entity Framework da solo. Quello che succede è che è necessario testare un metodo che fa alcune cose e capita anche di effettuare una chiamata EF al database. L'obiettivo è prendere in giro EF in modo da poter testare questo metodo senza richiedere un database sul server di compilazione.
The Muffin Man

4
Mi piace molto il viaggio. Grazie per l'aggiunta di modifiche nel tempo: è come leggere il controllo del codice sorgente e capire come si è evoluto il tuo modo di pensare. Apprezzo molto anche la distinzione funzionale (con EF) e unitaria (EF beffata).
Tom Leys,

21

Sforzo Esperienza Feedback qui

Dopo molte letture ho usato Effort nei miei test: durante i test il contesto è costruito da una fabbrica che restituisce una versione in memoria, che mi permette di testare ogni volta contro una lavagna vuota. Al di fuori dei test, la fabbrica viene risolta in una che restituisce l'intero contesto.

Tuttavia, ho la sensazione che i test su una finta funzionalità del database tendano a trascinare i test verso il basso; ti rendi conto che devi occuparti di impostare un sacco di dipendenze per testare una parte del sistema. Inoltre, si tende a spostarsi verso l'organizzazione di test che potrebbero non essere correlati, solo perché esiste un solo oggetto enorme che gestisce tutto. Se non presti attenzione, potresti trovarti a fare test di integrazione anziché test di unità

Avrei preferito testare qualcosa di più astratto piuttosto che un enorme DBContext, ma non sono riuscito a trovare il punto debole tra test significativi e test bare-bone. Gesso fino alla mia inesperienza.

Quindi trovo lo sforzo interessante; se hai bisogno di colpire il terreno correndo è un buon strumento per iniziare rapidamente e ottenere risultati. Tuttavia, penso che qualcosa di un po 'più elegante e astratto dovrebbe essere il passo successivo ed è quello che esaminerò in seguito. Favorire questo post per vedere dove va dopo :)

Modifica per aggiungere : lo sforzo impiega un po 'di tempo per riscaldarsi, quindi stai guardando a ca. 5 secondi all'avvio del test. Questo potrebbe essere un problema per te se hai bisogno che la tua suite di test sia molto efficiente.


Modificato per chiarimenti:

Ho usato Effort per testare un'app di webservice. Ogni messaggio M che entra viene instradato a una IHandlerOf<M>via Windsor. Castle.Windsor risolve il IHandlerOf<M>problema che rimuove le dipendenze del componente. Una di queste dipendenze è la DataContextFactory, che consente al gestore di richiedere la fabbrica

Nei miei test ho istanziato direttamente il componente IHandlerOf, deridendo tutti i sottocomponenti del SUT e gestendo lo sforzo avvolto DataContextFactorysul gestore.

Significa che non collaudo l'unità in senso stretto, poiché il DB è colpito dai miei test. Tuttavia, come ho detto sopra, mi ha permesso di colpire il terreno correndo e ho potuto testare rapidamente alcuni punti dell'applicazione


Grazie per l'input, quello che posso fare perché devo far funzionare questo progetto in quanto è un lavoro che paga in buona fede è iniziare con alcuni repository e vedere come ottengo, ma lo sforzo è molto interessante. Per interesse a quale livello hai utilizzato le tue applicazioni?
Modika,

2
solo se Effort avesse supportato correttamente le transazioni
Sedat Kapanoglu,

e la fatica ha un bug per le stringhe con il caricatore CSV, quando usiamo '' invece di null nelle stringhe.
Sam,

13

Se si desidera un codice di unit test, è necessario isolare il codice che si desidera testare (in questo caso il proprio servizio) da risorse esterne (ad esempio database). Probabilmente potresti farlo con una sorta di provider EF in memoria , tuttavia un modo molto più comune è quello di sottrarre l'implementazione EF, ad esempio con una sorta di modello di repository. Senza questo isolamento tutti i test che scrivi saranno test di integrazione, non test unitari.

Per quanto riguarda il test del codice EF - scrivo test di integrazione automatizzata per i miei repository che scrivono varie righe nel database durante la loro inizializzazione, quindi chiamo le implementazioni del mio repository per assicurarmi che si comportino come previsto (ad es. Assicurandomi che i risultati vengano filtrati correttamente, oppure che sono ordinati nell'ordine corretto).

Si tratta di test di integrazione non di test unitari, poiché i test si basano sulla presenza di una connessione al database e sul fatto che nel database di destinazione sia già installato lo schema aggiornato più recente.


Grazie @justin, conosco il modello del repository, ma leggere cose come ayende.com/blog/4784/… e lostechies.com/jimmybogard/2009/09/11/wither-the-repository tra gli altri mi hanno fatto pensare che io don Non voglio questo livello di astrazione, ma poi di nuovo parlano anche di un approccio Query che diventa molto confuso.
Modika,

7
@Modika Ayende ha scelto di criticare una scarsa implementazione del modello di repository e, di conseguenza, ha ragione al 100%: è troppo ingegnerizzato e non offre alcun vantaggio. Una buona implementazione isola le parti testabili dall'unità del tuo codice dall'implementazione DAL. L'uso di NHibernate e EF rende il codice direttamente difficile (se non impossibile) da testare l'unità e porta a una base di codice monolitica rigida. Sono ancora un po 'scettico sul modello di repository, tuttavia sono convinto al 100% che sia necessario isolare l'implementazione DAL in qualche modo e il repository è la cosa migliore che ho trovato finora.
Giustino,

2
@Modika Leggi di nuovo il secondo articolo. "Non voglio questo livello di astrazione" non è quello che dice. Inoltre, leggi il modello di repository originale da Fowler ( martinfowler.com/eaaCatalog/repository.html ) o DDD ( dddcommunity.org/resources/ddd_terms ). Non credere agli oppositori senza comprendere appieno il concetto originale. Ciò che criticano veramente è un recente uso improprio del modello, non del modello stesso (anche se probabilmente non lo sanno).
guillaume31,

1
@ guillaume31 non sono contrario al modello di repository (lo capisco) sto semplicemente cercando di capire se ne ho bisogno per astrarre ciò che è già un'astrazione a quel livello, e se posso ometterlo e testare EF direttamente deridendolo e usarlo nei miei test a un livello più alto nella mia applicazione. Inoltre, se non utilizzo un repository, ottengo il vantaggio del set di funzionalità estese EF, con un repository potrebbe non esserlo.
Modika,

Dopo aver isolato il DAL con un repository, ho bisogno di "deridere" il database (EF). Finora deridere il contesto e varie estensioni asincrone (ToListAsync (), FirstOrDefaultAsync (), ecc.) Mi hanno causato frustrazione.
Kevin Burton,

9

Quindi, ecco il punto, Entity Framework è un'implementazione, quindi nonostante il fatto che astragga la complessità dell'interazione del database, l'interazione diretta è ancora un accoppiamento stretto ed è per questo che confondere i test.

Il test unitario consiste nel testare la logica di una funzione e ciascuno dei suoi potenziali risultati in isolamento da eventuali dipendenze esterne, che in questo caso è l'archivio dati. Per fare ciò, devi essere in grado di controllare il comportamento dell'archivio dati. Ad esempio, se si desidera affermare che la funzione restituisce false se l'utente recuperato non soddisfa alcuni criteri, l'archivio dati [deriso] deve essere configurato in modo da restituire sempre un utente che non soddisfa i criteri e viceversa viceversa per l'affermazione opposta.

Detto questo, e accettando il fatto che EF sia un'implementazione, probabilmente favorirei l'idea di astrarre un repository. Ti sembra un po 'ridondante? Non lo è, perché stai risolvendo un problema che sta isolando il tuo codice dall'implementazione dei dati.

In DDD, i repository restituiscono sempre radici aggregate, non DAO. In questo modo, il consumatore del repository non deve mai conoscere l'implementazione dei dati (come non dovrebbe) e possiamo usarlo come esempio di come risolvere questo problema. In questo caso, l'oggetto generato da EF è un DAO e, come tale, dovrebbe essere nascosto dall'applicazione. Questo è un altro vantaggio del repository che hai definito. È possibile definire un oggetto business come tipo di ritorno anziché l'oggetto EF. Ora ciò che il repository fa è nascondere le chiamate a EF e mappare la risposta EF a quell'oggetto business definito nella firma del repository. Ora puoi usare quel repository al posto della dipendenza DbContext che inietti nelle tue classi e, di conseguenza, ora puoi deridere quell'interfaccia per darti il ​​controllo di cui hai bisogno per testare il tuo codice in isolamento.

È un po 'più di lavoro e molti si sforzano il naso, ma risolve un vero problema. C'è un fornitore in memoria che è stato menzionato in una risposta diversa che potrebbe essere un'opzione (non l'ho provato), e la sua stessa esistenza è la prova della necessità della pratica.

Non sono completamente d'accordo con la risposta migliore perché evita il vero problema che sta isolando il tuo codice e quindi va avanti tangendo sul test della tua mappatura. Se vuoi, prova la tua mappatura, ma affronta qui il problema reale e ottieni una copertura del codice reale.


8

Non vorrei un codice di test unitario che non possiedo. Cosa stai testando qui, che il compilatore MSFT funziona?

Detto questo, per rendere questo codice testabile, devi quasi rendere il tuo livello di accesso ai dati separato dal tuo codice di logica aziendale. Quello che faccio è prendere tutte le mie cose EF e metterle in una (o multipla) classe DAO o DAL che ha anche un'interfaccia corrispondente. Quindi scrivo il mio servizio in cui verrà iniettato l'oggetto DAO o DAL come dipendenza (iniezione del costruttore preferibilmente) indicata come interfaccia. Ora la parte che deve essere testata (il tuo codice) può essere facilmente testata deridendo l'interfaccia DAO e inserendola nell'istanza di servizio all'interno del test dell'unità.

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

    ...
}

Considererei i Livelli di accesso ai dati in tempo reale come parte dei test di integrazione, non dei test unitari. Ho visto ragazzi fare delle verifiche su quanti viaggi nel database ibernato fa prima, ma erano in un progetto che includeva miliardi di record nel loro archivio dati e quei viaggi extra contavano davvero.


1
Grazie per la risposta, ma quale sarebbe la differenza di questo dire un repository in cui si nascondono gli interni di EF a questo livello? Non voglio davvero astrarre EF, anche se potrei ancora farlo con l'interfaccia IContext? Sono nuovo di questo, sii gentile :)
Modika,

3
Anche @Modika A Repo va bene. Qualunque modello tu voglia. "Non voglio davvero astrarre EF" Vuoi un codice testabile o no?
Jonathan Henson,

1
@Modika il mio punto è che non avrai NESSUN codice testabile se non separi le tue preoccupazioni. L'accesso ai dati e la logica di business DEVONO essere in livelli separati per eseguire buoni test di manutenzione.
Jonathan Henson,

2
non mi è sembrato necessario avvolgere EF in un'astrazione del repository poiché essenzialmente gli IDbSet sono repository e il contesto UOW, aggiornerò un po 'la mia domanda in quanto potrebbe essere fuorviante. Il problema si presenta con qualsiasi astrazione e il punto principale è quello che sto testando esattamente perché le mie queiries non funzioneranno negli stessi limiti (linq-to-entity vs linq-to-objects) quindi se sto solo testando che il mio servizio fa un la chiamata sembra un po 'dispendiosa o sto bene qui?
Modika,

1
, Sebbene io sia d'accordo con i tuoi punti generali, DbContext è un'unità di lavoro e IDbSet sono sicuramente alcuni per l'implementazione del repository, e non sono il solo a pensarlo. Posso deridere EF, e ad un certo livello dovrò eseguire test di integrazione, è davvero importante se lo faccio in un repository o più in un servizio? Essere strettamente accoppiati a un DB non è davvero un problema, sono sicuro che accadrà, ma non ho intenzione di pianificare qualcosa che potrebbe non accadere.
Modika,

8

Mi sono armeggiato qualche volta per raggiungere queste considerazioni:

1- Se la mia applicazione accede al database, perché il test non dovrebbe? E se ci fosse qualcosa di sbagliato nell'accesso ai dati? I test devono saperlo in anticipo e avvisarmi del problema.

2- Il modello di repository è piuttosto difficile e richiede tempo.

Quindi ho escogitato questo approccio, che non penso sia il migliore, ma ho soddisfatto le mie aspettative:

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

Per farlo è necessario:

1- Installare EntityFramework nel progetto di test. 2- Inserire la stringa di connessione nel file app.config di Test Project. 3- Fare riferimento al dll System.Transactions in Test Project.

L'unico effetto collaterale è che il seed dell'identità aumenterà quando si tenta di inserire, anche quando la transazione viene interrotta. Ma poiché i test vengono eseguiti su un database di sviluppo, questo non dovrebbe essere un problema.

Codice di esempio:

[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
In realtà, questa soluzione mi piace molto. Scenari di test super semplici da implementare e più realistici. Grazie!
slopapa,

1
con EF 6, useresti DbContext.Database.BeginTransaction, vero?
SwissCoder,

5

In breve, direi di no, non vale la pena spremere il succo per testare un metodo di servizio con una sola riga che recupera i dati del modello. Nella mia esperienza, le persone che non conoscono TDD vogliono provare assolutamente tutto. La vecchia castagna di astrarre una facciata su un framework di terze parti solo così puoi creare una finta API di quei framework con cui bastardare / estendere in modo da poter iniettare dati fittizi ha poco valore nella mia mente. Ognuno ha una visione diversa di quanto sia meglio il test unitario. Tendo ad essere più pragmatico in questi giorni e mi chiedo se il mio test sta davvero aggiungendo valore al prodotto finale ea quale costo.


1
Sì al pragmatismo. Continuo a sostenere che la qualità dei test unitari è inferiore alla qualità del codice originale. Naturalmente c'è valore nell'uso di TDD per migliorare la tua pratica di codifica e anche per migliorare la manutenibilità, ma TDD può avere un valore decrescente. Eseguiamo tutti i nostri test sul database, perché ci dà la sicurezza che il nostro utilizzo di EF e delle tabelle stesse sia corretto. L'esecuzione dei test richiede più tempo, ma sono più affidabili.
Savage

3

Voglio condividere un approccio commentato e brevemente discusso, ma mostrare un esempio reale che sto attualmente utilizzando per aiutare a testare i servizi basati su EF.

Innanzitutto, mi piacerebbe usare il provider in-memory di EF Core, ma questo riguarda EF 6. Inoltre, per altri sistemi di archiviazione come RavenDB, sarei anche un fautore dei test tramite il provider di database in-memory. Ancora una volta, questo è specificamente per aiutare a testare il codice basato su EF senza molta cerimonia .

Ecco gli obiettivi che ho avuto quando ho elaborato uno schema:

  • Deve essere semplice da comprendere per gli altri sviluppatori del team
  • Deve isolare il codice EF al livello più basso possibile
  • Non deve comportare la creazione di strane interfacce multi-responsabilità (come un modello di repository "generico" o "tipico")
  • Deve essere facile da configurare e configurare in un unit test

Concordo con le dichiarazioni precedenti sul fatto che EF sia ancora un dettaglio di implementazione ed è giusto avere la sensazione di doverlo astrarre per fare un "puro" test unitario. Sono anche d'accordo che idealmente, vorrei assicurarmi che il codice EF stesso funzioni, ma ciò implica un database sandbox, un provider in memoria, ecc. Il mio approccio risolve entrambi i problemi: puoi testare l'unità in modo sicuro codice dipendente da EF e creare test di integrazione per testare specificamente il tuo codice EF.

Il modo in cui l'ho raggiunto è stato semplicemente incapsulando il codice EF in classi Query e Command dedicate. L'idea è semplice: basta racchiudere qualsiasi codice EF in una classe e dipendere da un'interfaccia nelle classi che l'avrebbe usato originariamente. Il problema principale che dovevo risolvere era quello di evitare di aggiungere numerose dipendenze alle classi e di impostare molto codice nei miei test.

È qui che entra in gioco una libreria utile e semplice: Mediatr . Consente una semplice messaggistica in-process e lo fa disaccoppiando le "richieste" dai gestori che implementano il codice. Ciò ha un ulteriore vantaggio nel disaccoppiare il "cosa" dal "come". Ad esempio, incapsulando il codice EF in piccoli blocchi consente di sostituire le implementazioni con un altro provider o un meccanismo totalmente diverso, poiché tutto ciò che si sta facendo è inviare una richiesta per eseguire un'azione.

Utilizzando l'iniezione di dipendenza (con o senza un framework - la tua preferenza), possiamo facilmente deridere il mediatore e controllare i meccanismi di richiesta / risposta per abilitare il test EF del codice EF.

Innanzitutto, supponiamo di avere un servizio con una logica aziendale che dobbiamo testare:

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

Inizi a vedere i vantaggi di questo approccio? Non solo tu incapsulando esplicitamente tutto il codice relativo a EF in classi descrittive, ma stai consentendo l'estensibilità rimuovendo la preoccupazione di implementazione di "come" viene gestita questa richiesta - a questa classe non importa se gli oggetti rilevanti provengono da EF, MongoDB, o un file di testo.

Ora per la richiesta e il gestore, tramite 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();
  }
}

Come puoi vedere, l'astrazione è semplice e incapsulata. E 'anche assolutamente verificabile , perché in un test di integrazione, si potrebbe testare questa classe singolarmente - non ci sono problemi di business mescolati qui.

A cosa assomiglia un test unitario del nostro servizio di funzionalità? È molto semplice. In questo caso, sto usando Moq per fare beffe (usa qualunque cosa ti renda felice):

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

}

Puoi vedere tutto ciò di cui abbiamo bisogno è una singola configurazione e non abbiamo nemmeno bisogno di configurare nulla in più - è un test di unità molto semplice. Cerchiamo di essere chiari: questo è assolutamente possibile fare a meno di qualcosa come Mediatr (si implementerebbe semplicemente un'interfaccia e la deriderebbe per i test, ad esempio IGetRelevantDbObjectsQuery), ma in pratica per una base di codice di grandi dimensioni con molte funzionalità e query / comandi, adoro l'incapsulamento e supporto DI innato offre Mediatr.

Se ti stai chiedendo come organizzo queste lezioni, è abbastanza semplice:

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

L'organizzazione per sezioni di funzioni è accanto al punto, ma ciò mantiene tutto il codice rilevante / dipendente insieme e facilmente individuabile. Soprattutto, separo le query rispetto ai comandi, seguendo il principio di separazione comando / query .

Questo soddisfa tutti i miei criteri: è a bassa cerimonia, è facile da capire e ci sono vantaggi extra nascosti. Ad esempio, come gestite il salvataggio delle modifiche? Ora puoi semplificare il tuo Db Context usando un'interfaccia di ruolo (IUnitOfWork.SaveChangesAsync()) e chiamate simulate all'interfaccia a ruolo singolo o potresti incapsulare il commit / il rollback all'interno dei tuoi RequestHandlers - tuttavia preferisci farlo dipende da te, purché sia ​​mantenibile. Ad esempio, sono stato tentato di creare una singola richiesta / gestore generico in cui avresti semplicemente passato un oggetto EF e lo avrebbe salvato / aggiornato / rimosso - ma devi chiedere qual è la tua intenzione e ricordare che se volevi scambiare il gestore con un altro provider / implementazione di archiviazione, probabilmente dovresti creare comandi / query espliciti che rappresentano ciò che intendi fare. Più spesso, un singolo servizio o funzionalità avrà bisogno di qualcosa di specifico: non creare cose generiche prima di averne bisogno.

Ci sono ovviamente avvertimenti a questo modello - si può andare troppo lontano con un semplice meccanismo pub / sub. Ho limitato la mia implementazione solo all'astrazione del codice relativo a EF, ma gli sviluppatori avventurosi potrebbero iniziare a utilizzare MediatR per esagerare e dimensionare il messaggio di tutto - qualcosa che dovrebbe catturare buone pratiche di revisione del codice e revisioni tra pari. Questo è un problema di processo, non un problema con MediatR, quindi sii consapevole di come stai usando questo schema.

Volevi un esempio concreto di come le persone stanno testando / deridendo le unità EF e questo è un approccio che sta funzionando con successo per noi nel nostro progetto - e il team è estremamente contento di quanto sia facile da adottare. Spero che aiuti! Come per tutto ciò che riguarda la programmazione, esistono diversi approcci e tutto dipende da ciò che si desidera ottenere. Apprezzo semplicità, facilità d'uso, manutenibilità e rilevabilità e questa soluzione soddisfa tutte queste esigenze.


Grazie per la risposta, è un'ottima descrizione del modello QueryObject usando un mediatore e qualcosa che sto iniziando a spingere anche nei miei progetti. Potrei dover aggiornare la domanda, ma non sto più testando l'unità EF, le astrazioni sono troppo impercettibili (SqlLite potrebbe anche essere ok), quindi ho solo test di integrazione delle mie cose che interrogano il database e le regole di business unit test e altre logiche.
Modika,

3

C'è Effort che è un provider di database del framework di entità in memoria. In realtà non l'ho provato ... Haa appena notato questo è stato menzionato nella domanda!

In alternativa, è possibile passare a EntityFrameworkCore che ha un provider di database in memoria integrato.

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

https://github.com/tamasflamich/effort

Ho usato una fabbrica per ottenere un contesto, quindi posso creare il contesto vicino al suo utilizzo. Questo sembra funzionare localmente in Visual Studio ma non sul mio server di build TeamCity, non so ancora perché.

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

Ciao andrew, il problema non è mai stato ottenere il contesto, puoi fabbricare il contesto che è quello che stavamo facendo, astrarre il contesto e farlo costruire dalla fabbrica. Il problema maggiore era la coerenza di ciò che era in memoria rispetto a ciò che fa Linq4Entities, non sono gli stessi che possono portare a test fuorvianti. Al momento, eseguiamo solo analisi del database di test di integrazione, potrebbe non essere il processo migliore per tutti quelli che la pensano.
Modika,

Questo aiuto Moq funziona ( codeproject.com/Tips/1045590/… ) se hai un contesto da deridere. Se il backup del contesto deriso con un elenco non si comporterà come un contesto supportato da un database sql.
Andrew Pate il

2

Mi piace separare i miei filtri da altre parti del codice e testarli come ho descritto sul mio blog qui http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Detto questo, la logica del filtro in fase di test non è identica alla logica del filtro eseguita quando il programma viene eseguito a causa della traduzione tra l'espressione LINQ e il linguaggio di query sottostante, come T-SQL. Tuttavia, ciò mi consente di convalidare la logica del filtro. Non mi preoccupo troppo delle traduzioni che accadono e di cose come la distinzione tra maiuscole e minuscole e la gestione nulla fino a quando non collaudo l'integrazione tra i livelli.


0

È importante testare ciò che ti aspetti che faccia il framework di entità (vale a dire convalidare le tue aspettative). Un modo per fare ciò che ho usato con successo è usare moq come mostrato in questo esempio (a lungo per copiare in questa risposta):

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

Tuttavia fai attenzione ... Non è garantito che un contesto SQL restituisca le cose in un ordine specifico a meno che tu non abbia un "OrderBy" appropriato nella tua query linq, quindi è possibile scrivere cose che passano quando esegui il test usando un elenco in memoria ( linq-to-entity) ma fallisce nel tuo ambiente uat / live quando (linq-to-sql) viene utilizzato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.