SlideShare a Scribd company logo
1 of 23
Download to read offline
Testing React with
the new ‘act’ function
Daniel Irvine

LeedsJS, 26 June 2019
Testing React hooks with ‘act’ @d_ir
• Independent software consultant

• aka a contractor

• I’m a generalist / polyglot. Currently doing Ruby on Rails
for Idean

• I care a lot about quality, people, TDD

• I organise Queer Code London
Hello! I’m Daniel
Testing React hooks with ‘act’ @d_ir
• React 16.8 introduced hooks, including the useEffect
hook which is used for running side effects in your
component, just like componentDidMount would have
done.

• If you write tests that instrument the useEffect hook, you’ll
get a warning when you run tests about using act.

• That’s because React prefers guard-rail driven
development and it wants to warn you about ‘better’
ways to do things
The story so far… (1 of 2)
Testing React hooks with ‘act’ @d_ir
• What we used to do flushing promises should now be
done by using act.

• There are two forms of act: sync and async. Async is for
side effects. The sync form batches up calls to setState
which forces a specific class of errors (that you probably
won’t run into often).

• Async act is absolutely necessary to get rid of warnings
but you’ll need to use the alpha release of React 16.9 to
use it.
The story so far… (2 of 2)
Testing React hooks with ‘act’ @d_ir
export class AppointmentFormLoader extends React.Component {

constructor(props) {

super(props);

this.state = { availableTimeSlots: [] }

}
async componentDidMount() {

const result = await window.fetch('/availableTimeSlots', {

method: 'GET',

credentials: 'same-origin',

headers: { 'Content-Type': 'application/json' }

})
this.setState({

availableTimeSlots: await result.json()

})

}



render() { ... }

}
Using
cDM to
fetch
data
Testing React hooks with ‘act’ @d_ir
export const AppointmentFormLoader = props => {

const [

availableTimeSlots, setAvailableTimeSlots

] = useState([]);



useEffect(() => {



const fetchAvailableTimeSlots = async () => {

const result = await window.fetch('/availableTimeSlots', {

method: 'GET',

credentials: 'same-origin',

headers: { 'Content-Type': 'application/json' }

});



setAvailableTimeSlots(await result.json());

};



fetchAvailableTimeSlots();

}, []);
return ...

};
Using
useEffect
to fetch
data
Testing React hooks with ‘act’ @d_ir
• A spy should always use at least two tests:

1. One test to assert the parameter values that are
passed

2. One test to check the return value is used correctly
Using spies to test dependencies
Testing React hooks with ‘act’ @d_ir
export const fetchResponseOk = body =>

Promise.resolve({

ok: true,

json: () => Promise.resolve(body)

});



const today = new Date();

const availableTimeSlots = [

{ when: today.setHours(9, 0, 0, 0) }

];



beforeEach(() => {

jest

.spyOn(window, 'fetch');

.mockReturnValue(fetchResponseOk(availableTimeSlots));

});



afterEach(() => {

window.fetch.mockRestore();

});
Spying
on fetch
Testing React hooks with ‘act’ @d_ir
import React from 'react';

import ReactDOM from 'react-dom';

import 'whatwg-fetch';

import { AppointmentFormLoader } from '../src/
AppointmentFormLoader';



describe('AppointmentFormLoader', () => {

let container;



beforeEach(() => {

container = document.createElement('div');

jest.spyOn(window, 'fetch');

});
it('fetches data when component is mounted', () => {

ReactDOM.render(<AppointmentFormLoader />, container);

expect(window.fetch).toHaveBeenCalledWith(

'/availableTimeSlots',

expect.objectContaining({

method: 'GET',

credentials: 'same-origin',

headers: { 'Content-Type': 'application/json' }

})

);

});

});
Testing a
fetch call,
pre hooks
Testing React hooks with ‘act’ @d_ir
● AppointmentFormLoader › fetches data when
component is mounted


expect(jest.fn()).toHaveBeenCalledWith(expected)





Expected mock function to have been called with:
["/availableTimeSlots", ObjectContaining
{"credentials": "same-origin", "headers":
{"Content-Type": "application/json"}, "method":
"GET"}]



