Test senza il sistema di database di produzione

In questa pagina vengono illustrate le tecniche per la scrittura di test automatizzati che non comportano il sistema di database in cui viene eseguita l'applicazione nell'ambiente di produzione, scambiando il database con un doppio test. Esistono vari tipi di test double e approcci per eseguire questa operazione ed è consigliabile leggere attentamente Scelta di una strategia di test per comprendere appieno le diverse opzioni. Infine, è anche possibile eseguire test sul sistema di database di produzione; questo argomento è descritto in Test sul sistema di database di produzione.

Suggerimento

Questa pagina illustra le tecniche xUnit , ma esistono concetti simili in altri framework di test, tra cui NUnit.

Schema Repository

Se si è deciso di scrivere test senza coinvolgere il sistema di database di produzione, la tecnica consigliata per farlo è il modello di repository; per altre informazioni su questo argomento, vedere questa sezione. Il primo passaggio dell'implementazione del modello di repository consiste nell'estrarre le query LINQ di EF Core in un livello separato, che verrà successivamente stub o fittizio. Ecco un esempio di interfaccia del repository per il sistema di blogging:

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

... ed ecco un'implementazione di esempio parziale per l'uso in produzione:

public class BloggingRepository : IBloggingRepository
{
    private readonly BloggingContext _context;

    public BloggingRepository(BloggingContext context)
        => _context = context;

    public Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

Non c'è molto da fare: il repository esegue semplicemente il wrapping di un contesto di EF Core ed espone metodi che eseguono le query e gli aggiornamenti del database. Un punto chiave da notare è che il GetAllBlogs metodo restituisce IEnumerable<Blog>e non IQueryable<Blog>. La restituzione di quest'ultimo significa che gli operatori di query possono ancora essere composti sul risultato, richiedendo che EF Core sia ancora coinvolto nella traduzione della query; ciò sconfigge lo scopo di avere un repository in primo luogo. IEnumerable<Blog> consente di eseguire facilmente lo stub o simulare ciò che il repository restituisce.

Per un'applicazione ASP.NET Core, è necessario registrare il repository come servizio nell'inserimento delle dipendenze aggiungendo quanto segue all'applicazione ConfigureServices:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Infine, i controller vengono inseriti con il servizio repository invece del contesto di EF Core ed eseguono metodi su di esso:

private readonly IBloggingRepository _repository;

public BloggingControllerWithRepository(IBloggingRepository repository)
    => _repository = repository;

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(name);

A questo punto, l'applicazione viene progettata in base al modello di repository: l'unico punto di contatto con il livello di accesso ai dati , EF Core, è ora tramite il livello del repository, che funge da mediatore tra il codice dell'applicazione e le query di database effettive. I test possono ora essere scritti semplicemente stubando il repository o simulandolo con la libreria di simulazione preferita. Di seguito è riportato un esempio di test basato su simulazione che usa la libreria Moq più diffusa:

[Fact]
public void GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByName("Blog2"))
        .Returns(new Blog { Name = "Blog2", Url = "http://blog2.com" });

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

    // Act
    var blog = controller.GetBlog("Blog2");

    // Assert
    repositoryMock.Verify(r => r.GetBlogByName("Blog2"));
    Assert.Equal("http://blog2.com", blog.Url);
}

Il codice di esempio completo può essere visualizzato qui.

SQLite in memoria

SQLite può essere facilmente configurato come provider EF Core per il gruppo di test invece del sistema di database di produzione (ad esempio SQL Server); Per informazioni dettagliate, consultare la documentazione del provider SQLite. Tuttavia, in genere è consigliabile usare la funzionalità di database in memoria di SQLite durante i test, poiché offre un facile isolamento tra i test e non richiede la gestione dei file SQLite effettivi.

