W czerwcu 2021 opisywałem nieco mój pomysł na pakiet testowy dla reszty moich projektów. Ostatecznie jednak postanowiłem, że nie będę go dłużej rozwijał, ponieważ jest zbyt problematyczny. I w tym poście postaram się wyjaśnić dlaczego.

Natywne wsparcie dla ESM w Node.js

Kiedy mocha-lib-tester (mlt) powstawał, natywne wsparcie dla modułów JS (ESM) w Node.js wydawało się odległą przyszłością. Było na etapie kłótni, czy lepiej to zrobić przy pomocy nowego pola w pliku package.json, czy nowego rozszerzenia plików .mjs. Ostatecznie wprowadzono obydwa rozwiązania i ekosystem powoli zaczął się przepisywać.

Niemniej mlt był mocno związany ze “starym” systemem modułów w Node.js, CommonJS (CJS) . Cała magia, która w nim się działa, opierała się właśnie na tym systemie, który pozwalał w locie modyfikować wczytywane pliki JS. Dzięki temu mogłem załączać chai oraz inne potrzebne mi biblioteki czy instrumentować kod przy pomocy Istanbula w celu liczenia pokrycia kodu testami.

Natywne wsparcie dla ESM w Node.js oferuje zupełnie inny mechanizm od tego, który nie pozwala na dynamiczne dopisywanie obsługi nowego rodzaju plików – coś, z czego właśnie korzystałem. Taką obsługę można dodać jedynie w trakcie odpalania całego pakietu, jako dodatkową flagę przekazywaną do Node.js. Dlatego też zacząłem prace nad esm-loader-managerem, który pozwalałby na dołączanie wielu takich loaderów przy pomocy składni podobnej do tej z pirates, które dotąd stosowałem w mlt. Projekt jak na razie jest dość biedny, ale działa i w sumie spełniałby swoje zadanie w mlt.

Tylko pojawił się inny problem: tryb watch, pozwalający na obserwowanie zmian w projekcie i odpalanie testów na nowo, gdy jakaś zmiana zostanie wykryta. W tym celu opróżniałem cache dla modułów wczytanych przez require(), co wymuszało ich ponowne pobranie, tym samym sprawiając, że wszystkie zmiany w kodzie zostały uwzględnione. Moduły ES nie mają API do sterowania cache’em – pobrane zostaną tylko raz i (a przynajmniej tak wynikało z moich testów kilka miesięcy temu) późniejszy ich import nawet nie przechodzi przez jakikolwiek custumowy loader, tylko leci bezpośrednio z cache’u. Możliwe, że da się to obejść (stosując techniki podobne do tych z przeglądarkowego świata), ale, szczerze, to nie miałem ochoty się z tym kopać.

Zwłaszcza, że to nie był jedyny problem związany z ESM. Oznaczałoby to także m.in. konieczność napisania od nowa całej integracji z Babelem. Obecna opiera się bowiem o @babel/register, który dodaje sobie nowy hook dla require() i w locie mieli pliki JS. Zrobienie tego przy pomocy mojego esm-loader-managera wymagałoby tak naprawdę napisania bardzo podobnej rzeczy samemu. I choć robialne, to zaczyna się to robić mocno nużące.

Natywne wsparcie dla ESM w Node.js bez wątpienia było największym gwoździem do trumny mlt. Ale zdecydowanie nie jedynym.

Wydajność

Nie będę się oszukiwał: mlt był wolny. Niesamowicie wolny. Wynikało to zarówno z podjętych decyzji projektowych, jak i wykorzystanej technologii.

Największym błędem z mojej strony było zaprojektowanie całego systemu tak, by wszystko działo się tam jedno po drugim. I jeśli faktycznie wysłanie informacji o code coverage do Codecov ma sens dopiero po puszczeniu testów, tak odpalenie ESLinta mogło dziać się spokojnie “w tle”, równocześnie z testami.

Powoli przymierzałem się, żeby w jakiś sposób ten problem ogarnąć. Wydzieliłem nawet poszczególne kroki (lintowanie, testowanie itd.) do oddzielnych wątków roboczych. To sprawiło, że całość stała się bardziej responsywna (spinner – tak, napisałem swój – mniej chrupał), ale w zamian za… bycie jeszcze wolniejszą. No cóż.

