Konrad Kowalski (rootsher)Principal Platform & Reliability Architect100000111001010111111011100000001100001100101100

Migration tests i data tests: bezpieczna zmiana stanu

data
kategoria
Testing
także w
Databases · Reliability Engineering
czytanie
2 min / 385 słów

Kod zwykle cofa się łatwiej niż dane.

Można przełączyć obraz kontenera.

Można wyłączyć feature flag.

Można wrócić do poprzedniego commita.

Ale jeżeli nowa wersja zapisała dane w formacie, którego stara wersja nie rozumie, rollback przestaje być prosty.

To jest miejsce dla migration tests i data tests.

Co to za rodzaj testu

Migration test sprawdza migrację schematu albo danych.

Może dotyczyć:

text
dodania kolumny
zmiany typu
nowego indeksu
backfillu
podziału tabeli
zmiany constraintu

Data compatibility test sprawdza, czy różne wersje kodu rozumieją dane, które mogą spotkać w systemie.

Najważniejsze pytania brzmią:

text
czy nowy kod czyta stare dane?
czy stary kod czyta nowe dane?
czy migracja może zostać uruchomiona drugi raz?
czy rollback zostawia dane w stanie zrozumiałym?

Expand and contract

Bezpieczna zmiana danych często idzie w kilku krokach.

text
dodaj nowe pole
pisz stare i nowe pole
przenieś czytanie na nowe pole
wyczyść stare pole
usuń stare pole

To podejście nazywa się często expand and contract.

Testy migracji powinny pilnować każdego kroku.

Jeżeli od razu usuniemy stare pole, stara wersja aplikacji może przestać działać zanim nowa wersja będzie wszędzie.

Jeżeli zaczniemy zapisywać tylko nowy format, worker w starszej wersji może przestać czytać kolejkę.

Co się dzieje, kiedy ich nie ma

Bez testów migracji pipeline może przepuścić zmianę, której nie da się cofnąć.

Przykłady:

text
migracja działa tylko na pustej bazie
backfill trwa godzinę zamiast minuty
NOT NULL wchodzi przed uzupełnieniem danych
rollback usuwa dane, których nie da się odtworzyć
nowa wersja zapisuje event nieczytelny dla starego konsumenta

To nie są problemy samego SQL.

To problemy operowania systemem w czasie zmiany.

Test na prawdziwym stanie

Migration test ma największą wartość, kiedy startuje z realistycznego stanu.

Nie musi kopiować produkcji.

Powinien jednak zawierać przypadki, które produkcja naprawdę ma:

text
brakujące wartości
stare rekordy
duże tabele
duplikaty
nieaktywne konta
rzadkie statusy

Migracja przetestowana tylko na idealnych fixture potrafi przejść lokalnie i paść na pierwszym starym rekordzie.

Dane jako kontrakt

Dane są kontraktem między wersjami kodu.

Nie tylko między usługami.

Także między:

text
starą i nową wersją aplikacji
aplikacją i workerem
backendem i raportami
eventem i konsumentem
backupem i restore

Jeżeli zmieniamy format danych, zmieniamy kontrakt.

Test powinien powiedzieć, która wersja kodu może czytać który format.

Bez tego rollback jest nadzieją, nie planem.

Narzędzie ze stacka

Do migration tests wybrałbym Testcontainers z prawdziwym PostgreSQL.

Test powinien uruchomić migracje tym samym mechanizmem, którego używa aplikacja, a potem sprawdzić dane przez normalny kod albo SQL.

Minimalny zestaw:

text
PostgreSQL w kontenerze
migracje projektu
fixture ze starym stanem
uruchomienie migracji
asercje na nowym stanie

Na backendzie Node można to spiąć z Vitest, bo runner nie jest tu najważniejszy.

Najważniejsze jest to, że test używa prawdziwej bazy i prawdziwej migracji.

Kiedy test jest za późno

Migration test odpalony dopiero po deploymencie jest za późno.

Może potwierdzić, że środowisko żyje, ale nie powinien być pierwszym miejscem, w którym sprawdzamy migrację.

Najpierw potrzebny jest test w pipeline.

Potem można mieć smoke test po wdrożeniu, który sprawdzi, że aplikacja widzi nowy schemat.

To są dwa różne pytania.

Pierwsze brzmi: czy migracja jest poprawna.

Drugie: czy wdrożone środowisko jest po migracji używalne.

Następna warstwa dotyczy sytuacji, w której system jest poprawny funkcjonalnie, ale nie wytrzymuje ruchu.