Per usare SQLite in memoria, è importante comprendere che viene creato un nuovo database ogni volta che viene aperta una connessione di basso livello e che viene eliminata quando tale connessione viene chiusa. Nell'utilizzo DbContext normale, EF Core apre e chiude le connessioni di database in base alle esigenze, ogni volta che viene eseguita una query, per evitare di mantenere la connessione per tempi inutilmente lunghi. Tuttavia, con SQLite in memoria questo comporta la reimpostazione del database ogni volta; quindi, come soluzione alternativa, si apre la connessione prima di passarla a EF Core e si prevede che venga chiusa solo al termine del test:

    public SqliteInMemoryBloggingControllerTest()
    {
        // Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
        // at the end of the test (see Dispose below).
        _connection = new SqliteConnection("Filename=:memory:");
        _connection.Open();

        // These options will be used by the context instances in this test suite, including the connection opened above.
        _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlite(_connection)
            .Options;

        // Create the schema and seed some data
        using var context = new BloggingContext(_contextOptions);

        if (context.Database.EnsureCreated())
        {
            using var viewCommand = context.Database.GetDbConnection().CreateCommand();
            viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
            viewCommand.ExecuteNonQuery();
        }

        context.AddRange(
            new Blog { Name = "Blog1", Url = "http://blog1.com" },
            new Blog { Name = "Blog2", Url = "http://blog2.com" });
        context.SaveChanges();
    }

    BloggingContext CreateContext() => new BloggingContext(_contextOptions);

    public void Dispose() => _connection.Dispose();

I test possono ora chiamare CreateContext, che restituisce un contesto usando la connessione configurata nel costruttore, assicurandosi di disporre di un database pulito con i dati di inizializzazione.

Il codice di esempio completo per i test in memoria di SQLite può essere visualizzato qui.

Provider in memoria

Come illustrato nella pagina di panoramica dei test, l'uso del provider in memoria per i test è fortemente sconsigliato; prendere in considerazione l'uso di SQLite o l'implementazione del modello di repository. Se si è deciso di usare in memoria, di seguito è riportato un tipico costruttore della classe di test che configura e inizialia un nuovo database in memoria prima di ogni test:

public InMemoryBloggingControllerTest()
{
    _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
        .UseInMemoryDatabase("BloggingControllerTest")
        .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
        .Options;

    using var context = new BloggingContext(_contextOptions);

    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    context.AddRange(
        new Blog { Name = "Blog1", Url = "http://blog1.com" },
        new Blog { Name = "Blog2", Url = "http://blog2.com" });

    context.SaveChanges();
}

Il codice di esempio completo per i test in memoria può essere visualizzato qui.

Denominazione del database in memoria

I database in memoria sono identificati da un nome di stringa semplice ed è possibile connettersi più volte allo stesso database specificando lo stesso nome(questo è il motivo per cui l'esempio precedente deve chiamare EnsureDeleted prima di ogni test). Si noti tuttavia che i database in memoria sono radicati nel provider di servizi interno del contesto; mentre nella maggior parte dei casi i contesti condividono lo stesso provider di servizi, la configurazione di contesti con opzioni diverse può attivare l'uso di un nuovo provider di servizi interno. In questo caso, passare in modo esplicito la stessa istanza di InMemoryDatabaseRoot a UseInMemoryDatabase per tutti i contesti che devono condividere i database in memoria (in genere viene eseguito con un campo statico InMemoryDatabaseRoot ).

Transazioni

Si noti che, per impostazione predefinita, se viene avviata una transazione, il provider in memoria genererà un'eccezione perché le transazioni non sono supportate. È possibile che le transazioni vengano ignorate in modo invisibile all'utente configurando EF Core per ignorare InMemoryEventId.TransactionIgnoredWarning come nell'esempio precedente. Tuttavia, se il codice si basa effettivamente sulla semantica transazionale, ad esempio dipende dal rollback delle modifiche, il test non funzionerà.

Viste

Il provider in memoria consente la definizione delle viste tramite query LINQ, usando ToInMemoryQuery:

modelBuilder.Entity<UrlResource>()
    .ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));