przemuh.dev

Uwaga na fixtury w cypress.io

6/26/2020

Dzisiaj opowiem wam kr贸tk膮 histori臋 o b艂臋dzie, kt贸ry kosztowa艂 mnie dwa dni poszukiwa艅. B艂臋dzie, kt贸ry koniec ko艅c贸w okaza艂 si臋 czym艣 bardzo trywialnym, a czas kt贸ry sp臋dzi艂em na debugowaniu spowodowany by艂 niedok艂adnym komunikatem o b艂臋dzie.

Hej Przemek! Czy m贸g艂by艣 mi pom贸c?

Kilka dni temu, zauwa偶y艂em, 偶e nasze testy VRT (Visual Regression Tests) zacz臋艂y si臋 sypa膰 dla jednego przypadku. Poprosi艂em moj膮 kole偶ank臋 z zespo艂u, Monik臋, aby rzuci艂a na to okiem. Monika przyj臋艂a wyzwanie i bezzw艂ocznie zacz臋艂a szuka膰 rozwi膮zania. Po ca艂ym dniu bezowocnych poszukiwa艅 powiedzia艂a, 偶e nie ma poj臋cia dlaczego to nie dzia艂a. Lokalnie test przechodzi艂 za ka偶dym razem, niestety na naszym GitlabCI by艂o zupe艂nie odwrotnie. Dziwna sprawa co nie? Jak to jest, 偶e "u mnie dzia艂a" a na CI ju偶 nie? Zrezygnowana Monika poprosi艂a mnie o pomoc. Po dw贸ch dniach kombinowania, commitowania, wypychania, czekania i sprawdzania w ko艅cu si臋 uda艂o.

Fake server

W naszej aplikacji do test贸w wykorzystujemy r贸偶ne narz臋dzia. Do unit test贸w mamy jest. Do test贸w E2E mamy py.testa z bindigami do webdrivera. Mamy te偶 testy UI, kt贸re sprawdzaj膮 nasz膮 aplikacj臋 pod k膮tem integracji pomi臋dzy komponentami, stronami czy widokami. Niedawno wprowadzili艣my tak偶e VRT - wizualne testy regresyjne. Te dwa ostatnie rodzaje test贸w wykorzystuj膮 cypress.io. Jest to 艣wietne narz臋dzie do pisania wszelakich test贸w - od unit贸w do E2E.

Nasza aplikacja od strony backendowej jest bardzo skomplikowana i ci臋偶ko j膮 postawi膰 w ca艂o艣ci na lokalnym komputerze, a nawet je艣li jest to mo偶liwe - kosztuje to wiele pracy i zasob贸w komputera. Dlatego do test贸w UI i VRT wykorzystujemy jeden z killer-feature贸w Cypressa, kt贸ry pozwala mockowa膰 zapytania do API. Cypress wpina si臋 pomi臋dzy aplikacj臋 i 偶膮danie do serwera, a my mo偶emy zdecydowa膰 o tym jak膮 odpowied藕 dostanie nasza aplikacja.

it("test with network stubbing", () => {
  // Po pierwsze musimy powiedzie膰, 偶e stawiamy fake-server
  cy.server()
  // Dalej deklarujemy jakie 艣cie偶ki chcemy mockowa膰
  cy.route("/api/endpoint", { value: 1 })
})

Wi臋cej o tym przeczytacie w oficjalnej dokumentacji Cypressa.

Fixtury

Fixtury to kolejna z funkcjonalno艣ci cypress.io, z kt贸rej korzystamy - szczeg贸lnie w testach VRT. Fixtura to nic innego jak pewne dane zapisane w pliku, kt贸re mo偶emy wykorzystywa膰 wiele razy. Pomaga to w organizacji test贸w i zarz膮dzaniu odpowiedziami z naszego cy.route. 呕eby za艂adowa膰 fixtur臋 korzystamy z komendy cy.fixture. Jako argument przyjmuje ona relatywn膮 艣cie偶k臋 do pliku, znajduj膮cego si臋 w katalogu z naszymi fixturami. Domy艣lnie katalog na fixtury nazywa si臋 po prostu fixtures, a same pliki z fixturami mog膮 mie膰 r贸偶ne rozszerzenia. Nie musz膮 te偶 by膰 u偶ywane w kontek艣cie cy.route. Wi臋cej o fixturach znajdziecie w oficjalnej dokumentacji cypressa.

