Udostępnij za pośrednictwem


Testowanie bez produkcyjnego systemu bazy danych

Na tej stronie omówiono techniki pisania testów automatycznych, które nie korzystają z systemu bazy danych, na którym aplikacja działa w środowisku produkcyjnym, poprzez zastąpienie bazy danych dublikatem testowym . Istnieją różne typy dublerów testowych oraz podejścia do tego celu i zaleca się dokładne przeczytanie Wybieranie strategii testowania, aby w pełni zrozumieć różne opcje. Na koniec możliwe jest również przetestowanie systemu produkcyjnej bazy danych; opisano to w Testowanie na systemie produkcyjnej bazy danych.

Napiwek

Na tej stronie przedstawiono techniki xUnit, ale podobne pojęcia istnieją w innych frameworkach testowych, w tym NUnit.

Wzorzec repozytorium

Jeśli zdecydowałeś się pisać testy bez wykorzystania produkcyjnego systemu bazy danych, zalecaną techniką jest wzorzec repozytorium; aby uzyskać więcej informacji na ten temat, zobacz tej sekcji. Pierwszym krokiem wdrażania wzorca repozytorium jest wyodrębnienie zapytań LINQ platformy EF Core do oddzielnej warstwy, którą później będziemy mogli zastąpić atrapami lub symulacjami. Oto przykład interfejsu repozytorium dla naszego systemu blogowania:

public interface IBloggingRepository
{
    Task<Blog> GetBlogByNameAsync(string name);

    IAsyncEnumerable<Blog> GetAllBlogsAsync();

    void AddBlog(Blog blog);

    Task SaveChangesAsync();
}

... a oto częściowa implementacja przykładowa do użycia w środowisku produkcyjnym:

public class BloggingRepository : IBloggingRepository
{
    private readonly BloggingContext _context;

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

    public async Task<Blog> GetBlogByNameAsync(string name)
        => await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);

    // Other code...
}

To nic skomplikowanego: repozytorium po prostu opakowuje kontekst EF Core i udostępnia metody, które wykonują zapytania i aktualizacje bazy danych. Kluczową kwestią do zapamiętania jest to, że nasza metoda GetAllBlogs zwraca IAsyncEnumerable<Blog> (lub IEnumerable<Blog>), a nie IQueryable<Blog>. Zwrócenie tego ostatniego oznaczałoby, że operatory zapytań mogą nadal być łączone z wynikiem, co wymaga, aby EF Core nadal brał udział w tłumaczeniu zapytania; niweczyłoby to sens posiadania repozytorium jako takiego. IAsyncEnumerable<Blog> pozwala nam łatwo stubować lub mockować to, co zwraca repozytorium.

W przypadku aplikacji ASP.NET Core musimy zarejestrować repozytorium jako usługę w iniekcji zależności, dodając następujące informacje do ConfigureServicesaplikacji:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Na koniec nasze kontrolery są wstrzykiwane za pomocą usługi repozytorium zamiast kontekstu EF Core i wykonują na niej metody:

private readonly IBloggingRepository _repository;

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

[HttpGet]
public async Task<Blog> GetBlog(string name)
    => await _repository.GetBlogByNameAsync(name);

W tym momencie aplikacja jest zaprojektowana zgodnie ze wzorcem repozytorium: jedynym punktem kontaktu z warstwą dostępu do danych — EF Core — jest teraz za pośrednictwem warstwy repozytorium, która działa jako mediator między kodem aplikacji a rzeczywistymi zapytaniami bazy danych. Testy można teraz pisać po prostu przez zastępowanie repozytorium lub korzystanie z ulubionej biblioteki do pozorowania. Oto przykład testu opartego na mockach, wykorzystujący popularną bibliotekę Moq.

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

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

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

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

Pełny przykładowy kod można wyświetlić tutaj.

SqLite w pamięci

