Aprile 2017

Volume 32 Numero 4

Il presente articolo è stato tradotto automaticamente.

Concetti sui dati - Suggerimenti per compilare test con EF Core e il provider InMemory associato

Da Julie Lerman

Julie LermanDurante la creazione di test automatizzati con metodi che attivano l'interazione con il database, vi sono casi quando si desidera effettivamente vedere cosa accade nel database e altre volte quando l'interazione del database non è affatto rilevante per l'asserzione del test. Il nuovo provider di Entity Framework Core InMemory possono rivelarsi utili nel caso quest'ultimo. In questo articolo si verrà fornita un'introduzione a questo strumento utile e condivide alcuni trucchi e suggerimenti sulla creazione di test automatizzati con Entity Framework principale che ho scoperto durante l'apprendimento dell'uso manuale.

Nei casi in cui lo sforzo di database non è importante per il risultato del test, le chiamate al database possono gravare sulle prestazioni o provocare risultati non accurati. Ad esempio, la quantità di tempo impiegato per comunicare con il database, oppure eliminare e ricreare un database di test, può includere un massimo di test. Un altro problema è se si è verificato un problema con lo stesso database. Forse latenza di rete o un videogioco momentanea fa sì che il test non riesce solo perché il database è disponibile, non come risultato un errore nella logica del test sta tentando di asserzione.

Ci tempo abbiamo cercato modi per ridurre al minimo questi effetti collaterali. Oggetti fittizi e Framework di simulazione sono soluzioni comuni. Questi modelli consentono di creare rappresentazioni in memoria dell'archivio dati, ma è molto coinvolte nell'impostazione di backup dei dati in memoria e il comportamento. Un altro approccio consiste nell'utilizzare un database leggero per attività di test che di destinazione nell'ambiente di produzione, ad esempio un PostgreSQL o SQLite database, anziché, ad esempio, un database di SQL Server che utilizzare per l'archivio dati di produzione. Entity Framework (EF) ha sempre consentito per l'assegnazione di diversi database con un unico modello grazie a diversi provider disponibili. Tuttavia, possono provocare sulle differenze nelle funzionalità di database raggiungimento dei problemi in cui ciò non sempre funziona (ma è comunque una buona opzione per mantenere nella casella degli strumenti). In alternativa, è possibile utilizzare uno strumento esterno, ad esempio l'estensione open source di impegno (github.com/tamasflamich/effort), che magicamente fornisce una rappresentazione in memoria dell'archivio dati senza il programma di installazione necessari per oggetti fittizi o Framework di simulazione. IMPEGNO funziona con Entity Framework 4.1 tramite Entity Framework 6, ma non i Core di Entity Framework.

Esistono già un numero di provider di database per Entity Framework Core. Microsoft include i provider SQL Server e SQLite come parte della famiglia di EntityFrameworkCore APIs. Sono inoltre provider per SQLCE e PostgreSQL, rispettivamente mediante gli MVP Erik Eilskov Jensen e Shay Rojansky. E sono disponibili provider di terze parti disponibili in commercio. Ma Microsoft ha creato un altro provider, ovvero non di rendere persistente in un database, ma di mantenere temporaneamente in memoria. Si tratta del provider InMemory: Microsoft. EntityFrameworkCore.InMemory, è possibile utilizzare come un modo rapido per fornire un sostituto per un database effettivo in molti scenari di test.

La preparazione di DbContext per il Provider InMemory

Poiché talvolta DbContext verrà utilizzata per connettersi a un archivio dati true e talvolta al provider InMemory, si desidera impostarlo per la massima flessibilità per quanto riguarda i provider, anziché dipendenti da qualsiasi provider specifico.

Quando si crea un elemento DbContext di Entity Framework Core, è necessario includere DbContextOptions che specificano il provider da utilizzare e, se necessario, una stringa di connessione. UseSqlServer e UseSqlite, ad esempio, richiedono che si passa una stringa di connessione, e ogni provider consente di accedere al metodo di estensione pertinente. Ecco l'aspetto se eseguita direttamente nella classe DbContext OnConfiguring (metodo), in cui è leggendo una stringa di connessione da un file di configurazione dell'applicazione:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
  var settings = ConfigurationManager.ConnectionStrings;
  var connectionString = settings["productionDb"].ConnectionString;
  optionsBuilder.UseSqlServer(connectionString);
 }

Un modello più flessibile, tuttavia, consiste nel passare un oggetto DbContextOptions preconfigurato nel costruttore dell'oggetto DbContext:

public SamuraiContext(DbContextOptions<SamuraiContext> options)
    :base(options) { }

Entity Framework Core verrà passare tali opzioni preconfigurate in DbContext sottostante e ad applicarli.

Con questo costruttore sul posto, si disporrà di un modo per specificare diversi provider (e altre opzioni, ad esempio una stringa di connessione) in tempo reale dalla logica che utilizza il contesto.

