Pomysły na konfigurację, organizację i konsolidację testów cyprysowych #frontend@twiliosendgrid

Opublikowany: 2020-12-15

Napisaliśmy wiele testów Cypress w różnych aplikacjach internetowych i zespołach frontendowych w Twilio SendGrid. Ponieważ nasze testy przeskalowały się do wielu funkcji i stron, natknęliśmy się na kilka przydatnych opcji konfiguracyjnych i opracowaliśmy sposoby na lepsze utrzymywanie rosnącej liczby plików, selektorów elementów naszej strony i wartości limitów czasu.

Naszym celem jest przedstawienie tych wskazówek i pomysłów na konfigurowanie, organizowanie i konsolidowanie własnych rzeczy związanych z Cypressem, więc nie krępuj się robić z nimi tego, co jest najlepsze dla Ciebie i Twojego zespołu.

Ten wpis na blogu zakłada, że ​​masz pewną praktyczną wiedzę na temat testów Cypress i szukasz pomysłów na lepsze utrzymywanie i ulepszanie testów Cypress. Jeśli jednak chcesz dowiedzieć się więcej o typowych funkcjach, asercjach i wzorcach, które mogą okazać się przydatne do pisania testów Cypress w różnych środowiskach, możesz zamiast tego zapoznać się z tym trzymetrowym, przeglądowym wpisem na blogu.

W przeciwnym razie przejdźmy dalej i najpierw zapoznajmy się z kilkoma wskazówkami dotyczącymi konfiguracji Cypress.

Konfigurowanie cypress.json

W pliku cypress.json można ustawić całą konfigurację testów Cypress, taką jak podstawowe limity czasu dla poleceń Cypress, zmienne środowiskowe i inne właściwości.

Oto kilka wskazówek, które możesz wypróbować w swojej konfiguracji:

  • Dostosuj swoje podstawowe limity czasu poleceń, aby nie zawsze dodawać { timeout: timeoutInMs } wszystkich poleceń Cypress. Majstruj przy liczbach, aż znajdziesz odpowiednią równowagę dla ustawień, takich jak „defaultCommandTimeout”, „requestTimeout” i „responseTimeout”.
  • Jeśli Twój test obejmuje elementy iframe, najprawdopodobniej musisz ustawić „chromeWebSecurity” na wartość false, aby uzyskać dostęp do elementów iframe z różnych źródeł w swojej aplikacji.
  • Spróbuj zablokować zewnętrzne skrypty marketingowe, analityczne i rejestrujące podczas wykonywania testów Cypress, aby zwiększyć szybkość i nie wywoływać niepożądanych zdarzeń. Możesz łatwo skonfigurować listę odrzuconych za pomocą ich właściwości „blacklistHosts” (lub właściwości „blockHosts” w Cypress 5.0.0), przyjmując tablicę globów pasujących do ścieżek hostów innych firm.
  • Dostosuj domyślne wymiary rzutni za pomocą „viewportWidth” i „viewportHeight”, gdy otwierasz GUI Cypress, aby wszystko było lepiej widoczne.
  • Jeśli w Dockerze występują problemy z pamięcią w przypadku ważkich testów lub chcesz, aby wszystko było wydajniejsze, możesz spróbować zmodyfikować „numTestsKeptInMemory” na mniejszą liczbę niż domyślna i ustawić „videoUploadOnPasses” na false, aby skupić się na przesyłaniu filmów w przypadku nieudanego testu działa tylko.

