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.
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
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ę →
1 comment