Problemem był też fakt, że sercem mlt była Mocha. Wciąż uważam, że w połączeniu z chai tworzy jeden z najprzyjemniejszych duetów do pisania testów. Tylko że jest zaprojektowana z myślą o tym, by testy odpalać seryjnie – jeden po drugim. To pozwala na pewne rzeczy trudne czy wręcz niemożliwe do zrobienia przy frameworkach takich jak ava, które odpalają testy równolegle, np. hook beforeEach() odpalany przed każdym testem i “czyszczący” po poprzednim. Ale oznacza też, że Mocha jest zdecydowanie wolniejsza od takiej avy.

Cały pakiet mlt to było połączenie wolnych rozwiązań. Swoje dokładały nawet hooki, mielące w locie pliki JS przy pomocy Babela czy dokładające kod Istanbula. Nic zatem dziwnego, że ostateczny produkt był najzwyczajniej w świecie wolny.

Output

Zresztą podjęte decyzje architektoniczne sprawiły, że mlt nie tylko był wolny, ale robił też absolutnie wszystko, by wydawać się wolny. A to za sprawą tego, że output był wyświetlany dopiero po zakończeniu danego kroku. I znów: w takiej avie wynik każdego testu jest wyświetlany w konsoli od razu po jego zakończeniu. W przypadku mlt tak nie jest – mlt pokazuje wyniki testów dopiero po wykonaniu wszystkich. Do tego czasu użytkownik może sobie poobserwować obracający się spinner – do tego najprawdopodobniej zacinający się nieco.

Sposób raportowania wyników testów to bez wątpienia coś, nad czym będę musiał dokładnie przysiąść, jeśli kiedykolwiek stwierdzę, że czas powrócić do stworzenia własnego pakietu testowego.

Typy

Typy, a dokładniej plik .d.ts z deklaracjami typów dla mlt, miał być odpowiedzią na problemy związane z globalnym expect(). I choć wymagał sporych zabaw (dodanie odpowiedniej konfiguracji w pliku jsconfig.json), to ostatecznie działało to dość fajnie i w testach pojawiały się podpowiedzi przy pisaniu asercji.

Do czasu. Pewnego dnia przestały i… prawdę mówiąc, nigdy nie miałem ochoty ani czasu zagłębiać się w to, co dokładnie się stało. Niemniej brak podpowiedzi przy pisaniu asercji niemal całkowicie eliminował jakąkolwiek sensowność globalnego expect(), bo obniżało to DX praktycznie do zera. Tak, piszę w chai od lat i mniej więcej pamiętam, jak wygląda składnia. Ale mimo wszystko byłoby fajnie widzieć jakiekolwiek potwierdzenie ze strony edytora, że nie piszę jakichś bzdur.

Możliwość rozwoju

No i wreszcie możliwość rozwoju tego cuda. Takim najprostszym ficzerem, jaki przyszedł mi do głowy, było dodanie obsługi kodu w TypeScripcie – zarówno źródłowego, jak i testów. Tylko że to wymagałoby dodania kolejnego hooka (prawdopodobnie opartego również na Babelu) i… wymyślenia, w jaki sposób przepisać to na ESM. Bo za każdym razem, gdy korzystam z jakiegokolwiek pakietu Sindre’a Sorhusa, przypominam sobie, że CJS jest skazane na zagładę i lepiej przesiąść się jak najszybciej, żeby potem nie płakać i nie utonąć w piekle dynamicznych importów.

A że ava nie ma problemów z natywnym wsparciem dla ESM w Node.js, jest o wiele wydajniejsza od mlt i dzięki swojemu API ma także bardzo ładne podpowiedzi w edytorze, to wybór wydaje się dość oczywisty. Zdecydowanie lepszym rozwiązaniem jest przesiadka na sprawdzone rozwiązanie, niż utopienie mnóstwa czasu, by próbować przepisać architekturę mlt. Tym bardziej, że w takim esm-loader-managerze, który – o ironio – powstał jako narzędzie dla mlt, używam właśnie avy – bo całość jest pisana z wykorzystaniem ESM.

Więc tak, mlt to kolejny projekt, który odesłałem na emeryturę, bo okazał się bardziej upierdliwy, niż początkowo zakładałem. I, o dziwo, po raz kolejny stało się tak z powodu natywnego wsparcia dla ESM w Node.js…