Za艂贸偶my, 偶e mamy nast臋puj膮c膮 struktur臋 plik贸w:

- fixtures
    - myFixture.json
    - someSubFolder
          - mySecondFixture.json

Kod, kt贸ry wykorzystuje fixtury:

it("test with fixtures", () => {
  // Nie musimy deklarowa膰 rozszerzenia pliku
  // Cypress spr贸buje sam je odgadn膮膰
  cy.fixture("myFixture").then(data => {
    // Tutaj mo偶emy odczyta膰 dane
  })

  // Mo偶emy zapisa膰 dane w postaci aliasu ...
  cy.fixture("someSubFolder/mySecondFixture").as("myAlias")

  // 呕eby m贸c go wykorzysta膰 przy mockowaniu network requesta
  cy.route("/api/endpoint", "@myAlias")
})

Tw贸rcy cypressa zadbali te偶 o to, 偶eby艣my nie musieli pisa膰 tak du偶o boilerplateu 馃敟馃敟馃敟. Do komendy cy.route jako parametr reprezentuj膮cy odpowied藕 z serwera mo偶emy poda膰 skr贸t do fixtury fixture albo fx

cy.route("/api/path", "fixture:myFixture")
cy.route("/api/endpoint", "fx:someSubFolder/mySecondFixture")

W ten spos贸b zamockowali艣my sobie odpowiedzi z serwera, a dane trzymane s膮 w re-u偶ywalnych plikach z fixturami. Rewelacja!

Gdzie si臋 podzia艂 g艂贸wny bohater opowie艣ci?

No dobra, ale gdzie ten b艂膮d, z kt贸rym walczyli艣my dwa dni?

Na potrzeby reprodukcji utworzy艂em bardzo prost膮 aplikacj臋. Na pocz膮tku wy艣wietla ona napis Loading鈥, wykonuje zapytanie do serwera, a nast臋pnie podmienia tekst na to, co zwr贸ci艂 backend.

Pobieranie danych w starym, dobrym stylu XHR 馃槑

<body>
  <div id="main">Loading...</div>
  <script>
    const mainEl = document.querySelector("#main")

    const req = new XMLHttpRequest()
    req.open("GET", "/api/endpoint", true)
    req.onreadystatechange = function() {
      if (req.readyState == 4) {
        const msg = req.status == 200 ? req.responseText : "Error"
        mainEl.innerHTML = msg
      }
    }
    req.send(null)
  </script>
</body>

Do tego napisa艂em test:

describe("Simple fixture test", () => {
  it("displays response", function() {
    cy.server()
    cy.route("/api/endpoint", "fixture:examplefixture")

    cy.visit("/")

    cy.get("#main").should("have.text", "Hello")
  })
})

Oraz utworzy艂em plik z fixtur膮 w pliku fixtures/exampleFixture.json z tak膮 zawarto艣ci膮:

Hello

Czy widzisz ju偶 gdzie le偶y b艂膮d?

W odnalezieniu przyczyny problemu pom贸g艂 mi zrzut ekranu, kt贸ry cypress wykonuje w momencie kiedy test nie przejdzie. Kolejny killer feature 馃敟.

Zrzut ekranu dla testu
Zrzut ekranu dla testu

Czy teraz domy艣lasz si臋 gdzie by艂 b艂膮d?

Moj膮 uwag臋 przyku艂 komunikat o tym, 偶e route zosta艂 co prawda zestubowany, ale odpowiedzia艂 kodem 400, a nie 200, co spowodowa艂o b艂膮d w kolejnej komendzie oczekuj膮cej na element z tekstem "FolderA". Przypominam, 偶e "lokalnie" ten test nie mia艂 z tym problemu 馃槈.

Liter贸wka i systemy plik贸w

Nasz b艂膮d, kt贸ry pr贸bowali艣my rozwi膮za膰 z Monik膮, polega艂 na banalnej liter贸wce. Nazwa pliku z fixtur膮 zapisana by艂a camelCase, natomiast w kodzie testu mieli艣my wszystko ma艂ymi literami.

exampleFixture.json vs cy.route("/api", "fixture:examplefixture")

No ok, ale dlaczego to dzia艂a艂o lokalnie, a nie dzia艂a艂o na CI?

