Effort – testy Entity Framework

Wprowadzenie

W ostatnim wpisie poruszyłem temat testowania, a w dzisiejszym wpisie pozostaniemy przy tym temacie. O ile z testowaniem warstwy logiki biznesowej na ogół nie mamy problemów, to już dużo gorzej wygląda to z warstwą dostępu do danych. Entity Framework z pudełka nie umożliwia prostego pisania testów jednostkowych. Istnieją różne rozwiązania tego problemu. Część osób idzie tak naprawdę w testy integracyjne i wykonuje zapytania na realnej bazie. Część osób korzysta z baz danych w pamięci (np. SQLite). Ja w tym wpisie natomiast pokażę Ci, jak wykorzystać bibliotekę Effort do pisania testów Entity Framework.

Repozytorium do testów

W przykładzie przetestujemy metodę GetAllActive z ProductRepository. Metoda jest bardzo prosta i zwraca z bazy produkty, które mają ustawioną flagę IsActive na true:

public class ProductRepository
{
private Lazy<DataContext> _db;
protected DataContext Db
{
get { return _db.Value; }
}
public ProductRepository(Lazy<DataContext> db)
{
_db = db;
}
public IEnumerable<Product> GetAllActive()
{
return Db.Products
.Where(p => p.IsActive)
.ToList();
}
}

Klasa Product zwracana przez repozytorium pojawiała się już wielokrotnie na blogu, ale przypomnę ją jeszcze raz:

public class Product : Model
{
public string Name { get; set; }
}
view raw Product.cs hosted with ❤ by GitHub
public class Model
{
public Model()
{
IsActive = true;
}
public int Id { get; set; }
public bool IsActive { get; set; }
}
view raw Model.cs hosted with ❤ by GitHub

Repozytorium w konstruktorze otrzymuje zależność dotyczącą klasy data contextu z Entity Framework. W testach nie będziemy tej klasy mockować, jak to jest w normalnych testach jednostkowych. Effort udostępnia specjalny rodzaj połączenia do bazy, które dopiero będzie mockować działanie bazy. Dlatego też w testach będziemy potrzebować klasy kontekstu, która wygląda tak:

public class DataContext : DbContext
{
public DataContext()
: base("Name=DefaultConnection")
{
}
public DataContext(DbConnection connection)
: base(connection, true)
{
}
public DbSet<Product> Products { get; set; }
}
view raw DataContext.cs hosted with ❤ by GitHub

Zwróć uwagę, że klasa posiada dwa konstruktory. Pierwszy bezparametrowy będzie wykorzystywany normalnie w aplikacji (z niego będzie korzystał kontener). Natomiast drugi konstruktor posłuży nam do testów. Za pomocą niego przekażemy połączenie do bazy utworzone przez bibliotekę Effort, która zamockuje nam bazę danych.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Effort

