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.
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.
Unit Tests
Jest
Jest, Jest DOM, React Testing Library
Integration Tests
UI
Tests
Cypress, TestCafe
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
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
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
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
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
Name tests to represent PASS conditions:
Test should have no uncertainty:
Test should not contain "while", "for" or "foreach" loops
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
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:
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
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
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:
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);
});
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
...
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
Assertions
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()
);
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()
);
}
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);
}
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.
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);
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:
Build some flexibility into your test doubles.
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.
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');
Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production.
$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();
}
));
$urlGenerator = $this->getMock('UrlGenerator');
$urlGenerator
->expects($this->any())
->method('match')
->will($this->returnArgument(0));
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));
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.
$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]
);
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.
$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');
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;
}
}
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();
}
}
At least use factory methods
public function testSomething()
{
$logger = $this->loggerDummy();
}
private function loggerDummy()
{
return $this->getMockBuilder('Logger')
->setMethods(array('debug', 'info', ...))
->getMock();
}
Safer refactoring
Smaller, tighter, decoupled code
Documentation of requirements
Continuous integration
Value of tests increase over time
Benefits
After/during coding
Focus on code
Thinking about algorithm
More refactoring
Easier initially
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)
When to write tests?
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.
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.
The point of integration testing, as the name suggests, is to test whether many separately developed modules work together as expected. (Martin Fowler)
Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
Jest aims to work out of the box, config free, on most JavaScript projects.
Make tests which keep track of large objects with ease. Snapshots live either alongside your tests, or embedded inline.
Tests are parallelized by running them in their own processes to maximize performance.
From it to expect - Jest has the entire toolkit in one place. Well documented, well maintained, well good.
Benefits over other test runners
Matchers
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
The simplest way to test a value is with exact equality.
Truthiness Matchers
Truthiness Matchers
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
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);
});
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/);
});
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');
});
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/);
});
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,
};
}
},
});
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),
});
});
Asynchronous code testing
// Don't do this!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
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
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.
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
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
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);
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
Mocking Modules
// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
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));
});
Mock Implementations
const returnsTrue = jest.fn(() => true);
console.log(returnsTrue());
// true;
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
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'
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);
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,
]);
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
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.
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.
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()
Other Matchers
HTTP server mocking
Nock over Moxios
Nock is not related to a specific HTTP client
Nock works by overriding Node's http.request function.
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', }, })
you can't go into production with less than 87% coverage
We can have 100% code coverage without any assertions
yarn test --coverage src/modules/Wallet
Problems with stribing to high test coverage
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.
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
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)
With great power comes great responsibility
Risks
False positive
Break test when implementation changes (state naming for instance)
The more your tests resemble the way your software is used, the more confidence they can give you.
Simple and complete testing utilities that encourage good testing practices
Write maintainable tests
Develop with confidence
Accessible by Default
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.
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.
Simple and complete React DOM testing utilities that encourage good testing practices.
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))
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 |
DOM Queries
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.
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>
)
}
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:
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')
})
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
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
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
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)
})
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')
})
What's going on if we remove a test?
So let's try something else and see how that changes things
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.
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)
})
Cleanup now enabled by default in React Testing Library
Before Cypress
Choose an assertion library (Chai, Expect.js)
Choose a Selenium wrapper (Protractor, Nightwatch, Webdriver)
Add additional libraries (Sinon, TestDouble)
All-in-one testing framework
Assertion library, with mocking and stubbing, all without Selenium
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’);
});
});
A test runner built for humans.