Se si utilizza un tipo di contenitore Inversion of Control (IoC) nell'applicazione, ad esempio StructureMap (structuremap.github.io) o servizi integrati in ASP.NET di base, è possibile configurare il provider per il contesto nel codice in cui si configurano altri servizi di IoC a livello di applicazione. Di seguito è riportato un esempio che utilizza servizi di base di ASP.NET in un file di startup.cs tipico:

public void ConfigureServices(IServiceCollection services) {
  services.AddDbContext<SamuraiContext>(
    options => options.UseSqlServer(
      Configuration.GetConnectionString("productionDb")));
  services.AddMvc();
}

In questo caso, SamuraiContext è il nome della classe che eredita da DbContext. Utilizzo di SQL Server nuovo e ho archiviati la stringa di connessione nel file appsettings.json ASP.NET Core sotto il nome productionDb. Il servizio è stato configurato in modo che ogni volta che un costruttore di classe richiede un'istanza di SamuraiContext, il runtime deve non solo creare un'istanza SamuraiContext, ma deve passare tramite le opzioni con la stringa di connessione e provider indicata in questo metodo.

Quando questa applicazione ASP.NET di base vengono utilizzati my SamuraiContext, per impostazione predefinita, verrà ora farlo con SQL Server e la stringa di connessione. Ma grazie alla flessibilità ho creato nella classe SamuraiContext, è possibile anche creare test che utilizzano la stessa SamuraiContext ma passare in un oggetto DbContextOptions che specifica l'utilizzo del provider InMemory invece, o specifica le altre opzioni che sono rilevanti per un determinato test.

Nella sezione successiva verrà illustrato due test diversi che coinvolgono Core Entity Framework. Il primo, figura 1, è progettato per eseguire il test che si è verificato l'interazione con il database corretto. Questo significa che si desidera effettivamente il test di accedere al database, pertanto è necessario creare DbContextOptions per utilizzare il provider SQL Server, ma con una connessione di stringa che fa riferimento una versione di prova del database è possibile creare e rilasciare in tempo reale.

Figura 1 test da un Database inserire funziona come previsto