But it was not called.
Running the
previous test
in React
16.8…
One solution is to use the useLayoutEffect hook instead, but...
Testing React hooks with ‘act’ @d_ir
it('fetches data when component is mounted', async () => {

ReactDOM.render(<AppointmentFormLoader />, container);



await new Promise(setTimeout);



expect(window.fetch).toHaveBeenCalledWith(

'/availableTimeSlots',

expect.objectContaining({

method: 'GET',

credentials: 'same-origin',

headers: { 'Content-Type': 'application/json' }

})

);

});
Flushing promises
Testing React hooks with ‘act’ @d_ir
console.error node_modules/react-dom/cjs/react-dom.development.js:
546
Warning: An update to AppointmentFormLoader inside a test was not
wrapped in act(...).



When testing, code that causes React state updates should be
wrapped into act(...):



act(() => {

/* fire events that update state */

});

/* assert on the output */



This ensures that you're testing the behavior the user would see
in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
in AppointmentFormLoader
But... guard rails
Testing React hooks with ‘act’ @d_ir
import { act } from 'react-dom/test-utils';



it('fetches data when component is mounted', () => {



act(() => {

ReactDOM.render(<AppointmentFormLoader />, container);

});



expect(window.fetch).toHaveBeenCalledWith(...);



});
Using act for the previous tests, in React 16.8
The test passes! But unfortunately, this doesn't fix the warning...
Testing React hooks with ‘act’ @d_ir
it('fetches data when component is mounted', async () => {



await act(async () => {

ReactDOM.render(<AppointmentFormLoader />, container);

});



expect(window.fetch).toHaveBeenCalledWith(...);



});
Using act for the previous tests, in React 16.9
To suppress the warning we have to use the async version of act.
The sync version still produces the warning
Testing React hooks with ‘act’ @d_ir
export const App = () => {

let [counter, setCounter] = useState(0);

return <button onClick={() => setCounter(counter + 1)}
>{counter}</button>;

}
it("should increment a counter", () => {

// we attach the element to document.body to ensure
events work

document.body.appendChild(container);



ReactDOM.render(<App />, container);



const button = container.childNodes[0];

for (let i = 0; i < 3; i++) {

button.dispatchEvent(new MouseEvent(

"click", { bubbles: true }));

}

expect(button.innerHTML).toBe("3");

});
So what
is sync
act
useful
for?

(1 of 2)
Adapted from https://github.com/threepointone/react-act-examples
Testing React hooks with ‘act’ @d_ir
act(() => {

for (let i = 0; i < 3; i++) {

button.dispatchEvent(new MouseEvent(

"click", { bubbles: true }));

}

});
expect(button.innerHTML).toBe(3);

// this fails, it's actually "1"!
So what
is sync
act
useful
for?

(2 of 2)
Adapted from https://github.com/threepointone/react-act-examples
Testing React hooks with ‘act’ @d_ir
const doSave = async () => {

setSubmitting(true);

const result = await window.fetch(

'/customers', { ... }

);

setSubmitting(false);



if (result.ok) {

setError(false);

const customerWithId = await result.json();

onSave(customerWithId);

} else if (result.status === 422) {

const response = await result.json();

setValidationErrors(response.errors);

} else {

setError(true);

}

};
Testing
intermediate
state: a
submitting
indicator
Testing React hooks with ‘act’ @d_ir
describe('submitting indicator', () => {

it('displays indicator when form is submitting', async () => {



ReactDOM.render(

<CustomerForm {...validCustomer} />, container);



act(() => {

ReactTestUtils.Simulate.submit(form('customer'));

});
await act(async () => {

expect(element('span.submittingIndicator')).not.toBeNull();

});

});

});
The first test
This ensures the expectation executes before your fetch call
Testing React hooks with ‘act’ @d_ir
describe('submitting indicator', () => {

it('initially does not display the submitting indicator', () => {

ReactDOM.render(<CustomerForm {...validCustomer} />, container);

expect(element('.submittingIndicator')).toBeNull();

});



it('hides indicator when form has submitted', async () => {

ReactDOM.render(<CustomerForm {...validCustomer} />, container);



await act(async () => {

ReactTestUtils.Simulate.submit(form('customer'));

});



expect(element('.submittingIndicator')).toBeNull();

});

});
The second and third tests
Testing React hooks with ‘act’ @d_ir
• Async act (React 16.9.0-alpha.0) is needed whenever
your tests have side effects (e.g. call fetch, animation etc)

