Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing
Mercari US end-to-end tests count
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
jest v29 7 years
mocha v10 11 years
npx available-versions mocha
tape v5 10 years
tap v16 11 years
ava v5 9 years
Node.js 13 years
May 2009
Write your own test runner!
JavaScript standard library is tiny
Node.js does not include test runner
Write your own test runner!
gt.test("get N '='", function () {
gt.ok(typeof getLines === "function",
"getLines is a function");
gt.equal(getLines(0), "", "0 character");
gt.equal(getLines(1), "=", "1 character");
});
Ported to v16.x
I am using v19.6.0
Test examples shown in this presentation: https://github.com/bahmutov/node-tests
2022-04-19
import test from 'node:test'
New built-in module "node:test"
$ node --test tests/*.mjs
New --test
Node CLI flag
import test from 'node:test'
import assert from 'node:assert/strict'
test('hello', () => {
const message = 'Hello'
assert.equal(message, 'Hello', 'checking the greeting')
})
Every test shown in this presentation: https://github.com/bahmutov/node-tests
import test from 'node:test'
import assert from 'node:assert/strict'
test('hello', () => {
const message = 'Hello'
assert.equal(message, 'Hello', 'checking the greeting')
})
$ node tests/demo.mjs
Tip: you can run tests in any JS file that has import "node:test" command
$ node --watch tests/demo.mjs
$ node --watch --tests tests/*.mjs
Tip: test runner is works with built-in Node --watch mode
Tip: finds the tests by default
$ node --tests
# runs tests in "test" subfolder
Start or end test file names with "test" to automatically recursively find them
tests/
names/
another.test.mjs
my_test.mjs
test-1.mjs
utils.mjs
$ node --test tests/names
- tests/names/another.test.mjs
- tests/names/my_test.mjs
- tests/names/test-1.mjs
import test from 'node:test'
import assert from 'node:assert/strict'
test('top level test', async (t) => {
await t.test('subtest 1', (t) => {
assert.strictEqual(1, 1)
})
await t.test('subtest 2', (t) => {
assert.strictEqual(2, 2)
})
})
Every test shown in this presentation: https://github.com/bahmutov/node-tests
Wrong "test" function
Every test shown in this presentation: https://github.com/bahmutov/node-tests
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
describe('top level test', () => {
it('subtest 1', () => {
assert.strictEqual(1, 1)
})
it('subtest 2', () => {
assert.strictEqual(2, 2)
})
})
Every test shown in this presentation: https://github.com/bahmutov/node-tests
$ node --test --test-reporter tap # default
$ node --test --test-reporter spec
$ node --test --test-reporter dot
$ node --test --test-reporter dot \
--test-reporter-destination stdout \
--test-reporter spec \
--test-reporter-destination out.txt
multiple reporters
Test Anything Protocol
Demo "node --test" vs "node --test | npx faucet"
# npm i -D faucet
$ node --test | npx faucet
"node --test" plus https://github.com/ljharb/faucet output
it('works', () => {
assert.equal(1, 1)
})
it('fails', () => {
assert.equal(2, 5)
})
it.todo('loads data')
// SKIP: <issue link>
it.skip('stopped working', () => {
assert.equal(2, 5)
})
before(() => {
console.log('before hook')
throw new Error('Setup fails')
})
it('works', () => {
assert.equal(1, 1)
})
it('works again', () => {
assert.equal(1, 1)
})
🐞
via built-in Node assert module
import assert from 'node:assert/strict'
assert.ok(truthy, message)
assert.equal(value, expected, ...)
assert.deepEqual(...)
assert.match(value, regexp)
assert.throws(fn)
assert.rejects(asyncFn)
plus ".notX" assertions
quite sparse compared to https://www.chaijs.com/api/bdd/
via built-in Node assert module
it('fails objects on purpose', () => {
const person = { name: { first: 'Joe' } }
assert.deepEqual(person,
{ name: { first: 'Anna' } }, 'people')
})
fail the assertion on purpose
import { it } from 'mocha'
import { expect } from 'chai'
it('fails objects on purpose', (t) => {
const person = { name: { first: 'Joe' } }
expect(person).to.deep.equal(
{ name: { first: 'Anna' } })
})
fail the assertion on purpose
with its "magic assert"
import test from 'ava'
test('fails objects on purpose', (t) => {
const person = { name: { first: 'Joe' } }
t.deepEqual(person,
{ name: { first: 'Anna' } }, 'people')
})
fail the assertion on purpose
1. Spy / stub methods when you have an object reference
const person = {
name () {
return 'Joe'
}
}
stub(person, 'name').return('Anna')
Sinon https://sinonjs.org/ 👑👑👑
1. Spy / stub methods when you have an object reference
import { it, mock } from 'node:test'
import assert from 'node:assert/strict'
it('returns name', () => {
const person = {
name() {
return 'Joe'
},
}
assert.equal(person.name(), 'Joe')
mock.method(person, 'name', () => 'Anna')
assert.equal(person.name(), 'Anna')
assert.equal(person.name.mock.calls.length, 1)
person.name.mock.restore()
assert.equal(person.name(), 'Joe')
})
t.plan(2)
jest.useFakeTimers()
deno test --fail-fast
test.failing('found a bug', t => {
// Test will count as passed
t.fail();
});
Feature | Node.js TR | Mocha | Ava | Jest |
---|---|---|---|---|
Included with Node | ✅ | 🚫 | 🚫 | 🚫 |
Watch mode | ✅ | ✅ | ✅ | ✅ |
Reporters | via TAP | strong | via TAP | lots |
Assertions | weak | via Chai ✅ | ✅ | ✅ |
Snapshots | 🚫 | 🚫 | ✅ | ✅ |
Hooks | ✅ | ✅ | ✅ | ✅ |
grep support | ✅ | ✅ | ✅ | ✅ |
spy and stub | ✅ | via Sinon ✅ | via Sinon ✅ | ✅✅ |
parallel execution | 🚫 | ✅ | ✅ | ✅ |
code coverage | 🚫 | via nyc | via c8 | ✅ |
TS support | via ts-node | via ts-node | via ts-node | via ts-jest |
node --test
on smaller new projectsBook "A Frontend Web Developer's Guide to Testing" by Eran Kinsbruner
Cypress vs Playwright – Clash of the Titans
https://www.zeljkovic.sh/cypress-vs-playwright-clash-of-the-titans-introduction/
On Migrating from Cypress to Playwright
Playwright vs Cypress: A Comparison
Playwright vs. Cypress: Which Cross-Browser Testing Solution Is Right for You?
Playwright vs Cypress: Which Framework to Choose For E2E Testing?
https://labs.eleks.com/2022/07/playwright-vs-cypress-e2e-testing.html
Five reasons why Playwright is better than Cypress
https://alisterbscott.com/2021/10/27/five-reasons-why-playwright-is-better-than-cypress/
Playwright vs Cypress
Cypress vs Playwright: Let the Code Speak
Playwright vs Cypress E2E Testing
https://github.com/cliffordfajardo/cypress-vs-playwright-comparison
Cypress vs Playwright: What’s the Best Test Automation Framework for Your Project?
Cypress vs. Playwright: end-to-end testing showdown
https://silvenon.com/blog/e2e-testing-with-cypress-vs-playwright
On Migrating from Cypress to Playwright
Playwright vs Cypress: A Comparison
Playwright vs. Cypress: Which Cross-Browser Testing Solution Is Right for You?
Playwright vs Cypress: Which Framework to Choose For E2E Testing?
https://labs.eleks.com/2022/07/playwright-vs-cypress-e2e-testing.html
Five reasons why Playwright is better than Cypress
https://alisterbscott.com/2021/10/27/five-reasons-why-playwright-is-better-than-cypress/
Playwright vs Cypress
Cypress vs Playwright: Let the Code Speak
Playwright vs Cypress E2E Testing
https://github.com/cliffordfajardo/cypress-vs-playwright-comparison
Cypress vs Playwright: What’s the Best Test Automation Framework for Your Project?
Cypress vs. Playwright: end-to-end testing showdown
https://silvenon.com/blog/e2e-testing-with-cypress-vs-playwright
Book "A Frontend Web Developer's Guide to Testing" by Eran Kinsbruner
Cypress vs Playwright – Clash of the Titans
https://www.zeljkovic.sh/cypress-vs-playwright-clash-of-the-titans-introduction/
👍
👍
Cypress vs Playwright: What’s the Best Test Automation Framework for Your Project?
Any comparison older than one month
Is this accurate?
how important is this for us?
should it be a core feature?
import { test } from '@playwright/test'
test('logs hello', async () => {
console.log('hello, world')
})
it('logs hello', () => {
console.log('hello, world')
})
// pw
// cy
// pw
// cy
Cypress iframes the app and its specs
Node.js
Playwright test code
Browser
application
Chrome Debugger Protocol
Node.js
Config / plugins file
Browser
application
Cypress test code
WebSocket
for
cy.task
Node.js
Playwright test code
Browser
application
Chrome Debugger Protocol
Node.js
Config / plugins file
Browser
application
Cypress test code
WebSocket
for
cy.task
Chrome Debugger Protocol
it('listens to the window.postMessage events', () => {
cy.visit('index.html', {
onBeforeLoad(win) {
cy.spy(win, 'postMessage').as('postMessage')
},
})
cy.get('@postMessage')
.should('have.been.calledTwice')
.and('have.been.calledWithExactly', 'one')
.and('have.been.calledWithExactly', 'two')
})
Direct access to the application's objects and browser APIs from the test code
import { todos } from '../fixtures/three.json'
it('copies the todos to clipboard', () => {
cy.request('POST', '/reset', { todos })
cy.visit('/')
cy.get('li.todo').should('have.length', todos.length)
cy.get('[title="Copy todos to clipboard"]').click()
})
async copyTodos({ state }) {
const markdown =
state.todos
.map((todo) => {
const mark = todo.completed ? 'x' : ' '
return `- [${mark}] ${todo.title}`
})
.join('\n') + '\n'
await navigator.clipboard.writeText(markdown)
}
// app.js
cy.window()
.its('navigator.clipboard')
.then((clipboard) => {
cy.stub(clipboard, 'writeText').as('writeText')
})
cy.get('[title="Copy todos to clipboard"]').click()
cy.get('@writeText').should(
'have.been.calledOnceWith',
Cypress.sinon.match.string,
)
// cy
cy.window()
.its('navigator.clipboard')
.then((clipboard) => {
cy.stub(clipboard, 'writeText').as('writeText')
})
cy.get('[title="Copy todos to clipboard"]').click()
cy.get('@writeText').should(
'have.been.calledOnceWith',
Cypress.sinon.match.string,
)
// cy
import { test, expect } from '@playwright/test'
test('adds todos', async ({ page, request }) => {
await page.goto('/')
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?')
await newTodo.fill('one')
await newTodo.press('Enter')
await newTodo.fill('two')
await newTodo.press('Enter')
const todoItems = page.locator('li.todo')
await expect(todoItems).toHaveCount(2)
})
// pw
npx cypress run
Headless Cypress test run
npx playwright test --trace on
Get useful information about the Playwright test run
Playwright watch mode discussion
import { test, expect } from '@playwright/test'
test('adds todos', async ({ page, request }) => {
await request.post('/reset', { data: { todos: [] } })
await page.goto('/')
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?')
await newTodo.fill('one')
await newTodo.press('Enter')
await newTodo.fill('two')
await newTodo.press('Enter')
const todoItems = page.locator('li.todo')
await expect(todoItems).toHaveCount(2)
})
it('adds todos', () => {
cy.request('POST', '/reset', { todos: [] })
cy.visit('/')
cy.get('.new-todo').type('one{enter}').type('two{enter}')
cy.get('li.todo').should('have.length', 2)
})
// cy
// pw
import { test, expect } from '@playwright/test'
test('adds todos', async ({ page, request }) => {
await request.post('/reset', { data: { todos: [] } })
await page.goto('/')
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?')
await newTodo.fill('one')
await newTodo.press('Enter')
await newTodo.fill('two')
await newTodo.press('Enter')
const todoItems = page.locator('li.todo')
await expect(todoItems).toHaveCount(2)
})
it('adds todos', () => {
cy.request('POST', '/reset', { todos: [] })
cy.visit('/')
cy.get('.new-todo').type('one{enter}').type('two{enter}')
cy.get('li.todo').should('have.length', 2)
})
// cy
// pw
value (subject) flows through the commands and assertions
import { test, expect } from '@playwright/test'
test('adds todos', async ({ page, request }) => {
await request.post('/reset', { data: { todos: [] } })
await page.goto('/')
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?')
await newTodo.fill('one')
await newTodo.press('Enter')
await newTodo.fill('two')
await newTodo.press('Enter')
const todoItems = page.locator('li.todo')
await expect(todoItems).toHaveCount(2)
})
it('adds todos', () => {
cy.request('POST', '/reset', { todos: [] })
cy.visit('/')
cy.get('.new-todo').type('one{enter}').type('two{enter}')
cy.get('li.todo').should('have.length', 2)
})
// cy
// pw
Swipe right
(data flows to the right)
Swipe left
(data is assigned to the left)
var k = 0;
for(k = 0; k < numbers.length; k += 1) {
console.log(numbers[k] * constant);
}
// 6 2 14
// _ is Lodash / Ramda
_(numbers)
.map(_.partial(mul, constant))
.forEach(print);
// 6 2 14
// functional
// imperative
Swipe right
(data flows to the right)
Swipe left
(data is assigned to the left)
const n = Number(await locator.getText())
// n is set
cy.get('#count').then(n =>
// n is set
)
// right
// left
Swipe right
(data flows to the right)
Swipe left
(data is assigned to the left)
const n = Number(await locator.getText())
// n is set
cy.get('#count').then(n =>
// n is set
)
// right
// left
What do you want to check on the page?
cy.get('li.todo').should('have.length', 2)
There should be 2 items
cy.get('li.todo')
.should('satisfy', $li => $li.length % 2 === 0)
There should be an even number of items
cy.wait('@load').its('response.body.length')
.then(n => {
cy.get('li.todo').should('have.length', n)
})
There should be the same number of items as returned by the server
cy.intercept('GET', '/todos', { fixture: 'three.json' })
cy.get('li.todo').should('have.length', 3)
If you can control the data in your tests, then the test syntax collapses into a simple and elegant fluent chain
$todo
"Good Cypress Test Syntax" https://www.youtube.com/watch?v=X8iIoTxu_8k
for Cypress and Playwright
core features
experimental features
plugins
external tools
cypress-real-events, cypress-aliases
import Todo from '../app/todo';
import React from 'react';
import { mount } from 'enzyme';
test('TodoComponent renders the text inside it', () => {
const todo = { id: 1, done: false, name: 'Buy Milk' };
const wrapper = mount(
<Todo todo={todo} />
);
const p = wrapper.find('.toggle-todo');
expect(p.text()).toBe('Buy Milk');
});
import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'
it('sets the clock to the given value', () => {
const timeGameStarted = moment().subtract(900, 'seconds')
cy.mount(
<SudokuContext.Provider value={{ timeGameStarted }}>
<section className="status">
<Timer />
</section>
</SudokuContext.Provider>,
)
cy.contains('15:00')
})
Timer.cy.ts
The Timer component with applied styling
Timer
Numbers
Difficulty
StatusSection
GameSection
Game
App
import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
it('shows the selected number', () => {
cy.mount(
<SudokuContext.Provider
value={{ numberSelected: '8' }}
>
<div className="innercontainer">
<section className="status">
<Numbers
onClickNumber={cy.stub().as('click')}
/>
</section>
</div>
</SudokuContext.Provider>,
)
cy.contains('.status__number', '8').should(
'have.class',
'status__number--selected',
)
cy.contains('.status__number', '9').click()
cy.get('@click').should(
'have.been.calledOnceWithExactly',
'9',
)
})
Props + Provider are framework specific
src/components/Numbers.cy.js
import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
it('shows the selected number', () => {
cy.mount(
<SudokuContext.Provider
value={{ numberSelected: '8' }}
>
<div className="innercontainer">
<section className="status">
<Numbers
onClickNumber={cy.stub().as('click')}
/>
</section>
</div>
</SudokuContext.Provider>,
)
cy.contains('.status__number', '8').should(
'have.class',
'status__number--selected',
)
cy.contains('.status__number', '9').click()
cy.get('@click').should(
'have.been.calledOnceWithExactly',
'9',
)
})
Props + Provider are framework specific
The rest of the test is Cypress API only
src/components/Numbers.cy.js
import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
it('shows the selected number', () => {
cy.mount(
<SudokuContext.Provider
value={{ numberSelected: '8' }}
>
<div className="innercontainer">
<section className="status">
<Numbers
onClickNumber={cy.stub().as('click')}
/>
</section>
</div>
</SudokuContext.Provider>,
)
cy.contains('.status__number', '8').should(
'have.class',
'status__number--selected',
)
cy.contains('.status__number', '9').click()
cy.get('@click').should(
'have.been.calledOnceWithExactly',
'9',
)
})
Props + Provider are framework specific
The rest of the test is Cypress API only
src/components/Numbers.cy.js
No more framework-specific syntax
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Toggle from "./toggle";
let container = null;
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("changes value when clicked", () => {
const onChange = jest.fn();
act(() => {
render(<Toggle onChange={onChange} />, container);
});
// get a hold of the button element, and trigger some clicks on it
const button = document.querySelector("[data-testid=toggle]");
expect(button.innerHTML).toBe("Turn on");
act(() => {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledTimes(1);
expect(button.innerHTML).toBe("Turn off");
act(() => {
for (let i = 0; i < 5; i++) {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
});
expect(onChange).toHaveBeenCalledTimes(6);
expect(button.innerHTML).toBe("Turn on");
});
import React from "react";
import Toggle from "./toggle";
it("changes value when clicked", () => {
cy.mount(<Toggle onChange={cy.stub().as('change')} />);
// get a hold of the button element, and trigger some clicks on it
cy.contains("[data-testid=toggle]", "Turn on").click()
cy.get('@change').should('have.been.calledOnce')
cy.contains("[data-testid=toggle]", "Turn off")
.click()
.click()
.click()
.click()
.click()
cy.get('@change').its('callCount').should('eq', 6)
cy.contains("[data-testid=toggle]", "Turn on")
});
equivalent Cypress component test
expect(formatTime({ seconds: 3 }))
.to.equal('00:03')
import { Component } from './Component'
cy.mount(<Component props=... />)
cy.get(...).click()
cy.visit('/')
cy.get(...).click()
Hmm, maybe Node test runner is too new to be in the AI knowledge model
wildly incorrect
Built-in Node test runner is here
Use a real browser
Cypress and Playwright
By Gleb Bahmutov
Today JavaScript developers are enjoying a cornucopia of testing tools. In this presentation, I will look at the existing Node testing tools Jest, Ava, Mocha, and the new player on the block: the built-in Node.js test runner module. We will also consider the component and end-to-end browser testing using Playwright and Cypress.io. Everyone who writes JavaScript for a living will benefit from knowing the features of these modern testing tools.
JavaScript ninja, image processing expert, software quality fanatic