code may be tested automatically to ensure it behaves as expected
popularity:
We are going to write and test a function that will shorten a string to a specified length:
shorten('loremipsum', 6);
// should return 'lor...'
Implementation that should be tested:
/**
* shortens a given string to a specified length,
* adding "..." at the end if it was shortened
*/
function shorten(text, maxlength) {
if (text.length > maxlength) {
return text.slice(0, maxlength - 3) + '...';
}
return text;
}
export default shorten;
simple tests:
// shorten.test.js
import assert from 'assert/strict';
import shorten from './shorten';
assert.equal(shorten('loremipsum', 4), 'l...');
assert.equal(shorten('loremipsum', 9), 'loremi...');
assert.equal(shorten('loremipsum', 10), 'loremipsum');
assert.equal(shorten('loremipsum', 11), 'loremipsum');
assert.equal
throws an exception if the condition is not met
Assertions can be written in different styles:
assert:
assert.equal(a, b);
expect (example from jest):
expect(a).toEqual(b);
should (example from cypress):
inputField.should('have.value', '');
assert (node):
assert.equal(a, b);
assert.deepEqual(a, b);
assert.throws(() => JSON.parse(''));
// ...
expect(a).to.equal(4);
expect(a).not.to.equal(2);
expect(a).to.be.greaterThan(3);
expect(a).to.be.a('number');
expect(() => JSON.parse('')).to.throw();
expect(a).toEqual(4);
expect(a).not.toEqual(2);
expect(a).toBe(4);
expect(a).toBeGreaterThan(3);
expect(a).toBeInstanceOf(Number);
expect(() => JSON.parse('')).toThrow();
deep equality: compares contents of objects / arrays
assert.deepEqual([1, 2], [1, 2]);
expect([1, 2]).to.eql([1, 2]);
expect([1, 2]).toEqual([1, 2]);
strict equality: behaves like ===
- may be used with primitives (or for asserting object identity)
assert.equal('abc', 'abc');
expect('abc').to.equal('abc');
expect('abc').toBe('abc');
Tests are commonly run via an npm script - e.g. via npm run test
(or npm test
for short)
Jest: by default looks for files inside of directories named __tests__
and for files ending in .test.js
or .spec.js
Mocha: by default looks for files inside the directory test
(custom pattern via e.g.: mocha "src/**/*.{test,spec}.{js,jsx}"
)
the definition of a test usually includes:
tests are commonly defined via by calling test()
or it()
example with node's built-in tools:
test(
'shortens "loremipsum" to "lor..." with limit 6',
() => {
const shortened = shorten('loremipsum', 6);
assert.equal(shortened, "lor...");
}
)
tests can be organized into groups (and sub-groups, ...)
grouping tests with node:test:
test('strings that are short enough', (t) => {
// t: test context
t.test('leaves "abc" unchanged with limit 3', () => {
assert.equal(shorten('abc', 3), 'abc');
});
t.test('leaves "a" unchanged with limit 1', () => {
assert.equal(shorten('a', 1), 'a');
});
});
grouping tests with jest:
describe('strings that are short enough', () => {
test('leaves "abc" unchanged with limit 3', () => {
expect(shorten('abc', 3)).toEqual('abc');
});
test('leaves "a" unchanged with limit 1', () => {
expect(shorten('a', 3)).toEqual('a');
});
});
Some testing libraries can report on how much of the code is covered by tests:
npx jest --coverage
in a create-react-app project:
npm test -- --coverage
For code that should be executed before and after each test in a group:
describe('database', () => {
beforeEach(() => {
createTestDB();
});
afterEach(() => {
clearTestDB();
});
it(/*...*/);
it(/*...*/);
});
write tests for the behavior of the string method .replace()
End-to-end tests: an instance of a browser is controlled to interact with a website and verify its behavior
tools:
uses mocha and chai in the background
for intialization, run npx cypress open
once
(will create folder cypress and file cypress.config.js in the current directory)
opening the graphical user interface:
npx cypress open
running all tests in the terminal:
npx cypress run
example: testing Wikipedia:
in cypress/e2e/wikipedia.cy.js:
describe('wikipedia', () => {
beforeEach(() => {
cy.visit('https://en.wikipedia.org');
});
it("page title includes 'wikipedia'", () => {
cy.title().should('match', /wikipedia/i);
});
});
global queries:
cy.contains("h1", "hello world")
cy.contains("hello world")
cy.get("main #name-input")
cy.get("main #name-input").first()
querying sub-elements:
.find()
: similar to .get()
, but for sub-elements.contains()
cy.contains('li', 'TODO: write tests').find('button');
cy.contains('li', 'TODO: write tests').contains(
'button',
'delete'
);
getting the parent:
cy.contains('h1', 'section title').parent();
interacting with elements:
cy.get("#reset-btn").click()
cy.get("#name-input").type("foo")
cy.get("#name-input").type("{esc}")
example assertion:
cy.get('#name-input').should('have.class', 'invalid');
other assertions::
cy.url().should("match", /\/items\/d+$/)
.should("have.class", "invalid")
.should("have.value", "")
.should("have.length", 10)
.should("include.text", "foo")
.should("be.visible")
example: Searching on Wikipedia
it('search', () => {
cy.get('#searchInput').type('cypress');
cy.get('#searchButton').click();
cy.get('p').first().should('include.text', 'Cypress');
});
Write tests for the todo app at:
npm packages:
we will use jest as a test runner
testing Wikipedia:
import puppeteer from 'puppeteer';
test('wikipedia title', async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://en.wikipedia.org');
const pageTitle = await page.title();
expect(pageTitle).toMatch(/Wikipedia/);
await browser.close();
});
restructuring code for multiple tests:
let browser: puppeteer.Browser;
let page: puppeteer.Page;
beforeAll(async () => {
browser = await puppeteer.launch();
});
beforeEach(async () => {
page = await browser.newPage();
await page.goto('https://en.wikipedia.org');
});
afterAll(async () => {
await browser.close();
});
test('wikipedia title', async () => {
const pageTitle = await page.title();
expect(pageTitle).toMatch(/Wikipedia/);
});
tests that actually open a browser window:
beforeAll(async () => {
browser = await puppeteer.launch({ headless: false });
});
querying elements is not trivial since there are two separate JavaScript environments (node and Chromium)
querying elements for getting their contents:
page.$eval()
for a single elementspage.$$eval()
for multiple elementsquerying elements for triggering actions:
page.$()
for single elementspage.$$()
for an array of elementsgetting elements to retrieve their content:
const firstLinkText = await page.$eval(
'a',
(element) => element.innerHTML
);
const thirdLinkText = await page.$$eval(
'a',
(elements) => elements[2].innerHTML
);
getting an element for triggering an action:
const firstLink = await page.$('a');
await firstLink.click();
await page.waitForNavigation();
example: Searching on Wikipedia
test('wikipedia search', async () => {
await page.click('#searchInput');
await page.keyboard.type('puppeteer');
await page.click('#searchButton');
await page.waitForNavigation();
const paragraphText = await page.$eval(
'p',
(element) => element.textContent
);
console.log(paragraphText);
expect(paragraphText).toMatch(/puppeteer/i);
});
notes: page.keyboard.press("Enter") would trigger full-text search; on some Wikipedia pages the first paragraph might be empty.
important methods:
page.$()
page.$$()
page.$eval()
page.$$eval()
page.keyboard.type("abc")
page.keyboard.press("Enter")
page.click("#selector")
element.click()
page.waitForNavigation()
Write tests for the todo app at:
includes expect from Jest
npm install @playwright/test
install browsers (Chromium, Firefox, Webkit):
npx playwright install
run tests:
npx playwright test
run in "headed" mode (actually opening a browser window):
npx playwright test --headed
testing Wikipedia:
import { test, expect } from '@playwright/test';
test('wikipedia title', async ({ page }) => {
await page.goto('https://en.wikipedia.org');
const pageTitle = await page.title();
expect(pageTitle).toMatch(/Wikipedia/);
});
restructuring code for multiple tests:
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://en.wikipedia.org');
});
test('wikipedia title', async ({ page }) => {
const pageTitle = await page.title();
expect(pageTitle).toMatch(/Wikipedia/);
});
clicking on a link:
await page.click('text=About Wikipedia');
example: Searching on Wikipedia
test('wikipedia search', async ({ page }) => {
await page.fill(
'input[aria-label="Search Wikipedia"]',
'foo'
);
await page.click('#searchButton');
await page.waitForNavigation();
const mainHeading = page.locator('h1');
await expect(mainHeading).toHaveText(/foo/i);
});
note: page.keyboard.press("Enter") would trigger full-text search
mocking: simulating objects / interfaces in a testing environment
Example: Mocking a network request
import fetchMock from 'fetch-mock';
fetchMock.mock('https://example.com', { foo: 'bar' });
mocking a module via jest.mock
:
jest.mock('axios', () => ({
get: () => Promise.resolve({ data: { foo: 'bar' } }),
}));
mocking modules via __mocks__ folders:
__mocks__/fs.js
__mocks__/axios.js
src/foo.js
src/foo.test.js
src/__mocks__/foo.js
// src/foo.test.js
jest.mock('fs');
jest.mock('axios'); // optional for contents of node_modules
jest.mock('./foo');
Note: inside a create-react-app project this would be e.g. src/__mocks__/axios.js
instead of __mocks__/axios.js
(see issue)
Mocking a function that can be called and inspected later:
const mockFn = jest.fn();
mockFn('foo');
expect(mockFn).toHaveBeenCalledWith('foo');