• Sync act is used to batch up state mutations to ensure
that you use the updater form of your tests

• But that relies on you always using act

• Testing libraries like Enzyme and react-testing-library are
still building in support for act.

• But you shouldn’t be afraid of rolling your own.
Recap
Testing React hooks with ‘act’ @d_ir
• Write lots of short tests that test very small things

• Testing dependencies with spies always requires at least
two tests

• Use arrange-act-assert form for your tests

• If your tests are hard to write, that's a sign that your
production code can be simplified
Some tips for writing better tests
Testing React hooks with ‘act’ @d_ir
My book is available on Amazon or
on packtpub.com.

496 pages of sheer goodness,
teaching TDD from the ground up.

It covers this plus much more,
including testing animation using
the same useEffect technique.

Or get in contact:

Email daniel@conciselycrafted.com

Twitter @d_ir
Want to learn more?
Testing React hooks with ‘act’ @d_ir
Slides will be online soon--I'll tweet them

Or get in contact:

Email daniel@conciselycrafted.com

Twitter @d_ir
Thanks!

More Related Content

What's hot

What's hot (20)

Angular Advanced Workshop (+ Challenges)
Angular Advanced Workshop (+ Challenges)Angular Advanced Workshop (+ Challenges)
Angular Advanced Workshop (+ Challenges)
 
2018 05-16 Evolving Technologies: React, Babel & Webpack
2018 05-16 Evolving Technologies: React, Babel & Webpack2018 05-16 Evolving Technologies: React, Babel & Webpack
2018 05-16 Evolving Technologies: React, Babel & Webpack
 
iOS testing
iOS testingiOS testing
iOS testing
 
Understanding Asynchronous JavaScript
Understanding Asynchronous JavaScriptUnderstanding Asynchronous JavaScript
Understanding Asynchronous JavaScript
 
Testing AngularJS
Testing AngularJSTesting AngularJS
Testing AngularJS
 
Async JavaScript Unit Testing
Async JavaScript Unit TestingAsync JavaScript Unit Testing
Async JavaScript Unit Testing
 
Tdd iPhone For Dummies
Tdd iPhone For DummiesTdd iPhone For Dummies
Tdd iPhone For Dummies
 
JavaOne 2013: Java 8 - The Good Parts
JavaOne 2013: Java 8 - The Good PartsJavaOne 2013: Java 8 - The Good Parts
JavaOne 2013: Java 8 - The Good Parts
 
Containers & Dependency in Ember.js
Containers & Dependency in Ember.jsContainers & Dependency in Ember.js
Containers & Dependency in Ember.js
 
Redux Sagas - React Alicante
Redux Sagas - React AlicanteRedux Sagas - React Alicante
Redux Sagas - React Alicante
 
Expert JavaScript tricks of the masters
Expert JavaScript  tricks of the mastersExpert JavaScript  tricks of the masters
Expert JavaScript tricks of the masters
 
Callbacks, promises, generators - asynchronous javascript
Callbacks, promises, generators - asynchronous javascriptCallbacks, promises, generators - asynchronous javascript
Callbacks, promises, generators - asynchronous javascript
 
Workshop 23: ReactJS, React & Redux testing
Workshop 23: ReactJS, React & Redux testingWorkshop 23: ReactJS, React & Redux testing
Workshop 23: ReactJS, React & Redux testing
 
Using React, Redux and Saga with Lottoland APIs
Using React, Redux and Saga with Lottoland APIsUsing React, Redux and Saga with Lottoland APIs
Using React, Redux and Saga with Lottoland APIs
 
The evolution of asynchronous javascript
The evolution of asynchronous javascriptThe evolution of asynchronous javascript
The evolution of asynchronous javascript
 
