├── .gitignore ├── cypress.config.js ├── cypress ├── e2e │ ├── sensitive-data │ │ └── sample.cy.js │ ├── duplication │ │ ├── sample4.cy.js │ │ ├── sample3.cy.js │ │ ├── sample2.cy.js │ │ └── sample1.cy.js │ ├── browser-testing │ │ ├── sample1.cy.js │ │ └── sample2.cy.js │ ├── slow-tests │ │ ├── sample3.cy.js │ │ ├── sample2.cy.js │ │ └── sample1.cy.js │ ├── unnecessary-complexity │ │ └── sample.cy.js │ ├── hardcoded-assertion │ │ └── sample.cy.js │ ├── flaky-test │ │ └── sample.cy.js │ ├── wrong-abstraction │ │ └── sample.cy.js │ ├── page-object │ │ └── sample.cy.js │ └── dependent-tests │ │ └── sample.cy.js ├── page-objects │ └── editDestination.js ├── support │ ├── commands.js │ └── e2e.js └── fixtures │ └── stories.json ├── package.json ├── README.md ├── lessons ├── 5.md ├── 7.md ├── congratulations.md ├── 10.md ├── 3.md ├── 6.md ├── 4.md ├── 1.md ├── 11.md ├── 0.md ├── 9.md ├── 2.md └── 8.md ├── LICENSE └── src ├── style.css ├── index.html ├── script.js └── meals.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | cypress.env.json 3 | cypress/downloads/ 4 | cypress/screenshots/ 5 | cypress/videos/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: {}, 5 | video: false, 6 | }) 7 | -------------------------------------------------------------------------------- /cypress/e2e/sensitive-data/sample.cy.js: -------------------------------------------------------------------------------- 1 | describe('Sensitive data bad practice', () => { 2 | beforeEach(() => { 3 | cy.visit('https://notes-serverless-app.com/login') 4 | }) 5 | 6 | it('fills the form leaking sensitive data', () => { 7 | cy.get('#email').type('joe@example.com') 8 | cy.get('#password').type('s3Cr€7-p@s5w0rd') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /cypress/e2e/duplication/sample4.cy.js: -------------------------------------------------------------------------------- 1 | describe('Code duplication bad practice - multiple checks', () => { 2 | beforeEach(() => { 3 | cy.visit('https://bit.ly/2XSuwCW') 4 | }) 5 | 6 | it('checks all checkboxes from a specific fieldset', () => { 7 | cy.get('#friend').check() 8 | cy.get('#publication').check() 9 | cy.get('#social-media').check() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/page-objects/editDestination.js: -------------------------------------------------------------------------------- 1 | const editDestination = { 2 | updateInfo: data => { 3 | cy.get('#destination_name') 4 | .clear() 5 | .type(data.name) 6 | cy.get('#destination_description') 7 | .clear() 8 | .type(data.description) 9 | cy.get('input[type="submit"]') 10 | .click() 11 | } 12 | } 13 | 14 | module.exports = editDestination 15 | -------------------------------------------------------------------------------- /cypress/e2e/browser-testing/sample1.cy.js: -------------------------------------------------------------------------------- 1 | describe('Browser testing bad practice - anchor href', () => { 2 | beforeEach(() => { 3 | cy.visit('https://notes-serverless-app.com') 4 | }) 5 | 6 | it('directs the user to the login page when clicking the login link', () => { 7 | cy.contains('.nav a', 'Login').click() 8 | 9 | cy.url().should('be.equal', 'https://notes-serverless-app.com/login') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/slow-tests/sample3.cy.js: -------------------------------------------------------------------------------- 1 | describe('Unnecessary waiting bad practice', () => { 2 | beforeEach(() => { 3 | cy.visit('./src/index.html') 4 | }) 5 | 6 | it('searches for a meal by typing and clicking the submit button', () => { 7 | cy.get('[data-test-id="search-field"]').type('Ramen') 8 | cy.get('[data-test-id="search-button"]') 9 | .click() 10 | .blur() 11 | cy.wait(10000) 12 | 13 | cy.contains('h2', 'Ramen (sopa)') 14 | .should('be.visible') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add('search', term => { 2 | cy.get('input[type="text"]') 3 | .should('be.visible') 4 | .clear() 5 | .type(`${term}{enter}`) 6 | }) 7 | 8 | Cypress.Commands.add('assertResults', () => { 9 | cy.get('.table-row').then(rows => { 10 | expect(rows.length).to.be.at.least(1) 11 | }) 12 | }) 13 | 14 | Cypress.Commands.add('randomlyTogglePurchaseAgreement', () => { 15 | if (Math.random() > 0.5) { 16 | cy.get('#agree') 17 | .click() 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /cypress/fixtures/stories.json: -------------------------------------------------------------------------------- 1 | { 2 | "hits": [ 3 | { 4 | "objectID": "0", 5 | "title": "Agile Testing", 6 | "author": "Lisa Crispin & Janet Gregory", 7 | "url": "https://example.com/lc-jg", 8 | "num_comments": 85, 9 | "points": 1000 10 | }, 11 | { 12 | "objectID": "1", 13 | "title": "Clean Code", 14 | "author": "Robert C. Martin", 15 | "url": "https://example.com/rm", 16 | "num_comments": 98, 17 | "points": 1953 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /cypress/e2e/slow-tests/sample2.cy.js: -------------------------------------------------------------------------------- 1 | describe('Slow tests bad practice - click a link to visit a page', () => { 2 | it('does not enable signup button when passwords do not match', () => { 3 | cy.visit('https://notes-serverless-app.com') 4 | 5 | cy.contains('.btn', 'Signup') 6 | .click() 7 | 8 | cy.get('#email').type('joe@example.com') 9 | cy.get('#password').type('foobarbaz') 10 | cy.get('#confirmPassword').type('foobahbaz') 11 | 12 | cy.contains('button', 'Signup') 13 | .should('be.disabled') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /cypress/e2e/browser-testing/sample2.cy.js: -------------------------------------------------------------------------------- 1 | describe('Browser testing bad practice - anchor with target _blank', () => { 2 | beforeEach(() => { 3 | cy.visit('https://cac-tat.s3.eu-central-1.amazonaws.com/index.html') 4 | }) 5 | 6 | it('directs the user to the privacy page after removing the target and clicking the link', () => { 7 | cy.contains('a', 'Política de Privacidade') 8 | .invoke('removeAttr', 'target') 9 | .click() 10 | 11 | cy.url() 12 | .should('be.equal', 'https://cac-tat.s3.eu-central-1.amazonaws.com/privacy.html') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boas-praticas-em-automacao-de-testes-com-cypress", 3 | "version": "1.0.0", 4 | "description": "Repositório da versão 2 do curso de boas práticas com Cypress da Escola Talking About Testing no Udemy", 5 | "scripts": { 6 | "test": "cypress run", 7 | "cy:open": "cypress open" 8 | }, 9 | "keywords": [ 10 | "cypress.io", 11 | "talking about testing", 12 | "good practices" 13 | ], 14 | "author": "Walmyr Filho (https://walmyr.dev)", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@faker-js/faker": "^7.6.0", 18 | "cypress": "^12.8.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cypress/e2e/unnecessary-complexity/sample.cy.js: -------------------------------------------------------------------------------- 1 | describe('Unnecessary complexity bad practice', () => { 2 | beforeEach(() => { 3 | cy.visit('https://bit.ly/2XSuwCW') 4 | cy.randomlyTogglePurchaseAgreement() 5 | }) 6 | 7 | Cypress._.times(5, () => { 8 | it('checks the checkbox only if not checked', () => { 9 | cy.get('body', { log: false }).then($body => { 10 | if ($body.find('#agree:checked').length) { 11 | cy.log('check box was checked') 12 | return 13 | } 14 | cy.log('check box was unchecked') 15 | $body.find('#agree').click() 16 | return 17 | }) 18 | 19 | cy.get('#agree', { log: false }) 20 | .should('be.checked') 21 | }) 22 | }) 23 | }) -------------------------------------------------------------------------------- /cypress/e2e/hardcoded-assertion/sample.cy.js: -------------------------------------------------------------------------------- 1 | describe('Hardcoded assertion bad practice', () => { 2 | beforeEach(() => { 3 | cy.intercept( 4 | 'GET', 5 | '**/search**', 6 | { fixture: 'stories' } 7 | ).as('getStories') 8 | 9 | cy.visit('https://hackernews-seven.vercel.app') 10 | cy.wait('@getStories') 11 | }) 12 | 13 | it('searches', () => { 14 | cy.search('cypress.io') 15 | cy.wait('@getStories') 16 | 17 | cy.get('.table-row') 18 | .as('tableRows') 19 | .should('have.length', 2) 20 | cy.get('@tableRows') 21 | .eq(0) 22 | .should('contain', 'Agile Testing') 23 | cy.get('@tableRows') 24 | .eq(1) 25 | .should('contain', 'Clean Code') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/slow-tests/sample1.cy.js: -------------------------------------------------------------------------------- 1 | describe('Slow tests bad practice - use the API to test the frontend', () => { 2 | beforeEach(() => { 3 | cy.intercept( 4 | 'GET', 5 | '**/search**' 6 | ).as('getStories') 7 | 8 | cy.visit('https://hackernews-seven.vercel.app') 9 | cy.wait('@getStories') 10 | 11 | cy.get('input[type="text"]') 12 | .should('be.visible') 13 | .and('have.value', 'redux') 14 | .as('searchField') 15 | .clear() 16 | }) 17 | 18 | it('searches by typing and hitting enter', () => { 19 | cy.get('@searchField') 20 | .type('frontend testing{enter}') 21 | 22 | cy.wait('@getStories') 23 | 24 | cy.get('.table-row') 25 | .should('have.length', 100) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/e2e/duplication/sample3.cy.js: -------------------------------------------------------------------------------- 1 | describe('Code duplication bad practice - repetitive actions and assertions', () => { 2 | beforeEach(() => { 3 | cy.intercept( 4 | 'GET', 5 | '**/search**' 6 | ).as('getStories') 7 | 8 | cy.visit('https://hackernews-seven.vercel.app') 9 | cy.wait('@getStories') 10 | }) 11 | 12 | it('searches for the same term 3 times', () => { 13 | cy.search('cypress.io') 14 | 15 | cy.get('.table-row') 16 | .its('length') 17 | .should('be.at.least', 1) 18 | 19 | cy.search('cypress.io') 20 | 21 | cy.get('.table-row') 22 | .its('length') 23 | .should('be.at.least', 1) 24 | 25 | cy.search('cypress.io') 26 | 27 | cy.get('.table-row') 28 | .its('length') 29 | .should('be.at.least', 1) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /cypress/e2e/flaky-test/sample.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | 3 | describe('Flaky tests bad practice', () => { 4 | beforeEach(() => { 5 | cy.visit('https://wlsf82-hacker-stories.web.app') 6 | 7 | cy.contains('p','Loading ...') 8 | .should('be.visible') 9 | cy.contains('p','Loading ...') 10 | .should('not.exist') 11 | }) 12 | 13 | Cypress._.times(10, () => { 14 | it('shows a max of 5 buttons for the last searched terms', () => { 15 | Cypress._.times(6, () => { 16 | cy.search(faker.random.word()) 17 | }) 18 | 19 | cy.contains('p','Loading ...') 20 | .should('be.visible') 21 | cy.contains('p','Loading ...') 22 | .should('not.exist') 23 | 24 | cy.get('.last-searches button') 25 | .should('have.length', 5) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boas práticas em automação de testes com Cypress 2 | 3 | 👋 Seja bem vinda(o)! 4 | 5 | É muito bom tê-la(o) aqui. Tenho certeza que você vai adorar este curso. ❤️ 6 | 7 | ## O que você vai aprender 8 | 9 | Durante o curso de boas práticas em automação de testes com Cypress você vai aprender 10 más práticas na escrita de testes automatizados e como resolvê-las. 10 | 11 | As más práticas em questão são as seguintes: 12 | 13 | - _Browser testing_ 14 | - Duplicação de código 15 | - _Flaky tests_ 16 | - _Hardcoded assertions_ 17 | - Complexidade desnecessária 18 | - _Page Objects_ 19 | - Dados sensíveis versionados 20 | - Testes lentos 21 | - Dependência entre testes 22 | - Abstrações erradas 23 | 24 | ## Vamos começar? 25 | 26 | Vá para a seção de [pré-requisitos](./lessons/0.md). 27 | 28 | ___ 29 | 30 | Um curso da [Escola Talking About Testing](https://udemy.com/user/walmyr). 31 | -------------------------------------------------------------------------------- /cypress/e2e/wrong-abstraction/sample.cy.js: -------------------------------------------------------------------------------- 1 | describe('Wrong abstraction bad practice', () => { 2 | beforeEach(() => { 3 | cy.intercept( 4 | 'GET', 5 | '**/search**' 6 | ).as('getStories') 7 | 8 | cy.visit('https://hackernews-seven.vercel.app') 9 | cy.wait('@getStories') 10 | }) 11 | 12 | it('uses custom command for assertion just for the sake of reusability', () => { 13 | cy.search('cypress') 14 | cy.wait('@getStories') 15 | 16 | cy.assertResults() 17 | }) 18 | 19 | it('uses custom command for assertion just for the sake of reusability', () => { 20 | cy.search('selenium') 21 | cy.wait('@getStories') 22 | 23 | cy.assertResults() 24 | }) 25 | 26 | it('uses custom command for assertion just for the sake of reusability', () => { 27 | cy.search('playwright') 28 | cy.wait('@getStories') 29 | 30 | cy.assertResults() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /cypress/e2e/duplication/sample2.cy.js: -------------------------------------------------------------------------------- 1 | describe('Code duplication bad practice - repetitive tests', () => { 2 | beforeEach(() => { 3 | cy.intercept( 4 | 'GET', 5 | '**/search**' 6 | ).as('getStories') 7 | 8 | cy.visit('https://hackernews-seven.vercel.app') 9 | cy.wait('@getStories') 10 | 11 | cy.get('input[type="text"]') 12 | .should('be.visible') 13 | .and('have.value', 'redux') 14 | .as('searchField') 15 | .clear() 16 | }) 17 | 18 | it('searches for "reactjs"', () => { 19 | cy.get('@searchField') 20 | .type('reactjs{enter}') 21 | 22 | cy.wait('@getStories') 23 | 24 | cy.get('.table-row') 25 | .should('have.length', 100) 26 | }) 27 | 28 | it('searches for "vuejs"', () => { 29 | cy.get('@searchField') 30 | .type('vuejs{enter}') 31 | 32 | cy.wait('@getStories') 33 | 34 | cy.get('.table-row') 35 | .should('have.length', 100) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /cypress/e2e/page-object/sample.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | 3 | const editDestinationPage = require('../../page-objects/editDestination') 4 | 5 | describe('Page Object bad practice', () => { 6 | const randomDestination = Math.floor(Math.random() * 15) + 1 7 | 8 | beforeEach(() => { 9 | cy.visit(`https://lit-chamber-61567.herokuapp.com/destinations/${randomDestination}/edit`) 10 | }) 11 | 12 | it('updates destination info', () => { 13 | const info = { 14 | name: faker.random.words(5), 15 | description: faker.random.words(5) 16 | } 17 | 18 | editDestinationPage.updateInfo(info) 19 | 20 | cy.url() 21 | .should( 22 | 'be.equal', 23 | `https://lit-chamber-61567.herokuapp.com/destinations/${randomDestination}` 24 | ) 25 | cy.contains('h2', info.name) 26 | .should('be.visible') 27 | cy.contains('p', info.description) 28 | .should('be.visible') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /lessons/5.md: -------------------------------------------------------------------------------- 1 | # Complexidade desnecessária 2 | 3 | As vezes, por não conhecermos as funcionalidades que certa ferramenta nos oferece (ex.: Cypress), acabamos escrevendo código mais complicado do que necessário para resolver problemas simples. 4 | 5 | Porém, devemos evitar complexidade ao máximo. Afinal, melhor trabalhar com código simples, não é? 6 | 7 | ## Conteúdos sugeridos 8 | 9 | Antes de seguir adiante, tenho algumas recomendações de conteúdos sobre o assunto pra te indicar: 10 | 11 | - [Como marcar um checkbox com Cypress sem correr o risco de desmarcá-lo](https://youtu.be/O8PJRPpfLl8) 12 | - [`cy.check()` - Cypress docs](https://docs.cypress.io/api/commands/check) (em inglês) 13 | 14 | ## Exercício 15 | 16 | Abra o arquivo [`cypress/e2e/unnecessary-complexity/sample.cy.js`](../cypress/e2e/unnecessary-complexity/sample.cy.js) e remova toda a complexidade desnecessária do teste com o simples uso do comando `.check()` do Cypress. 17 | 18 | ___ 19 | 20 | E chega de complexidade! 💯 21 | 22 | Vá para a [aula 6](./6.md) para conhecer a sexta má prática (_Page Objects_) e como lidar com ela. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Walmyr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Arial, Helvetica, sans-serif; 3 | background-color: #fef6e4; 4 | color: #001858; 5 | margin-left: 6px; 6 | } 7 | #loading { 8 | display: none; 9 | } 10 | label { 11 | font-weight: bold; 12 | } 13 | select { 14 | border: 2px solid #001858; 15 | border-radius: 4px; 16 | margin-left: 21px; 17 | height: 1.5rem; 18 | } 19 | #search-container { 20 | margin-top: 12px; 21 | } 22 | #search-container input[type="text"] { 23 | width: 180px; 24 | border: 2px solid #001858; 25 | border-radius: 4px; 26 | } 27 | ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ 28 | color: #f582ae; 29 | opacity: 1; /* Firefox */ 30 | } 31 | :-ms-input-placeholder { /* Internet Explorer 10-11 */ 32 | color: #f582ae; 33 | } 34 | ::-ms-input-placeholder { /* Microsoft Edge */ 35 | color: #f582ae; 36 | } 37 | h1 { 38 | color: #f582ae; 39 | } 40 | button { 41 | padding: 4px 8px; 42 | background-color: #f582ae; 43 | border-radius: 8px; 44 | border: 2px solid #001858; 45 | } 46 | #generate-meal-button { 47 | font-size: large; 48 | } 49 | @media only screen and (max-width: 600px) { 50 | h1 { 51 | text-align: center; 52 | } 53 | #generate-meal-button { 54 | margin: 0 auto; 55 | display: block; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cypress/e2e/duplication/sample1.cy.js: -------------------------------------------------------------------------------- 1 | describe('Code duplication bad practice - repetitive steps', () => { 2 | it('searches by typing and hitting enter', () => { 3 | cy.intercept( 4 | 'GET', 5 | '**/search**' 6 | ).as('getStories') 7 | 8 | cy.visit('https://hackernews-seven.vercel.app') 9 | cy.wait('@getStories') 10 | 11 | cy.get('input[type="text"]') 12 | .should('be.visible') 13 | .and('have.value', 'redux') 14 | .clear() 15 | .type('frontend testing{enter}') 16 | 17 | cy.wait('@getStories') 18 | 19 | cy.get('.table-row') 20 | .should('have.length', 100) 21 | }) 22 | 23 | it('searches by typing and pressing the search button', () => { 24 | cy.intercept( 25 | 'GET', 26 | '**/search**' 27 | ).as('getStories') 28 | 29 | cy.visit('https://hackernews-seven.vercel.app') 30 | cy.wait('@getStories') 31 | 32 | cy.get('input[type="text"]') 33 | .should('be.visible') 34 | .and('have.value', 'redux') 35 | .clear() 36 | .type('frontend testing') 37 | 38 | cy.contains('button', 'Search') 39 | .should('be.visible') 40 | .click() 41 | 42 | cy.wait('@getStories') 43 | 44 | cy.get('.table-row') 45 | .should('have.length', 100) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /cypress/e2e/dependent-tests/sample.cy.js: -------------------------------------------------------------------------------- 1 | describe('Dependent tests bad practice', () => { 2 | beforeEach(() => { 3 | cy.visit('http://notes-serverless-app.com') 4 | 5 | cy.get('.navbar-nav a:contains(Login)').click() 6 | 7 | cy.get('#email').type(Cypress.env('user_email')) 8 | cy.get('#password').type(Cypress.env('user_password'), { log: false }) 9 | cy.get('button[type="submit"]').click() 10 | 11 | cy.contains('h1', 'Your Notes').should('be.visible') 12 | }) 13 | 14 | it('creates a note', () => { 15 | cy.contains('Create a new note').click() 16 | 17 | cy.get('#content').type('My note') 18 | cy.contains('Create').click() 19 | 20 | cy.get('.list-group').should('contain', 'My note') 21 | }) 22 | 23 | it('edits a note', () => { 24 | cy.get('.list-group').contains('My note').click() 25 | cy.get('#content').type(' updated') 26 | cy.contains('Save').click() 27 | 28 | cy.get('.list-group').should('contain', 'My note updated') 29 | cy.get('.list-group:contains(My note updated)').should('be.visible') 30 | }) 31 | 32 | it('deletes a note', () => { 33 | cy.get('.list-group').contains('My note updated').click() 34 | cy.contains('Delete').click() 35 | 36 | cy.get('.list-group:contains(My note updated)').should('not.exist') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sugestão de Refeição Vegana 8 | 9 | 10 |

Refeição vegana 🌱

11 |
12 | 13 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 |

Loading...

28 |
29 |
30 |

31 |

32 | 33 |
34 |
35 | 36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /lessons/7.md: -------------------------------------------------------------------------------- 1 | # Dados sensíveis versionados 2 | 3 | Dados sensíveis, tais como contas de usuários, senhas, números de cartão de crédito, dentre outros, não devem ser versionados por questões de segurança. 4 | 5 | Uma abordagem comum quando tais dados sensíveis são necessários durante a execução de testes automatizados é armazená-los em variáveis de ambiente, as quais podem ser configuradas tanto localmente (nos computadores de cada desenvolvedor(a)), ou em ferramentas de integração contínua. 6 | 7 | ## Conteúdos sugeridos 8 | 9 | Antes de seguir adiante, tenho algumas recomendações de conteúdos sobre o assunto pra te indicar: 10 | 11 | - [Como proteger dados sensíveis com Cypress](https://talkingabouttesting.com/2021/02/09/como-proteger-dados-sensiveis-com-cypress/) 12 | - [Live TAT - Logs do Cypress](https://youtu.be/7cFM4CelOUc) 13 | 14 | ## Exercício 15 | 16 | Primeiro, crie um arquivo chamado `cypress.env.json` na raiz do projeto. 17 | 18 | > Obs.: Tal arquivo não será versionado, visto que está listado no [`.gitignore`](../.gitignore). 19 | 20 | Dentro do arquivo `cypress.env.json`, copie e cole o seguinte conteúdo: 21 | 22 | ```json 23 | { 24 | "user_email": "joe@example.com", 25 | "user_password": "s3Cr€7-p@s5w0rd" 26 | } 27 | ``` 28 | 29 | Agora, abra o arquivo [`cypress/e2e/sensitive-data/sample.cy.js`](../cypress/e2e/sensitive-data/sample.cy.js) e em vez de digitar o usuário e senha com dados _hardcoded_, utilize as variáveis recém configuradas. 30 | 31 | Além disso, não deixe o Cypress imprimir no log de comandos do _runner_ a senha digitada. 32 | 33 | ___ 34 | 35 | Legal, agora os dados sensíveis estão protegidos. 🔒 36 | 37 | Vá para a [aula 8](./8.md) para conhecer a oitava má prática (testes desnecessariamente lentos) e como lidar com ela. 38 | -------------------------------------------------------------------------------- /lessons/congratulations.md: -------------------------------------------------------------------------------- 1 | # 🥳 Parabéns, você conseguiu! 🎉 2 | 3 | Que jornada, hein? 4 | 5 | Espero que você tenha gostado do curso tanto quanto gostei de ensinar à você. 6 | 7 | Neste mesmo repositório, você encontrará o branch [solução final](https://github.com/wlsf82/boas-praticas-em-automacao-de-testes-com-cypress-v2/tree/final-solution), caso queira consultar. 8 | 9 | Vamos recapitular o que você aprendeu. 10 | 11 | No curso de boas práticas em automação de testes com Cypress você aprendeu como resolver problemas quando lidando com as seguintes más práticas: 12 | 13 | - _Browser testing_ ✔️ 14 | - Duplicação de código ✔️ 15 | - _Flaky tests_ ✔️ 16 | - _Hardcoded assertions_ ✔️ 17 | - Complexidade desnecessária ✔️ 18 | - _Page Objects_ ✔️ 19 | - Dados sensíveis versionados ✔️ 20 | - Testes lentos ✔️ 21 | - Dependência entre testes ✔️ 22 | - Abstrações erradas ✔️ 23 | 24 | Agora é hora de colocar os novos conhecimentos em prática. 25 | 26 | Quer manter contato comigo? Faça parte do grupo [**Talking About Testing** no LinkedIn](https://www.linkedin.com/groups/12492726/) e te inscreve na [**_Newsletter_ da TAT**](https://mailchi.mp/6b1f35857228/newsletter-talking-about-testing). 27 | 28 | Aproveita também pra deixar uma estrela ⭐ no [repostitório do curso no GitHub](https://github.com/wlsf82/boas-praticas-em-automacao-de-testes-com-cypress-v2). Ficarei muito agradecido! 29 | 30 | Por fim, compartilha o certificado do curso no teu LinkedIn pra mostrar que você sabe criar testes automatizados com Cypress utilizando as melhores práticas. Use as seguintes _hashtags_ #TalkingAboutTesting #EscolaTAT #Cypress e me marque em sua publicação. [Segue o meu perfil](https://www.linkedin.com/in/walmyr-lima-e-silva-filho/). 31 | 32 | 👋 Espero vê-la(o) nos [próximos cursos](https://www.udemy.com/user/walmyr/). 33 | 34 | Até a próxima e bons testes! 🚀 35 | -------------------------------------------------------------------------------- /lessons/10.md: -------------------------------------------------------------------------------- 1 | # Abstrações erradas 2 | 3 | No início do curso, vimos que duplicação de código pode ser considerada uma má prática, porém, em alguns casos, tais como em verificações (ou _assertions_), não me incomodo com código duplicado, visto que quero entender ao ler os testes exatamente o que eles esperam que ocorra após alguma pré-condição e um certo conjunto de ações. 4 | 5 | Ou seja, vale mais a pena deixar algum código duplicado no teste para facilitar o entendimento do que exatamente tal teste espera (o que está sendo testado), ao invés de abstrair tal verificação para um comando customizado. 6 | 7 | ## Conteúdos sugeridos 8 | 9 | Antes de seguir adiante, tenho algumas recomendações de conteúdos que irão te ajudar com os exercícios a seguir: 10 | 11 | - [Implicit and explicit assertions - Cypress Kitchen Sink](https://example.cypress.io/commands/assertions) (em inglês) 12 | - [`its` - DOM Elements - Cypress docs](https://docs.cypress.io/api/commands/its#DOM-Elements) (em inglês) 13 | 14 | ## Conteúdos relacionados 15 | 16 | Também tenho alguns conteúdos (relacionados) pra te indicar: 17 | 18 | - [AHA Programming – Kent C. Dodds](https://youtu.be/wuVy7rwkCfc) (em inglês) 19 | - [The Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction) (em inglês) 20 | - [RailsConf 2014 - All the Little Things by Sandi Metz](https://youtu.be/8bZh5LMaSmE) (em inglês) 21 | 22 | > **Obs.:** os conteúdos acima não tratam exatamente de abstrações erradas em testes, mas considero-os relacionados e portanto achei que você iria gostar. 23 | 24 | ## Exercício 25 | 26 | Abra o arquivo [`cypress/e2e/wrong-abstraction/sample.cy.js`](../cypress/e2e/wrong-abstraction/sample.cy.js) e em vez de utilizar o comando customizado `.assertResults()` para fazer as verificações nos testes, deixe tais verificações explícitas. 27 | 28 | > Não faz mal ter um pouco de duplicação quando isso facilita o entendimento do teste. 29 | 30 | ## Exercício extra 1 31 | 32 | Ainda no arquivo [`cypress/e2e/wrong-abstraction/sample.cy.js`](../cypress/e2e/wrong-abstraction/sample.cy.js), modifique a verificação, para que em vez de ser explícita (`expect(rows.length).to.be.at.least(1)`), seja implícita (`cy.get('.table-row').its('length').should('be.at.least', 1)`). 33 | 34 | ___ 35 | 36 | Siga para a [aula 11](./11.md). Vou te apresentar algumas séries de conteúdos que mantenho, específicas sobre Cypress. 37 | -------------------------------------------------------------------------------- /lessons/3.md: -------------------------------------------------------------------------------- 1 | # _Flaky tests_ 2 | 3 | _Flaky tests_ são testes que as vezes passam e outras falham, sem haver nenhuma modificação nos testes propriamente ditos, ou mesmo no código da aplicação sendo testada. 4 | 5 | Tais testes devem ser evitados, visto que, no início, geram uma sensação de que as coisas estão erradas com a aplicação sendo testada, quando nem sempre isso é verdade. Depois de um tempo o time perde a confiança nos testes, e quando eles estão acusando uma falha real, podem passar despercebidos. 6 | 7 | Uma má prática em testes automatizados é esperar por um estado intermediário antes de seguir adiante. Algo como esperar por um componente de _fallback_ que é apresentado ao usuário enquanto uma chamada está sendo feita para uma API (por exemplo) estar visível e depois não estar mais. 8 | 9 | Uma alternativa que o Cypress oferece para lidar com tal situação, em vez de aguardar por tal estado intermediário, é aguardar pela requisição sendo feita retornar, para então seguir adiante. 10 | 11 | Confira a lista de conteúdos abaixo que as coisas ficarão mais claras. 12 | 13 | ## Conteúdos sugeridos 14 | 15 | Antes de seguir adiante, tenho algumas recomendações de conteúdos sobre o assunto pra te indicar: 16 | 17 | - [Seriam os testes de UI flaky por natureza?](https://talkingabouttesting.com/2016/08/09/seriam-os-testes-de-ui-flaky-por-natureza/) 18 | - [Como aguardar uma requisição acabar antes de seguir adiante com Cypress](https://talkingabouttesting.com/2021/02/12/como-aguardar-um-requisicao-acabar-antes-de-seguir-adiante-com-cypress/) 19 | - [Como utilizar fixtures com Cypress para isolar os testes do frontend](https://talkingabouttesting.com/2021/02/16/como-utilizar-fixtures-com-cypress-para-isolar-os-testes-do-frontend/) 20 | - [Categoria 'Flake' no blog oficial do Cypress.io](https://cypress.io/blog/tag/flake/) (em inglês) 21 | 22 | ## Exercício 23 | 24 | Abra o arquivo [`cypress/e2e/flaky-test/sample.cy.js`](../cypress/e2e/flaky-test/sample.cy.js) e em vez de verificar que o componente com o texto `Loading ...` é exibido e depois não mais, aguarde por uma requisição do tipo `GET` para a rota `**/search**` antes de seguir adiante. 25 | 26 | ## Exercício extra 1 27 | 28 | Ainda no arquivo [`cypress/e2e/flaky-test/sample.cy.js`](../cypress/e2e/flaky-test/sample.cy.js), isole os testes da API externa com o uso de _fixtures_. 29 | 30 | > 🧙🏻 Já existe uma _fixture_ criada, é só usar. 31 | 32 | ## Exercício extra 2 33 | 34 | Ainda no arquivo [`cypress/e2e/flaky-test/sample.cy.js`](../cypress/e2e/flaky-test/sample.cy.js), agora refatorado, remova as chamadas ao `cy.wait('@getStories')`, visto que o Cypress já espera por requisições automaticamente! 🌲 35 | 36 | ___ 37 | 38 | Show de bola! ⚽ Não temos mais testes _flaky_. 39 | 40 | Vá para a [aula 4](./4.md) para conhecer a quarta má prática (_hardcoded assertions_) e como lidar com ela. 41 | -------------------------------------------------------------------------------- /lessons/6.md: -------------------------------------------------------------------------------- 1 | # _Page Objects_ 2 | 3 | Há algum tempo o Cypress gerou polêmica com um conteúdo chamado [_Stop using Page Objects and Start using App Actions_](https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions/). 4 | 5 | Altamente recomendo a leitura do conteúdo, visto que fica bastante claro o motivo de tal sugestão por parte do time do Cypress. 6 | 7 | No Cypress, podemos facilmente criar comandos customizados e reutilizá-los sem a necessidade do padrão _Page Objects_. 8 | 9 | Algumas vantagens dessa abordagem (na minha opinião) são: 10 | 11 | - Um comando customizado exige menos código que um _Page Object_ 12 | - Quando utilizando comandos customizados, tais comandos ficam disponíveis através do objeto global `cy`, ou seja, não há a necessidade de importar nada, como é necessário quando se usa o padrão _Page Objects_ 13 | - Ao não usar o padrão _Page Objects_, nos damos a liberdade para criar não só comandos customizados que interagem com a aplicação através da interface gráfica de usuário, como também as famosas _App Actions_, as quais podem criar estado na aplicação em teste para otimizar os testes, com diferentes mecanismos, tais como chamdas à APIs, execução de comandos à nível de sistema operacional, tarefas (_tasks_) para população e limpeza do banco de dados, etc. 14 | 15 | Além disso, mantenho um [projeto exemplo](https://github.com/wlsf82/gitlab-cypress) (em maior escala), onde não faço uso do padrão _Page Objects_, e ainda assim, prezo por questões de legibilidade, manutenabilidade, escalabilidade, etc. Confira o projeto [gitlab-cypress](https://github.com/wlsf82/gitlab-cypress)! 16 | 17 | ## Conteúdos sugeridos 18 | 19 | Antes de seguir adiante, tenho mais algumas recomendações de conteúdos sobre o assunto pra te indicar: 20 | 21 | - [Page Objects, App Actions e Custom Commands](https://www.linkedin.com/posts/walmyr-lima-e-silva-filho_cypress-page-objects-app-actions-e-custom-commands-activity-6792769387549003776-Gbfw) 22 | - [Cypress Page Object vs GUI Custom Commands](https://youtu.be/1OkfwHUJ-fk) 23 | - [Cypress Custom Commands e App Actions são coisas diferentes](https://youtu.be/6lMy3NXjw7E) 24 | 25 | Conheça também a [APOA - Asssociação dos _Page Objects_ Anônimos](https://page-objects-anonimos.vercel.app/). 26 | 27 | ## Exercício 28 | 29 | Abra o arquivo [`cypress/e2e/page-object/sample.cy.js`](../cypress/e2e/page-object/sample.cy.js) e em vez de utilizar o método `.updateInfo()` do módulo `editDestinationPage` (ou seja, do _Page Object_), crie um comando customizado chamado `updateDestination`, o qual pode ser utilizado em vez do método do _Page Object_. 30 | 31 | > 🧙🏿 Lembre de remover o `require()` do _Page Object_ que não será mais utilizado. 32 | 33 | ___ 34 | 35 | Pronto! Sem _Page Object_ e com menos linhas de código. 👏 36 | 37 | Vá para a [aula 7](./7.md) para conhecer a sétima má prática (dados sensíveis versionados) e como lidar com ela. 38 | -------------------------------------------------------------------------------- /src/script.js: -------------------------------------------------------------------------------- 1 | const mealEnum = Object.freeze({ 2 | hot: 'prato quente', 3 | salad: 'salada', 4 | sandwich: 'sanduíche', 5 | soup: 'sopa' 6 | }) 7 | 8 | const mealContainer = document.getElementById('meal-container') 9 | const loading = document.getElementById('loading') 10 | const mealName = document.getElementById('meal-name') 11 | const ingredientsLabel = document.getElementById('ingredients-label') 12 | const ingredientsList = document.getElementById('ingredients-list') 13 | const generateMealButton = document.getElementById('generate-meal-button') 14 | const mealTypeFilter = document.getElementById('meal-type-filter') 15 | const searchField = document.getElementById('search-field') 16 | const searchButton = document.querySelector('#search-container button[type="submit"]') 17 | 18 | // eslint-disable-next-line no-undef 19 | let filteredMeals = [...meals] 20 | 21 | mealTypeFilter.addEventListener('change', e => { 22 | const selectedType = e.target.value 23 | if (selectedType === 'all') { 24 | // eslint-disable-next-line no-undef 25 | filteredMeals = [...meals] 26 | generateMeal() 27 | } else { 28 | // eslint-disable-next-line no-undef 29 | filteredMeals = meals.filter(meal => meal.type === selectedType) 30 | generateMeal() 31 | } 32 | }) 33 | 34 | let searchedMeal 35 | 36 | searchField.addEventListener('change', e => { 37 | searchedMeal = e.target.value.toLowerCase().trim() 38 | 39 | filteredMeals.forEach(filteredMeal => { 40 | if (filteredMeal.name.toLowerCase().includes(searchedMeal)) { 41 | const randomTimeoutBetweenZeroAndTenSeconds = Math.floor(Math.random() * 11) * 1000 42 | console.log(`${randomTimeoutBetweenZeroAndTenSeconds } milliseconds for meal to show.`) 43 | mealContainer.style.display = 'none' 44 | loading.style.display = 'block' 45 | setTimeout(() => { 46 | mealContainer.style.display = 'block' 47 | loading.style.display = 'none' 48 | showMealName(filteredMeal) 49 | showIngredients(filteredMeal.ingredients) 50 | }, randomTimeoutBetweenZeroAndTenSeconds) 51 | } 52 | }) 53 | }) 54 | 55 | searchButton.addEventListener('click', e => { 56 | e.preventDefault() 57 | }) 58 | 59 | function generateMeal() { 60 | const randomMeal = filteredMeals[Math.floor(Math.random() * filteredMeals.length)] 61 | showMealName(randomMeal) 62 | showIngredients(randomMeal.ingredients) 63 | searchField.value = '' 64 | } 65 | 66 | function showMealName(meal) { 67 | mealName.innerHTML = `Refeição: ${meal.name} (${mealEnum[meal.type]})` 68 | } 69 | 70 | function showIngredients(ingredients) { 71 | ingredientsList.innerHTML = '' 72 | for (const ingredient of ingredients) { 73 | const listItem = document.createElement('li') 74 | ingredientsLabel.innerHTML = 'Ingredientes:' 75 | listItem.innerHTML = ingredient 76 | ingredientsList.appendChild(listItem) 77 | } 78 | } 79 | 80 | window.onload = () => { 81 | generateMeal() 82 | } 83 | 84 | generateMealButton.addEventListener('click', generateMeal) 85 | -------------------------------------------------------------------------------- /lessons/4.md: -------------------------------------------------------------------------------- 1 | # _Hardcoded assertions_ 2 | 3 | Uma das grande vantagens de usar o Cypress é a possibilidade de testar o _frontend_ da aplicação de forma completamente isoada do _backend_. Isso pode ser atingido com o uso do comando [`cy.intercept()`](https://docs.cypress.io/api/commands/intercept), conforme visto em algumas das aulas anteriores. 4 | 5 | Para atingirmos isso, fazemos o uso de _fixtures_. E como temos acesso aos dados das _fixtures_ (afinal elas são versionadas), podemos cometer o equívoco de deixarmos verificações _hardcoded_. 6 | 7 | ## Conteúdos sugeridos 8 | 9 | Antes de seguir adiante, tenho algumas recomendações de conteúdos sobre o assunto pra te indicar: 10 | 11 | - [Pair Testing #03 - Testes de frontend mockando o backend (com Samuel Lucas e Walmyr Filho)](https://youtu.be/7n8QfnBshmA) 12 | - [Desestruturação do objeto - Mozilla Docs](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#desestruturação_de_objeto) 13 | 14 | ## Exercício 15 | 16 | Primeiro, abra o arquivo [`cypress/fixtures/stories.json`](../cypress/fixtures/stories.json) e adicione mais um objeto ao array de `hits`, com os seguintes dados: 17 | 18 | ```json 19 | { 20 | "objectID": "2", 21 | "title": "Software Craftsmanship", 22 | "author": "Sandro Mancuso", 23 | "url": "https://example.com/sm", 24 | "num_comments": 133, 25 | "points": 420 26 | } 27 | ``` 28 | 29 | Logo após, execute o teste `cypress/e2e/hardcoded-assertion/sample.cy.js` com o seguinte comando: 30 | 31 | ```sh 32 | npx cypress run --spec cypress/e2e/hardcoded-assertion/sample.cy.js 33 | ``` 34 | 35 | Visto que as verificações estão _hardcoded_ no arquivo `cypress/e2e/hardcoded-assertion/sample.cy.js`, o teste deve falhar, conforme demonstrado abaixo: 36 | 37 | ```sh 38 | 1) Hardcoded assertion anti-pattern 39 | searches: 40 | 41 | AssertionError: Timed out retrying after 4000ms: Too many elements found. Found '3', expected '2'. 42 | + expected - actual 43 | 44 | -3 45 | +2 46 | ``` 47 | 48 | Vamos resolver esse problema tornado o teste apdaptável à mudanças na _fixture_. 49 | 50 | Abra o arquivo [`cypress/e2e/hardcoded-assertion/sample.cy.js`](../cypress/e2e/hardcoded-assertion/sample.cy.js) e em vez de utilizar _hardcoded assertions_, utilize a funcionalidade [`cy.fixture()`](https://docs.cypress.io/api/commands/fixture) para utilizar os dados de tal fixture nas verificações. 51 | 52 | > 🧙‍♂️ Lembre de encadear ao comando `cy.fixture()` o `.then()`, e na função de _callback_ do `.then()` realizar as verificações. 53 | 54 | ## Exercício extra 1 55 | 56 | Ainda no arquivo [`cypress/e2e/hardcoded-assertion/sample.cy.js`](../cypress/e2e/hardcoded-assertion/sample.cy.js), em vez de obter os dados da fixture com o uso do `cy.fixture().then()`, utilize a funcionalidade de desestruturação de objetos do JavaScript para obter os `hits` da _fixture_ `cypress/fixtures/stories.json`. 57 | 58 | ___ 59 | 60 | Ok, acabamos com as _hardcoded assertions_. 🥳 61 | 62 | Vá para a [aula 5](./5.md) para conhecer a quinta má prática (complexidade desnecessária) e como lidar com ela. 63 | -------------------------------------------------------------------------------- /lessons/1.md: -------------------------------------------------------------------------------- 1 | # _Browser testing_ 2 | 3 | No mundo dos testes automatizados é comum nos confundirmos sobre o que testar, e em vez de focarmos em testar a aplicação sendo desenvolvida (ou mantida), testarmos o comportamento padrão do navegador. 4 | 5 | Um erro comum é clicar em um link só para verificar que fomos direcionados para a URL correta, ou para verificarmos que, não só fomos redirecionados para a URL correta, como também que uma nova aba foi aberta no navegador. 6 | 7 | É comportamento padrão dos navegadores web que quando um usuário clica em uma tag _anchor_ (`Click here`) o mesmo será direcionado para a URL da propriedade `href` de tal elemento, e que caso tal elemento possua a propriedade `target` com o valor `_blank`, quando clicado, tal URL será aberta em um nova aba do navegador. 8 | 9 | Ou seja, visto que tais comportamentos são padrão em navegadores web, não precisamos testá-los. Os times desenvolvendo os navegadores já testam isso. ✅ 10 | 11 | Nossa preocupação deve ser que a aplicação funciona, não o navegador. 12 | 13 | Com Cypress, facilmente podemos testar se um elemento HTML possui determinadas propriedades, tais como `href` ou `target`. Por exemplo, para verificar que um link possui o `href` com o valor `https://example.com`, basta encadear um `.should('have.attr', 'href', 'https://example.com')` ao `cy.get()` que identifica o elemento em questão. 14 | 15 | ## Exercício 16 | 17 | Abra o arquivo [`cypress/e2e/browser-testing/sample1.cy.js`](../cypress/e2e/browser-testing/sample1.cy.js), e em vez de clicar link que contém o texto `Login`, verifique somente que tal elemento possui o valor correto para a propriedade `href`, além de verificar que tal elemento não possui a propriedade `target`. 18 | 19 | > 🧙 Dica: para verificar que uma propriedade não existe você pode usar `.should('not.have.attr', 'target')` 20 | 21 | **Obs.:** Lembre de executar o teste após a mudança para garantir que funciona. 22 | 23 | > Use `npx cypress run --spec cypress/e2e/browser-testing/sample1.cy.js` para executar o teste em modo _headless_, ou `npm run cy:open`, para abrir o Cypress em modo interativo. 24 | 25 | ## Exercício extra 1 26 | 27 | Abra o arquivo [`cypress/e2e/browser-testing/sample2.cy.js`](../cypress/e2e/browser-testing/sample2.cy.js), e em vez de remover o target da âncora com o texto Política de Privacidade e clicar no mesmo, simplesmente verifique que tal elemento possui os valores corretos para as propriedades `href` e `target`. 28 | 29 | ## Exercício extra 2 30 | 31 | Leia sobre testes em múltiplas abas na [documentação oficial do Cypress](https://docs.cypress.io/guides/references/trade-offs#Multiple-tabs) (em inglês) e entenda o motivo para tal abordagem não ser suportada. 32 | 33 | ## Exercício extra 3 34 | 35 | Assiste a _Live_ [Como NÃO testar com Cypress](https://www.youtube.com/live/VOq0LnnHzdk?feature=share) e aprenda a focar nas regras de negócio da aplicação em teste, em vez dos comportamentos padrão do navegador. 36 | 37 | ___ 38 | 39 | Ufa! Uma má prática a menos. 😅 40 | 41 | Vá para a [aula 2](./2.md) para conhecer a segunda má prática (duplicação de código) e como lidar com ela. 42 | -------------------------------------------------------------------------------- /lessons/11.md: -------------------------------------------------------------------------------- 1 | # Conteúdos de Cypress da Talking About Testing 2 | 3 | Pra te ajudar nas tuas aventuras com Cypress, tenho criando conteúdos dos mais diversos tipos, desde _blog posts_ em formato de tutoriais, _Lives_ no YouTube, vídeos "mão-na-massa" e entrevistas com profissionais sobre o uso da ferramenta. 4 | 5 | Deixo aqui cada um deles, como fonte de estudo auxiliar ao curso. 6 | 7 | ## Pitadas de Cypress 8 | 9 | Na [categoria Cypress no blog Talking About Testing](https://talkingabouttesting.com/category/cypress/), você vai encontrar conteúdos no formato de tutoriais sobre como resolver os mais diversos problemas utilizando Cypress. 10 | 11 | ## `cy.handsOn()` 12 | 13 | A _playlist_ [`cy.handsOn()`](https://www.youtube.com/playlist?list=PL-eblSNRj0QHIRCg9hYUYzSY87EyWo4k_) é parecida com as "pitadas de Cypress", porém, em vez de texto, são tutoriais em formato de vídeos. 14 | 15 | ## `cy.chat()` 16 | 17 | Na _playlist_ [`cy.chat()`](https://www.youtube.com/playlist?list=PL-eblSNRj0QH95Kx6iMR_Fwk5WsCM89Ha), entrevisto profissionais que usam Cypress, onde estas (e estes) compartilham suas experiências sobre o uso da ferramenta. 18 | 19 | Nesta _playlist_, você encontrará conteúdos em português e em inlgês, inclusive, entrevistas com outras(os) [embaixadoras(es) do Cypress.io](https://www.cypress.io/ambassadors/). 20 | 21 | ## Explorando a Cypress Real-World App (RWA) 22 | 23 | Quando comecei a fazer _Lives_ no YouTube, [quatro delas](https://youtube.com/playlist?list=PL-eblSNRj0QGU6gO4Yhb27ZwaCASG-lQl) foram: explorando os testes _end-to-end_; testes de API; testes de unidade; e a integração contínua da [Cypress RWA](https://github.com/cypress-io/cypress-realworld-app). 24 | 25 | ## Explorando os exemplos do Cypress 26 | 27 | Além disso, fiz uma _playlist_ de [seis episódios](https://youtube.com/playlist?list=PL-eblSNRj0QFFRzKi2GP0U-I5xFlUHW5E), exploranado diversos exemplos criados pelo time do Cypress. 28 | 29 | ## _API Testing_ 30 | 31 | [Nesta _playlist_](https://www.youtube.com/playlist?list=PL-eblSNRj0QGkMqsqxUvy7VI4VfXEUp-G), você vai encontrar vídeos "mão-na-massa" dos mais diversos no que diz respeito a testes de API com Cypress. 32 | 33 | ## `cy.session` 34 | 35 | [Nesta _playlist_](https://www.youtube.com/playlist?list=PL-eblSNRj0QF1RA4fd9FrDVov_uyYfCAL), você vai encontrar conteúdos "mão-na-massa" sobre o uso da funcionalidade [`cy.session()`](https://docs.cypress.io/api/commands/session) do Cypress. 36 | 37 | ## Outros cursos de Cypress da Escola TAT 38 | 39 | Agora que a [Escola Talking About Testing](https://www.udemy.com/user/walmyr/) tem 6 cursos de Cypress, algumas pessoas ficam confusas sobre o que cada curso aborda, por qual começar e como um complementa o outro. 40 | 41 | Nesta [_Live_](https://youtu.be/lckxlz10zZU), esclareço todas estas dúvidas. 42 | 43 | > Lembrando que na época da _Live_, os cursos básico, intermediário e de boas práticas ainda estavam na versão 1, e agora, já estão na v2 (que são melhores ainda!) 44 | 45 | ___ 46 | 47 | Uau! Você merece uma medalha por ter chego até aqui. 🥇 48 | 49 | Vá para a [próxima aula](./congratulations.md) para recapitularmos tudo que você aprendeu. 50 | -------------------------------------------------------------------------------- /lessons/0.md: -------------------------------------------------------------------------------- 1 | # Pré-requisitos 2 | 3 | Antes de começar, garanta que os seguintes sistemas estejam instalados em seu computador. 4 | 5 | - [git](https://git-scm.com/) (estou usando a versão `2.34.1` enquanto escrevo esta aula) 6 | - [Node.js](https://nodejs.org/en/) (estou usando a versão `v18.13.0` enquanto escrevo esta aula) 7 | - npm (estou usando a versão `8.19.3` enquanto escrevo esta aula) 8 | - [Visual Studio Code](https://code.visualstudio.com/) (estou usando a versão `1.75.1` enquanto escrevo esta aula) ou alguma outra IDE de sua preferência 9 | 10 | **Obs.:** Recomendo utilizar as mesmas versões, ou versões mais recentes dos sistemas citados acima. Além disso, sempre use sempre versões LTS (_Long Term Support_). 11 | 12 | **Obs. 2:** Ao instalar o Node.js o npm é instalado automaticamente. 13 | 14 | **Obs. 3:** Para verificar as versões do git, Node.js e npm instaladas em seu computador, execute o comando `git --version && node --version && npm --version` no seu terminal de linha de comando. 15 | 16 | **Obs. 4:** Na lista de requisitos acima, deixei links para encontrar os instaladores, no caso de não tê-los instalados ainda. 17 | 18 | ## Clonando o projeto 🐑 19 | 20 | Abra o navegador, acesse a URL https://github.com/wlsf82/boas-praticas-em-automacao-de-testes-com-cypress-v2, clique no botão **Code**, escolha uma opção de clone (HTTPS ou SSH), copie o link de clone do projeto, e em seu terminal de linha de comando (em uma pasta onde você armazene seus projetos de software), execute o comando `git clone [cole-o-link-copiado-aqui]`. 21 | 22 | > 👨‍🏫 Eu dou preferência ao clone via SSH, pois considero mais prático. 23 | > 24 | > Para detalhes sobre como criar e configurar uma chave SSH no GitHub, leia a [documentação oficial](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh). 25 | 26 | Após o clone do projeto, acesse o diretório recém clonado (ex.: `cd boas-praticas-em-automacao-de-testes-com-cypress-v2/`). 27 | 28 | Dentro do diretório `boas-praticas-em-automacao-de-testes-com-cypress-v2/` você terá os sub-diretórios `.git/` (diretório oculto), `cypress/`, `lessons/` e `src/`, e os arquivos `.gitignore` (arquivo oculto), `cypress.config.js`, `LICENSE`, `package-lock.json`, `package.json` e `README.md`. 29 | 30 | ## Instalação das dependências de desenvolvimento 31 | 32 | Com o projeto clonado a partir do **GitHub**, é hora de instalar as depedências de desenvolvimento. 33 | 34 | Visto que tais dependências já estão listadas no arquivo [`package.json`](../package.json), basta executar o comando `npm install` (ou `npm i` - versão curta) na raiz do projeto. 35 | 36 | > 🧙🏿 Este comando irá baixar o `cypress` e o `@faker-js/faker`, visto que estes estão listados na seção de `devDependencies`. 37 | 38 | Execute o comando `npm test` (ou `npm t` - versão curta) para garantir que tudo está funcionando como deveria. 39 | 40 | > **Observação**: É possível que ocorram algumas falhas na suite de testes `Flaky tests bad practice`, visto que os testes implementados não estão seguindo boas práticas. Além disso, a suite de testes `Products CRUD` deve falhar devido a falta da definição do arquivo `cypress.env.json` com credenciais válidas. **Não se preocupe, iremos corrigir tudo isso ao longo do curso.** 41 | 42 | ### Informações adicionais 43 | 44 | - O [`cypress`](https://cypress.io) é o framework de testes o qual vou te ensinar algumas boas práticas 45 | - O [`@faker-js/faker`](https://www.npmjs.com/package/@faker-js/faker) é utilizado em alguns testes para a geração de dados randômicos 46 | 47 | ___ 48 | 49 | Legal, os pre-requisitos estão prontos. ☑️ 50 | 51 | Vá para a [aula 1](./1.md) para conhecer a primeira má prática (_browser testing_) e como lidar com ela. 52 | -------------------------------------------------------------------------------- /lessons/9.md: -------------------------------------------------------------------------------- 1 | # Dependências entre testes 2 | 3 | > 🧑‍🏫 Testes devem ser independentes uns dos outros! 4 | 5 | É comum encontrar testes automatizados que precisam ser executados em ordem para que todos passem. 6 | 7 | Imagine um simples CRUD (_Create, Read, Update, Delete_) de produtos. 8 | 9 | Você poderia ter a seguinte suíte de testes, por exemplo. 10 | 11 | ```js 12 | describe('Products CRUD', () => { 13 | it('creates a product', () => { 14 | // Passos para criar o produto 15 | 16 | // Verificação de que o produto foi criado 17 | }) 18 | 19 | it('reads a product', () => { 20 | // Passos para ler o produto criado pelo teste anterior 21 | 22 | // Verificação de que o produto existe 23 | }) 24 | 25 | it('updates a product', () => { 26 | // Passos para atualizar o produto lido no teste anterior 27 | 28 | // Verificação de que o produto foi atualizado 29 | }) 30 | 31 | it('deletes a product', () => { 32 | // Passos para deletar o produto atualizado no teste anterior 33 | 34 | // Verificação de que o produto foi deletado 35 | }) 36 | }) 37 | 38 | ``` 39 | 40 | Porém, se o primero teste falhar, todos os outros falharão também. Isso acontece, pois, a criação (_Create_) é pré-condição para leitura (_Read_), a qual é pré-condição para a atualização (_Update_), a qual, por fim, é pré-condição para a deleção (_Delete_). 41 | 42 | Em casos como este, é melhor ter só um teste, do que vários testes dependentes uns dos outros (veja um exemplo abaixo). 43 | 44 | ```js 45 | describe('Products CRUD', () => { 46 | it('CRUDs a product', () => { 47 | // Passos para criar o produto 48 | 49 | // Verificação de que o produto foi criado 50 | 51 | // Passos para ler o produto 52 | 53 | // Verificação de que o produto existe 54 | 55 | // Passos para atualizar o produto 56 | 57 | // Verificação de que o produto foi atualizado 58 | 59 | // Passos para deletar o produto 60 | 61 | // Verificação de que o produto foi deletado 62 | }) 63 | }) 64 | 65 | ``` 66 | 67 | Essa não é a situacão ideal, mas ao menos, o teste é independente. 68 | 69 | > 🧑‍🏫 Para melhores forma de otimizar testes e torná-los ainda mais independentes, recomendo o [curso intermediário de Cypress](https://www.udemy.com/course/testes-automatizados-com-cypress-intermediario/?referralCode=F14505FB0076672E51A2) da [Escola Talking About Testing](https://www.udemy.com/user/walmyr/). 70 | 71 | Lembre-se! 72 | 73 | > Testes devem ser independentes uns dos outros! 74 | 75 | 76 | ## Exercício 77 | 78 | > ⚠️ Antes de seguir adiante, acesse a URL http://notes-serverless-app.com/signup, crie uma conta na aplicação em teste e salve seu usuário e senha no arquivo (não versionado) `cypress.env.json`, nas propriedades `user_email` e `user_password` respectivamente❗ 79 | > 80 | > Seu arquivo deve se parecer com o demonstrado abaixo. 81 | 82 | ```json 83 | { 84 | "user_email": "seu-usuario@exemplo.com", 85 | "user_password": "sua-senha-secreta-utilizada-no-signup-anterior" 86 | } 87 | 88 | ``` 89 | 90 | > ⚠️ Para completar o processo de _signup_, você terá que preencher um código de seis dígitos enviado para o email cadastrado, poranto, utilize um email válido❗ 91 | 92 | Abra o arquivo [cypress/e2e/dependent-tests/sample.cy.js](../cypress/e2e/dependent-tests/sample.cy.js) e refatore-o para que em vez de um teste depender do outro, haja só um teste para todos os passos e verificações do CRUD. 93 | 94 | > Perceba que o novo teste consolidado é mais rápidos que os testes separados, visto que agora, só precisamos fazer _login_ uma vez. 🏎️ 95 | 96 | ___ 97 | 98 | Estamos quase lá. 🏁 99 | 100 | Vá para a [aula 10](./10.md) para conhecer a décima má prática (abstrações erradas) e como lidar com ela. 101 | -------------------------------------------------------------------------------- /lessons/2.md: -------------------------------------------------------------------------------- 1 | # Duplicação de código 2 | 3 | Código duplicado é aquele que faz exatamente a mesma coisa em vários lugares (ou praticamente a mesma coisa, com pequenas alterações). 4 | 5 | O maior problema de código duplicado é que, se tal código está errado, ou mesmo se precisa ser motificado por alguma outra necessidade, temos que lembrar de todos os locais onde tal código é implementado para fazer a mudança. 6 | 7 | E se esquecermos de ajustar em algum lugar❓ 8 | 9 | Geramos um bug. 🐛 10 | 11 | Código duplicado pode ser considerado um "cheiro ruim" no código 🦨, e saber identificá-lo e melhorá-lo é uma habilidade que diferencia alguém que resolve problemas imediatos versus alguém que se preocupa com a evolução do projeto no longo prazo. 12 | 13 | Existem diferentes formas de remover duplicação de código, tais como a extração de código duplicado em funções; uso de iteração; uso de bibliotecas externas, ou até mesmo o simples entendimento e uso das ferramentas sendo utilizadas. 14 | 15 | ## Conteúdos sugeridos 16 | 17 | Antes de seguir adiante, tenho algumas recomendações de conteúdos sobre o assunto pra te indicar: 18 | 19 | - [Refatoração - Mentoria Talking About Testing](https://youtu.be/p1OB4vgNFow) 20 | - [Como rodar o mesmo teste com dados diferentes](https://youtu.be/oYnma0rmA4E) 21 | - [Como rodar um teste várias vezes com Cypress para provar que ele é estável](https://talkingabouttesting.com/2021/02/06/como-rodar-um-teste-varias-vezes-com-cypress-para-provar-que-ele-e-estavel/) 22 | - [Como criar comandos customizados com Cypress](https://talkingabouttesting.com/2021/02/10/como-criar-comandos-customizados-com-cypress/) 23 | - [Como marcar vários checkboxes de uma só vez com Cypress](https://talkingabouttesting.com/2021/06/14/como-marcar-varios-checkboxes-de-uma-so-vez-com-cypress/) 24 | - [Cypress hooks docs](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Hooks) (em inglês) 25 | - [`cy.check()` - Cypress docs](https://docs.cypress.io/api/commands/check) (em inglês) 26 | 27 | > 🦉 Cada um dos conteúdos acima irá te ajudar com os exercícios que vem por aí. 28 | 29 | ## Exercício 30 | 31 | Abra o arquivo [`cypress/e2e/duplication/sample1.cy.js`](../cypress/e2e/duplication/sample1.cy.js) e remova a duplicação do mesmo com o uso do _hook_ `beforeEach`. 32 | 33 | ## Exercício extra 1 34 | 35 | Abra o arquivo [`cypress/e2e/duplication/sample2.cy.js`](../cypress/e2e/duplication/sample2.cy.js) e remova a duplicação existente nas ações dos testes (não nas verificações) com o uso de um comando customizado. 36 | 37 | > Sugestão: `cy.search()` 38 | 39 | ## Exercício extra 2 40 | 41 | Ainda no arquivo [`cypress/e2e/duplication/sample2.cy.js`](../cypress/e2e/duplication/sample2.cy.js), logo após o `beforeEach` e antes dos testes, crie uma variável chamada `terms` a qual representará um array com os valores `reactjs` e `vuejs` (como _strings_). 42 | 43 | Logo após, com o uso da funcionalidade [`.forEach()` do JavaScript](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach), itere sobre cada elemento do array executando um único bloco `it`, o qual irá criar um teste para cada item do array. 44 | 45 | ## Exercício extra 3 46 | 47 | Abra o arquivo [`cypress/e2e/duplication/sample3.cy.js`](../cypress/e2e/duplication/sample3.cy.js), e em vez de executar 3 vezes a mesma ação e verificação, utilize a funcionalidade [`.times()`](https://lodash.com/docs/4.17.15#times) do lodash (que já vem empacotado com o Cyperss) para remover toda e qualquer duplicacão. 48 | 49 | ## Exercício extra 4 50 | 51 | Abra o arquivo [`cypress/e2e/duplication/sample4.cy.js`](../cypress/e2e/duplication/sample4.cy.js), e em vez de fazer um `.check()` por checkbox, tire proveito do fato de que tal comando pode marcar mais de um checkbox com uma única chamada. Basta você encontrar o seletor correto. 52 | 53 | 🙊 😃 54 | 55 | ```js 56 | 'fieldset div input[type="checkbox"]' 57 | ``` 58 | 59 | ___ 60 | 61 | Beleza, acabamos com as duplicações! 🎊 62 | 63 | Vá para a [aula 3](./3.md) para conhecer a terceira má prática (_flaky tests_) e como lidar com ela. 64 | -------------------------------------------------------------------------------- /lessons/8.md: -------------------------------------------------------------------------------- 1 | # Testes lentos 2 | 3 | Testes automatizados servem para prover feedback rápido à times que desenvolvem software. 4 | 5 | Testes _end-to-end_ (e2e) são famosos por serem lentos, principalmente quando comparados com testes de APIs, ou testes de unidade. 6 | 7 | Às vezes, criarmos testes desnecessariamente lentos, ao testarmos algo e2e, quando poderíamos testar, por exemplo, o _frontend_ isolado do _backend_. 8 | 9 | Outro caso comum que torna testes mais lentos que o necessário é navegarmos até a página em teste através de cliques, em vez de visitarmos a página em teste diretamente. 10 | 11 | Por fim, há também as esperas desnecessárias, sendo estas, as piores das más práticas. Ainda assim, este é um tema recorrente quando se fala em testes automatizados de interface gráfica de usuário. 12 | 13 | > A mensagem é a seguinte. Não use `cy.wait(3000)`, `cy.wait(10000)`, ou seja lá qual for o valor em milissegundos. Isso torna os testes mais lentos do que o necessário, além de as vezes torná-los _flaky_. 14 | > 15 | > O Cypress já possui esperas e _retries_ automáticos por padrão, e se tal padrão não for o suficiente para certos casos, é possível sobrescrever tais valores (_timeouts_) via configuração, ou até mesmo, para um comando específico. 16 | 17 | Por exemplo, digamos que os testes executem em um ambiente com poucos recursos computacionais, e portanto, a aplicação demora para carregar. Neste caso, imagine que ao tentar fazer login, o teste falhe no preenchimento do formulário, devido aos elementos de tal formulário ainda não terem renderizado na página. 18 | 19 | Imaginemos um teste como o seguinte: 20 | 21 | ```js 22 | // cypress/e2e/login.cy.js 23 | 24 | describe('Login', () => { 25 | it('successfully', () => { 26 | cy.visit('https://example.com/login') 27 | 28 | // Imagine que os campos abaixo demoram para renderizer 29 | cy.get('[data-cy="email-field"]') 30 | .type(Cypress.env('user_email')) 31 | cy.get('[data-cy="password-field"]') 32 | .type(Cypress.env('user_password')) 33 | cy.get('button[type="submit"]') 34 | .click() 35 | 36 | cy.get('[data-cy="avatar"]') 37 | .should('be.visible') 38 | }) 39 | }) 40 | ``` 41 | 42 | Uma alternativa simples para resolver tal problem é sobrescrever o _timeout_ padrão para o comando em que o teste está falhando. 43 | 44 | Digamos que o teste sempre falhe na hora de preencher o email do usuário, visto que tal campo demora para renderizar. 45 | 46 | Uma alternativa para resolver tal problema seria a seguinte. 47 | 48 | ```js 49 | // cypress/e2e/login.cy.js 50 | 51 | describe('Login', () => { 52 | it('successfully', () => { 53 | cy.visit('https://example.com/login') 54 | 55 | cy.get('[data-cy="email-field"]', { timeout: 10000 }) 56 | .type(Cypress.env('user_email')) 57 | cy.get('[data-cy="password-field"]') 58 | .type(Cypress.env('user_password')) 59 | cy.get('button[type="submit"]') 60 | .click() 61 | 62 | cy.get('[data-cy="avatar"]') 63 | .should('be.visible') 64 | }) 65 | }) 66 | ``` 67 | 68 | Ou seja, em vez do Cypress esperar por no máximo `4000` milissegundos antes de falhar (seu _timeout_ padrão), irá esperar um máximo de `10000` milissegundos, porém, se após, digamos, `6000` milissegundos o campo aparecer, o teste irá seguir adiante. 69 | 70 | ## Conteúdos sugeridos 71 | 72 | Antes de seguir adiante, tenho algumas recomendações de conteúdos sobre os assuntos discutidos acima pra te indicar: 73 | 74 | - [Definindo fixtures para testes de frontend com Cypress](https://youtu.be/2RK3f0tGOIs) 75 | - [Testando o frontend desacoplado do backend com Cypress](https://www.linkedin.com/posts/walmyr-lima-e-silva-filho_testando-o-frontend-desacoplado-do-backend-activity-6779095750941966336-myw8) 76 | - [Como utilizar fixtures com Cypress para isolar os testes do frontend](https://talkingabouttesting.com/2021/02/16/como-utilizar-fixtures-com-cypress-para-isolar-os-testes-do-frontend/) 77 | - [Por que não se deve utilizar sleeps em testes automatizados](https://talkingabouttesting.com/2017/11/20/por-que-nao-se-deve-utilizar-sleeps-em-testes-automatizados/) 78 | - [Unnecessary Waiting - Cypress docs](https://docs.cypress.io/guides/references/best-practices#Unnecessary-Waiting) (em inglês) 79 | 80 | ## Exercício 81 | 82 | Abra o arquivo [`cypress/e2e/slow-tests/sample1.cy.js`](../cypress/e2e/slow-tests/sample1.cy.js) e torne-o mais rápido mockando a API com o uso de _fixtures_. 83 | 84 | ## Exercício extra 1 85 | 86 | Abra o arquivo [`cypress/e2e/slow-tests/sample2.cy.js`](../cypress/e2e/slow-tests/sample2.cy.js) e em vez de navegar para a página de _Sign up_ através da _homepage_, visite a página diretamente com o uso do comando `cy.visit()`. 87 | 88 | ## Exercício extra 2 89 | 90 | Abra o arquivo [`cypress/e2e/slow-tests/sample3.cy.js`](../cypress/e2e/slow-tests/sample3.cy.js) e em vez de esperar sempre por 10 segundos antes de fazer a vefificação, aumente o `timeout` do comando `cy.contains('h2', 'Ramen (sopa)')` para esperar por **no máximo** 10 segundos, mas seguir adiante antes de fechar os 10 segundos caso o elemento que deseja-se verificar apareça antes. 91 | 92 | 🙊 😃 93 | 94 | ```js 95 | cy.contains('h2', 'Ramen (sopa)', { timeout: 10000 }) 96 | ``` 97 | 98 | ___ 99 | 100 | Ótimo! Diminuímos o tempo de execução de três testes. 🏎️ 101 | 102 | Vá para a [aula 9](./9.md) para conhecer a nona má prática (dependência entre testes) e como lidar com ela. 103 | -------------------------------------------------------------------------------- /src/meals.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const meals = [ 3 | { 4 | name: 'Escondidinho', 5 | ingredients: ['batata ou aipim', 'picado carnal', 'molho de tomate', 'cebola', 'alho'], 6 | type: 'hot' 7 | }, 8 | { 9 | name: 'Arroz exótico', 10 | ingredients: ['arroz', 'tofu', 'brócolis', 'ervilha', 'cenoura'], 11 | type: 'hot' 12 | }, 13 | { 14 | name: 'À la minuta', 15 | ingredients: ['batata frita', 'arroz', 'proteína', 'feijão'], 16 | type: 'hot' 17 | }, 18 | { 19 | name: 'Almondegas com arroz', 20 | ingredients: ['almondegas', 'molho de tomate', 'arroz',], 21 | type: 'hot' 22 | }, 23 | { 24 | name: 'Lasanha de berinjela', 25 | ingredients: ['berinjela', 'molho de tomate', 'molho branco', 'queijo vegano', 'massa de lasanha'], 26 | type: 'hot' 27 | }, 28 | { 29 | name: 'Espaguete de abobrinha', 30 | ingredients: ['abobrinha', 'tomate', 'manjericão', 'azeite', 'massa de espaguete'], 31 | type: 'hot' 32 | }, 33 | { 34 | name: 'Massa à bolonhesa', 35 | ingredients: ['azeite', 'massa de espaguete', 'molho de tomate', 'cebola', 'alho'], 36 | type: 'hot' 37 | }, 38 | { 39 | name: 'Chilly', 40 | ingredients: ['picado carnal', 'feijão vermelho', 'molho de tomate', 'pimenta', 'milho', 'abacate', 'cebola', 'tomate', 'alho', 'pimentão', 'nachos'], 41 | type: 'hot' 42 | }, 43 | { 44 | name: 'Carreteiro', 45 | ingredients: ['proteína', 'arroz', 'cebola', 'alho', 'molho de tomate', 'tempero verde'], 46 | type: 'hot' 47 | }, 48 | { 49 | name: 'Nhoque', 50 | ingredients: ['massa de nhoque', 'cebola', 'alho', 'molho vermelho', 'pimenta do reino'], 51 | type: 'hot' 52 | }, 53 | { 54 | name: 'Strogonoff', 55 | ingredients: ['proteína', 'arroz', 'cebola', 'alho', 'molho de tomate', 'creme vegetal', 'batata palha ou frita'], 56 | type: 'hot' 57 | }, 58 | { 59 | name: 'Galeto de frangos felizes', 60 | ingredients: ['maionese', 'batata rosa', 'cenoura', 'cebola', 'alho', 'milho', 'molho de tomate', 'espagueti', 'proteína'], 61 | type: 'hot' 62 | }, 63 | { 64 | name: 'Feijão tropeiro', 65 | ingredients: ['feijão', 'farinha de mandioca', 'cenoura', 'couve', 'cebola', 'alho'], 66 | type: 'hot' 67 | }, 68 | { 69 | name: 'Arroz e feijão', 70 | ingredients: ['arroz', 'feijão', 'cebola', 'alho'], 71 | type: 'hot' 72 | }, 73 | { 74 | name: 'Tofu e legumes ao shoyo com arroz', 75 | ingredients: ['arroz', 'tofu', 'pimentão', 'cebola', 'cenoura', 'abobrinha', 'shoyo'], 76 | type: 'hot' 77 | }, 78 | { 79 | name: 'Almôndegas de seitan com arroz negro', 80 | ingredients: ['almôndsgas de seitan', 'arroz negro', 'molho de tomate', 'ervilhas'], 81 | type: 'hot' 82 | }, 83 | { 84 | name: 'Salada de quinoa', 85 | ingredients: ['quinoa', 'tomate', 'pimentão', 'abacate', 'limão', 'coentro'], 86 | type: 'salad' 87 | }, 88 | { 89 | name: 'Salada de falafel', 90 | ingredients: ['falafel', 'folhas verdes', 'tomate', 'azeitonas', 'humus'], 91 | type: 'salad' 92 | }, 93 | { 94 | name: 'Saladão', 95 | ingredients: ['folhas verdes', 'tomate', 'azeitonas', 'milho', 'proteína (ex.: Heura)', 'crutons', 'pimenta'], 96 | type: 'salad' 97 | }, 98 | { 99 | name: 'Salada de quinoa e pistaches', 100 | ingredients: ['quinoa', 'pistache', 'cenoura ralada', 'ervilha', 'edamame', 'molho de gengibre', 'coentro', 'pepino ralado', 'agave'], 101 | type: 'salad' 102 | }, 103 | { 104 | name: 'Iron Energy', 105 | ingredients: ['folhas', 'quinoa', 'cranberries', 'nozes', 'uva verde', 'molho agridoce', 'brocolis', 'repolho'], 106 | type: 'salad' 107 | }, 108 | { 109 | name: 'Salada de grão de bico', 110 | ingredients: ['grão de bico', 'cenoura', 'pimentão', 'coentro', 'suco de limão', 'pimenta'], 111 | type: 'salad' 112 | }, 113 | { 114 | name: 'Salada de rúcula com tomates secos', 115 | ingredients: ['rúcula', 'tomates secos'], 116 | type: 'salad' 117 | }, 118 | { 119 | name: 'Ramen', 120 | ingredients: ['massa de ramen', 'brócolis', 'cenoura', 'edamame', 'tofu', 'cebolinha', 'misô', 'milho'], 121 | type: 'soup' 122 | }, 123 | { 124 | name: 'Sopa de legumes', 125 | ingredients: ['cenoura', 'abóbora', 'cebola', 'temperos'], 126 | type: 'soup' 127 | }, 128 | { 129 | name: 'Canja sem galinha', 130 | ingredients: ['cenoura', 'cebola', 'alho', 'batata', 'arroz', 'tempero verde', 'proteína (grão de bico ou Heura)'], 131 | type: 'soup' 132 | }, 133 | { 134 | name: 'Minestrone', 135 | ingredients: ['massa para sopa', 'feijão', 'ervilha', 'cenoura', 'molho de tomate'], 136 | type: 'soup' 137 | }, 138 | { 139 | name: 'Sopa de batata doce cremosa', 140 | ingredients: ['batata doce', 'leite de côco', 'pimenta'], 141 | type: 'soup' 142 | }, 143 | { 144 | name: 'Sopa de cenoura cremosa', 145 | ingredients: ['cenoura', 'cebola', 'alho'], 146 | type: 'soup' 147 | }, 148 | { 149 | name: 'Sopa de moranga cremosa', 150 | ingredients: ['moranaga', 'cebola', 'alho'], 151 | type: 'soup' 152 | }, 153 | { 154 | name: 'Caldo verde', 155 | ingredients: ['espinafre', 'brócolis', 'ervilha', 'cebola', 'alho', 'couve'], 156 | type: 'soup' 157 | }, 158 | { 159 | name: 'Sopa de lentilha', 160 | ingredients: ['lentilha', 'cenoura', 'cebola', 'alho', 'tempero verde'], 161 | type: 'soup' 162 | }, 163 | { 164 | name: 'Sopa cremosa de brócolis', 165 | ingredients: ['brócolis', 'batata', 'cenoura', 'ervilha', 'creme vegetal', 'grão de bico'], 166 | type: 'soup' 167 | }, 168 | { 169 | name: 'Creme de couve-flor', 170 | ingredients: ['couve-flor', 'batata', 'alho', 'tempero verde', 'manjericão'], 171 | type: 'soup' 172 | }, 173 | { 174 | name: 'Sopa de brócolis, batata e grão de bico', 175 | ingredients: ['brócolis', 'batata', 'grão de bico', 'alho', 'cebola', 'caldo de legumes', 'tempero verde'], 176 | type: 'soup' 177 | }, 178 | { 179 | name: 'Sopa de côco ao curry com grão de bico e vegetais', 180 | ingredients: ['leite de côco', 'grão de bico', 'caldo de legumes', 'champignon', 'curry', 'cebola', 'alho', 'gengibre', 'brócolis', 'cenoura', 'alho poró'], 181 | type: 'soup' 182 | }, 183 | { 184 | name: 'Creme de milho', 185 | ingredients: ['margarina', 'milho', 'batata', 'leite vegetal', 'caldo de legumes'], 186 | type: 'soup' 187 | }, 188 | { 189 | name: 'Sanduíche de milanesa de seitan', 190 | ingredients: ['maionese ou humus', 'pão integral', 'bife à milanesa de seitan', 'tomate', 'alface'], 191 | type: 'sandwich' 192 | }, 193 | { 194 | name: 'Xis Macaco', 195 | ingredients: ['pão de sanduíche', 'pasta de amendoim', 'banana', 'canela'], 196 | type: 'sandwich' 197 | }, 198 | { 199 | name: 'Subway', 200 | ingredients: ['maionese ou humus', 'baguete', 'tomate', 'alface', 'cebola roxa', 'Heura'], 201 | type: 'sandwich' 202 | }, 203 | { 204 | name: 'Hamburger', 205 | ingredients: ['maionese ou humus', 'pão de hamburger', 'tomate', 'alface', 'proteína'], 206 | type: 'sandwich' 207 | }, 208 | { 209 | name: 'Cachorro quente', 210 | ingredients: ['pão de cachorro quente', 'salsicha', 'molho de tomate', 'milho', 'ervilha', 'batata palha', 'catchup', 'mostarda'], 211 | type: 'sandwich' 212 | }, 213 | { 214 | name: 'Sanduíche de tomate e pepino', 215 | ingredients: ['humus ou margarina', 'pão francês', 'tomate', 'pepino', 'pimenta do reino'], 216 | type: 'sandwich' 217 | }, 218 | { 219 | name: 'Sanduíche de abacate', 220 | ingredients: ['abacate', 'pão', 'pimenta do reino', 'sal'], 221 | type: 'sandwich' 222 | }, 223 | ] 224 | --------------------------------------------------------------------------------