4.05.20237 min
Ildar Sharafeev

Ildar SharafeevSenior Software Engineer / Tech Lead

Enzyme is dead - jak z niego migrować?

Sprawdź dlaczego oraz jak migrować z Enzyme'a w React.

Enzyme is dead - jak z niego migrować?

Enzyme to popularna biblioteka testowa dla aplikacji React. Ma jednak pewne ograniczenia. Jedną z głównych krytycznych uwag na temat Enzyme jest to, że zachęca do stylu testowania skupionego na szczegółach implementacji, a nie na zachowaniu. Może to prowadzić do kruchości testów, które łatwo się załamują, gdy tylko zmienia się implementacja komponentu. Kolejnym ograniczeniem biblioteki Enzyme jest to, że jest ona ściśle powiązana z biblioteką React. Staje się to sporym wyzwaniem, jeśli chcemy zastosować ją z innymi bibliotekami lub frameworkami i może utrudnić pisanie testów, które są naprawdę odizolowane od implementacji komponentu.

Jeśli Twój projekt używa Reacta i Enzyme'a i marzysz o aktualizacji Reacta do wersji 18 (i cieszeniu się fajnymi funkcjami jak renderowanie po stronie serwera lub współbieżne) to mam dla Ciebie złą wiadomość - Enzyme nie będzie kompatybilny z przyszłymi wersjami Reacta (nadal możesz znaleźć kilka nieoficjalnych bibliotek, ale raczej im nie ufaj).

Celem tego artykułu jest nie tylko wyjaśnienie podejścia, jakie możesz zastosować, aby przenieść swój kod testowy do nowej biblioteki, ale również pokazanie Ci, jak zautomatyzować monitorowanie postępu migracji.

A więc czas na migrację!

Migracja z Enzyme do innej biblioteki testowej może być trudnym zadaniem, szczególnie jeśli pracujesz nad dużą bazą kodu. Jednak dzięki podejściu przyrostowemu można sprawić, że proces ten będzie znacznie łatwiejszy do opanowania.

Migracja przyrostowa polega na stopniowym przenoszeniu testów z Enzyme do nowej biblioteki, zamiast próby robienia wszystkiego naraz. Takie podejście ma kilka zalet:

  • Dzięki temu można przetestować nową bibliotekę w mniejszym, bardziej kontrolowanym środowisku przed zaangażowaniem się w całościową migrację.
  • Minimalizuje ryzyko wprowadzenia regresji lub przerwania istniejących testów.
  • To z kolei umożliwia naukę i zapoznanie się z nową biblioteką w miarę upływu czasu, zamiast próbować przyswoić wszystko na raz.

Tutaj przykład jak możesz podejść do przyrostowej migracji z Enzyme do nowej biblioteki, np. React Testing Library (RTL):

  1. Zacznij od zidentyfikowania testów w swojej bazie kodów, które używają biblioteki Enzym. Są to zazwyczaj testy, które importują metody biblioteki Enzym lub specyficzne dla niej (takie jak shallow lub mount).
  2. Zacznij od migracji małego podzbioru tych testów do RTL. Może to być komponent lub zestaw komponentów, z którymi jesteś już zaznajomiony, lub sekcja bazy danych, która nie ma wielu zależności.
  3. Podczas migracji każdego testu, zwróć uwagę na to, jak test jest zbudowany i jak współdziała z testowanym komponentem. Zwróć uwagę na wszelkie różnice w tym, jak nowa biblioteka obsługuje takie rzeczy jak zapytania o elementy lub symulowanie zdarzeń.
  4. W miarę migracji większej liczby testów, zaczniesz lepiej rozumieć, jak RTL różni się od Enzymu. Wykorzystaj tę okazję do nauki, aby móc refaktoryzować swoje testy i poprawić ich ogólną strukturę i czytelność.
  5. Powtarzaj kroki 2-4, aż wszystkie twoje testy będą używać nowej biblioteki.
  6. I na koniec, po zakończeniu migracji, upewnij się, że uruchomiłeś zestaw testów, aby upewnić się, że wszystko działa zgodnie z oczekiwaniami.