99% naszego zespo艂u frontendowego pracuje na MacBookach podczas gdy GitlabCi odpala testy w kontenerze dockerowym, kt贸ry opiera si臋 na Linuxie. Co to ma wsp贸lnego z fixturami i nasz膮 liter贸wk膮? Ot贸偶 system plik贸w u偶ywany domy艣lnie w Linuxie jest caseSensitive (zwraca uwag臋 na wielko艣膰 liter w nazwach plik贸w). MacOS oraz Windows domy艣lnie opieraj膮 si臋 o system plik贸w, kt贸ry caseSensitive nie jest. Co to oznacza w praktyce?

Na Linuxie mo偶emy utworzy膰 pliki o tej samej nazwie, ale pisane nieco inaczej np.

  • myAwesomeFile.js
  • myawesomefile.js

Linux potraktuje je jako dwa osobne pliki. Natomiast Windows i MacOS nie pozwol膮 na stworzenie drugiego pliku o tej samej nazwie (pisanej np. camelCase). Przy pr贸bie odczytu pliku np. w node.js na MacOS nie ma znaczenia czy wpiszemy myFixture czy mYFiXtURe - plik zostanie za艂adowany. Na Linuxie natomiast dostaniemy b艂膮d odczytu - plik nie zosta艂 odnaleziony.

Sprawdzam

I faktycznie tak jest. Gdy zmodyfikujemy kod naszego testu w ten spos贸b:

cy.route("/api/endpoint", "fixture:ExAmPlEFiXTuRe")

Test na Macu przechodzi zawsze. Na Linuxie natomiast, cypress logger poka偶e nam stuba z kodem 400:

Zrzut ekranu z kodem 400
Zrzut ekranu z kodem 400

a w konsoli dostaniemy b艂膮d:

CypressError: The following error originated from your application code, not from Cypress.

When Cypress detects uncaught errors originating from your application it will automatically fail the current test.

This behavior is configurable, and you can choose to turn this off by listening to the `uncaught:exception` event.

https://on.cypress.io/uncaught-exception-from-application

Ale jak to 鈥渂艂膮d nie jest w cypressie tylko w mojej apce鈥? Przecie偶 apka dzia艂a, test na Macu te偶 przechodzi - wi臋c o co kaman?

Spr贸bujmy skorzysta膰 z naszej fixtury za pomoc膮 cy.fixture zamiast skr贸tu:

// Celowy b艂膮d w nazwie fixtury
cy.fixture("examplEFixture").as("response")
cy.route("/api/endpoint", "fixture:examplefixture")

// Bonusem takiego podej艣cia jest mo偶liwo艣膰 u偶ycia aliasu do odczytu danych
// zamiast hardcodowa膰 "Hello" w te艣cie
cy.get("@response").then(data => {
  cy.get("#main").should("have.text", data)
})

Jaki teraz dostaniemy b艂膮d?

Error: A fixture file could not be found at any of the following paths:

> cypress/fixtures/examplEFixture
> cypress/fixtures/examplEFixture{{extension}}

Cypress looked for these file extensions at the provided path:
.json, .js, .coffee, .html, .txt, .csv, .png, .jpg, .jpeg, .gif, .tif, .tiff, .zip

Provide a path to an existing fixture file.

馃憦 no i taki komunikat b艂臋du jest o wiele lepszy. Od razu wiemy gdzie nale偶y szuka膰 馃槑

Podsumowanie

Z tej historii mo偶na wyci膮gn膮膰 dwa wnioski:

  • ma艂a liter贸wka mo偶e przysporzy膰 Ci kilka dni debugowania
  • jeste艣 tak dobry jak komunikaty o b艂臋dzie z twojego test runnera 馃槈

My艣l臋, 偶e cypress m贸g艂by zwraca膰 鈥減oprawny鈥 komunikat o b艂臋dnej fixturze zamiast CypressError- dlatego zg艂osi艂em issue, kt贸rego status mo偶ecie 艣ledzi膰 tutaj.

Dzi臋ki, 偶e wytrwali艣cie ze mn膮 w tej historii do ko艅ca. Lec臋 spr贸bowa膰 naprawi膰 issue, kt贸re sam zg艂osi艂em 馃槈. Mo偶e uda mi si臋 do艂o偶y膰 kolejn膮 cegie艂k臋 do OpenSource i sprawdzi膰 by Cypress, kt贸ry jest niesamowitym narz臋dziem, sta艂 si臋 jeszcze lepszy 馃榿