Z drugiej strony, po ulepszeniu konfiguracji Cypress, możesz również dodać TypeScript i lepiej wpisać swoje testy Cypress, tak jak zrobiliśmy to w tym poście na blogu. Jest to szczególnie korzystne w przypadku automatycznego uzupełniania, ostrzeżeń typu lub błędów podczas wywoływania i łączenia funkcji (takich jak cy.task('someTaskPluginFunction) , Cypress.env('someEnvVariable') lub cy.customCommand() ).

Możesz także poeksperymentować z konfiguracją podstawowego pliku cypress.json i ładowaniem oddzielnego pliku konfiguracyjnego Cypress dla każdego środowiska testowego, takiego jak staging.json , gdy uruchamiasz różne skrypty Cypress w swoim package.json . Dokumentacja Cypress świetnie radzi sobie z przejściem przez to podejście.

Organizowanie folderu Cypress

Cypress już konfiguruje folder cypress najwyższego poziomu ze strukturą z przewodnikiem, gdy po raz pierwszy uruchamiasz Cypress w swojej bazie kodu. Obejmuje to inne foldery, takie jak integration dla plików specyfikacji, fixtures dla plików danych JSON, plugins dla cy.task(...) i innej konfiguracji oraz folder support dla niestandardowych poleceń i typów.

Dobrą zasadą, której należy przestrzegać podczas organizowania w folderach Cypress, komponentach React lub ogólnie w dowolnym kodzie, jest umieszczanie razem rzeczy, które mogą się razem zmieniać. W tym przypadku, ponieważ mamy do czynienia z aplikacjami internetowymi w przeglądarce, jednym z rozwiązań, które dobrze się skaluje, jest grupowanie elementów w folderze według strony lub nadrzędnej funkcji.

Utworzyliśmy osobny folder pages podzielony według nazwy funkcji, takiej jak „SenderAuthentication”, aby przechowywać wszystkie obiekty strony pod trasą /settings/sender_auth , takie jak „DomainAuthentication” (/settings/sender_auth/domains/**/*) i „LinkBranding” (/settings/sender_auth/links/**/*). W naszym folderze plugins zrobiliśmy to samo, organizując wszystkie pliki wtyczek cy.task(...) dla określonej funkcji lub strony w tym samym folderze dla uwierzytelniania nadawcy i zastosowaliśmy to samo podejście dla naszego folderu integration dla pliki specyfikacji. Przeskalowaliśmy nasze testy do setek plików specyfikacji, obiektów stron, wtyczek i innych plików w jednej z naszych baz kodu — a nawigacja po nich jest łatwa i wygodna.

Oto zarys tego, jak zorganizowaliśmy folder cypress .

Jeszcze jedną rzeczą do rozważenia podczas organizowania folderu integration (w którym znajdują się wszystkie specyfikacje) jest potencjalnie dzielenie plików specyfikacji na podstawie priorytetu testu. Na przykład możesz mieć wszystkie testy o najwyższym priorytecie i wartości w folderze „P1”, a testy o drugim priorytecie w folderze „P2”, dzięki czemu możesz łatwo uruchomić wszystkie testy „P1”, ustawiając opcję --spec , taką jak --spec 'cypress/integration/P1/**/*' .

Utwórz hierarchię folderów specyfikacji, która będzie dla Ciebie odpowiednia, abyś mógł łatwo grupować specyfikacje nie tylko według strony lub funkcji, takiej jak --spec 'cypress/integration/SomePage/**/*' , ale także według innych kryteriów, takich jak priorytet, produkt lub środowisko.

Konsolidacja selektorów elementów

Podczas tworzenia komponentów React na naszej stronie zazwyczaj dodajemy pewien poziom testów jednostkowych i integracyjnych za pomocą Jest i Enzyme. Pod koniec opracowywania funkcji dodajemy kolejną warstwę testów Cypress E2E, aby upewnić się, że wszystko działa z backendem. Zarówno testy jednostkowe dla naszych komponentów React, jak i obiekty strony dla naszych testów Cypress E2E wymagają selektorów do komponentów/elementów DOM na stronie, z którą chcemy wejść w interakcję i na której chcemy przeprowadzić asercję.

Kiedy aktualizujemy te strony i komponenty, istnieje ryzyko wystąpienia dryftu i błędów wynikających z konieczności synchronizacji wielu miejsc, od selektorów testów jednostkowych do obiektu strony Cypress do samego kodu komponentu. Gdybyśmy polegali wyłącznie na nazwach klas związanych ze stylami, trudno byłoby pamiętać o aktualizacji wszystkich miejsc, które mogą się zepsuć. Zamiast tego dodajemy „data-hook”, „data-testid” lub inny spójnie nazwany atrybut „data-*” do określonych komponentów i elementów na stronie, które chcemy zapamiętać, i zapisujemy dla nich selektor w naszych warstwach testowych.

Możemy dodać atrybuty „data-hook” do wielu naszych elementów, ale nadal potrzebowaliśmy sposobu na zgrupowanie ich w jednym miejscu, aby zaktualizować i ponownie wykorzystać w innych plikach. Wymyśliliśmy typowy sposób zarządzania wszystkimi tymi selektorami „zaczepu danych”, które można rozmieścić na naszych komponentach React i wykorzystać w naszych testach jednostkowych i selektorach obiektów strony, aby zapewnić więcej możliwości ponownego użycia i łatwiejszej konserwacji eksportowanych obiektów.

Dla folderu najwyższego poziomu każdej strony utworzylibyśmy plik hooks.ts , który zarządza i eksportuje obiekt z czytelną nazwą klucza dla elementu i rzeczywistym selektorem łańcucha CSS dla elementu jako wartością. Nazywamy je „selektorami odczytu”, ponieważ musimy odczytać i użyć formularza selektora CSS dla elementu w wywołaniach, takich jak wrapper.find(“[data-hook='selector']”) w naszych testach jednostkowych lub Cypress cy.get(“[data-hook='selector']”) w naszych obiektach strony. Te wywołania będą wtedy wyglądały czyściej, jak wrapper.find(Selectors.someElement) lub cy.get(Selectors.someElement) .

Poniższy fragment kodu zawiera więcej informacji o tym, dlaczego i jak używamy tych selektorów odczytu w praktyce.

Podobnie eksportujemy również obiekty z czytelną nazwą klucza dla elementu i obiektem takim jak { “data-hook”: “selector” } jako wartość. Nazwiemy je „selektorami zapisu”, ponieważ musimy zapisać lub rozmieścić te obiekty w komponencie React jako rekwizyty, aby pomyślnie dodać atrybut „data-hook” do podstawowych elementów. Celem byłoby zrobienie czegoś takiego a rzeczywisty element DOM poniżej — zakładając, że właściwości są prawidłowo przekazywane do elementów JSX — również będzie miał ustawiony atrybut „data-hook=”.

Poniższy fragment kodu zawiera więcej informacji o tym, dlaczego i jak używamy tych selektorów zapisu w praktyce.

Możemy tworzyć obiekty, aby skonsolidować nasze selektory odczytu i zapisywać selektory, aby aktualizować mniej miejsc, ale co by było, gdybyśmy musieli napisać wiele selektorów dla niektórych z naszych bardziej złożonych stron? Może to być bardziej podatne na błędy podczas samodzielnego budowania, więc stwórzmy funkcje, aby łatwo generować te selektory odczytu i selektory zapisu, aby ostatecznie wyeksportować dla określonej strony.

W przypadku funkcji generatora selektorów odczytu przejdziemy w pętli przez właściwości obiektu wejściowego i utworzymy ciąg selektora CSS [data-hook=”selector”] dla każdej nazwy elementu. Jeśli odpowiadająca mu wartość klucza to null , przyjmiemy, że nazwa elementu w obiekcie wejściowym będzie taka sama jak wartość „data-hook”, np. { someElement: null } => { someElement: '[data-hook=”someElement”] } . W przeciwnym razie, jeśli odpowiednia wartość klucza nie jest pusta, możemy nadpisać tę wartość „data-hook”, na przykład { someElement: “newSelector” } => { someElement: '[data-hook=”newSelector”]' } .

W przypadku funkcji generatora selektorów zapisu przejdziemy w pętli przez właściwości obiektu wejściowego i utworzymy obiekty { “data-hook”: “selector” } dla każdej nazwy elementu. Jeśli odpowiadająca mu wartość klucza to null , przyjmiemy, że nazwa elementu w obiekcie wejściowym będzie taka sama jak wartość „data-hook”, np. { someElement: null } => { someElement: { “data-hook”: “someElement” } } . W przeciwnym razie, jeśli odpowiednia wartość klucza nie jest null , możemy nadpisać tę wartość „data-hook”, np. { someElement: “newSelector” } => { someElement: { “data-hook”: “newSelector” }' } .

Korzystając z tych dwóch funkcji generatora, budujemy nasz selektor odczytu i obiekty selektora zapisu dla strony i eksportujemy je do ponownego użycia w naszych testach jednostkowych i obiektach strony Cypress. Kolejną zaletą jest to, że te obiekty są wpisywane w taki sposób, że nie możemy przypadkowo zrobić Selectors.unknownElement lub WriteSelectors.unknownElement w naszych plikach TypeScript. Przed wyeksportowaniem naszych selektorów odczytu zezwalamy również na dodanie dodatkowych mapowań elementów i selektorów CSS dla komponentów innych firm, nad którymi nie mamy kontroli. W niektórych przypadkach nie możemy dodać atrybutu „data-hook” do niektórych elementów, więc nadal musimy wybierać według innych atrybutów, identyfikatorów i klas oraz dodać więcej właściwości do obiektu selektora odczytu, jak pokazano poniżej.

Ten wzorzec pomaga nam zachować porządek we wszystkich naszych selektorach dla strony i kiedy musimy zaktualizować rzeczy. Zalecamy zbadanie sposobów zarządzania wszystkimi tymi selektorami w jakimś obiekcie lub w inny sposób, aby zminimalizować liczbę plików, które trzeba zmienić w przypadku przyszłych zmian.

Limity czasu konsolidacji

Jedną z rzeczy, które zauważyliśmy po napisaniu wielu testów Cypressa, było to, że nasze selektory dla niektórych elementów wygasały i zabierały więcej czasu niż domyślne wartości limitu czasu ustawione w naszym cypress.json , takie jak „defaultCommandTimeout” i „responseTimeout”. Wracaliśmy i dostosowywaliśmy niektóre strony, które wymagały dłuższych limitów czasu, ale z czasem liczba arbitralnych wartości limitów czasu rosła i utrzymywanie tego stało się trudniejsze w przypadku zmian na dużą skalę.

W rezultacie skonsolidowaliśmy nasze limity czasu w obiekcie zaczynającym się powyżej naszego „defaultCommandTimeout”, który znajduje się gdzieś w zakresie od pięciu do dziesięciu sekund, aby pokryć większość ogólnych limitów czasu dla naszych selektorów, takich jak cy.get(...) lub cy.contains(...) . Poza tym domyślnym limitem czasu skalowalibyśmy się do „krótkiego”, „średniego”, „długiego”, „xlong” i „xxlong” w obiekcie limitu czasu, który możemy zaimportować w dowolnym miejscu w naszych plikach, aby użyć go w naszych poleceniach Cypress, takich jak cy.get(“someElement”, { timeout: timeouts.short }) .get(“someElement” cy.get(“someElement”, { timeout: timeouts.short }) lub cy.task('pluginName', {}, { timeout: timeouts.xlong }) . Po zastąpieniu naszych limitów czasu tymi wartościami z naszego importowanego obiektu, mamy jedno miejsce do aktualizacji, aby skalować w górę lub w dół czas potrzebny na określone limity czasu.

Przykład, jak łatwo zacząć od tego, pokazano poniżej.

Zawijanie

Gdy twoje zestawy testów Cypress rosną, możesz napotkać te same problemy, które zrobiliśmy, zastanawiając się, jak najlepiej skalować i utrzymywać testy Cypress. Możesz organizować swoje pliki według strony, funkcji lub innej konwencji grupowania, dzięki czemu zawsze wiesz, gdzie szukać istniejących plików testowych i gdzie dodawać nowe, gdy więcej programistów wnosi wkład do Twojej bazy kodu.

Gdy interfejs użytkownika się zmieni, możesz użyć pewnego rodzaju wpisanego obiektu (takiego jak selektory odczytu i zapisu), aby zachować selektory atrybutów „data-” dla kluczowych elementów każdej strony, na których chcesz zatwierdzić lub wchodzić w interakcje z testami jednostkowymi i Cypressem testy. Jeśli zaczniesz stosować zbyt wiele arbitralnych wartości dla takich rzeczy, jak wartości limitu czasu dla poleceń Cypress, być może nadszedł czas, aby skonfigurować obiekt wypełniony wartościami skalowanymi, aby móc zaktualizować te wartości w jednym miejscu.

Ponieważ rzeczy zmieniają się w interfejsie użytkownika, interfejsie API zaplecza i testach Cypress, zawsze powinieneś myśleć o sposobach łatwiejszego utrzymania tych testów. Mniej miejsc do aktualizacji i popełniania błędów oraz mniej niejasności co do tego, gdzie umieścić rzeczy, ma ogromne znaczenie, ponieważ wielu programistów dodaje nowe strony, funkcje i (nieuchronnie) testy Cypressa.

Interesują Cię więcej postów na Cypress? Zajrzyj do następujących artykułów:

  • Co wziąć pod uwagę podczas pisania testów E2E
  • 1000 stóp Przegląd pisania testów na cyprys
  • TypeScript Wszystkie rzeczy w twoich testach cyprysowych
  • Radzenie sobie z przepływami wiadomości e-mail w testach cyprysowych
  • Integracja testów Cypress z Docker, Buildkite i CICD