Jak użyć Sql Server Snapshots do resetowania danych w testach

Wprowadzenie

W kilku ostatnich wpisach na blogu zająłem się tematem testowania WebApi z wykorzystaniem testów w Postmanie. Ostatnio mogłeś zobaczyć, w jaki sposób wykonywać automatycznie testy w Azure DevOps. Użyty w tamtym wpisie przykład był bardzo prosty i tak naprawdę nie do końca był namiastką realnej aplikacji. Nie używał on bazy danych, a dane generował dzięki bibliotece NBuilder i przechowywał je w pamięci. Przez to powrót do znanego stanu przed uruchomieniem testów był dość prosty.

W realnej aplikacji nie ma już tak prosto. Gdy mamy bazę danych, musimy w jakiś sposób przywrócić jej zawartość do znanego stanu, aby wykonywanie testów było powtarzalne. W przypadku Sql Servera możemy to zrobić na kilka sposób. Na dzień pisania tego artykułu w tym celu używam Sql Server Snapshots i dzisiaj pokażę Ci, jak wdrożyć go w swojej aplikacji.

Sql Server Snapshots

Sql Server Snapshots jest relatywnie nowym mechanizmem w Sql Serverze, który dość fajnie sprawdza się przy tworzeniu automatycznych testów i przy resecie danych do znanego stanu. Zapisuje on stan bazy danych z określonego momentu i możemy później dość łatwo się do niego cofnąć. Do tego działa on efektywniej niż zrealizowanie tego samego poprzez backup bazy danych.

Mechanizm ten działa na poziomie strony danych w Sql Serverze. W momencie zmiany danych w obrębie strony jej oryginalna zawartość trafia do migawki, a zmodyfikowana wartość trafia do bazy. Wybacz, nie jestem specem od baz danych, więc po więcej po szczegółów odsyłam do dokumentacji – https://docs.microsoft.com/en-us/sql/relational-databases/databases/database-snapshots-sql-server?view=sql-server-2017.

Tutaj widać przewagę snapshotu w stosunku do backupu bazy. W momencie przywrócenia bazy do wersji ze snapshotu wystarczy, że przywrócimy tylko zmienione strony. W automatycznych testach WebApi na ogół nie zmieniamy za bardzo zawartości bazy, więc przywrócenie stanu jest dość szybką operacją (nawet przy dużych bazach) w stosunku do przywracania całego backupu.

Aby utworzyć snapshot bazy, należy skorzystać z takiej komendy:

CREATE DATABASE [nazwa bazy]_Snapshot ON
( NAME = [nazwa bazy], FILENAME = 'C:\db\snapshot\[nazwa bazy]_Snapshot.ss' )
AS SNAPSHOT OF [nazwa bazy];
GO
view raw snapshot1.sql hosted with ❤ by GitHub

Korzystamy z komendy CREATE DATABASE, gdzie nowa baza danych jest migawką bazy przekazanej w komendzie. Oczywiście nazwę nowej bazy oraz ścieżkę zapisu migawki możesz sobie zmienić.

Samo odtworzenie bazy z migawki robimy tak:

ALTER DATABASE [nazwa bazy] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
RESTORE DATABASE [nazwa bazy] FROM DATABASE_SNAPSHOT = '[nazwa bazy]_Snapshot';
ALTER DATABASE [nazwa bazy] SET MULTI_USER;
GO
view raw snapshot2.sql hosted with ❤ by GitHub

W komendzie RESTORE DATABASE przekazujemy nazwę utworzonej wcześniej migawki.

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?

Zmiany w kodzie

Wiedząc, jak utworzyć snapshot oraz odtworzyć bazę na jego podstawie, możemy przejść do zmian w kodzie.

W aplikacji wykorzystuję prostą usługę (DatabaseRestoreService), która zawiera w sobie logikę odtwarzania bazy ze snapshotu. Interfejs usługi wygląda tak:

public interface IDatabaseRestoreService
{
Result Restore();
}

Natomiast sama implementacja tak:

public class DatabaseRestoreService : IDatabaseRestoreService
{
private Lazy<DataContext> _dataContext;
protected DataContext DataContext => _dataContext.Value;
public DatabaseRestoreService(Lazy<DataContext> dataContext)
{
_dataContext = dataContext;
}
public Result Restore()
{
var databaseName = DataContext.Database.Connection.Database;
SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(DataContext.Database.Connection.ConnectionString)
{ InitialCatalog = "master" };
using (var conn = new SqlConnection(connectionBuilder.ConnectionString))
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = $@"ALTER DATABASE {databaseName} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
RESTORE DATABASE {databaseName} FROM DATABASE_SNAPSHOT = '{databaseName}_Snapshot';
ALTER DATABASE {databaseName} SET MULTI_USER;";
cmd.ExecuteNonQuery();
}
}
return Result.Ok();
}
}

W implementacji warto zauważyć, że w pierwszej kolejności wyciągam z klasy DataContext connection stringa do bazy, z której korzysta aplikacja. Oczywiście ten krok zależy od tego, z jakiego dostępu do bazy danych korzystamy. Możemy również z konfiguracji aplikacji pobrać connection stringa o określonej nazwie.

W kolejnym kroku zmieniamy tego connection stringa. Nie możemy się połączyć do naszej bazy i w ramach jej kontekstu jej przywrócić – dlatego w tym rozwiązaniu łączymy się do bazy master i w ramach jej kontekstu przywracamy bazę.