Introduction to ReactJS and Redux
Introduction to ReactJS and ReduxIntroduction to ReactJS and Redux
Introduction to ReactJS and Redux
 
Full Stack Unit Testing
Full Stack Unit TestingFull Stack Unit Testing
Full Stack Unit Testing
 
JavaScript Promises
JavaScript PromisesJavaScript Promises
JavaScript Promises
 
Avoiding Callback Hell with Async.js
Avoiding Callback Hell with Async.jsAvoiding Callback Hell with Async.js
Avoiding Callback Hell with Async.js
 
Integrating React.js with PHP projects
Integrating React.js with PHP projectsIntegrating React.js with PHP projects
Integrating React.js with PHP projects
 

Similar to Testing React hooks with the new act function

react-slides.pdf gives information about react library
react-slides.pdf gives information about react libraryreact-slides.pdf gives information about react library
react-slides.pdf gives information about react library
janet736113
 

Similar to Testing React hooks with the new act function (20)

MeetJS Summit 2016: React.js enlightenment
MeetJS Summit 2016: React.js enlightenmentMeetJS Summit 2016: React.js enlightenment
MeetJS Summit 2016: React.js enlightenment
 
Intro react js
Intro react jsIntro react js
Intro react js
 
Angular resolver tutorial
Angular resolver tutorialAngular resolver tutorial
Angular resolver tutorial
 
Javascript tdd byandreapaciolla
Javascript tdd byandreapaciollaJavascript tdd byandreapaciolla
Javascript tdd byandreapaciolla
 
React custom renderers
React custom renderersReact custom renderers
React custom renderers
 
NestJS
NestJSNestJS
NestJS
 
Rhino Mocks
Rhino MocksRhino Mocks
Rhino Mocks
 
Ember.js - A JavaScript framework for creating ambitious web applications
Ember.js - A JavaScript framework for creating ambitious web applications  Ember.js - A JavaScript framework for creating ambitious web applications
Ember.js - A JavaScript framework for creating ambitious web applications
 
Unit-testing and E2E testing in JS
Unit-testing and E2E testing in JSUnit-testing and E2E testing in JS
Unit-testing and E2E testing in JS
 
Adding a modern twist to legacy web applications
Adding a modern twist to legacy web applicationsAdding a modern twist to legacy web applications
Adding a modern twist to legacy web applications
 
React render props
React render propsReact render props
React render props
 
jQuery for beginners
jQuery for beginnersjQuery for beginners
jQuery for beginners
 
react-slides.pptx
react-slides.pptxreact-slides.pptx
react-slides.pptx
 
Good karma: UX Patterns and Unit Testing in Angular with Karma
Good karma: UX Patterns and Unit Testing in Angular with KarmaGood karma: UX Patterns and Unit Testing in Angular with Karma
Good karma: UX Patterns and Unit Testing in Angular with Karma
 
React native by example by Vadim Ruban
React native by example by Vadim RubanReact native by example by Vadim Ruban
React native by example by Vadim Ruban
 
AngularJs $provide API internals & circular dependency problem.
AngularJs $provide API internals & circular dependency problem.AngularJs $provide API internals & circular dependency problem.
AngularJs $provide API internals & circular dependency problem.
 
Building an angular application -1 ( API: Golang, Database: Postgres) v1.0
Building an angular application -1 ( API: Golang, Database: Postgres) v1.0Building an angular application -1 ( API: Golang, Database: Postgres) v1.0
Building an angular application -1 ( API: Golang, Database: Postgres) v1.0
 
react-slides.pdf
react-slides.pdfreact-slides.pdf
react-slides.pdf
 
react-slides.pdf gives information about react library
react-slides.pdf gives information about react libraryreact-slides.pdf gives information about react library
react-slides.pdf gives information about react library
 
Reactotron - A Debugging Agent
Reactotron -  A Debugging AgentReactotron -  A Debugging Agent
Reactotron - A Debugging Agent
 

Recently uploaded

Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Victor Rentea
 
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Safe Software
 
Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire business
panagenda
 

Recently uploaded (20)

DBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor PresentationDBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor Presentation
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a Fresher
 
DEV meet-up UiPath Document Understanding May 7 2024 Amsterdam
DEV meet-up UiPath Document Understanding May 7 2024 AmsterdamDEV meet-up UiPath Document Understanding May 7 2024 Amsterdam
DEV meet-up UiPath Document Understanding May 7 2024 Amsterdam
 
Rising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdf
Rising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdfRising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdf
Rising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdf
 
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost SavingRepurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
 
Vector Search -An Introduction in Oracle Database 23ai.pptx
Vector Search -An Introduction in Oracle Database 23ai.pptxVector Search -An Introduction in Oracle Database 23ai.pptx
Vector Search -An Introduction in Oracle Database 23ai.pptx
 
Boost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfBoost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdf
 
WSO2's API Vision: Unifying Control, Empowering Developers
WSO2's API Vision: Unifying Control, Empowering DevelopersWSO2's API Vision: Unifying Control, Empowering Developers
WSO2's API Vision: Unifying Control, Empowering Developers
 
Elevate Developer Efficiency & build GenAI Application with Amazon Q​
Elevate Developer Efficiency & build GenAI Application with Amazon Q​Elevate Developer Efficiency & build GenAI Application with Amazon Q​
Elevate Developer Efficiency & build GenAI Application with Amazon Q​
 
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
 
Introduction to Multilingual Retrieval Augmented Generation (RAG)
Introduction to Multilingual Retrieval Augmented Generation (RAG)Introduction to Multilingual Retrieval Augmented Generation (RAG)
Introduction to Multilingual Retrieval Augmented Generation (RAG)
 
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
 
Corporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxCorporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptx
 
Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire business
 
MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024
 
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
 
FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024
 
AWS Community Day CPH - Three problems of Terraform
AWS Community Day CPH - Three problems of TerraformAWS Community Day CPH - Three problems of Terraform
AWS Community Day CPH - Three problems of Terraform
 
"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
"I see eyes in my soup": How Delivery Hero implemented the safety system for ..."I see eyes in my soup": How Delivery Hero implemented the safety system for ...
"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
 
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
 

