Każdy tutorial o testowaniu w Unity pokazuje, jak napisać test. Prawie żaden nie mówi, dlaczego twoja gra będzie się przy tym opierać — a ta część to architektura, nie narzędzia.
Oto kształt zestawu, który się nie opierał, zmierzony, a nie zapamiętany:
| Gdzie | Plików | Testów | Wymaga działającej gry |
|---|---|---|---|
| Edit Mode | 179 | 1541 | Nie |
| Play Mode | 18 | 83 | Tak |
Dziewięćdziesiąt pięć procent z nich działa bez sceny, bez MonoBehaviour
i bez klatki. Ta proporcja nie jest dyscypliną ani upodobaniem. Jest bezpośrednią
konsekwencją jednej reguły, a ta reguła to cały ten artykuł.
Jeden obiekt może zmieniać stan. To jest ta reguła.
Tym, co czyni grę testowalną, nie jest framework, biblioteka do mocków ani wstrzykiwanie zależności. Jest nim to, że istnieje dokładnie jedno miejsce, w którym świat może się zmienić, i przechodzi przez nie wszystko: każdy przycisk, każdy tik, każdy system.
W moim projekcie ten obiekt nosi taki komentarz i jest to najbardziej nośny akapit w całej bazie kodu:
Przeczytaj twierdzenie przyczynowe w środku: i dlatego test może przegrać cztery lata. Testowalność nie jest funkcją, którą ktoś zbudował. Jest tym, co dostajesz za darmo, gdy nic nie może zmienić świata za plecami tej bramy.
Drugi akapit robi cichszą robotę. Ustalona, udokumentowana kolejność operacji oznacza, że test może sprawdzać stan po dowolnym dniu i dostawać tę samą odpowiedź za każdym razem — z tego samego powodu, dla którego kampania w ogóle jest powtarzalna, czyli z tematu artykułu o deterministycznym losowaniu. Kolejność i determinizm to ten sam problem w dwóch kapeluszach.
Pomiar, który mówi, czy to masz
Jest jednolinijkowy sprawdzian na to, czy twoja logika gry jest testowalna, i jest uczciwszy niż jakiekolwiek deklaracje. Policz, ile plików twojej symulacji importuje silnik:
U mnie odpowiedź brzmi 0, na 98 plikach. Nie dlatego, że importowanie silnika jest
zakazane jakąś regułą, którą egzekwuję, tylko dlatego, że gdy symulacja operuje na własnym
stanie wyłącznie przez jedną bramę, nigdy nie ma powodu sięgać po Vector3,
Debug.Log czy Time.deltaTime. Silnik okazuje się rzeczą, której
logika nigdy nie potrzebowała.
Jeśli twoja liczba jest duża, to właśnie jest ta robota. Każde using UnityEngine
w pliku z regułami to mały zakład o to, że ta reguła zawsze będzie działać wewnątrz gry —
i to ten zakład sprawia później, że czteroletniej kampanii nie da się przetestować w sekundę.
Dwa typy z silnika, które wchodzą niepostrzeżenie i kosztują najwięcej
Time.deltaTime czyni liczbę klatek wejściem do symulacji, więc ta sama
kampania na szybszej maszynie jest inną kampanią. Symulacja ma posuwać się w jednostkach,
które sama posiada — dzień, tik, tura — a warstwa prezentacji niech decyduje,
jak szybko ją wywoływać.
Vector3 i spółka wyglądają niewinnie i wciągają za sobą cały assembly
silnika. Jeśli twoja logika naprawdę potrzebuje współrzędnych, czterolinijkowa własna
struktura utrzymuje granicę czystą i nie kosztuje nic.
Assembly definitions to część, która tego pilnuje
Reguła, której nikt nie złamie przez przypadek, jest warta więcej niż reguła, z którą wszyscy się zgadzają. Assembly definitions są sposobem, w jaki Unity ci to daje, i są krokiem pomijanym przez większość tutoriali, bo przykład-zabawka ich nie potrzebuje.
W moim projekcie robią to cztery pliki:
| Assembly | Do czego służy |
|---|---|
ScalingLaws.Runtime | Sama gra: symulacja, dane, zapisy, interfejs. |
ScalingLaws.Editor | Narzędzia wyłącznie edytorowe, które nigdy nie mogą trafić do builda gracza. |
ScalingLaws.Tests.EditMode | Te 1541. Bez sceny, bez klatki. |
ScalingLaws.Tests.PlayMode | Te 83, które naprawdę potrzebują silnika. |
Plik asmdef testów wart jest przeczytania linijka po linijce, bo trzy jego pola to te, które ludzie ustawiają źle:
"includePlatforms": ["Editor"]to jest to, co w ogóle czyni z nich testy Edit Mode. Dokumentacja Unity mówi, że assembly testowe celujące wyłącznie w Editor jest domyślnie traktowane jako Edit Mode."defineConstraints": ["UNITY_INCLUDE_TESTS"]trzyma całe assembly poza buildem gracza. Bez tego możesz wysłać graczom swój kod testowy."autoReferenced": falsesprawia, że nic nie może przypadkiem zależeć od twoich testów. Powinno być prawdą w każdym assembly testowym i prawie nigdy nie jest.
Cichą korzyścią jest lista referencji. Ponieważ assembly testowe deklaruje dokładnie, co widzi, błąd architektoniczny — na przykład plik symulacji sięgający do interfejsu — przestaje być kwestią code review, a staje się błędem kompilacji.
Do czego służą pozostałe osiemdziesiąt trzy testy
Artykuł byłby schludniejszy, gdyby odpowiedź brzmiała „do niczego, wrzuć wszystko do Edit Mode". Jest błędna, a te osiemdziesiąt trzy to miejsce, gdzie mieszka konkretna klasa błędów.
Testy Play Mode mają pokrywać wszystko, czego tematem jest silnik: ładowanie scen, podpięcie prefabów, interfejs istniejący dopiero po wyrenderowaniu dokumentu, korutyny, fizykę. Ale kategoria, którą ludzie pomijają, jest tą ważną — testy dowodzące, że rdzeń bez sceny jest faktycznie podłączony do tego, co widzi gracz.
Dlaczego to waży więcej, niż brzmi
Symulacja z 1541 przechodzącymi testami nadal może wydać ekran, który otwiera się pusty. Logika jest udowodniona; podłączenie nie. Każdy błąd tego kształtu, jaki miałem, wygląda od środka tak samo: jeden stan, dwa miejsca, które go ustawiają, i tylko jedno zawiadamia ekran.
Edit Mode nie może tego zobaczyć z definicji, bo tam ekran nie istnieje. Jeśli napiszesz tylko trzy testy Play Mode, niech to będą: „każdy ekran otwiera się i coś pokazuje pierwszego dnia", „zmiana w symulacji dociera do ekranu" i „kliknięcie na ekranie dociera do symulacji".
Czym się te 1541 testów okazało
„Tysiąc sześćset testów" brzmi jak liczba zaprojektowana, żeby robić wrażenie, i sama w sobie by nią była. Pożyteczniej rozbić ją na cztery zadania, które wykonują, bo czwarte ma prawie nikt:
| Rodzaj | Na jakie pytanie odpowiada |
|---|---|
| Testy reguł | Czy to jedno obliczenie robi to, co deklaruje? Zwyczajna większość i najtańsze w pisaniu. |
| Testy spójności | Czy zawartość jest spójna? Że każdy wymóg badań istnieje, że drzewo nie ma cykli, że każda zablokowana rzecz ma dokładnie jedną bramkę. Czytają katalogi, a nie kod, i wyłapują pomyłki, które robisz o drugiej w nocy w pliku z danymi. |
| Testy zapisu | Czy zapis przeżywa podróż w obie strony i czy stary zapis nadal się wczytuje? Każdy z osobna tani; razem są powodem, dla którego kampania gracza sprzed trzech miesięcy wciąż się otwiera. |
| Testy grywalności | Czy to nadal jest gra? Skryptowany bot rozgrywa pełne cztery lata, a asercje opisują przedział trudności. Piętnaście testów, opisanych w osobnym artykule, i znalazły błędy, których żaden test jednostkowy nie mógł zobaczyć. |
Warstwę spójności dodałbym w cudzym projekcie jako pierwszą. Test, który przechodzi twoją zawartość i sprawdza, że jest poprawnie zbudowana, kosztuje godzinę i nigdy nie przestaje się zwracać, bo to w zawartości solowy twórca robi najwięcej pomyłek i ma najmniej kontroli.
Ile to kosztuje i kiedy się nie opłaca
Pisałbym reklamę, a nie poradnik, gdybym skończył w tym miejscu.
Prawdziwym kosztem nie jest pisanie testów. Jest nim to, że architekturę trzeba ustalić
wcześnie, a jej dorobienie później jest drogie. Oddzielenie symulacji od silnika po fakcie
oznacza znalezienie każdego miejsca, gdzie reguła czyta Time.deltaTime albo
zahacza o MonoBehaviour, a w projekcie dowolnej wielkości to tygodnie, nie
popołudnie. Opisałem jedyny raz, kiedy
musiałem to zrobić, i nie było przyjemnie.
Drugim kosztem jest to, że rdzeń bez sceny jest trochę mniej wygodnym sposobem pisania gry. Nie przeciągniesz wartości na komponent w inspektorze tak, żeby symulacja ją zobaczyła. Wszystko jest jawne, co jest lepsze w dwunastym miesiącu i oznacza więcej pisania w pierwszym tygodniu.
Uczciwy argument przeciwko temu wszystkiemu
Jeśli twoja gra jest krótka, autorska i ręcznie układana — gra narracyjna, logiczna z projektowanymi poziomami, platformówka, w której zawartością są sceny — większość tego kupuje ci znacznie mniej. Nie ma czteroletniej kampanii do symulowania ani kumulującej się ekonomii do ochrony. Te same godziny wydane na poziomy dadzą lepszą grę.
Sprawdzian, który to rozstrzyga: czy człowiek jest w stanie przechodzić całą twoją grę na tyle często, żeby zauważyć regresję? Jeśli tak, graj. Jeśli pełne przejście to cztery lata gry i nikt tego nigdy nie zrobi ręcznie po każdej zmianie, zestaw testów nie jest rytuałem jakości — jest jedynym przyrządem, jaki masz do obserwowania tego, co zbudowałeś.
Kolejność, w której zrobiłbym to ponownie
- Umieść reguły tam, gdzie nie ma silnika. Jeden folder, zero
using UnityEngine, zweryfikowane grepem powyżej. Wszystko inne z tego wynika i nic bez tego nie działa. - Uczyń jeden obiekt jedyną drogą do zmiany stanu. Akcje gracza i tik przechodzą przez niego, w udokumentowanej kolejności.
- Dodaj cztery assembly definitions, żeby granica była błędem kompilacji, a nie nawykiem.
- Napisz testy spójności na zawartości zanim napiszesz testy reguł. Najtańszy współczynnik trafień w całym zestawie.
- Dodaj trzy testy podłączenia w Play Mode, żeby zielony zestaw nie mógł współistnieć z pustym ekranem.
Pytania, które ludzie zadają
Czy da się testować logikę gry w Unity bez wchodzenia w Play Mode?
Tak i większość powinna tak działać. Dokumentacja Unity oddziela testy Edit Mode, działające
w edytorze bez pętli gry, od testów Play Mode, które uruchamiają grę; Edit Mode używa
zwykłego atrybutu [Test] z NUnit i chodzi znacznie szybciej. W mojej wydanej
symulacji podział to 1541 wobec 83, czyli około dziewięćdziesiąt pięć procent zestawu nigdy
nie uruchamia gry. Ograniczeniem nie jest framework, tylko to, czy twoja logika w ogóle
potrafi działać bez sceny.
Co w ogóle czyni logikę gry testowalną?
Jedna reguła podjęta wcześnie: dokładnie jeden obiekt może zmieniać stan gry i przechodzi
przez niego każda akcja gracza oraz każdy tik. Gdy to obowiązuje, test może zbudować stan,
wywołać bramę i krokować dalej bez sceny, bez MonoBehaviour i bez klatki, bo
nic innego nie mogło nic zmienić za jej plecami. Te 98 plików symulacji importujących
UnityEngine zero razy to konsekwencja tej reguły, a nie osobny cel.
Co powinno zostać w testach Play Mode?
Wszystko, czego tematem naprawdę jest silnik: ładowanie scen, podpięcie prefabów, interfejs istniejący dopiero po wyrenderowaniu dokumentu, korutyny, fizyka. Plus kategoria pomijana przez większość — sprawdzenia dowodzące, że rdzeń bez sceny jest naprawdę podłączony do tego, co widzi gracz. To pominięcie jest powodem, dla którego gra może mieć tysiące zielonych testów i ekran otwierający się pusty.
Czy do testowania kodu Unity potrzebne są assembly definitions?
Do czegokolwiek poza zabawką tak. Assembly testowe ustawia includePlatforms
wyłącznie na Editor, dodaje TestAssemblies do opcjonalnych referencji Unity
i ogranicza się przez UNITY_INCLUDE_TESTS, żeby kompilator pominął je w buildzie
gracza. Wymuszają też architekturę: referencja, której nie zadeklarowałeś, staje się błędem
kompilacji zamiast cichą zależnością.
Jak długo chodzi 1624 testów?
Większość w Edit Mode jest szybka, bo nie ma pętli gry, na którą trzeba czekać; drogie są te kilkanaście, które symulują cztery albo pięć lat gry, i to one dominują czas całkowity. Praktyczne ustawienie to uruchamiać wszystko na commicie, a długie testy grywalności trzymać poza pętlą, którą odpalasz co trzydzieści sekund przy edytowaniu reguły.
Czy warto testować solowy projekt gry?
To zależy od gry. Przy symulacji albo strategii, gdzie liczby kumulują się przez godziny, testy są jedynym sposobem obserwowania tego, co budujesz, bo nikt nie rozegra czterech lat gry ręcznie po każdej zmianie. Przy krótkim, autorskim doświadczeniu ten sam wysiłek kupuje znacznie mniej i uczciwa odpowiedź brzmi: wydaj go na poziomy.