Tutaj chciałbym podkreślić kilka rzeczy. Po pierwsze mechanizm ten wrzucam w jakąś dyrektywę preprocesora (np. #if DEBUG), aby wspomniane klasy oraz interfejsy były dostępne tylko w określonych wersjach buildu. W szczególności aby nie były dostępne w wersji produkcyjnej aplikacji.

Po drugie korzystam z dedykowanego serwera baz danych dla tych automatycznych testów. Dzięki temu nie mam problemu z tym, że użytkownik, w kontekście którego łączymy się z bazą, ma podwyższone uprawnienia. W tym przypadku podatności w stylu sql injection nie powinny spowodować jakichś dużych problemów.

Usługa DatabaseRestoreService jest wykorzystywana w DebugController:

To do niego jest wykonywane pierwsze żądanie podczas uruchamiania grupy testów i ono resetuje nam bazę do znanego stanu:

postman reset test data request

Azure i Sql Server Snapshots

W poprzednim wpisie pokazałem, jak wykonywać automatycznie testy podczas Release w Azure DevOps. Testową aplikację hostowałem w Azure App Service, więc pierwszą myślą odnośnie do tego, gdzie można hostować bazę do testów, jest również Azure. Niestety w tym momencie Azure Sql Database nie ma wsparcia dla migawek. Musimy więc skorzystać z czegoś innego.

Tym czymś innym jest SQL Server on Virtual Machines (https://azure.microsoft.com/en-us/services/virtual-machines/sql-server/), czyli tak naprawdę maszyna wirtualna, a w niej zainstalowany Sql Server. Mamy nawet dostępne gotowe obrazy systemów z zainstalowanymi różnymi wersjami Sql Servera. Dzięki temu możemy sobie dość szybko postawić taką prostą maszynę i później używać jej w automatycznych testach.

Plusem takiej dedykowanej maszyny jest to, że może ona być wykorzystywana tylko do automatycznych testów. W efekcie działanie aplikacji z użyciem użytkownika bazy danych, który ma podwyższone uprawnienia, nie powinno być dużym problemem.

Dodatkowo – w zależności od naszego podejścia – do wykonywania automatycznych testów WebApi możemy sobie uruchamiać tę maszynę w nocy, wykonywać wszystkie testy, a na końcu ją wyłączać. Dzięki temu nie zapłacimy dużo za tę dodatkową maszynę wirtualną, a będziemy mogli korzystać ze snapshotów.

Po postawieniu maszyny logujemy się do Sql Servera i importujemy do niego testową bazę danych, a następnie możemy utworzyć dla niej snapshot. Na końcu w definicji release w Azure DevOps, w kroku wrzucającym aplikację do Azure App Service konfigurujemy zmianę web.configa, która podmieni connection stringa na tego do maszyny wirtualnej z bazą danych.

Po wykonaniu tych wszystkich kroków możemy cieszyć się działającymi automatycznie testami wykonywanymi w Azure DevOps. Testami, które są powtarzalne dzięki przywracaniu bazy z migawki.

postman reset test data release results

Kiedy tworzyć Snapshot bazy do testów?

Na końcu zastanówmy się chwilkę, kiedy tworzyć snapshot. Migawkę możemy utworzyć po wrzuceniu bazy na serwer – ale to rozwiązanie nie jest idealne. Z biegiem czasu schemat bazy danych się zmienia, więc po odtworzeniu bazy z migawki musielibyśmy na niej wykonać wszystkie migracje, a później dopiero uruchomić testy. To powodowałoby, że wykonywanie testów stopniowo zajmowałoby coraz więcej czasu.

Innym rozwiązaniem jest wykonanie snapshota podczas wdrażania aplikacji zaraz po wykonaniu migracji, ale to rozwiązanie wymaga trochę więcej czasu i uwagi. Wrócę do niego w jednym z kolejnych wpisów.

Przykład

Na githubie znajduje się przykład zmodyfikowany w stosunku do tego, co było w poprzednim wpisie. Tym razem zmiany naniosłem na drugi branch database – https://github.com/danielplawgo/WebApiTests/tree/database. Jest w nim dodanie Entity Framework do testowej aplikacji oraz dodanie samego DatabaseRestoreService.

Po pobraniu przykładu należy ustawić connection stringa do bazy testowej, wykonać wszystkie migracje Entity Framework, a następnie utworzyć snapshot, używając komendy z początku wpisu. Później można uruchamiać testy z Postmana.

Podsumowanie

W przypadku automatycznych testów WebApi w Postmanie musimy pomyśleć o jakimś mechanizmie przywracania bazy do znanego stanu, aby wykonywanie testów było powtarzalne. Snapshoty ze Sql Servera, jak sądzę, sprawdzają się w tym celu bardzo dobrze. Mają swoje ograniczenia oraz problemy (w szczególności gdy chcemy wszystko wykonywać w Azure DevOps), ale jesteśmy dzięki nim w stanie osiągnąć to, czego potrzebujemy.

Oczywiście możemy spróbować wykorzystywać inne rozwiązania, jak na przykład bibliotekę Respawn (https://github.com/jbogard/Respawn), którą opiszę w kolejnym wpisie.

1 thought on “Jak użyć Sql Server Snapshots do resetowania danych w testach

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.