Testowanie bez systemu produkcyjnej bazy danych

Na tej stronie omówiono techniki pisania testów automatycznych, które nie obejmują systemu bazy danych, na którym 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.

Napiwek

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

Wzorzec repozytorium

Jeśli zdecydujesz się na pisanie testów bez udziału produkcyjnego systemu 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śmiewany lub pozorny. Oto przykład interfejsu repozytorium dla naszego systemu blogowania:

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

... 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 Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

Nie ma wiele do niego: repozytorium po prostu opakowuje kontekst 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 tłumaczeniu zapytania; w pierwszej kolejności pokonałoby to cel posiadania repozytorium. IEnumerable<Blog> pozwala nam łatwo wyśmiewać lub wyśmiewać, 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 informacje 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 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 wyśmiewanie repozytorium lub wyśmiewanie go z ulubionej biblioteki pozorowania. 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ę programu EF Core dla zestawu testowego zamiast produkcyjnego systemu bazy danych (np. programu 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, 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 program EF Core DbContext otwiera i zamyka połączenia z bazą danych zgodnie z potrzebami — przy każdym wykonaniu zapytania — aby uniknąć niepotrzebnego długiego czasu utrzymywania 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ć 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

Jak opisano na stronie przeglądu testowania, zdecydowanie odradza się korzystanie z dostawcy testów w pamięci; rozważ użycie biblioteki SQLite lub zaimplementowanie wzorca repozytorium. Jeśli zdecydujesz się używać w pamięci, poniżej przedstawiono typowy konstruktor klasy testowej, który konfiguruje i wysiekuje 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 można nawiązać połączenie z tą samą bazą danych kilka razy, podając tę samą nazwę (dlatego powyższy przykład musi wywołać 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 InMemoryDatabaseRootUseInMemoryDatabase dla wszystkich kontekstów, które powinny współużytkować bazy danych w pamięci (zazwyczaj odbywa się to przez użycie pola statycznego InMemoryDatabaseRoot ).

Transakcje

Należy pamiętać, że domyślnie, jeśli transakcja zostanie uruchomiona, dostawca w pamięci zgłosi wyjątek, ponieważ transakcje nie są obsługiwane. Zamiast tego możesz chcieć zignorować transakcje w trybie dyskretnym, konfigurując program EF Core tak, aby był ignorowany InMemoryEventId.TransactionIgnoredWarning tak jak w powyższym przykładzie. Jeśli jednak kod rzeczywiście opiera się na semantyce transakcyjnej — np. zależy od wycofania 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 }));