Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
April 24th, 2019
Add your questions during this webinar
"The mother of all demo apps" — Exemplary fullstack Medium.com clone
tip: these slides are arranged in columns,
so press the Down arrow to see the next slide
Web app at port 4100
API at port 3000
git clone
npm install
npm start
User can log in
User can write a blog post
User can read posts
User features covered in order of importance
$ npm install -D cypress
tip: these slides are arranged in columns,
so press the Down arrow to see the next slide
{
"baseUrl": "http://localhost:4100",
"viewportHeight": 1000,
"viewportWidth": 1000
}
cypress.json file with all Cypress settings
{
"scripts": {
"start": "concurrently npm:start:client npm:start:server",
"start:client": "cd client && npm start",
"start:server": "cd server && npm start",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
}
}
npm start
starts the application
npm run cypress:open
User can NOT log in
demo login-spec.js
Without a valid account
tip: these slides are arranged in columns,
so press the Down arrow to see the next slide
User can NOT log in
describe('Conduit Login', () => {
beforeEach(() => {
cy.visit('/')
})
it('does not work with wrong credentials', () => {
cy.contains('a.nav-link', 'Sign in').click()
cy.get('input[type="email"]').type('wrong@email.com')
cy.get('input[type="password"]').type('no-such-user')
cy.get('button[type="submit"]').click()
// error message is shown and we remain on the login page
cy.contains('.error-messages li', 'User Not Found')
cy.url().should('contain', '/login')
})
})
demo login-spec.js
User can log in
Hmm, we need a test account
Tip: observe what the application does during new user sign up
XHR call
We can do the same before the test
Cypress.Commands.add('registerUserIfNeeded', () => {
cy.request({
method: 'POST',
url: 'http://localhost:3000/api/users',
body: {
user: {
username: 'testuser',
image: 'https://robohash.org/6FJ.png?set=set3&size=150x150',
// email, password from cypress.json
// or from environment variables
...Cypress.env('user'),
},
},
// ignore duplicate user error
failOnStatusCode: false,
})
})
Custom command "cy.registerUserIfNeeded" in cypress/support/index.js file
User can log in
demo login-spec.js
tip: these slides are arranged in columns,
so press the Down arrow to see the next slide
User can log in
describe('Conduit Login', () => {
before(() => cy.registerUserIfNeeded())
beforeEach(() => { cy.visit('/') })
it('logs in', () => {
cy.contains('a.nav-link', 'Sign in').click()
const user = Cypress.env('user')
cy.get('input[type="email"]').type(user.email)
cy.get('input[type="password"]').type(user.password)
cy.get('button[type="submit"]').click()
// when we are logged in, there should be two feeds
cy.contains('a.nav-link', 'Your Feed').should('have.class', 'active')
cy.contains('a.nav-link', 'Global Feed')
.should('not.have.class', 'active')
cy.url().should('not.contain', '/login')
})
})
demo login-spec.js
Username and password
{
"baseUrl": "http://localhost:4100",
"env": {
"user": {
"email": "tester@test.com",
"password": "password1234"
}
},
"viewportHeight": 1000,
"viewportWidth": 1000
}
cypress.json file with all Cypress settings
// from tests
const user = Cypress.env('user')
cy.get('input[type="email"]').type(user.email)
cy.get('input[type="password"]').type(user.password)
User can write a blog post
demo new-post-spec.js
1. Login
* via API ✅✅✅
Organizing Tests, Logging In, Controlling State
2. Add new blog post
3. Check if it appears in the feed
tip: these slides are arranged in columns,
so press the Down arrow to see the next slide
Observe what your web application is doing during sign in
Web application makes a POST request to log in
We can do the same before each test
The response includes a token
This token is saved in local storage under "jwt" key
Login via API
Cypress.Commands.add('login', () => {
cy.request('POST', 'http://localhost:3000/api/users/login', {
user: Cypress.env('user'),
})
.its('body.user.token')
.should('exist')
.then((token) => {
localStorage.setItem('jwt', token)
})
// visit will run AFTER cy.request
// finishes successfully
cy.visit('/')
})
login command in cypress/support/index.js
User can write a blog post
demo new-post-spec.js
describe('New post', () => {
before(() => cy.registerUserIfNeeded())
beforeEach(() => {
cy.login()
})
it('writes a post and comments on it', () => {
cy.contains('a.nav-link', 'New Post').click()
// editor is displayed
...
})
})
How to select these input fields?
<fieldset className='form-group'>
<input
className='form-control form-control-lg'
type='text'
placeholder='Article Title'
data-cy='title'
value={this.props.title}
onChange={this.changeTitle}
/>
</fieldset>
Add selectors for testing
User can write and comment
it('writes a post and comments on it', () => {
cy.contains('a.nav-link', 'New Post').click()
// submit new post
// I have added "data-cy" attributes to select input fields
cy.get('[data-cy=title]').type('my title')
cy.get('[data-cy=about]').type('about X')
cy.get('[data-cy=article]').type('this post is **important**.')
cy.get('[data-cy=tags]').type('test{enter}')
cy.get('[data-cy=publish]').click()
// comment on the post
cy.get('[data-cy=comment-text]').type('great post 👍')
cy.get('[data-cy=post-comment]').click()
// check that comment appears
cy.contains('[data-cy=comment]', 'great post 👍').should('be.visible')
})
demo new-post-spec.js
tip: these slides are arranged in columns,
so press the Down arrow to see the next slide
User can write a blog post
// move long post fields into own JS file
import { title, about, article, tags } from '../fixtures/post'
it('adds a new post', () => {
cy.contains('a.nav-link', 'New Post').click()
cy.get('[data-cy=title]').type(title)
cy.get('[data-cy=about]').type(about)
...
})
demo new-post-spec.js
import post contents from another JavaScript file
tip: these slides are arranged in columns,
so press the Down arrow to see the next slide
User can add tags
const tags = ['code', 'testing', 'cypress.io']
cy.get('[data-cy=tags]').type(tags.join('{enter}') + '{enter}')
cy.get('[data-cy=publish]').click()
// check that each tag is displayed after post is shown
cy.url().should('match', /my-title$/)
tags.forEach(tag => cy.contains('.tag-default', tag))
set and check a list of tags: Cypress is just JavaScript™
User can add tags
demo new-post-spec.js
// move long post fields into own JS file
import { title, about, article, tags } from '../fixtures/post'
it('adds a new post', () => {
cy.contains('a.nav-link', 'New Post').click()
cy.get('[data-cy=title]').type(title)
cy.get('[data-cy=about]').type(about)
cy.get('[data-cy=article]')
.type(article)
})
demo new-post-spec.js
import { stripIndent } from 'common-tags'
const post = stripIndent`
# Fast tests
> Speed up your tests using direct access to DOM elements
You can set long text all at once and then trigger \`onChange\` event.
`
cy.get('[data-cy=article]')
.invoke('val', post)
.type('{enter}')
Redux events in this application
if (window.Cypress) {
window.store = store
}
// dispatch Redux actions
cy.window()
.its('store')
.invoke('dispatch', {
type: 'UPDATE_FIELD_EDITOR',
key: 'body',
value: article
})
application index.js
from test
Verify the post is saved
// after submitting the new blog post
cy.request('http://localhost:3000/api/articles?limit=10&offset=0')
.its('body')
.should(body => {
expect(body).to.have.property('articlesCount', 1)
expect(body.articles).to.have.length(1)
const firstPost = body.articles[0]
expect(firstPost).to.contain({
title,
description: about,
body: article
})
})
demo new-post-spec.js
Problem: when we run the test for the second time, it fails
Before each test:
Connect to the database and clean any existing data
Cypress tests run in the browser
Database connection cannot be done directly from the browser
const { join } = require('path')
const knexFactory = require('knex')
module.exports = (on, config) => {
on('task', {
deleteAllArticles () {
const filename = join(__dirname, '..', '..', 'server', '.tmp.db')
const knex = knexFactory({
client: 'sqlite3',
connection: {
filename
},
useNullAsDefault: true
})
// truncates all tables which removes data left by previous tests
return Promise.all([
knex.truncate('Articles'),
knex.truncate('ArticleTags'),
knex.truncate('Comments')
])
}
})
}
Write backend Nodejs code in the Cypress plugins file
Before each test:
Connect to the database and clean any existing data
before(() => cy.registerUserIfNeeded())
beforeEach(() => {
cy.task('deleteAllArticles')
cy.login()
})
cy.task calls the Cypress backend to run Nodejs code
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- <link rel="stylesheet" href="/main.css" /> -->
Functional tests still pass!
Take it, Gil!
By Gleb Bahmutov
Presentation for Cypress.io + Applitools webinar covering: creating a test user, logging in via API, test shortcuts, clearing the data before tests. Uses the real-world app as an example.
JavaScript ninja, image processing expert, software quality fanatic