Testing React Components
Drivers, Common use cases, Best practices, TDD
Agenda 🤔
- About me
- Infra team
- Test Drivers
- Component Unit testing
- Component Integration testing
- Testing Async actions
- Testing Network requests
- Redux, MobX & friends
Testing Components with state management
About me
Shlomi Toussia-Cohen
29 years old, Married to Dana, Lives in Tel-Aviv.
Hobbies: Motorcycles, Gardening.
Â
~4 years FED, 11 months in Wix.
FED Guild Master in the Infra Team.
Â
Current Team: Wix-Stores (Cart & Checkout)
Previous Team: CX-OSÂ (Business-Manager)
Original Team: FED-Infra
![](https://s3.amazonaws.com/media-p.slid.es/uploads/554796/images/3631449/6093192.jpeg)
wix-js-stack
Your first stop for new stack projects
#wix-js-stack
![](https://s3.amazonaws.com/media-p.slid.es/uploads/554796/images/3647803/slack-icon-10.png)
wix-js-stack
- Generators:
- client / server / fullstack / universal
- component library / node library
- Example projects:
- Experiments
- Typescript
- HMR
- ...
- How to work with the stack
- FAQ and how to (test, deploy, debug...)
yoshi
A tool for common tasks in Javascript projects
![](https://s3.amazonaws.com/media-p.slid.es/uploads/554796/images/3647828/yoshi_logo-sm.png)
Get inspired by a project
- Fullstack Redux based project
- Client and Server Experiments
- Translated components
- Server RPC calls
- wix-style-react components
- Fully tested
Test Drivers
What test drivers are
-
Make tests more readable and maintainable.
-
While tests tells What needs to happen, drivers encapsulate How things are done (implementation).
Component Driver Pattern
// Component.driver.jsx
export class ComponentDriver {
constructor() { }
given = { };
when = { };
get = { };
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/554796/images/3730018/NameComponent.png)
Example: <NameComponent/>
Receives a saved name
Requests saving a new name
Driver: when (1)
// NameComponent.driver.jsx
import {mount} from 'enzyme';
import {NameComponent} from './NameComponent';
class NameComponentDriver {
when = {
created: () => {
this.component = mount(<NameComponent/>);
return this;
}
};
}
The most basic usage - mount the component to the DOM.
Driver: when (2)
class NameComponentDriver {
when = {
saveName: () => {
this.component
.find('button')
.simulate('click');
return this;
},
fillName: name => {
this.component
.find('input')
.simulate('change', {target: {event: name}});
return this;
}
};
}
Interaction with the component
Driver: get
class NameComponentDriver {
get = {
_byDataHook: hook => {
return this.component.find(`[data-hook="${hook}"]`);
},
_savedName: () => {
return this.get._byDataHook('saved-name');
}
savedNameValue: () => {
return this.get._savedName().text();
}
};
}
Querying the component
Tip #1
Hide technology from your test
Tests shouldn't care if we use React, Enzyme, etc...
Prefer exposing values instead of enzyme wrappers.
Driver: given
class NameComponentDriver {
given = {
savedName: name => {
this.savedName = name;
return this;
},
};
when = {
created: () => {
this.component = mount(
<NameComponent savedName={this.savedName}/>
);
return this;
}
};
}
Setting props for the component
Driver: Reuse
class NameComponentDriver {
constructor(component){
this.component = component;
}
}
Reuse the driver inside other drivers, by receiving a mounted component
Driver is an encapsulation tool, keeping our test clean from implementation
Component Unit testing
Example
// NameComponent.spec.js
import {NameComponentDriver} from './NameComponentDriver.driver';
describe('NameComponent', () => {
let driver;
beforeEach(() => {
driver = new NameComponentDriver();
});
it('should display the saved name', () => {
driver
.given.savedName('Shlomi')
.when.created();
expect(driver.get.savedNameValue()).toEqual('Shlomi');
});
it('should fill the saved name input', () => {
driver
.when.created()
.when.fillName('Alexey')
expect(driver.get.filledNameValue()).toEqual('Alexey');
});
});
Component unit testing is not the same as unit testing a function
- Test the output and side effects based on the component logic.
- Test edge cases and small details.
- Test interactions and scenarios.
Â
A component test is already an integration test
Testing callback functions
describe('NameComponent', () => {
it('should save the new name', () => {
const onSave = jest.fn();
driver
.given.onSaveHandler(onSave)
.when.created()
.when.saveName();
expect(onSave).toHaveBeenCalled();
});
});
class NameComponentDriver {
given = {
onSaveHandler: fn => {
this.onSave = fn;
return this;
}
}
}
Testing CSS classes
describe('NameComponent', () => {
it('should have the correct save button styling', () => {
driver
.when.created();
expect(driver.get.saveButtonHasClass('saveStyle')).toBeTruthy();
});
});
class NameComponentDriver {
get = {
saveButtonHasClass: className =>
this.get._saveButton().hasClass(className)
}
}
Same technique will work when using css modules (behind the scenes we use proxy for .scss files)
Unit testing is not always enough
The component lives inside you app, so you need to test the integration as well.
Components Integration testing
Test the wiring and interactions with components and data
<App/>
The component is responsible to fetch and display a saved name from the server and update it.
A short TDD desclaimer
Step 0 - Boilerplate
// App.jsx
import React from 'react';
export const App = () => <div>App</div>;
// App.driver.jsx
import React from 'react';
import {App} from './App';
export class AppDriver {
given = {};
when = {created: () => this.component = mount(<App/>); return this;};
get = {_byDataHook: hook => this.component.find(`[data-hook="${hook}]"`)};
}
// App.spec.js
import {AppDriver} from './App.driver';
describe('App', () => {
let driver;
beforeEach(() => {
driver = new AppDriver();
});
it('should pass', () => {
driver.when.created();
expect(true).toEqual(true)
});
});
// App.spec.js
describe('App', () => {
it('should show the saved name', () => {
driver.create();
expect(driver.get.savedNameValue()).toEqual('Shlomi');
});
});
Step 1: Expect a hard coded value
// App.driver.jsx
class AppDriver {
get = {
_nameComponent: () =>
this.component.find('NameComponent'),
savedNameValue: () =>
this.get._nameComponent().find('[data-hook="saved-name"]').text()
}
}
Step 2: Implement the driver
//App.driver.jsx
import {NameComponentDriver} from '../NameComponent/NameComponent.driver';
class AppDriver {
get = {
_nameComponent: () =>
this.component.find('NameComponent'),
_nameComponentDriver: () =>
new NameComponentDriver(this.get._nameComponent()),
savedNameValue: () =>
this.get._nameComponentDriver().get.savedNameValue()
}
}
Step 2: Implement the driver (reuse driver)
Step 3: Change the component
// App.jsx
import React from 'react';
import {NameComponent} from './NameComponent/NameComponent';
export const App = () => (
<div>
<NameComponent name="Shlomi"/>
</div>
);
Questions so far?
![](https://s3.amazonaws.com/media-p.slid.es/uploads/554796/images/3717023/now_it_the_fucking_time.gif)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/554796/images/3717024/now_it_the_perfect_time.jpg)
How to: Asynchronous testing
"Wait for it..."
Async: fill name
describe('App', () => {
it('should change the saved name', () => {
driver
.given.savedName('Shlomi')
.when.created()
.when.fillName('Alexey')
.when.saveName();
expect(driver.get.savedNameValue()).toEqual('Alexey');
// Fail: Expected "Shlomi" to equal "Alexey"
});
});
Assume that all functions are implemented, but saveName is an asynchronous action. This test will fail:
How can we fix the test?
Async: naively - explicit timeout
describe('App', () => {
it('should change the saved name', (done) => {
driver
.given.savedName('Shlomi')
.when.created()
.when.fillName('Alexey')
.when.saveName();
setTimeout(() => {
expect(driver.get.savedNameValue()).toEqual('Alexey');
done();
}, 1000);
});
});
Async: better - polling and retry
describe('App', () => {
it('should change the saved name', (done) => {
driver
.given.savedName('Shlomi')
.when.created()
.when.fillName('Alexey')
.when.saveName();
const timeout = setTimeout(() => done('error'), 1000);
const interval = setInterval(() => {
try {
expect(driver.get.savedNameValue()).toEqual('Alexey');
clearTimeout(timeout);
clearInterval(interval);
done();
} catch (e) {return;}
}, 50);
});
});
Async: perfect - wix-eventually
import eventually from 'wix-eventually';
describe('App', () => {
it('should change the saved name', () => {
driver
.given.savedName('Shlomi')
.when.created()
.when.fillName('Alexey')
.when.saveName();
return eventually(() =>
expect(driver.get.savedNameValue()).toEqual('Alexey')
);
});
});
Async: async-await
import eventually from 'wix-eventually';
describe('App', () => {
it('should change the saved name', async() => {
driver
.given.savedName('Shlomi')
.when.created()
.when.fillName('Alexey')
.when.saveName();
await eventually(() =>
expect(driver.get.savedNameValue()).toEqual('Alexey')
);
console.log('and do more things after fulfilled');
});
});
Tip #2
Create a preconfigured eventually
import wixEventually from 'wix-eventually';
const defaultTimeouts = {timeout: 500, interval: 10};
async function eventually(condition, timeouts = defaultTimeouts) {
const timeouts = {...defaultTimeouts, ...timeouts};
return await wixEventually.with(timeouts)(condition);
}
Back to the <App/>
Recap
- Created the component and driver
- Displayed some hard coded name
- Used the <NameComponent/> to display the hard coded name
Let's make an HTTP request!
Step 4: Expect for server data
// App.spec.js
import eventually from 'wix-eventually';
describe('App', () => {
it('should show the saved name', async () => {
driver
.given.fetchSavedName({savedName: 'Shlomi'})
.create();
await eventually(() =>
expect(driver.get.savedNameValue()).toEqual('Shlomi')
);
});
});
Step 5: Mock server data
// App.driver.jsx
import nock from 'nock';
class AppDriver {
given = {
fetchSavedName: res => {
nock('http://www.my-server.com')
.get('/savedName')
.reply(200, res);
return this;
}
}
}
Step 6: Component fetches data
Red
// App.jsx
import React from 'react';
import {NameComponent} from './NameComponent/NameComponent';
import axios from 'axios';
export class App extends React.Component {
constructor() {
this.state = {savedName: ''};
}
componentDidMount() {
axios.get('/savedName')
.then(data => this.setState({savedName: data.savedName}));
}
render() {
return (
<div>
<NameComponent savedName={this.state.savedName}/>
</div>
);
}
}
How would you test the update name flow? (test + driver)
Possible implementation: Test
describe('App', () => {
it('should update the saved name', async() => {
const savedName = 'Shlomi';
const newName = 'Alexey';
driver
.given.fetchSavedName({savedName})
.given.updateSavedName({newName})
.given.fetchSavedName({savedName: newName})
.when.created()
.when.fillName(newName)
.when.saveName();
await eventually(() =>
expect(driver.get.savedNameValue()).toEqual(newName)
);
});
});
Possible implementation: Driver
class AppDriver {
given = {
updateSavedName: (params) => {
nock('http://www.my-server.com')
.post('/savedName', params)
.reply(200);
return this;
}
};
when = {
fillName: name => {
this.get._nameComponentDriver().when.fillName(name);
return this;
},
saveName: () => {
this.get._nameComponentDriver().when.saveName();
return this;
}
};
}
Tip #3 http mocks
Verify all requests are fulfilled
Avoid unused mocks from leaking to next tests
export function validateAllMocksCalled() {
if (!nock.isDone()) {
const errorMessage = `pending mocks: ${nock.pendingMocks()}`;
this.clearMocks();
throw new Error(errorMessage);
}
}
export function clearMocks() {
nock.cleanAll();
nock.emitter.removeAllListeners('no match');
}
afterEach(() => {
validateAllMocksCalled();
clearMocks();
});
Mocking data is exhausting
nock('......')
.get('/site')
.reply(200, {
id: '7ac7e961-253c-4256-a007-b7e434242c8e',
name: 'My cool site!',
published: true,
isOwner: true,
permissions: []
});
Also prone to mistakes and could easily break
Help is needed!
Typescript to the rescue :)
export interface ISiteDTO {
id: string;
name: string;
published: boolean;
isOwner: boolean;
permissions: string[];
}
Give it a try, you might even like it 👿 😇
Typescript prevents costly errors during compilation
Definitions are helpful in your code, but also in test.
Create Test Builders
// test/builders/SiteDTOBuilder.ts
import { ISiteDTO } from '../../src/types/siteDTO';
import { Chance } from 'chance';
const chance = Chance();
export class SiteDTOBuilder {
private id: string = chance.guid();
private name: string = chance.word();
private published: boolean = chance.bool();
private isOwner: boolean = chance.bool();
private permissions: string[] = [chance.word(), chance.word()];
withId(id: string) {
this.id = id;
return this;
}
public build(): ISiteDTO {
return {
id: this.id,
name: this.name,
published: this.published,
isOwner: this.isOwner,
permissions: this.permissions
};
}
}
export const aSiteDTOBuilder = () => new SiteDTOBuilder();
And now your test is great!
import { aSiteDTOBuilder } from '../../test/builders/SiteDTOBuilder';
describe('Some Component', () => {
it('should display a site', () => {
const site = aSiteDTOBuilder()
.withName('My cool site!')
.build();
driver
.given.fetchSite(site)
.when.created()
expect(driver.get.siteName()).toEqual(site.name);
});
});
Question - The API should return a new field in the response. How would you test and implement this addition?
(test, definition, builder, component)
Time to take a deep breath
![](https://s3.amazonaws.com/media-p.slid.es/uploads/554796/images/3729595/take_a_deep_breath.gif)
Redux, MobX & friends
What should I test?
In Redux
- Actions
- Reducers
- Selectors
- Components
In MobX
- Observers and Observables
- Computed Values
- Actions
- Stores
- Components
Test the behavior and logic through the connected component
Connected Component
Container pattern
//App.tsx
import * as React from 'react';
export class App <{siteName: string; fetchSite: Function}, void> {
componentWillMount() {
this.props.fetchSite();
}
render() {
return (<div data-hook="site-name">{this.props.siteName}</div>);
}
}
//App.container.ts
import {connect} from 'react-redux';
import {fetchSite} from '../actions/site';
import {App as AppComponent} from './App';
function mapStateToProps(state) {
return {siteName: state.site.name};
}
export const App = connect(mapStateToProps, {fetchSite})(AppComponent);
A typical redux entry point
//client.tsx
import * as React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {App} from './App/App.container';
import {configureStore} from './stores/configureStores';
render(
<Provider store={configureStore()}
<App/>
</Provider>,
document.querySelector('#root')
);
- Wrap with Provider and pass Redux store
- configureStore() wraps Redux's createStore()
- Render the connected app to the DOM
How do we test <App/>?
A typical redux app
import axios from 'axios';
import { ISiteDTO } from '../../common/types/siteDTO';
import * as types from '../actionTypes/siteActionTypes';
export async function fetchSite(siteId: string) {
return dispatch => {
const res = await axios.get(`/site?siteId=${siteId}`);
dispatch(fetchSiteSuccess(res.data))
};
}
function fetchSiteSuccess(site: ISiteDTO) {
return {
type: types.FETCH_SITE_SUCCESS,
site
};
}
fetchSite async action (using thunks)
A typical redux app
import { ISiteDTO } from '../../common/types/site';
import * as types from '../actionTypes/siteActionTypes';
const initialState: ISiteDTO = {
id: '',
name: '',
published: false,
isOwner: true,
permissions: null
};
export function site(state = initialState, action): ISiteDTO {
switch (action.type) {
case types.FETCH_SITE_SUCCESS:
return Object.assign({}, state, action.site);
default:
return state;
}
}
The site reducer
Testing the container
// App.driver.jsx
class AppDriver {
created: () => {
this.component = mount(
<Provider store={configureStore()}
<App/>
</Provider>
);
return this;
}
}
Wrap with Redux Provider and use the exact same store!
Actual actions and reducers means you test the same behavior and not a mocked behvaior.
You might have multiple Providers
render(
<Provider store={store}>
<I18nextProvider i18n={translations}>
<Router history={history} onUpdate={onUpdate}>
<App/>
</Router>
</I18nextProvider>
</Provider>,
document.querySelector('#root')
);
It becomes ugly wrapping each component when testing
Tip #4
react-test-wrappers
https://github.com/wix-private/wix-js-framework/tree/master/react-test-wrappers
//testWrappers.ts
import configureStore from './configureStore';
import {ComponentWrapper} from 'react-test-wrappers/dist/src/componentWrapper';
import {reduxWrapper} from 'react-test-wrappers/dist/src/wrappers/redux';
import {i18nextWrapper} from 'react-test-wrappers/dist/src/wrappers/i18next';
import {routerWrapper} from 'react-test-wrappers/dist/src/wrappers/router';
export function testWrapper(initialState = {}) {
return new ComponentWrapper()
.wrapWith(reduxWrapper(configureStore(initialState)))
.wrapWith(i18nextWrapper())
.wrapWith(routerWrapper());
}
//App.spec.tsx
import {testWrapper} from './testWrappers'
import {App} from './App';
import {mount} from 'enzyme';
mount(testWrapper().build(<App/>));
🔥🔥🔥🔥🔥🔥
🔥🔥🔥🔥🔥🔥
Tip #5
Prefer using dependency injection mechanism instead of keeping a state in multiple modules
react-test-wrappers helps you write a code with injectable dependencies ===> testable code
😎
A simple counter
https://github.com/wix-private/mobx-counter-container-example
Even for a small project like the above counter, is a big refactor.
only one change was needed in tests.
Where?
Switching from MobX to Redux without breaking
to-mobx-container (From stores to flux) to-redux (From flux to redux)
Almost done! 😅
JSDOM and React Performance
- Tests run in node env => use JSDOM to simulate DOM
- JSDOM is nice, but has some serious performance issues.
-
Don't recreate the DOM in every test:
- It is slow!
- React DOM doesn't cleanup well.
-
Do:
- If you change some DOM behavior, be responsible to clean afterwards.
- Mount and Unmount your component properly (next slide)
-
Suggestion:
- Try using Jest testing framework, very efficient when it comes to JSDOM.
Proper mounting cleanup
// test/mocha-setup.js
import 'jsdom-global/register';
// App.driver.jsx
export class AppDriver {
when = {
created: {
this.component = mount(
(<App/>),
{attachTo: document.createElement('div')}
);
return this;
}
};
cleanup() {
this.component.detach();
};
}
// App.spec.js
import {AppDriver} from './App.driver';
describe('App', () => {
afterEach(() => {
driver.cleanup();
});
});
Summary
- Test drivers - hide the implementation, make tests readable and clear.
- Component testing - Test the behavior and the interactions.
- helpful tools (nock, wix-eventually, react-test-wrappers)
- Typescript - Great for API calls
- TDD - Makes sure your code is covered, use it as a tool.
Questions?
Thank you! 🤗
Testing React Components
By Shlomi Toussia Cohen
Testing React Components
- 2,093