Testing React hooks with the new act function

  • 1. Testing React with the new ‘act’ function Daniel Irvine LeedsJS, 26 June 2019
  • 2. Testing React hooks with ‘act’ @d_ir • Independent software consultant • aka a contractor • I’m a generalist / polyglot. Currently doing Ruby on Rails for Idean • I care a lot about quality, people, TDD • I organise Queer Code London Hello! I’m Daniel
  • 3. Testing React hooks with ‘act’ @d_ir • React 16.8 introduced hooks, including the useEffect hook which is used for running side effects in your component, just like componentDidMount would have done. • If you write tests that instrument the useEffect hook, you’ll get a warning when you run tests about using act. • That’s because React prefers guard-rail driven development and it wants to warn you about ‘better’ ways to do things The story so far… (1 of 2)
  • 4. Testing React hooks with ‘act’ @d_ir • What we used to do flushing promises should now be done by using act. • There are two forms of act: sync and async. Async is for side effects. The sync form batches up calls to setState which forces a specific class of errors (that you probably won’t run into often). • Async act is absolutely necessary to get rid of warnings but you’ll need to use the alpha release of React 16.9 to use it. The story so far… (2 of 2)
  • 5. Testing React hooks with ‘act’ @d_ir export class AppointmentFormLoader extends React.Component {
 constructor(props) {
 super(props);
 this.state = { availableTimeSlots: [] }
 } async componentDidMount() {
 const result = await window.fetch('/availableTimeSlots', {
 method: 'GET',
 credentials: 'same-origin',
 headers: { 'Content-Type': 'application/json' }
 }) this.setState({
 availableTimeSlots: await result.json()
 })
 }
 
 render() { ... }
 } Using cDM to fetch data
  • 6. Testing React hooks with ‘act’ @d_ir export const AppointmentFormLoader = props => {
 const [
 availableTimeSlots, setAvailableTimeSlots
 ] = useState([]);
 
 useEffect(() => {
 
 const fetchAvailableTimeSlots = async () => {
 const result = await window.fetch('/availableTimeSlots', {
 method: 'GET',
 credentials: 'same-origin',
 headers: { 'Content-Type': 'application/json' }
 });
 
 setAvailableTimeSlots(await result.json());
 };
 
 fetchAvailableTimeSlots();
 }, []); return ...
 }; Using useEffect to fetch data
  • 7. Testing React hooks with ‘act’ @d_ir • A spy should always use at least two tests: 1. One test to assert the parameter values that are passed 2. One test to check the return value is used correctly Using spies to test dependencies
  • 8. Testing React hooks with ‘act’ @d_ir export const fetchResponseOk = body =>
 Promise.resolve({
 ok: true,
 json: () => Promise.resolve(body)
 });
 
 const today = new Date();
 const availableTimeSlots = [
 { when: today.setHours(9, 0, 0, 0) }
 ];
 
 beforeEach(() => {
 jest
 .spyOn(window, 'fetch');
 .mockReturnValue(fetchResponseOk(availableTimeSlots));
 });
 
 afterEach(() => {
 window.fetch.mockRestore();
 }); Spying on fetch
  • 9. Testing React hooks with ‘act’ @d_ir import React from 'react';
 import ReactDOM from 'react-dom';
 import 'whatwg-fetch';
 import { AppointmentFormLoader } from '../src/ AppointmentFormLoader';
 
 describe('AppointmentFormLoader', () => {
 let container;
 
 beforeEach(() => {
 container = document.createElement('div');
 jest.spyOn(window, 'fetch');
 }); it('fetches data when component is mounted', () => {
 ReactDOM.render(<AppointmentFormLoader />, container);
 expect(window.fetch).toHaveBeenCalledWith(
 '/availableTimeSlots',
 expect.objectContaining({
 method: 'GET',
 credentials: 'same-origin',
 headers: { 'Content-Type': 'application/json' }
 })
 );
 });
 }); Testing a fetch call, pre hooks
  • 10. Testing React hooks with ‘act’ @d_ir ● AppointmentFormLoader › fetches data when component is mounted 
 expect(jest.fn()).toHaveBeenCalledWith(expected)
 
 
 Expected mock function to have been called with: ["/availableTimeSlots", ObjectContaining {"credentials": "same-origin", "headers": {"Content-Type": "application/json"}, "method": "GET"}]
 
 But it was not called. Running the previous test in React 16.8… One solution is to use the useLayoutEffect hook instead, but...
  • 11. Testing React hooks with ‘act’ @d_ir it('fetches data when component is mounted', async () => {
 ReactDOM.render(<AppointmentFormLoader />, container);
 
 await new Promise(setTimeout);
 
 expect(window.fetch).toHaveBeenCalledWith(
 '/availableTimeSlots',
 expect.objectContaining({
 method: 'GET',
 credentials: 'same-origin',
 headers: { 'Content-Type': 'application/json' }
 })
 );
 }); Flushing promises
  • 12. Testing React hooks with ‘act’ @d_ir console.error node_modules/react-dom/cjs/react-dom.development.js: 546 Warning: An update to AppointmentFormLoader inside a test was not wrapped in act(...).
 
 When testing, code that causes React state updates should be wrapped into act(...):
 
 act(() => {
 /* fire events that update state */
 });
 /* assert on the output */
 
 This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act in AppointmentFormLoader But... guard rails
  • 13. Testing React hooks with ‘act’ @d_ir import { act } from 'react-dom/test-utils';
 
 it('fetches data when component is mounted', () => {
 
 act(() => {
 ReactDOM.render(<AppointmentFormLoader />, container);
 });
 
 expect(window.fetch).toHaveBeenCalledWith(...);
 
 }); Using act for the previous tests, in React 16.8 The test passes! But unfortunately, this doesn't fix the warning...
  • 14. Testing React hooks with ‘act’ @d_ir it('fetches data when component is mounted', async () => {
 
 await act(async () => {
 ReactDOM.render(<AppointmentFormLoader />, container);
 });
 
 expect(window.fetch).toHaveBeenCalledWith(...);
 
 }); Using act for the previous tests, in React 16.9 To suppress the warning we have to use the async version of act. The sync version still produces the warning
  • 15. Testing React hooks with ‘act’ @d_ir export const App = () => {
 let [counter, setCounter] = useState(0);
 return <button onClick={() => setCounter(counter + 1)} >{counter}</button>;
 } it("should increment a counter", () => {
 // we attach the element to document.body to ensure events work
 document.body.appendChild(container);
 
 ReactDOM.render(<App />, container);
 
 const button = container.childNodes[0];
 for (let i = 0; i < 3; i++) {
 button.dispatchEvent(new MouseEvent(
 "click", { bubbles: true }));
 }
 expect(button.innerHTML).toBe("3");
 }); So what is sync act useful for?
 (1 of 2) Adapted from https://github.com/threepointone/react-act-examples
  • 16. Testing React hooks with ‘act’ @d_ir act(() => {
 for (let i = 0; i < 3; i++) {
 button.dispatchEvent(new MouseEvent(
 "click", { bubbles: true }));
 }
 }); expect(button.innerHTML).toBe(3);
 // this fails, it's actually "1"! So what is sync act useful for?
 (2 of 2) Adapted from https://github.com/threepointone/react-act-examples
  • 17. Testing React hooks with ‘act’ @d_ir const doSave = async () => {
 setSubmitting(true);
 const result = await window.fetch(
 '/customers', { ... }
 );
 setSubmitting(false);
 
 if (result.ok) {
 setError(false);
 const customerWithId = await result.json();
 onSave(customerWithId);
 } else if (result.status === 422) {
 const response = await result.json();
 setValidationErrors(response.errors);
 } else {
 setError(true);
 }
 }; Testing intermediate state: a submitting indicator
  • 18. Testing React hooks with ‘act’ @d_ir describe('submitting indicator', () => {
 it('displays indicator when form is submitting', async () => {
 
 ReactDOM.render(
 <CustomerForm {...validCustomer} />, container);
 
 act(() => {
 ReactTestUtils.Simulate.submit(form('customer'));
 }); await act(async () => {
 expect(element('span.submittingIndicator')).not.toBeNull();
 });
 });
 }); The first test This ensures the expectation executes before your fetch call
  • 19. Testing React hooks with ‘act’ @d_ir describe('submitting indicator', () => {
 it('initially does not display the submitting indicator', () => {
 ReactDOM.render(<CustomerForm {...validCustomer} />, container);
 expect(element('.submittingIndicator')).toBeNull();
 });
 
 it('hides indicator when form has submitted', async () => {
 ReactDOM.render(<CustomerForm {...validCustomer} />, container);
 
 await act(async () => {
 ReactTestUtils.Simulate.submit(form('customer'));
 });
 
 expect(element('.submittingIndicator')).toBeNull();
 });
 }); The second and third tests
  • 20. Testing React hooks with ‘act’ @d_ir • Async act (React 16.9.0-alpha.0) is needed whenever your tests have side effects (e.g. call fetch, animation etc) • Sync act is used to batch up state mutations to ensure that you use the updater form of your tests • But that relies on you always using act • Testing libraries like Enzyme and react-testing-library are still building in support for act. • But you shouldn’t be afraid of rolling your own. Recap
  • 21. Testing React hooks with ‘act’ @d_ir • Write lots of short tests that test very small things • Testing dependencies with spies always requires at least two tests • Use arrange-act-assert form for your tests • If your tests are hard to write, that's a sign that your production code can be simplified Some tips for writing better tests
  • 22. Testing React hooks with ‘act’ @d_ir My book is available on Amazon or on packtpub.com. 496 pages of sheer goodness, teaching TDD from the ground up. It covers this plus much more, including testing animation using the same useEffect technique. Or get in contact: Email daniel@conciselycrafted.com
 Twitter @d_ir Want to learn more?
  • 23. Testing React hooks with ‘act’ @d_ir Slides will be online soon--I'll tweet them Or get in contact: Email daniel@conciselycrafted.com
 Twitter @d_ir Thanks!