Ogólnie rzecz biorąc, migracja przyrostowa jest świetnym sposobem na odejście od Enzymu i przejście do nowej biblioteki testowej. Stosując podejście krok po kroku, można zminimalizować ryzyko wystąpienia awarii, nauczyć się nowej biblioteki w trakcie pracy i sprawić, że ogólny proces migracji będzie znacznie łatwiejszy do przeprowadzenia. Trudno jednak stwierdzić, czy takie podejście jest skuteczne, czy nie, nie znając metryk opisujących ten sukces. Jako tech lead zespołu, skąd możesz wiedzieć, że zespół podąża za strategią, którą wymyśliłeś i jak daleko jest jej koniec (krok #6)?

TLDR; link do kodu

Repozytorium na GitHubie

Napisz plugin do śledzenia postępów!

Jakiego narzędzia zwykle używasz, aby egzekwować styl kodowania i znaleźć potencjalne błędy w projekcie? Prawdopodobnie robisz to dobrze  — ESLint! (Jeśli odpowiedziałeś TSLint, to trochę się opuściłeś w nauce!). ESLint jest wysoce konfigurowalny, możesz ustawić własne reguły, użyć własnego formatera lub połączyć oba wewnątrz jednej wtyczki! Można go również łatwo zintegrować z przepływem pracy nad rozwojem, takim jak potok ciągłej integracji, tak aby automatycznie zgłaszać wszelkie problemy przed ich dopuszczeniem.

Napisanie własnego pluginu do ESLinta nie jest tak trudne, jak mogłoby się wydawać, ale może być konieczne poświęcenie większej ilości czasu na głębsze poznanie Abstract Syntax Tree (drzewiastej reprezentacji abstrakcyjnej struktury syntaktycznej kodu źródłowego napisanego w języku programowania) oraz selektorów ESLinta aby przemierzać to drzewo. Bez dalszych ceregieli pozwolę sobie przedstawić moje rozwiązanie:

const noShallowRule = require("./rules/no-shallow");
const noMountRule = require("./rules/no-mount");

const rules = {
  "no-shallow": noShallowRule,
  "no-mount": noMountRule,
};
module.exports = {
  rules,
  configs: {
    recommended: {
      rules,
    },
  },
};


KONIEC!


Żartowałem! Teraz będzie najciekawsza część.

Reguły to reguły

Zagłębmy się bardziej w kod reguły. Obie reguły no-shallow i no-mount używają tej samej logiki (pomysł, aby je rozdzielić jest po prostu po to, aby dać użytkownikom większą elastyczność w tym, czego chcieliby się pozbyć), więc zagłębmy się w temat jednego z nich (wybrałem shallow):

const schema = require("./schema");
const astUtils = require("ast-utils");

const resolveEnzymeIdentifierInScope = (scope, name) => {
  if (!scope) {
    return false;
  }
  const node = scope.set.get(name);
  if (node != null) {
    const nodeDef = node.defs[0];
    if (
      nodeDef.type === "ImportBinding" &&
      nodeDef.parent.source.value === "enzyme"
    ) {
      return true;
    }

    if (
      astUtils.isStaticRequire(nodeDef.node.init) &&
      astUtils.getRequireSource(nodeDef.node.init) === "enzyme"
    ) {
      return true;
    }
  }

  return false;
};

module.exports = {
  meta: {
    messages: {
      noShallowCall: "Enzyme is deprecated: do not use shallow API.",
    },
    docs: {
      description: "Disallow Enzyme shallow rendering",
      category: "Tests",
      recommended: true,
    },
    schema,
    fixable: null,
  },

  create(context) {
    const [options = {}] = context.options || [];
    return {
      "CallExpression"(node) {
        if (
          node.callee.name !== "shallow" &&
          node.callee.property?.name !== "shallow"
        ) {
          return;
        }
        let targetDeclarationName = "shallow";
        if (node.callee.property?.name === "shallow") {
          targetDeclarationName = node.callee.object.name;
        }
        const resolved = context
          .getScope()
          .references.find(
            ({ identifier }) => identifier.name === targetDeclarationName
          ).resolved;
        const isEnzyme = resolveEnzymeIdentifierInScope(
          resolved?.scope,
          targetDeclarationName
        );
        if (isEnzyme || options.implicitlyGlobal) {
          context.report({ node, messageId: "noShallowCall" });
        }
      },
    };
  },
};
  • CallExpressionto selektor biblioteki Enzym, który mówi jej, że interesują nas tylko wywołania funkcji. Selektory te są dość podobne do selektorów CSS, więcej dowiesz się o nich tutaj.
  • node.callee.nameodnosi się do nazwy wywołanej funkcji (w naszym przypadku shallow), natomiast node.callee.property?.name sprawdza, czy ta funkcja została wywołana jako właściwość obiektu wyższego rzędu (na przykład, const enzymeApi = require('enzyme'); enzymeApi.shallow(<Component />).
  • context.getScope()podaje odwołanie do zakresu, w którym wywołano funkcję docelową (shallow) oraz posiada odwołanie do obiektu, który jest właścicielem tej metody. Generalnie, to co musimy tutaj sprawdzić to czy metoda shallow należy do źródła Enzymu - zwykle Enzymu zaimportowanego do modułu testowego, lub wymaganego jeśli używasz CommonJS (jeśli ciekawi Cię, jak możesz napisać bibliotekę produkującą build zarówno dla EcmaScript Modules jak i targetów CommonJS, przejdź do tego artykułu).
  • options.implicitlyGlobalto opcja, która może być dostarczona przez konsumenta reguły, na przykład w pliku konfiguracyjnym .eslintrc.js. W tym konkretnym przykładzie pozwala użytkownikom powiedzieć regule, że nie są zainteresowani źródłem, z którego pochodzi shallow (być może, przypisałeś ją do globalnego zakresu gdzieś w swoim testowym przepływie konfiguracji - IMHO zły pomysł).

Raportuj postępy

Dla tych dzielnych śmiałków, którym udało się dotrzeć do tej części artykułu, dziękuję i kontynuujmy dalej! W tej chwili mamy reguły, których możemy użyć, aby zapobiec umieszczaniu przestarzałych interfejsów API jako części nowego i zmienionego kodu w swoich PR-ach (miejmy nadzieję, że używasz API findRelatedTests jako części swojego przepływu Git pre-commit).

Ale nadal nie wiemy, jak sprawy wyglądają z lotu ptaka - być może zespół pracuje głównie nad nowymi częściami projektu, całkowicie zapominając o niektórych starszych systemach (lub omijając hook pre-commit - yikes!). W tym przypadku musimy napisać niestandardowy formater, aby wyprowadzić statystyki.

Nie chcę wrzucać tutaj ogromnych bloków kodu - możesz je znaleźć w moim repozytorium na GitHubie, ale postaram się krótko wyjaśnić, jak to działa. Po uruchomieniu reguł i zebraniu błędów dla każdego pliku testowego, wtyczka ESLint przekazuje te metadane do formatera:

{ 
    filePath: string; 
    messages: Array<{ruleId: string;}> 
}


W formaterze, grupujemy te dane według ścieżki pliku i id naruszonej reguły (np. enzyme-deprecation/no-shallow), i przekazujemy te przetworzone dane do systemu wizualizacji, który może wyprowadzić te dane w różnych formatach. Kilka pomysłów jakie mogą być te formaty:

  • Wykresy drukowane w ASCII w terminalu (do pracy w środowisku lokalnym/deweloperskim)
  • Plik oparty na Markdown zapisywany do systemu plików i wrzucany do repozytorium Git (do przeglądania postępów po każdym PR)
  • Strona HTML wykorzystująca piękne biblioteki wykresów (jak D3.js) zapisana w folderze coverage (zakładając, że możesz mieć już jakieś integracje z tym folderem w swoim narzędziu do code review)
  • Wiadomość typu plain string przekazana do jakiegoś adresu URL elementu webhook (np. powiadomienie ze Slacka na kanał code review)

Jak to wykorzystać w swoim projekcie?

Wariant 1: Zdefiniuj oddzielny config ESLint dla migracji

.eslintrc.migration.js:

module.exports = {
  parser: '<your-parser>',
  extends: ['plugin:enzyme-deprecation/recommended'],
  env: {
    browser: true,
  },
  rules: {
    'enzyme-deprecation/no-shallow': 2
  }
};


I w swoim pliku package.json zdefiniuj polecenie:

"track:migration": "NODE_ENV=development eslint --no-eslintrc --config .eslintrc.migration.js -f node_modules/eslint-plugin-enzyme-deprecation/lib/formatter --ext .test.jsx src/"


Wariant 2: Używanie API Node.js

Przykład znajdziesz tutaj (uruchom polecenie npm run demo w katalogu głównym)

Podsumowanie

Podsumowując, podejście do migracji przyrostowej, w połączeniu z automatyzacją monitorowania, może być bardzo pomocne w kontrolowanej i efektywnej migracji bazy kodu do nowej biblioteki testowej. Pozwoli ci to napisać bardziej spójny, wolny od błędów kod i wyłapać problemy na wczesnym etapie procesu rozwoju.



Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>