Skip to content

Commit 5f28a74

Browse files
committed
moar 5e
1 parent 7ff4bb4 commit 5f28a74

File tree

1 file changed

+60
-180
lines changed

1 file changed

+60
-180
lines changed

src/content/5/fi/osa5e.md

Lines changed: 60 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ test('login fails with wrong password', async ({ page }) =>{
675675
})
676676
```
677677
678-
### Operaatioiden tekeminen käyttöliittymän "ohi"
678+
### Testien apufunktiot
679679
680680
Sovelluksemme testit näyttävät tällä hetkellä seuraavalta:
681681
@@ -719,60 +719,14 @@ Ensin siis testataan kirjautumistoimintoa. Tämän jälkeen omassa describe-lohk
719719
720720
Kuten aiemmin jo todettiin, jokainen testi suoritetaan alkutilasta, eli vaikka testi on koodissa alempana, se ei aloita samasta tilasta mihin ylempänä koodissa olevat testit ovat jääneet!
721721
722-
Vanha E2E-testaajan viisausu kuuluu _Fully test the login flow – but only once_. Eli sen sijaan että tekisimme <i>beforeEach</i>-lohkossa kirjaantumisen lomaketta käyttäen, on parempi idea että kirjaantuminen tehdään UI:n ohi, tekemällä suoraan backendiin kirjaantumista vastaava HTTP-operaatio. Syynä tälle on se, että suoraan backendiin tehtynä kirjautuminen on huomattavasti nopeampi kuin lomakkeen täyttämällä.
723-
724-
Muutetaan _beforeEach_-lohkossa tapahtuvaa kirjautumista seuraavasti:
725-
726-
```js
727-
describe('Note app', () => {
728-
// ...
729-
730-
describe('when logged in', () => {
731-
beforeEach(async ({ page, request }) => {
732-
// highlight-start
733-
const response = await request.post('http://localhost:3001/api/login', {
734-
data: {
735-
name: 'Matti Luukkainen',
736-
username: 'mluukkai',
737-
password: 'salainen'
738-
}
739-
})
740-
741-
await page.evaluate((body) => {
742-
localStorage.setItem('loggedNoteappUser', JSON.stringify(body));
743-
}, await response.json())
744-
page.reload()
745-
// highlight-end
746-
})
747-
748-
it('a new note can be created', function() {
749-
// ...
750-
})
751-
752-
// ...
753-
})
754-
755-
// ...
756-
})
757-
```
758-
759-
API:n läpi tapahtuva kirjautuminen tehdään tuttuun tapaan parametrina saatavan olion _request_ metodilla [post](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-post). Vastauksena saatu token talletetaan localStorageen testattavaa sovellusta vastaavan olion _page_ metodilla [evaluate](https://playwright.dev/docs/api/class-page#page-evaluate). Sivu uudelleenladataan metodilla [reload](https://playwright.dev/docs/api/class-page#page-reload) tokenin tallennuksen jäleen.
760-
761-
Jos ja kun sovellukselle kirjoitetaan lisää testejä, joudutaan kirjautumisen hoitavaa koodia soveltamaan useassa paikassa. Koodi kannattaakin eristää apufunktioksi, joka sijoitetaan esim. tiedostoon _tests/helper.js_:
722+
Myös testeissä kannattaa pyrkiä toisteettomaan koodiin. Eristetään kirjautumisen hoitava koodi apufunktioksi, joka sijoitetaan esim. tiedostoon _tests/helper.js_:
762723
763724
```js
764-
const loginWith = async (page, request, username, password) => {
765-
const response = await request.post('http://localhost:3001/api/login', {
766-
data: { username, password, }
767-
})
768-
769-
const jsonResponse = await response.json()
770-
771-
await page.evaluate((body) => {
772-
localStorage.setItem('loggedNoteappUser', JSON.stringify(body));
773-
}, jsonResponse)
774-
775-
page.reload()
725+
const loginWith = async (page, username, password) => {
726+
await page.getByRole('button', { name: 'log in' }).click()
727+
await page.getByTestId('username').fill(username)
728+
await page.getByTestId('password').fill(password)
729+
await page.getByRole('button', { name: 'login' }).click()
776730
}
777731

778732
export { loginWith }
@@ -782,11 +736,14 @@ Testi yksinkertaistuu ja selkeytyy:
782736
783737
```js
784738
describe('Note app', () => {
785-
// ...
739+
test('user can log in', async ({ page }) => {
740+
await loginWith(page, 'mluukkai', 'salainen')
741+
await expect(await page.getByText('Matti Luukkainen logged in')).toBeVisible()
742+
})
786743

787744
describe('when logged in', () => {
788-
beforeEach(async ({ page, request }) => {
789-
await loginWith(page, request, 'mluukkai', 'salainen')
745+
beforeEach(async ({ page }) => {
746+
await loginWith(page, 'mluukkai', 'salainen')
790747
})
791748

792749
test('a new note can be created', () => {
@@ -826,35 +783,23 @@ describe('Note app', function() {
826783
})
827784
```
828785
786+
Playwright tarjoaa myös [ratkaisun](https://playwright.dev/docs/auth) missä kirjaantuminen suoritetaan kertaalleen ennen testejä, ja jokainen testi aloittaa tilanteeasta missä sovellukseen ollaan jo kirjaantuneena. Jotta voisimme hyödyntää tätä tapaa, tulisi sovelluksen testidata alustaminen tehdä hienojakoisemmin kuin nyt. Nykyisessä ratkaisussahan tietokanta nollataan ennen jokaista testiä, ja tämän takia kirjaantuminen ennen testejä on mahdotonta. Jotta voisimme käyttää Plywrightin tarjoamaa ennen testejä tehtävää kirjautumista, tulisi käyttäjä alustaa vain kertaalleen ennen testejä. Pitäydymme yksinkertaisuuden vuoksi nykyisessä ratkaisussamme.
787+
829788
Eristetään myös muistiinpanon lisääminen omaksi komennoksi, joka tekee lisäämisen suoraan HTTP POST:lla. Tiedosto _tests/helper.js_ laajenee seuraavasti:
830789
831790
```js
832-
let jsonResponse = null // highlight-line
833-
834-
const loginWith = async (page, request, username, password) => {
835-
const response = await request.post('http://localhost:3001/api/login', {
836-
data: { username, password, }
837-
})
838-
839-
jsonResponse = await response.json()
840-
841-
await page.evaluate((jsonResponse) => {
842-
localStorage.setItem('loggedNoteappUser', JSON.stringify(jsonResponse));
843-
}, jsonResponse)
844-
845-
page.reload()
791+
const loginWith = async (page, username, password) => {
792+
await page.getByRole('button', { name: 'log in' }).click()
793+
await page.getByTestId('username').fill(username)
794+
await page.getByTestId('password').fill(password)
795+
await page.getByRole('button', { name: 'login' }).click()
846796
}
847797

848798
// highlight-start
849-
const createNote = async (page, request, content, important) => {
850-
await request.post('http://localhost:3001/api/notes', {
851-
data: { content, important, },
852-
headers: {
853-
'Authorization': `Bearer ${jsonResponse.token}`
854-
}
855-
})
856-
857-
page.reload()
799+
const createNote = async (page, content) => {
800+
await page.getByRole('button', { name: 'new note' }).click()
801+
await page.getByRole('textbox').fill(content)
802+
await page.getByRole('button', { name: 'save' }).click()
858803
}
859804
// highlight-end
860805

@@ -875,8 +820,8 @@ describe('Note app', () => {
875820
})
876821

877822
describe('and a note exists', () => {
878-
beforeEach(async ({ page, request }) => {
879-
await createNote(page, request, 'another note by playwright', true) // highlight-line
823+
beforeEach(async ({ page }) => {
824+
await createNote(page, 'another note by playwright', true)
880825
})
881826

882827
test('it can be made important', ({ page }) => {
@@ -938,53 +883,38 @@ Testit ja frontendin koodi on kokonaisuudessaan [GitHubissa](https://github.com/
938883
Tarkastellaan vielä aiemmin tekemäämme testiä, joka varmistaa että muistiinpanon tärkeyttä on mahdollista muuttaa. Muutetaan testin alustuslohkoa siten, että se luo yhden sijaan kolme muistiinpanoa:
939884
940885
```js
941-
describe('when logged in', function() {
942-
describe('and several notes exist', function () {
943-
beforeEach(function () {
886+
describe('when logged in', () => {
887+
// ...
888+
describe('and several notes exists', () => {
889+
beforeEach(async ({ page, request }) => {
944890
// highlight-start
945-
cy.createNote({ content: 'first note', important: false })
946-
cy.createNote({ content: 'second note', important: false })
947-
cy.createNote({ content: 'third note', important: false })
891+
await createNote(page, request, 'first note', true)
892+
await createNote(page, request, 'second note', true)
893+
await createNote(page, request, 'third note', true)
948894
// highlight-end
949895
})
950896

951-
it('one of those can be made important', function () {
952-
cy.contains('second note')
953-
.contains('make important')
954-
.click()
897+
test('one of those can be made important', async ({ page }) => {
898+
const secondNoteElement = await page.getByText('second note')
955899

956-
cy.contains('second note')
957-
.contains('make not important')
900+
await secondNoteElement.getByRole('button', { name: 'make not important' }).click()
901+
await expect(secondNoteElement.getByText('make important')).toBeVisible()
958902
})
959903
})
960904
})
961905
```
962906
963-
Miten komento [cy.contains](https://docs.cypress.io/api/commands/contains.html) tarkalleen ottaen toimii?
964-
965-
Kun klikkaamme komentoa _cy.contains('second note')_ Cypressin [test runnerista](https://docs.cypress.io/guides/core-concepts/test-runner.html) nähdään, että komento löytää elementin, jonka sisällä on teksti <i>second note</i>:
966-
967-
![Klikatessa vasemmalla olevasta testisteppien listasta komentoa, renderöityy oikealle sovelluksen sen hetkinen tila, missä löydetty elementti on merkattuna korostettuna.](../../images/5/34new.png)
968-
969-
Klikkaamalla seuraavaa riviä _.contains('make important')_, nähdään että löydetään nimenomaan
970-
<i>second note</i>:a vastaava tärkeyden muutoksen tekevä nappi:
907+
Testi etsii nyt metodin _getByRole_ avulla toisena luodun muistiinpanoa vastaavan elementin ja tallettaa sen muuttujaan. Tämän jälkeen elementin sisältä etsitään nappi missä on teksti _make not important_ ja painetaan sitä. Lopuksi teksi varmistaa että napin teksiksi on muuttunut _make important_.
971908
972-
![Klikatessa vasemmalla olevasta testisteppien listasta komentoa, korostuu oikealle valintaa vastaava nappi](../../images/5/35new.png)
973-
974-
Peräkkäin ketjutettuna toisena oleva <i>contains</i>-komento siis <i>jatkaa</i> hakua ensimmäisen komennon löytämän komponentin sisältä.
975-
976-
Jos emme ketjuttaisi komentoja, eli olisimme kirjoittaneet
909+
Testi olisi voitu kirjoittaa myös ilman apumuuttujaa:
977910
978911
```js
979-
cy.contains('second note')
980-
cy.contains('make important').click()
981-
```
982-
983-
tulos olisi ollut aivan erilainen, toinen rivi painaisi väärän muistiinpanon nappia:
984-
985-
![Renderöityy virhe AssertionError: Timed out retrying after 4000ms: Expected to find content 'make not important'.](../../images/5/36new.png)
912+
test('one of those can be made important', async ({ page }) => {
913+
await page.getByText('second note').getByRole('button', { name: 'make not important' }).click()
986914

987-
Testejä tehdessä kannattaa siis ehdottomasti varmistaa test runnerista, että testit etsivät niitä elementtejä, joita niiden on tarkoitus tutkia!
915+
await expect(wait page.getByText('second note').getByText('make important')).toBeVisible()
916+
})
917+
```
988918
989919
Muutetaan komponenttia _Note_ siten, että muistiinpanon teksti renderöitään <i>span</i>-komponentin sisälle
990920
@@ -1002,92 +932,42 @@ const Note = ({ note, toggleImportance }) => {
1002932
}
1003933
```
1004934
1005-
Testit hajoavat! Kuten test runner paljastaa, komento _cy.contains('second note')_ palauttaakin nyt ainoastaan tekstin sisältävän komponentin, ja nappi on sen ulkopuolella:
935+
Testit hajoavat! Kuten test runner paljastaa, komento _await page.getByText('second note')_ palauttaakin nyt ainoastaan tekstin sisältävän komponentin, ja nappi on sen ulkopuolella.
1006936
1007-
![Oikealle puolelle havainnollistuu, että fokus osuu napin sijaan pelkkään tekstiin](../../images/5/37new.png)
1008937
1009938
Eräs tapa korjata ongelma on seuraavassa:
1010939
1011940
```js
1012-
it('other of those can be made important', function () {
1013-
cy.contains('second note').parent().find('button').click()
1014-
cy.contains('second note').parent().find('button')
1015-
.should('contain', 'make not important')
1016-
})
1017-
```
1018-
1019-
Ensimmäisellä rivillä etsitään komennon [parent](https://docs.cypress.io/api/commands/parent.htm) tekstin <i>second note</i> sisältävän elementin vanhemman alla oleva nappi ja painetaan sitä. Toinen rivi varmistaa, että napin teksti muuttuu.
941+
test('one of those can be made important', async ({ page }) => {
942+
const secondNoteText = await page.getByText('second note') // highlight-line
943+
const secondNoteElement = await secondNoteText.locator('..') // highlight-line
1020944

1021-
Huomaa, että napin etsimiseen käytetään komentoa [find](https://docs.cypress.io/api/commands/find.html#Syntax). Komento [cy.get](https://docs.cypress.io/api/commands/get.html) ei sovellu tähän tilanteeseen, sillä se etsii elementtejä aina <i>koko</i> sivulta ja palauttaisi nyt kaikki sovelluksen viisi nappia.
1022-
1023-
Testissä on ikävästi copypastea, rivien alku eli napin etsivä koodi on sama.
1024-
Tälläisissä tilanteissa on mahdollista hyödyntää komentoa [as](https://docs.cypress.io/api/commands/as.html):
1025-
1026-
```js
1027-
it('other of those can be made important', function () {
1028-
cy.contains('second note').parent().find('button').as('theButton')
1029-
cy.get('@theButton').click()
1030-
cy.get('@theButton').should('contain', 'make not important')
945+
await secondNoteElement.getByRole('button', { name: 'make not important' }).click()
946+
await expect(secondNoteElement.getByText('make important')).toBeVisible()
1031947
})
1032948
```
1033949
1034-
Nyt ensimmäinen rivi etsii oikean napin, ja tallentaa sen komennon <i>as</i> avulla nimellä <i>theButton</i>. Seuraavat rivit pääsevät nimettyyn elementtiin käsiksi komennolla <i>cy.get('@theButton')</i>.
1035-
1036-
### Testien suoritus ja debuggaaminen
1037-
1038-
Vielä osan lopuksi muutamia huomioita Cypressin toimintaperiaatteesta sekä testien debuggaamisesta.
950+
Ensimmäinen rivi etsii nyt toiseen muistiinpanoon liittyvän tekstin sisältävän _span_-elementin. Toisella rivillä käytetään funktiota _locator_ ja annetaan parametriksi _.._, joka hakee elementin vanhempielementin. Funktio locator on hyvin joustava, ja hyödynnämme tässä sitä että se hyväksyy [parametrikseen](https://playwright.dev/docs/locators#locate-by-css-or-xpath) CSS-selektorien lisäksi myös [XPath](https://developer.mozilla.org/en-US/docs/Web/XPath)-muotoisen selektorin. Sama olisi mahdollista ilmaista myös CSS:n avulla, mutta tässä tapauksessa XPath tarjoaa yksinkertaisimman tavan elementin vanhemman etsimiseen.
1039951
1040-
Cypressissä testien kirjoitusasu antaa vaikutelman, että testit ovat normaalia JavaScript-koodia, ja että voisimme esim. yrittää seuraavaa:
952+
Testi voidaan toki kirjoittaa myös ainoastaan yhtä apumuuttujaa käyttäen:
1041953
1042954
```js
1043-
const button = cy.contains('log in')
1044-
button.click()
1045-
debugger
1046-
cy.contains('logout').click()
1047-
```
1048-
1049-
Näin kirjoitettu koodi ei kuitenkaan toimi. Kun Cypress suorittaa testin, se lisää jokaisen _cy_-komennon suoritusjonoon. Kun testimetodin koodi on suoritettu loppuun, suorittaa Cypress yksi kerrallaan suoritusjonoon lisätyt _cy_-komennot.
1050-
1051-
Cypressin komennot palauttavat aina _undefined_, eli yllä olevassa koodissa komento _button.click()_ aiheuttaisi virheen ja yritys käynnistää debuggeri ei pysäyttäisi koodia Cypress-komentojen suorituksen välissä, vaan jo ennen kuin yhtään Cypress-komentoa olisi suoritettu.
1052-
1053-
Cypress-komennot ovat <i>promisen kaltaisia</i>, joten jos niiden palauttamia arvoja halutaan käsitellä, se tulee tehdä komennon [then](https://docs.cypress.io/api/commands/then.html) avulla. Esim. seuraava testi tulostaisi sovelluksen <i>kaikkien</i> nappien lukumäärän ja klikkaisi napeista ensimmäistä:
1054-
1055-
```js
1056-
it('then example', function() {
1057-
cy.get('button').then( buttons => {
1058-
console.log('number of buttons', buttons.length)
1059-
cy.wrap(buttons[0]).click()
1060-
})
955+
test('one of those can be made important', async ({ page }) => {
956+
const secondNoteElement = await page.getByText('second note').locator('..')
957+
await secondNoteElement.getByRole('button', { name: 'make not important' }).click()
958+
await expect(secondNoteElement.getByText('make important')).toBeVisible()
1061959
})
1062960
```
1063961
1064-
Myös testien suorituksen pysäyttäminen debuggeriin on [mahdollista](https://docs.cypress.io/api/commands/debug.html). Debuggeri käynnistyy vain jos Cypress test runnerin developer-konsoli on auki.
1065-
1066-
Developer-konsoli on monin tavoin hyödyllinen testejä debugatessa. Network-tabilla näkyvät testattavan sovelluksen tekemät HTTP-pyynnöt, ja console-välilehti kertoo testin komentoihin liittyviä tietoja:
1067-
1068-
![Console-välilehti havainnollistaa testien löytämiä elementtejä.](../../images/5/38new.png)
1069-
1070-
Olemme toistaiseksi suorittaneet Cypress-testejä ainoastaan graafisen test runnerin kautta. Testit on luonnollisesti mahdollista suorittaa myös [komentoriviltä](https://docs.cypress.io/guides/guides/command-line.html). Lisätään vielä sovellukselle npm-skripti tätä tarkoitusta varten
962+
### Testien suoritus ja debuggaaminen
1071963
1072-
```js
1073-
"scripts": {
1074-
"start": "react-scripts start",
1075-
"build": "react-scripts build",
1076-
"test": "react-scripts test",
1077-
"eject": "react-scripts eject",
1078-
"eslint": "eslint .",
1079-
"cypress:open": "cypress open",
1080-
"test:e2e": "cypress run" // highlight-line
1081-
},
1082-
```
964+
Lokaattorien etsiminen
1083965
1084-
Nyt siis voimme suorittaa Cypress-testit komentoriviltä komennolla <i>npm run test:e2e</i>
966+
VSCode
1085967
1086-
![Komennon suoritus tulostaa konsoliin tekstuaalisen raportin joka kertoo 5 läpimenneestä testistä.](../../images/5/39new.png)
1087968
1088-
Huomaa, että testien suorituksesta tallentuu video hakemistoon <i>cypress/videos/</i>, hakemisto lienee syytä gitignoroida. Videoiden teko on myös mahdollista ottaa [pois päältä](https://docs.cypress.io/guides/guides/screenshots-and-videos#Videos).
1089969
1090-
Testien ja frontendin koodin lopullinen versio on kokonaisuudessaan [GitHubissa](https://github.com/fullstack-hy2020/part2-notes-frontend/tree/part5-11), branchissa <i>part5-11</i>.
970+
Testien lopullinen versio on kokonaisuudessaan [GitHubissa](https://github.com/fullstack-hy2020/part2-notes-frontend/tree/part5-11), branchissa <i>part5-11</i>.
1091971
1092972
</div>
1093973

0 commit comments

Comments
 (0)