Effort (https://entityframework-effort.net) jest darmową biblioteką, która ułatwia testowanie kodu wykorzystującego Entity Framework. Jest tworzona przez tę samą firmę, co opisywana ostatnio biblioteka Entity Framework Plus. W tym momencie Effort wspiera tylko normalnego Entity Frameworka (5 oraz 6). Niestety nie ma jeszcze wsparcia dla wersji Core.

Biblioteka udostępnia w pamięci bazę danych, na której możemy wykonywać operacje. Dzięki temu możemy w izolacji wykonać testy repozytoriów, bez potrzeby użycia zewnętrznych baz danych. Dodatkowo Effort umożliwia wczytanie początkowego stanu bazy danych z plików csv. Możemy przygotować różne wersje danych do testów bez oprogramowania tego w kodzie.

Effort – użycie

W swoich projektach dla warstwy dostępu do danych tworzę dedykowany projekt z testami. Każda klasa z testami repozytoriów będzie potrzebowała używać zamockowanego data contextu. Dlatego w projekcie tworzę bazową klasę dla testów, w której znajduje się kod tworzący instancje klasy DataContext, która korzysta z połączenia do bazy dostarczonego przez Effort:

public class RepositoryTests
{
protected DataContext CreateContext(string path = null)
{
IDataLoader loader = new CsvDataLoader(path ?? "Data");
return new DataContext(DbConnectionFactory.CreateTransient(loader));
}
}

Metoda CreateContext będzie uruchamiana przez każdy z testów repozytorium. Korzysta ona z klasy CsvDataLoader z Effort, która jest odpowiedzialna za załadowanie startowej zawartości bazy danych z plików csv (niżej pokażę, jak tworzyć pliki csv z danymi). Domyślnie metoda wczytuje dane z katalogu Data, w którym znajduje się podstawowa wersja danych pokrywająca większość scenariuszów testowych.

Metoda dodatkowo udostępnia opcjonalny parametr path, za pomocą którego możemy przekazać ścieżkę do innego folderu z danymi. Wykorzystuję to w niektórych testach, gdzie domyślna zawartość bazy danych nie umożliwia sprawdzenia danego przypadku.

Doświadczenie pokazało mi, że takie rozwiązanie sprawdza się najlepiej. Z jednej strony moglibyśmy dla każdego testu tworzyć specyficzny zestaw danych, aby zmiana wspólnego zestawu nie wpływała na testy. Z drugiej strony jednak takie podejście wydłuża pisanie testów. Dodatkowo jakieś większe zmiany w strukturze bazy danych powodują, że musimy zaktualizować większą liczbę plików z danymi testowymi.

Test repozytorium

Dla testów klas w swoich projektach tworzę dedykowany folder (w tym przypadku jest to folder o nazwie ProductRepositoryTests). W nim tworzę oddzielną klasę z testami dla każdej z metod. Dodatkowo dodaję również klasę bazową, w której znajdują się wspólne elementy (głównie metoda Create tworząca klasę, którą będę testował, oraz ewentualne mocki). W przykładzie klasa bazowa wygląda tak:

public class BaseTests : RepositoryTests
{
protected ProductRepository Create(string path = null)
{
var context = CreateContext(path);
return new ProductRepository(new Lazy<DataContext>(() => context));
}
}
view raw BaseTests.cs hosted with ❤ by GitHub

Dziedziczy ona po klasie RepositoryTests, dzięki czemu ma dostęp do metody CreateContext, którą wywołuje przez utworzenie repozytorium w metodzie Create.

Metoda Create również posiada opcjonalny parametr ze ścieżką, który przekazuje do metody CreateContext.

Dla metody GetAllActive z ProductRepository przygotowałem dwa testy. Pierwszy wykorzystuje domyślny zestaw danych i zwraca z niego tylko aktywne produkty. Drugi test natomiast korzysta ze specyficznego zestawu danych, w którym znajdują się tylko nieaktywne obiekty. Klasa z testami wygląda tak:

public class GetAllActiveTests : BaseTests
{
[Fact]
public void Return_Only_Active_Products()
{
var repository = Create();
var products = repository.GetAllActive();
products.Should().NotBeNull();
products.Count().Should().Be(9);
products.Should().OnlyContain(p => p.IsActive);
}
[Fact]
public void Return_Empty_Collection()
{
var repository = Create(@"ProductRepositoryTests\Data\NoActiveProducts");
var products = repository.GetAllActive();
products.Should().NotBeNull();
products.Count().Should().Be(0);
}
}

W testach używałem do assertów opisywanego w poprzednim wpisie Fluent Assertions. Pierwszy test sprawdza, czy lista zwróconych produktów zawiera 9 elementów (tyle jest aktywnych produktów w testowych danych w domyślnym zestawie) oraz czy wszystkie produkty mają ustawioną flagę IsActive na true.

Drugi test natomiast ładuje przygotowane specjalne dla niego dane (przekazuje ścieżkę do metody Create) i sprawdza, czy zwrócona lista jest pusta (dane zawierają tylko nieaktywne produkty).

Dzięki wykorzystaniu biblioteki Effort testy te wykonają się bez konieczności konfiguracji środowiska w jakiś sposób. W szczególności nie potrzebujemy żadnej bazy testowej w systemie.

Warto jeszcze zobaczyć, jak wygląda solution w testowym projekcie:

effort solution

Widać ładny domyślny katalog Data z danymi w głównym folderze projektu oraz katalog NoActiveProducts w katalogu z testami ProductRepository, z danymi specyficznymi dla drugiego testu. Warto też zauważyć, że nie potrzebujemy w tym przypadku wszystkich tabel z bazy. Możemy wrzucać tylko te, które są używane w danym teście.

Przygotowanie danych testowych

Aby móc korzystać z biblioteki Effort za pomocą wyżej przedstawionej techniki, potrzebujemy do tego danych testowych. Można je przygotować na kilka sposobów. Ja najczęściej wykorzystuję do tego bibliotekę Bogus, która generuje mi bazę testową. Bazę tę wykorzystuję normalnie do testów oraz na jej podstawie generuję pliki csv dla podstawowego zestawu testów.

Do wygenerowania plików csv wykorzystuję narzędzie dostarczone przez twórców Efforta. Effort.CsvTool (https://entityframework-effort.net/export-data-to-csv) umożliwia przekazanie connection stringa do bazy danych, z której chcemy wygenerować dane, oraz katalogu, w którym zostaną zapisane pliki. Narzędzie wygeneruje pliki w takiej postaci, aby sama biblioteka mogła bez przeszkód ich użyć.

Największym problemem tego narzędzia jest to, że jest ono dostępne w formie kodu, który trzeba pobrać i skompilować u siebie w komputerze.

Do generowania specyficznych danych dla testów wykorzystuję głównie SQL Management Studio. W nim przygotowuję zapytanie, które zwróci mi z testowej bazy dane specyficzne dla danego przypadku. W przypadku drugiego testu wykonałem na bazie danych takie zapytanie:

SELECT *
FROM [dbo].[Products]
where IsActive = 0
view raw query.sql hosted with ❤ by GitHub

Jego wynik zapisałem do pliku Products.csv w katalogu ProductRepositoryTests/Data/NoActiveProducts.

Przykład

Na githubie znajduje się przykład do tego wpisu (https://github.com/danielplawgo/EffortTests). Po jego pobraniu nie trzeba niczego dodatkowo konfigurować, można od razu wykonać testy.

Podsumowanie

Testowanie automatyczne warstwy dostępu do danych nie jest prostym zadaniem. Na szczęście takie biblioteki jak Effort bardzo to ułatwiają.

Dzięki Effortowi możesz łatwo przetestować kod Entity Framework, a to wszystko bez potrzeby korzystania z realnej bazy danych.

A jak Ty testujesz kod Entity Framework?

Szkolenie Entity Framework Core

Szkolenie Entity Framework Core

Zainteresował Ciebie ten temat? A może chcesz więcej? Jak tak to zapraszam na moje autorskie szkolenie z Entity Framework Core.

5 thoughts on “Effort – testy Entity Framework

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.