Workshop CWC Testing
I don't have time to write tests because I am too busy
Confidence to change code: well-written tests allow you to refactor code with confidence that you’re not breaking anything, and without wasting time updating the tests.
Documentation: tests explain how code works and what’s the expected behavior. Tests, in comparison to any written documentation, are always up to date.
Bugs and regression prevention: by adding test cases for every bug, found in your app, you can be sure that these bugs will never come back. Writing tests will improve your understanding of the code and the requirements, you’ll critically look at your code and find issues that you’d miss otherwise.
Why automate testing
Static analysis catches syntax errors, bad practices and incorrect use of APIs: — Code formatters, like Prettier; — Linters, like ESLint; — Type checkers, like TypeScript and Flow.
Unit tests verify that tricky algorithms work correctly. Tools: Jest.
Integration tests give you confidence that all features of your app work as expected. Tools: Jest and Enzyme or react-testing-library.
End-to-end tests make sure that your app work as a whole: the frontend and the backend and the database and everything else. Tools: Cypress.
The different tests
The different tests
Unit Tests
Jest
Jest, Jest DOM, React Testing Library
Integration Tests
UI
Tests
Cypress, TestCafe
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
Multiple runs of the test should consistently return true or consistently return false, provided no changes
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
Only two possible results : PASS or FAIL
No partially successful tests
No side effects between tests
Isolation of tests
- Different execution order must yield same results
- Test B should not depend on outcome of Test A (use mocks instead)
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
One test should be responsible for one scenario only
Test behavior, not methods
- One method, multiple behaviors -> multiple tests
- One behavior, multiple methods -> one test
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
Unit test must be easy to read and understand
- variable, method and class names must be self descriptive
- no conditional logic
- no loops
Name tests to represent PASS conditions:
- canMakeReservation
- totalBillEqualsSumOfMenuItemPrices
Test should have no uncertainty:
- All inputs should be known
- Method behavior should be predictable
- Expected output should be strictly defined
- Split in to two tests rather than using "if" or "switch"
Test should not contain "while", "for" or "foreach" loops
- If test logic has to be repeated, it probably means the test is too complicated
- Call method multiple times rather than looping inside of method
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
Indicate expected exception
Catch only the expected type of exception
Fail test if expected exception is not caught
Let other exceptions go uncaught
By reading the assertion message, one should know why the test failed and what to do
Include business logic information in the assertion message
Good assertion messages:
- Improve code documentation
- Inform developers about the problem if the test fails
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
Unit tests
Best practices
1. Consistent
2. Atomic
3. Single Responsibility
4. Self-descriptive
5. No conditional logic or loops
6. No exception handling
7. Informative Assertion messages
8. No test logic in production code
- Separate Unit tests ans Production code in separate projects
- Do not create Methods or properties used only by unit tests.
- Use Dependency Injection or Mocks to isolate Production code
Pattern Arrange/Act/Assert
(aka AAA)
1. Arrange—Perform the setup and initialization required for the test.
2. Act—Take action(s) required for the test
3. Assert—Verify the outcome(s) of the test.
AAA is a pattern for organizing tests. It breaks tests down into three clear and distinct steps:
Pattern Arrange/Act/Assert
(aka AAA)
it('should do something', function() {
//arrange...
var dummyData = { foo: 'bar' };
var expected = 'the result we want';
//act...
var result = functionUnderTest(dummyData);
//assert...
expect(result).to.equal(expected);
});
Pattern Arrange/Act/Assert
(aka AAA)
Benefits :
-
Creates a clear separation between a test’s setup, operations and results
-
Makes the code easier to read and understand
-
Enforces a certain degree of discipline when writing tests
-
assertSame
-
assertEquals
-
assertTrue
-
assertFalse
-
assertCount
-
assertInstanceOf
-
...
Unit tests
Assertions
Only one assertion per tests if possible but not an absolute rule
Multiple assertions in one test method can reveal sometimes a wrong granularity of tests
Unit tests
Assertions
Unit tests
Assertions
// Arrange, Act and...
$this->assertTrue(
$container->hasDefinition('logger')
);
$this->assertSame(
'FileLogger',
$container->getDefinition('logger')->getClass()
);
$this->assertEquals(
array('dev.log'),
$container->getDefinition('logger')->getArguments()
);
Unit tests
Group Assertions
public function testFileLoggerService()
{
$this->containerHasServiceWithClassAndArguments(
'logger',
'FileLogger'
array('dev.log')
);
}
private function containerHasServiceWithClassAndArguments(
ContainerInterface $container,
$serviceId,
$expectedClass,
array $expectedArguments
) {
$this->assertTrue(
$container->hasDefinition($serviceId)
);
$this->assertSame(
$expectedClass,
$container->getDefinition($serviceId)->getClass()
);
$this->assertEquals(
$expectedArguments,
$container->getDefinition($serviceId)->getArguments()
);
}
Unit tests
Describe what you are doing
public function testSizeOfTheCollectionReflectsNewElements()
{
$collection = $this->emptyCollection();
$collection->add($this->anyElement());
$this->collectionHasOneElement($collection);
}
private function emptyCollection()
{
return new Collection();
}
private function anyElement()
{
return 'any element';
}
private function collectionHasOneElement($collection)
{
$this->assertSame(1, count($collection);
}
Mocking
Definition
A object that you want to test, the subject under test (SUT), may have dependencies (aka collaborator) on other complex objects.
To isolate the behavior of the object you want to test you replace the other objects by mocks that simulate the behavior of the real objects.
Mocking
Prevent unreadable long-list
of set-up code for all the mocks
$object = ...;
$validationViolationList = $this->getMock('ValidationViolationList');
$validationViolationList
->expects($this->any())
->method('count')
->will($this->returnValue(1));
$validator = $this->getMock('Validator');
$validator
->expects($this->once())
->method('validate')
->with($object)
->will($this->returnValue($validationViolationList);
Mocking
Mocks can badly influence the stability of your test suite
Prevent your test to fail whenever you make structural changes to the SUT by not being too rigid about:
- the number of times a method is called
- the order in which methods are called
- the arguments used when calling a method
Build some flexibility into your test doubles.
Mocking
Test Doubles
Gerard Meszaro worked at capturing patterns for using the various Xunit frameworks.
The generic term he uses is a Test Double. Test Double is a generic term for any case where you replace a production object for testing purposes.
Mocking
Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
// Logger is an interface
$logger = $this->getMock('Logger');
Mocking
Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production.
Mocking
$urlsByRoute = array(
'index' => '/',
'about_me' => '/about-me'
);
$urlGenerator = $this->getMock('UrlGenerator');
$urlGenerator
->expects($this->any())
->method('generate')
->will($this->returnCallback(
function ($routeName) use ($urlsByRoute) {
if (isset($urlsByRoute[$routeName])) {
return $urlsByRoute[$routeName];
}
throw new UnknownRouteException();
}
));
Mocking
$urlGenerator = $this->getMock('UrlGenerator');
$urlGenerator
->expects($this->any())
->method('match')
->will($this->returnArgument(0));
Mocking
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
$logger = $this->getMock('Logger');
$logger
->expects($this->any())
->method('getLevel')
->will($this->returnValue(Logger::INFO));
Mocking
Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
Mocking
$logger = $this->getMock('Logger');
$logger
->expects($spy = $this->any())
->method('debug');
$invocations = $spy->getInvocations();
$this->assertSame(1, count($invocations));
$firstInvocation = $invocations[0];
$this->assertEquals(
'An error occurred',
$firstInvocation->parameters[0]
);
Mocking
Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.
Mocking
$logger = $this->getMock('Logger');
$logger
->expects($this->at(0))
->method('debug')
->with('A debug message');
$logger
->expects($this->at(1))
->method('info')
->with('An info message');
Mocking
Manually create your test double classes
class LoggerDummy implements Logger
{
public function debug()
{
return null; // not even necessary here
}
...
}
class InfoLevelLoggerStub implements Logger
{
...
public function getLevel()
{
return self::INFO;
}
}
Mocking
Manually create your test double classes
class PredefinedUrlsUrlGeneratorFake implements UrlGenerator
{
private $urlsByRoute;
public function __construct(array $urlsByRoute)
{
$this->urlsByRoute = $urlsByRoute;
}
public function generate($routeName)
{
if (isset($this->urlsByRoute[$routeName])) {
return $this->urlsByRoute[$routeName];
}
throw new UnknownRouteException();
}
}
Mocking
At least use factory methods
public function testSomething()
{
$logger = $this->loggerDummy();
}
private function loggerDummy()
{
return $this->getMockBuilder('Logger')
->setMethods(array('debug', 'info', ...))
->getMock();
}
Mocking
-
Safer refactoring
-
Smaller, tighter, decoupled code
-
Documentation of requirements
-
Continuous integration
-
Value of tests increase over time
Unit tests
Benefits
After/during coding
-
Focus on code
-
Thinking about algorithm
-
More refactoring
-
Easier initially
Unit tests
When to write tests?
Before coding (TDD, BDD)
-
Focus on requirements
-
Thinking about how code will be consumed
-
Stop coding when requirements met
-
Harder initially (needs some practice)
Unit tests
When to write tests?
TDD Benefits :
- Writing the tests first requires you to really consider what do you want from the code.
-
You receive fast feedback.
-
TDD creates a detailed specification.
-
TDD reduces time spent on rework.
-
You spend less time in the debugger.
-
You are able to identify the errors and problems quickly.
-
TDD tells you whether your last change (or refactoring) broke previously working code.
-
TDD forces the radical simplification of the code. You will only write code in response to the requirements of the tests.
TDD Benefits :
9. You're forced to write small classes focused on one thing.
10. TDD creates SOLID code that is maintainable, flexible, and easily extensible.
11. The resulting unit tests are simple and act as documentation for the code. Since TDD use cases are written as tests, other programmers can view the tests as usage examples of how the code is intended to work.
12. The development time to market is shorter.
13. The programmer’s productivity is increased.
14. Quality is improved.
15. Bugs are reduced.
16. Development costs are cut in long term.
Integration Tests
The point of integration testing, as the name suggests, is to test whether many separately developed modules work together as expected. (Martin Fowler)
Integration Tests
Jest
Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
Jest
zero config
Jest aims to work out of the box, config free, on most JavaScript projects.
snapshots
Make tests which keep track of large objects with ease. Snapshots live either alongside your tests, or embedded inline.
Jest
isolated
Tests are parallelized by running them in their own processes to maximize performance.
great api
From it to expect - Jest has the entire toolkit in one place. Well documented, well maintained, well good.
Jest
Benefits over other test runners
- Very fast.
- Interactive watch mode that only runs tests which are relevant to your changes.
- Helpful failure messages.
- Simple configuration, or even zero configuration.
- Mocks and spies.
- Coverage reports.
- Rich matchers API.
Jest
Matchers
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
The simplest way to test a value is with exact equality.
Jest
Truthiness Matchers
- toBeNull matches only null
- toBeUndefined matches only undefined
- toBeDefined is the opposite of toBeUndefined
- toBeTruthy matches anything that an if statement treats as true
- toBeFalsy matches anything that an if statement treats as false
Jest
Truthiness Matchers
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
Jest
Number Matchers
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
// toBe and toEqual are equivalent for numbers
expect(value).toBe(4);
expect(value).toEqual(4);
});
Jest
String Matchers
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
Jest
Array and iterable Matchers
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];
test('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer');
expect(new Set(shoppingList)).toContain('beer');
});
Jest
Exception Matchers
function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
expect(compileAndroidCode).toThrow(Error);
// You can also use the exact error message or a regexp
expect(compileAndroidCode).toThrow('you are using the wrong JDK');
expect(compileAndroidCode).toThrow(/JDK/);
});
Jest
Custom Matchers
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
Jest
Custom Matchers
test('numeric ranges', () => {
expect(100).toBeWithinRange(90, 110);
expect(101).not.toBeWithinRange(0, 100);
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
Jest
Asynchronous code testing
// Don't do this!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
Jest
Asynchronous code testing
test('the data is peanut butter', done => {
function callback(data) {
expect(data).toBe('peanut butter');
done();
}
fetchData(callback);
});
Jest will wait until the done callback is called before finishing the test.
If done() is never called, the test will fail
Jest
Asynchronous code testing
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Just return a promise from your test, and Jest will wait for that promise to resolve. If the promise is rejected, the test will automatically fail.
Jest
Setup and Teardown
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
Repeating Setup For Many Tests
Jest
Setup and Teardown
beforeAll(() => {
return initializeCityDatabase();
});
afterAll(() => {
return clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
One-Time Setup
Jest
Mock Functions
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
Jest
Mock Return Values
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
Jest
Mocking Modules
// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
Jest
Mocking Modules
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
Jest
Mock Implementations
const returnsTrue = jest.fn(() => true);
console.log(returnsTrue());
// true;
Jest
Mock Implementations
// foo.js
module.exports = function() {
// some implementation;
};
// test.js
jest.mock('../foo');
// this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
Jest
Mock Implementations
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'
Jest
Custom Matchers
// The mock function was called at least once
expect(mockFunc).toBeCalled();
// The mock function was called at least once with the specified args
expect(mockFunc).toBeCalledWith(arg1, arg2);
// The last call to the mock function was called with the specified args
expect(mockFunc).lastCalledWith(arg1, arg2);
Jest
Custom Matchers
// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);
Jest Dom
Custom Jest matchers to test the state of the DOM
These will make your tests more declarative, clear to read and to maintain.
@testing-library/jest-dom
Jest Dom
toBeDisabled
<button data-testid="button" type="submit" disabled>submit</button>
<fieldset disabled><input type="text" data-testid="input" /></fieldset>
<a href="..." disabled>link</a>
// Using document.querySelector
expect(document.querySelector('[data-testid="button"]')).toBeDisabled()
expect(document.querySelector('[data-testid="input"]')).toBeDisabled()
expect(document.querySelector('a')).not.toBeDisabled()
// Using DOM Testing Library
expect(getByTestId(container, 'button')).toBeDisabled()
expect(getByTestId(container, 'input')).toBeDisabled()
expect(getByText(container, 'link')).not.toBeDisabled()
This allows you to check whether an element is disabled from the user's perspective.
Jest Dom
toBeInvalid
This allows you to check if a form element, or the entire form, is currently invalid.
An input, select, textarea, or form element is invalid if it has an aria-invalid attribute with no value or a value of "true", or if the result of checkValidity() is false.
Jest Dom
toBeInvalid
<input data-testid="no-aria-invalid" />
<input data-testid="aria-invalid" aria-invalid />
<input data-testid="aria-invalid-value" aria-invalid="true" />
<input data-testid="aria-invalid-false" aria-invalid="false" />
<form data-testid="valid-form">
<input />
</form>
<form data-testid="invalid-form">
<input required />
</form>
// Using document.querySelector
expect(queryByTestId('no-aria-invalid')).not.toBeInvalid()
expect(queryByTestId('aria-invalid')).toBeInvalid()
expect(queryByTestId('aria-invalid-value')).toBeInvalid()
expect(queryByTestId('aria-invalid-false')).not.toBeInvalid()
expect(queryByTestId('valid-form')).not.toBeInvalid()
expect(queryByTestId('invalid-form')).toBeInvalid()
// Using DOM Testing Library
expect(getByTestId(container, 'no-aria-invalid')).not.toBeInvalid()
expect(getByTestId(container, 'aria-invalid')).toBeInvalid()
expect(getByTestId(container, 'aria-invalid-value')).toBeInvalid()
expect(getByTestId(container, 'aria-invalid-false')).not.toBeInvalid()
expect(getByTestId(container, 'valid-form')).not.toBeInvalid()
expect(getByTestId(container, 'invalid-form')).toBeInvalid()
Jest Dom
Other Matchers
- toBeEnabled
- toBeEmpty
- toBeInTheDocument
- toBeRequired
- toBeValid
- toBeVisible
- toContainElement
- toContainHTML
- toHaveAttribute
- toHaveClass
- toHaveFocus
- toHaveFormValues
- toHaveStyle
- toHaveTextContent
- toHaveValue
Mocking
HTTP server mocking
Nock over Moxios
Nock is not related to a specific HTTP client
Nock works by overriding Node's http.request function.
Mocking
HTTP server mocking
const nock = require('nock') const scope = nock('https://api.github.com') .get('/repos/atom/atom/license') .reply(200, { license: { key: 'mit', name: 'MIT License', spdx_id: 'MIT', url: 'https://api.github.com/licenses/mit', node_id: 'MDc6TGljZW5zZTEz', }, })
Code Coverage
you can't go into production with less than 87% coverage
Code Coverage
- Metric uses to determine code covered by unit tests
- Useful to find untested code
- Do not guarantee test quality and use cases coverage
We can have 100% code coverage without any assertions
Code Coverage
yarn test --coverage src/modules/Wallet
Code Coverage
Code Coverage
Problems with stribing to high test coverage
- High test coverage gives you a false sense of security. “Covered code” means the code was executed during a test run but it doesn’t mean that tests were actually verifying what this code does. With less than 100% test coverage you can be sure you’re not testing some code, but even with 100% coverage, you can’t be sure you’re testing everything.
- Some features are really hard to test, like file upload in a browser or drag’n’drop. You start mocking or accessing component internals, so your tests no longer resemble how your users use your app, and hard to maintain. Eventually, you start spending more time on writing less useful tests — so-called problem of diminishing returns.
- Some features does not really need to be tested
Enzyme
Enzyme is a JavaScript Testing utility for React that makes it easier to test your React Components' output. You can also manipulate, traverse, and in some ways simulate runtime given the output.
Enzyme
Usage
-
Shallow rendering does call React lifecycle methods such as componentDidMount and componentDidUpdate
-
Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs
-
Static Rendered Markup allow to generate HTML from your React tree, and analyze the resulting HTML structure
Enzyme
disclaimer
-
Shallow rendering (https://kentcdodds.com/blog/why-i-never-use-shallow-rendering)
-
Inappropriate touching of privates (prop, state, setState instance)
-
Many ways to select elements on DOM (props, component’s constructor, component’s displayName, CSS Classes)
Enzyme
With great power comes great responsibility
Enzyme
Risks
-
False positive
-
Break test when implementation changes (state naming for instance)
Guiding principle
The more your tests resemble the way your software is used, the more confidence they can give you.
Testing Library
Simple and complete testing utilities that encourage good testing practices
Testing Library
-
Write maintainable tests
-
Develop with confidence
-
Accessible by Default
Testing Library
The problem
-
You want tests for your UI that avoid including implementation details and rather focus on making your tests give you the confidence for which they are intended.
-
You want your tests to be maintainable so refactors (changes to implementation but not functionality) don't break your tests and slow you and your team down.
Testing Library
The solution
The Testing Library family of libraries is a very light-weight solution for testing without all the implementation details.
The main utilities it provides involve querying for nodes similarly to how users would find them. In this way, testing-library helps ensure your tests give you confidence in your UI code.
React Testing Library
Simple and complete React DOM testing utilities that encourage good testing practices.
React Testing Library
Test behavior over implementation
-
Small API, uses the DOM
-
Components tested like they’re used (Privates stay private!)
-
Encourages/Enforces accessible applications (getByLabelText, getByPlaceholderText, getByText, getByAltText)
-
Escape-hatches for everything else (getByTestId, container.querySelector (DOM APIs))
React Testing Library
DOM Queries
No Match | 1 Match | 1+ Match | Await? | |
---|---|---|---|---|
getBy | throw | return | throw | No |
findBy | throw | return | throw | Yes |
queryBy | null | return | throw | No |
getAllBy | throw | array | array | No |
findAllBy | throw | array | array | Yes |
queryAllBy | [] | array | array | No |
React Testing Library
DOM Queries
- ByLabelText find by label or aria-label text content
- ByPlaceholderText find by input placeholder value
- ByText find by element text content
- ByDisplayValue find by form element current value
- ByAltText find by img alt attribute
- ByTitle find by title attribute or svg title tag
- ByRole find by aria role
- ByTestId find by data-testid attribute
React Testing Library
Async Utilities
Several utilities are provided for dealing with asynchronous code. These can be useful to wait for an element to appear or disappear in response to an action.
- wait
- waitForElement
- waitForDomChange
- waitForElementToBeRemoved
React Testing Library
Example
import React, { useState } from 'react'
import axios from 'axios'
export default function Fetch({ url }) {
const [greeting, setGreeting] = useState('')
const [buttonClicked, setButtonClicked] = useState(false)
const fetchGreeting = async () => {
const response = await axios.get(url)
const data = response.data
const { greeting } = data
setGreeting(greeting)
setButtonClicked(true)
}
const buttonText = buttonClicked ? 'Ok' : 'Load Greeting'
return (
<div>
<button
onClick={fetchGreeting}
data-testid="ok-button"
disabled={buttonClicked}
>
{buttonText}
</button>
{greeting ? <h1 data-testid="greeting-text">{greeting}</h1> : null}
</div>
)
}
React Testing Library
Debugging
Sometimes you want to check the rendered React tree, use the debug() method:
const { debug } = render(<p>Hello Jest!</p>);
debug();
// -> <p>Hello Jest!</p>
debug(getByText(/expand/i));
You can also print an element:
React Testing Library
Example
import React from 'react'
import { render, fireEvent, waitForElement } from '@testing-library/react'
import axiosMock from 'axios'
import Fetch from '../fetch'
test('loads and displays greeting', async () => {
// Arrange
const url = '/greeting'
const { getByText, getByTestId } = render(<Fetch url={url} />)
axiosMock.get.mockResolvedValueOnce({
data: { greeting: 'hello there' },
})
// Act
fireEvent.click(getByText('Load Greeting'))
const greetingTextNode = await waitForElement(() =>
getByTestId('greeting-text')
)
// Assert
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
expect(getByTestId('ok-button')).toHaveAttribute('disabled')
})
React Hooks testing
When to use this library?
-
You're writing a library with one or more custom hooks that are not directly tied a component
-
You have a complex hook that is difficult to test through component interactions
React Hooks testing
When not to use this library
-
Your hook is defined along side a component and is only used there
-
Your hook is easy to test by just testing the components using it
React Hooks testing
import { useState, useCallback } from 'react'
function useCounter() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount((x) => x + 1), [])
return { count, increment }
}
export default useCounter
React Hooks testing
import { renderHook, act } from '@testing-library/react-hooks'
import useCounter from './useCounter'
test('should increment counter', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
Test isolation
-
Improve the reliability of the tests
-
Simplify the code
-
Increase the confidence on your tests
import React from 'react'
function Counter(props) {
const initialProps = React.useRef(props).current
const {initialCount = 0, maxClicks = 3} = props
const [count, setCount] = React.useState(initialCount)
const tooMany = count >= maxClicks
const handleReset = () => setCount(initialProps.initialCount)
const handleClick = () => setCount(currentCount => currentCount + 1)
return (
<div>
<button onClick={handleClick} disabled={tooMany}>
Count: {count}
</button>
{tooMany ? <button onClick={handleReset}>reset</button> : null}
</div>
)
}
export {Counter}
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import {Counter} from '../counter'
const {getByText} = render(<Counter maxClicks={4} initialCount={3} />)
const counterButton = getByText(/^count/i)
test('the counter is initialized to the initialCount', () => {
expect(counterButton).toHaveTextContent('3')
})
test('when clicked, the counter increments the click', () => {
fireEvent.click(counterButton)
expect(counterButton).toHaveTextContent('4')
})
test(`the counter button is disabled when it's hit the maxClicks`, () => {
fireEvent.click(counterButton)
expect(counterButton).toHaveAttribute('disabled')
})
test(`the counter button does not increment the count when clicked when it's hit the maxClicks`, () => {
expect(counterButton).toHaveTextContent('4')
})
test(`the reset button has been rendered and resets the count when it's hit the maxClicks`, () => {
fireEvent.click(getByText(/reset/i))
expect(counterButton).toHaveTextContent('3')
})
Test isolation
What's going on if we remove a test?
So let's try something else and see how that changes things
Test isolation
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import {Counter} from '../counter'
let getByText, counterButton
beforeEach(() => {
const utils = render(<Counter maxClicks={4} initialCount={3} />)
getByText = utils.getByText
counterButton = utils.getByText(/^count/i)
})
test('the counter is initialized to the initialCount', () => {
expect(counterButton).toHaveTextContent(/3)
})
test('when clicked, the counter increments the click', () => {
fireEvent.click(counterButton)
expect(counterButton).toHaveTextContent(/4)
})
test(`the counter button is disabled when it's hit the maxClicks`, () => {
fireEvent.click(counterButton)
expect(counterButton).toHaveAttribute('disabled')
})
test(`the counter button does not increment the count when clicked when it's hit the maxClicks`, () => {
fireEvent.click(counterButton)
fireEvent.click(counterButton)
expect(counterButton).toHaveTextContent(/4)
})
test(`the reset button has been rendered and resets the count when it's hit the maxClicks`, () => {
fireEvent.click(counterButton)
fireEvent.click(getByText(/reset/i))
expect(counterButton).toHaveTextContent(/3)
})
It's better but that lead to tests that are harder to understand.
Test isolation
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import {Counter} from '../counter'
function renderCounter(props) {
const utils = render(<Counter maxClicks={4} initialCount={3} {...props} />)
const counterButton = utils.getByText(/^count/i)
return {...utils, counterButton}
}
test('the counter is initialized to the initialCount', () => {
const {counterButton} = renderCounter()
expect(counterButton).toHaveTextContent(/3)
})
test('when clicked, the counter increments the click', () => {
const {counterButton} = renderCounter()
fireEvent.click(counterButton)
expect(counterButton).toHaveTextContent(/4)
})
test(`the counter button is disabled when it's hit the maxClicks`, () => {
const {counterButton} = renderCounter({
maxClicks: 4,
initialCount: 4,
})
expect(counterButton).toHaveAttribute('disabled')
})
test(`the counter button does not increment the count when clicked when it's hit the maxClicks`, () => {
const {counterButton} = renderCounter({
maxClicks: 4,
initialCount: 4,
})
fireEvent.click(counterButton)
expect(counterButton).toHaveTextContent(/4)
})
test(`the reset button has been rendered and resets the count when it's hit the maxClicks`, () => {
const {getByText, counterButton} = renderCounter()
fireEvent.click(counterButton)
fireEvent.click(getByText(/reset/i))
expect(counterButton).toHaveTextContent(/3)
})
Test isolation
Cleanup now enabled by default in React Testing Library
E2E tests using Cypress
Before Cypress
- Choose a framework (Mocha, Jasmine, QUnit, Karma)
-
Choose an assertion library (Chai, Expect.js)
-
Choose a Selenium wrapper (Protractor, Nightwatch, Webdriver)
-
Add additional libraries (Sinon, TestDouble)
E2E tests using Cypress
All-in-one testing framework
Assertion library, with mocking and stubbing, all without Selenium
E2E tests using Cypress
describe(‘wallet’, () => {
before(() => {
cy.login();
});
it(‘Visits the wallet page’, () => {
cy.server();
cy.route(‘GET’, ‘product-referentials*’, ‘fixture:product-referentials.json’);
cy.route(‘GET’, ‘transactions*’, ‘fixture:transactions.json’);
cy.route(‘GET’, ‘cards*’, ‘fixture:cards.json’);
cy.visit(‘/wallet’);
});
});
E2E tests using Cypress
A test runner built for humans.
Any questions?
CWC Testing Workshop
By Nicolas Eeckeloo
CWC Testing Workshop
- 772