Narzędzie SQLite można łatwo skonfigurować jako dostawcę programu EF Core dla pakietu testowego zamiast produkcyjnego systemu bazy danych (e.g. SQL Server); Aby uzyskać szczegółowe informacje, zapoznaj się z dokumentami dostawcy SQLite. Jednak zwykle dobrym pomysłem jest użycie funkcji bazy danych sqLite w pamięci podczas testowania, ponieważ zapewnia łatwą izolację między testami i nie wymaga pracy z rzeczywistymi plikami SQLite.

Aby korzystać z bazy danych SQLite w pamięci, ważne jest, aby zrozumieć, że nowa baza danych jest tworzona za każdym razem, gdy zostanie otwarte połączenie niskiego poziomu i że zostanie usunięte po zamknięciu tego połączenia. W normalnym użyciu DbContext platformy EF Core otwiera i zamyka połączenia z bazą danych zgodnie z potrzebami — za każdym razem, gdy zapytanie jest wykonywane — aby uniknąć niepotrzebnego długiego czasu nawiązywania połączenia. Jednak w przypadku sqlite w pamięci może to prowadzić do resetowania bazy danych za każdym razem; aby obejść ten problem, otwieramy połączenie przed przekazaniem go do programu EF Core i organizujemy zamknięcie go tylko po zakończeniu testu:

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

Testy mogą teraz wywoływać CreateContext, który zwraca kontekst przy użyciu połączenia skonfigurowanego w konstruktorze, upewniając się, że mamy czystą bazę danych z zainicjalizowanymi danymi.

Pełny przykładowy kod na potrzeby testowania w pamięci SQLite można wyświetlić tutaj.

Dostawca w pamięci

Jak opisano na stronie przeglądu testowania , zdecydowanie odradza się użycie dostawcy pamięci wewnętrznej do testowania; zaleca się rozważenie użycia bazy danych SQLite zamiast tegolub implementacji wzorca repozytorium. Jeśli zdecydujesz się korzystać z bazy danych w pamięci, poniżej przedstawiono typowy konstruktor klasy testowej, który konfiguruje i inicjalizuje nową bazę danych w pamięci przed każdym testem.

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

Pełny przykładowy kod testowy do testowania w pamięci można zobaczyć tutaj.

Nazewnictwo bazy danych w pamięci operacyjnej

Bazy danych w pamięci są identyfikowane przez prostą, nazwę ciągu i można nawiązać połączenie z tą samą bazą danych kilka razy, podając tę samą nazwę (dlatego w powyższym przykładzie należy wywołać metodę EnsureDeleted przed każdym testem). Należy jednak pamiętać, że bazy danych w pamięci są zakorzenione w wewnętrznym dostawcy usług kontekstu; Podczas gdy w większości przypadków konteksty współdzielą tego samego dostawcę usług, konfigurowanie kontekstów z różnymi opcjami może spowodować użycie nowego wewnętrznego dostawcy usług. W takim przypadku jawnie przekaż to samo wystąpienie InMemoryDatabaseRoot do UseInMemoryDatabase dla wszystkich kontekstów, które powinny współużytkować bazy danych w pamięci (zazwyczaj odbywa się to przez statyczne pole InMemoryDatabaseRoot).

Transakcji

Należy pamiętać, że jeśli transakcja zostanie uruchomiona, domyślnie dostawca w pamięci operacyjnej wywoła wyjątek, ponieważ transakcje nie są obsługiwane. Zamiast tego możesz chcieć ignorować transakcje dyskretnie, konfigurując program EF Core tak, aby ignorował InMemoryEventId.TransactionIgnoredWarning, jak w powyższym przykładzie. Jeśli jednak Twój kod rzeczywiście opiera się na semantyce transakcyjnej — np. polega na tym, że wycofanie rzeczywiście cofa zmiany — test nie będzie działać.

Widoki

Dostawca pamięci wewnętrznej umożliwia definiowanie widoków za pomocą zapytań LINQ przy użyciu ToInMemoryQuery:

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