[TestMethod]
  public void CanInsertSamuraiIntoDatabase() {
    var optionsBuilder = new DbContextOptionsBuilder();
    optionsBuilder.UseSqlServer
      ("Server = (localdb)\\mssqllocaldb; Database =
        TestDb; Trusted_Connection = True; ");
    using (var context = new SamuraiContext(optionsBuilder.Options)) {
      context.Database.EnsureDeleted();
      context.Database.EnsureCreated();
      var samurai = new Samurai();
      context.Samurais.Add(samurai);
      var efDefaultId = samurai.Id;
      context.SaveChanges();
      Assert.AreNotEqual(efDefaultId, samurai.Id);
    }
  }

Usare i metodi EnsureDeleted ed EnsureCreated per visualizzare una versione completamente nuova del database per il test e funzioneranno anche se non si dispone delle migrazioni. In alternativa, è possibile utilizzare EnsureDeleted e migrazione per ricreare il database se si dispone di file di migrazione.

Successivamente, creare una nuova entità (samurai), indicare a Entity Framework per iniziare a tenerne traccia e quindi si noti che il valore della chiave temporaneo il provider SQL Server fornisce. Dopo aver chiamato il metodo SaveChanges, verificare che SQL Server sia applicato il proprio valore generati dal database per la chiave, assicurando me che questo oggetto è stato, infatti, inserito nel database correttamente.

Eliminazione e ricreazione di un database di SQL Server potrebbe influire sul tempo necessario per eseguire il test. È possibile utilizzare in questo caso SQLite per ottenere gli stessi risultati più rapidamente, assicurando che il test è comunque raggiungimento di un database effettivo. Si noti inoltre che, analogamente al provider di SQL Server, SQLite imposta un valore di chiave temporaneo anche quando si aggiunge un'entità al contesto.

Se si dispone di metodi che si utilizza Entity Framework Core, ma si desidera testarli senza raggiungere il database, si tratta in cui il provider di InMemory è pertanto utile. Ma tenere presente che InMemory non è un database e non emulare tutte le versioni di comportamento del database relazionale, ad esempio, l'integrità referenziale. Quando questo è importante per il test, è preferibile usare l'opzione SQLite o, come il nucleo di Entity Framework suggeriscono documenti, la modalità di SQLite in memoria come indicato al bit.ly/2l7M71p.

Ecco un metodo che ho creato in un'applicazione che esegue una query con Entity Framework Core e restituisce un elenco di oggetti KeyValuePair:

public List<KeyValuePair<int, string>> GetSamuraiReferenceList() {
  var samurais = _context.Samurais.OrderBy(s => s.Name)
    .Select(s => new {s.Id, s.Name})
    .ToDictionary(t => t.Id, t => t.Name).ToList();
  return samurais;
}

Si desidera verificare che il metodo restituisce effettivamente un elenco di KeyValuePair. Non è necessario che la query per dimostrare questo.

Di seguito è un test per eseguire questa operazione utilizzando il provider InMemory (che è già installato nel progetto di test):

[TestMethod]
  public void CanRetrieveListOfSamuraiValues() {
    _options = new DbContextOptionsBuilder<SamuraiContext>()
               .UseInMemoryDatabase().Options;
    var context = new SamuraiContext(_options);
    var repo = new DisconnectedData(context);
    Assert.IsInstanceOfType(repo.GetSamuraiReferenceList(),
                            typeof(List<KeyValuePair<int, string>>));
  }

Questo test non richiedono nemmeno che i dati di esempio per essere disponibile per la rappresentazione in memoria del database perché è sufficiente per restituire un elenco vuoto di KeyValuePairs. Quando si esegue il test, Entity Framework Core sarà che quando il GetSamuraiReferenceList esegue la query, il provider verrà allocare risorse di memoria per eseguire in relazione a Entity Framework. La query ha esito positivo e pertanto il test.

Che cosa accade se si desidera verificare che il numero corretto di risultati viene restituito? Ciò significa che sarà necessario fornire i dati per l'inizializzazione provider InMemory. Molto come un fake o simulazione, ciò richiede la creazione dei dati e nel caricarlo nell'archivio dati del provider. Quando si utilizza fake e simulazioni, potrebbe creare un oggetto elenco e compilare che, quindi eseguire una query in base all'elenco. Il provider InMemory si occupa del contenitore. È sufficiente utilizzare i comandi di Entity Framework per il popolamento preliminare. Il provider InMemory si occupa anche della maggior parte dell'overhead e altro codice che sono necessari quando si utilizzano dati falsi o oggetti fittizi.

Ad esempio, figura 2 viene illustrato un metodo utilizzato per inizializzare il provider InMemory prima i test di interagiscono con esso:

Figura 2 il Seeding di un Entity Framework di base InMemory Provider

private void SeedInMemoryStore() {
    using (var context = new SamuraiContext(_options)) {
      if (!context.Samurais.Any()) {
        context.Samurais.AddRange(
          new Samurai {
            Id = 1,
            Name = "Julie",
          },
          new Samurai {
            Id = 2,
            Name = "Giantpuppy",
        );
        context.SaveChanges();
      }
    }
  }

Se i dati in memoria sono vuoti, questo metodo aggiunge due nuove samurais e quindi chiama SaveChanges. A questo punto è pronto per essere utilizzato da un test.

Ma come archivio dati personali InMemory avrebbe dati in essa se è stato appena creata un'istanza del contesto? Il contesto non è l'archivio dati InMemory. Si tratta di nell'archivio dati in un oggetto elenco-contesto creerà in tempo reale, se necessario. Ma una volta che è stato creato, rimane in memoria per la durata dell'applicazione. Se si esegue un singolo metodo di test, non sarà possibile senza sorprese. Ma se si esegue un numero di metodi di test, ogni metodo di test verrà utilizzato lo stesso set di dati e non è consigliabile inserire al suo interno una seconda volta. È più necessario conoscere ciò che sarà in grado di descrivere dopo che viene visualizzato un po' più codice.

Questo test successivo è un po' complicato, ma è progettata per illustrare l'utilizzo di un archivio InMemory popolato. Il test di sapere che appena ho seeding la memoria con due samurais, chiama lo stesso metodo GetSamuraiReferenceList e afferma che esistono due elementi nell'elenco risultante:

[TestMethod]
  public void CanRetrieveAllSamuraiValuePairs() {
    var context = new SamuraiContext(_options);
    var repo = new DisconnectedData(context);
    Assert.AreEqual(2, repo.GetSamuraiReferenceList().Count);
  }

Si sarà notato che non chiamare il metodo di inizializzazione o di creare le opzioni. Ho spostata tale logica al costruttore della classe di test in modo non è necessario ripeterlo in test. Per l'ambito completo della classe viene dichiarata la variabile _options:

private DbContextOptions<SamuraiContext> _options;
  public TestDisconnectedData() {
    _options =
      new DbContextOptionsBuilder<SamuraiContext>().UseInMemoryDatabase().Options;
    SeedInMemoryStore();
  }

Ora che ho spostata il metodo di inizializzazione nel costruttore, si potrebbe pensare (come ho fatto) che verrà chiamato una sola volta. Ma che non è così. Non tutti sanno che viene raggiunto un costruttore di classe di test da ogni metodo di test che viene eseguito. In tutti honesty avevo dimenticato questo finché non ho notato che sono stati superati durante l'esecuzione, ma con esito negativo quando si è eseguito loro insieme. Che è stato prima aggiunto nel controllo per verificare se i dati samurai esistevano già in memoria. Ogni metodo che ha attivato il chiamata al metodo seed sarebbe il seeding della stessa raccolta. Questa situazione si verifica se che stavo chiamando il metodo di inizializzazione in ogni metodo di test o una sola volta nel costruttore. Il controllo per i dati preesistenti protegge me in entrambi i casi.

Esiste un modo più accattivante per evitare il problema di archivi dati in memoria in conflitto. InMemory consente di specificare un nome per il proprio archivio dati.

Se si desidera spostare nuovamente la creazione di DbContextOptions al metodo di test e per ogni metodo, specificando un nome univoco di un parametro di UseInMemory assicureranno che ogni metodo utilizza il proprio archivio dati.

È stato rielaborato la classe di test rimuovendo la variabile a livello di classe _options e il costruttore della classe. Al contrario, utilizzo un metodo per la creazione di opzioni per un archivio di dati denominato e seeding l'archivio dati specifico che accetta il nome desiderato come parametro:

private DbContextOptions<SamuraiContext> SetUpInMemory(string uniqueName) {
  var options = new DbContextOptionsBuilder<SamuraiContext>()
                    .UseInMemoryDatabase(uniqueName).Options;
  SeedInMemoryStore(options);
  return options;
}

Ho modificato la firma e la prima riga del SeedInMemoryStore per utilizzare le opzioni configurate per l'archivio dati univoci:

private void SeedInMemoryStore(DbContextOptions<SamuraiContext> options) {
  using (var context = new SamuraiContext(options)) {

E ogni metodo di test utilizza questo metodo con un nome univoco per creare un'istanza di DbContext. Di seguito è la rivista CanRetrieveAllSamuraiValuePairs. L'unica differenza è che si passa a questo punto il nuovo metodo SetUpInMemory insieme al nome dell'archivio dati univoco. Un modello utile consigliato dal team di Entity Framework consiste nell'utilizzare il nome del test come il nome della risorsa InMemory:

[TestMethod]
  public void CanRetrieveListOfSamuraiValues() {
  using (var context = 
      new SamuraiContext(SetUpInMemory("CanRetrieveListOfSamuraiValues"))) {
    var repo = new DisconnectedData(context);
    Assert.IsInstanceOfType(repo.GetSamuraiReferenceList(),
                            typeof(List<KeyValuePair<int, string>>));
   }
 }

Altri metodi di test nella classe test hanno i propri dati univoci archiviare nomi. E ora visualizzato sono disponibili modelli per l'utilizzo di un set univoco di dati, o condivisione di un set comune di dati tra i metodi di test. Quando i test scrive dati nell'archivio dati in memoria, i nomi univoci consentono di evitare gli effetti collaterali su altri test. Tenere presente che Entity Framework Core 2.0 richiedono sempre un nome da fornire, anziché il parametro facoltativo in Entity Framework Core 1.1.

Esiste un ultimo suggerimento che si desidera condividere sull'archivio dati InMemory. Scrittura sul primo test, ho fatto notare che provider di SQL Server e di SQLite inserire un valore temporaneo a proprietà di chiave del Samurai quando l'oggetto viene aggiunto al contesto. Che se si specifica il valore manualmente, il provider non sovrascritta i che ancora. Ma in entrambi i casi, poiché utilizzo il comportamento predefinito del database, il database verrà sovrascritto il valore con il proprio valore di chiave primaria generato. Con il provider InMemory, tuttavia, se si fornisce un valore della proprietà chiave, che sarà il valore che utilizza l'archivio dati. Se non viene specificato uno, il provider InMemory utilizza un generatore di chiavi sul lato client il cui valore viene utilizzato come il valore assegnato dell'archivio dati.

Gli esempi che ho utilizzato provengono dal mio Core Entity Framework: Raggiungere avviato corso Pluralsight (bit.ly/PS_EFCoreStart), dove per ulteriori informazioni su Entity Framework Core, nonché il test con Entity Framework Core. Nell'esempio di codice è inoltre incluso per il download di questo articolo.


Julie Lermanè direttore regionale Microsoft, Microsoft MVP, mentore team software e consulente che risiede del Vermont.  È possibile trovare le sue presentazioni su accesso ai dati e altri argomenti in gruppi di utenti e conferenze in tutto il mondo. Anna blog all'indirizzo thedatafarm.com ed è autore di "Programming Entity Framework" nonché Code First e un'edizione di DbContext, tutto da o ' Reilly Media. Seguirla su Twitter: @julielerman e vedere proprio corsi Pluralsight juliel.me/PS-video.

Grazie al seguente esperto tecnico Microsoft per la revisione dell'articolo: Rowan Miller