🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Testowanie EF Core — InMemory vs SQLite, którego providera użyć?

W testach integracyjnych ASP.NET Core podmienialiśmy prawdziwą bazę danych na UseInMemoryDatabase. To wygodne, ale ma pułapkę: InMemory provider nie zachowuje się jak prawdziwa relacyjna baza danych — i potrafi przepuścić test, który w produkcji wybuchnie.

UseInMemoryDatabase — szybkie, ale nierealistyczne

services.AddDbContext<SklepDbContext>(options =>
    options.UseInMemoryDatabase("TestowaBaza"));
[Fact]
public void DodajProdukt_DuplikatKoduSku_PowinienRzucicWyjatek()
{
    using var kontekst = TworzKontekstInMemory();
    kontekst.Produkty.Add(new Produkt { Sku = "ABC-123", Nazwa = "Klawiatura" });
    kontekst.SaveChanges();

    kontekst.Produkty.Add(new Produkt { Sku = "ABC-123", Nazwa = "Mysz" }); // duplikat SKU
    kontekst.SaveChanges(); // ❌ InMemory NIE rzuci wyjątku — brak unikalnego indeksu!
}

Provider InMemory nie egzekwuje ograniczeń relacyjnych: unikalnych indeksów, kluczy obcych, CHECK constraints. Jeśli w konfiguracji EF Core masz HasIndex(p => p.Sku).IsUnique(), test na InMemory przejdzie, mimo że w prawdziwym SQL Server ten sam kod rzuciłby DbUpdateException.

SQLite w pamięci — bliżej rzeczywistego zachowania bazy

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

var polaczenie = new SqliteConnection("DataSource=:memory:");
polaczenie.Open(); // połączenie musi zostać otwarte przez cały czas życia testu

var opcje = new DbContextOptionsBuilder<SklepDbContext>()
    .UseSqlite(polaczenie)
    .Options;

using var kontekst = new SklepDbContext(opcje);
kontekst.Database.EnsureCreated(); // tworzy tabele na podstawie modelu EF Core, WŁĄCZNIE z indeksami
[Fact]
public void DodajProdukt_DuplikatKoduSku_RzucaWyjatek()
{
    using var kontekst = TworzKontekstSqlite();
    kontekst.Produkty.Add(new Produkt { Sku = "ABC-123", Nazwa = "Klawiatura" });
    kontekst.SaveChanges();

    kontekst.Produkty.Add(new Produkt { Sku = "ABC-123", Nazwa = "Mysz" });

    Assert.Throws<DbUpdateException>(() => kontekst.SaveChanges()); // ✅ SQLite egzekwuje unikalny indeks
}

SQLite to prawdziwa relacyjna baza danych (uruchomiona w pamięci, bez pliku na dysku) — egzekwuje unikalne indeksy, klucze obce i większość ograniczeń zdefiniowanych w modelu EF Core. Test wyłapuje błąd, którego InMemory by nie zauważył.

Uwaga na połączenie SQLite — musi zostać otwarte

public class SklepDbContextFixture : IDisposable
{
    public SqliteConnection Polaczenie { get; }
    public SklepDbContextFixture()
    {
        Polaczenie = new SqliteConnection("DataSource=:memory:");
        Polaczenie.Open(); // ⚠️ zamknięcie połączenia = utrata całej bazy w pamięci
    }
    public void Dispose() => Polaczenie.Dispose();
}

Baza SQLite :memory: istnieje tylko dopóki połączenie jest otwarte — jeśli je zamkniesz (albo pozwolisz mu wyjść poza zasięg i zostać zgarniętym przez GC) między testami, dane znikają. Standardowy wzorzec to fixture (IClassFixture<T> w xUnit), które trzyma jedno otwarte połączenie przez cały czas życia klasy testowej.

Czego żaden z nich nie zastąpi

Ani InMemory, ani SQLite nie wyłapią różnic specyficznych dla konkretnego silnika produkcyjnego: składni RAW SQL pisanej pod SQL Server, konkretnych typów danych (datetime2 vs TEXT), czy zachowania transakcji pod dużym obciążeniem. Dla naprawdę pewnego testu przed wdrożeniem migracji na produkcję nie ma zamiennika dla testów na prawdziwym silniku bazy danych (np. w kontenerze Docker z SQL Server) — SQLite to kompromis: szybszy niż prawdziwy SQL Server, ale znacznie bliższy rzeczywistości niż InMemory.

Kiedy który

UseInMemoryDatabase — szybkie testy logiki aplikacji, gdzie nie sprawdzasz ograniczeń relacyjnych (np. czy kontroler poprawnie zwraca 404, gdy FindAsync nie znajdzie rekordu). Szybszy start, zero dodatkowych zależności.

SQLite in-memory — gdy testujesz cokolwiek związanego z unikalnością, kluczami obcymi, kaskadowym usuwaniem, albo samym zapytaniem LINQ, które musi wygenerować poprawny SQL. Bliższe realnemu zachowaniu bazy danych kosztem odrobinę wolniejszego startu testu.

Podsumowanie

UseInMemoryDatabase jest szybki, ale nie egzekwuje ograniczeń relacyjnych (unikalne indeksy, klucze obce) — test może przejść, mimo że ten sam kod na prawdziwej bazie danych rzuciłby wyjątkiem. SQLite w trybie :memory: to prawdziwa relacyjna baza danych uruchomiona w pamięci procesu — wolniejsza od InMemory, ale wyłapuje dużo więcej realnych błędów, kosztem pamiętania, żeby trzymać połączenie otwarte przez cały czas życia testu.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

1 comment

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →