Testowanie bez systemu produkcyjnej bazy danych

Na tej stronie omówiono techniki pisania testów automatycznych, które nie obejmują systemu bazy danych, względem którego aplikacja działa w środowisku produkcyjnym, zamieniając bazę danych na test dwukrotnie. Istnieją różne typy podwajań testów i podejść do tego celu. Zaleca się dokładne zapoznanie się z tematem 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 temacie Testowanie pod kątem produkcyjnego systemu bazy danych.

Porada

Na tej stronie przedstawiono techniki xUnit , ale podobne koncepcje istnieją w innych strukturach testowania, w tym NUnit.

Wzorzec repozytorium

Jeśli zdecydujesz się na pisanie testów bez użycia systemu produkcyjnej bazy danych, zalecaną techniką jest to wzorzec repozytorium. Aby uzyskać więcej informacji na ten temat, zobacz tę sekcję. Pierwszym krokiem implementacji wzorca repozytorium jest wyodrębnienie zapytań LINQ platformy EF Core do oddzielnej warstwy, która później zostanie wyśmiewana lub pozorna. Oto przykład interfejsu repozytorium dla naszego systemu rejestrowania:

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

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

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

Nie ma wiele do tego: repozytorium po prostu opakowuje kontekst platformy EF Core i uwidacznia metody, które wykonują zapytania i aktualizacje bazy danych. Kluczową kwestią do zapamiętania jest to, że nasza GetAllBlogs metoda zwraca IEnumerable<Blog>wartość , a nie IQueryable<Blog>. Zwrócenie tego ostatniego oznaczałoby, że operatory zapytań mogą nadal składać się z wyniku, co wymaga, aby program EF Core nadal brał udział w przetłumaczeniu zapytania; w pierwszej kolejności pokonałoby to cel posiadania repozytorium. IEnumerable<Blog> pozwala nam łatwo wycinków lub pozorować, co zwraca repozytorium.

W przypadku aplikacji ASP.NET Core musimy zarejestrować repozytorium jako usługę we wstrzyknięciu zależności, dodając następujące elementy do aplikacji ConfigureServices:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Na koniec kontrolery są wstrzykiwane do usługi repozytorium zamiast kontekstu EF Core i wykonują na nim metody:

private readonly IBloggingRepository _repository;

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

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

W tym momencie aplikacja jest zaprojektowana zgodnie z 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 pośrednik między kodem aplikacji a rzeczywistymi zapytaniami bazy danych. Testy można teraz pisać po prostu przez wycięcie repozytorium lub wyśmiewanie go z ulubioną drwiną biblioteką. Oto przykład testu opartego na makiecie przy użyciu popularnej biblioteki Moq :

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

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

SqLite w pamięci

Narzędzie SQLite można łatwo skonfigurować jako dostawcę platformy EF Core dla zestawu testowego zamiast produkcyjnego systemu bazy danych (np. SQL Server). Aby uzyskać szczegółowe informacje, zapoznaj się z dokumentami dostawcy SQLite. Jednak zwykle dobrym pomysłem jest użycie funkcji bazy danych w pamięci SQLite 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, należy pamiętać, że nowa baza danych jest tworzona za każdym razem, gdy zostanie otwarte połączenie niskiego poziomu i że zostanie usunięte to połączenie jest zamknięte. W normalnym użyciu program EF Core DbContext 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 utrzymywania połączenia. Jednak w przypadku sqlite w pamięci doprowadziłoby to do zresetowania bazy danych za każdym razem; aby obejść ten problem, otwieramy połączenie przed przekazaniem go do platformy EF Core i organizujemy jego zamknięcie 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ć CreateContextmetodę , która zwraca kontekst przy użyciu połączenia skonfigurowanego w konstruktorze, zapewniając, że mamy czystą bazę danych z danymi wstępnie wypełnionymi.

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

Dostawca w pamięci

Zgodnie z opisem na stronie przeglądu testowania zdecydowanie odradza się używanie dostawcy w pamięci do testowania; Zamiast tego rozważ użycie biblioteki SQLite lub zaimplementowanie wzorca repozytorium. Jeśli zdecydujesz się użyć w pamięci, oto typowy konstruktor klasy testowej, który konfiguruje i wysadza 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 do testowania w pamięci można wyświetlić tutaj.

Nazewnictwo bazy danych w pamięci

Bazy danych w pamięci są identyfikowane przez prostą nazwę ciągu i istnieje możliwość nawiązania połączenia z tą samą bazą danych kilka razy, podając taką samą nazwę (dlatego powyższy przykład musi wywołać przed EnsureDeleted 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ółużytkuje tego samego dostawcę usług, skonfigurowanie 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 InMemoryDatabaseRootUseInMemoryDatabase dla wszystkich kontekstów, które powinny współużytkować bazy danych w pamięci (zwykle odbywa się to przez pole statyczne InMemoryDatabaseRoot ).

Transakcje

Należy pamiętać, że domyślnie, jeśli transakcja jest uruchomiona, dostawca w pamięci zgłosi wyjątek, ponieważ transakcje nie są obsługiwane. Zamiast tego możesz chcieć ignorować transakcje w trybie dyskretnym, konfigurując program EF Core do ignorowania InMemoryEventId.TransactionIgnoredWarning , tak jak w powyższym przykładzie. Jeśli jednak kod faktycznie opiera się na semantyce transakcyjnej — np. zależy od wycofywania zmian — test nie będzie działać.

Widoki

Dostawca w pamięci umożliwia definiowanie widoków za pośrednictwem zapytań LINQ przy użyciu polecenia ToInMemoryQuery:

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