Skip to content

Instantly share code, notes, and snippets.

@luishrd
Created June 20, 2018 18:20
Show Gist options
  • Save luishrd/954862926071752b49ce2f4c1a3c3965 to your computer and use it in GitHub Desktop.
Save luishrd/954862926071752b49ce2f4c1a3c3965 to your computer and use it in GitHub Desktop.
CS10 - React Testing
// src/App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Header from './components/Header';
class App extends Component {
render() {
return (
<div className="App">
<Header />
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
// src/App.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import { shallow } from 'enzyme';
import App from './App';
import Header from './components/Header';
// it('renders without crashing', () => {
// const div = document.createElement('div');
// ReactDOM.render(<App />, div);
// ReactDOM.unmountComponentAtNode(div);
// });
describe('App component', () => {
it('renders without crashing', () => {
shallow(<App />); // notice this only renders App and will not render any child components
});
it.skip('should render exactly one Header component', () => {
const app = shallow(<App />);
const headers = app.find('Header');
expect(headers.length).toEqual(2);
// expect(shallow(<App />).find('header').length).toEqual(1)
});
});
// src/components/Header.js
import React, { Component } from 'react';
import logo from '../logo.svg';
import '../App.css';
class Header extends Component {
state = {
isOn: false,
};
render() {
const title = this.props.title || 'Welcome to React';
return (
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">{title}</h1>
<h3 className="display" />
<button className="toggle" onClick={this.toggleOn}>
Toggle
</button>
</header>
);
}
toggleOn = () => {
alert('toggle clicked!');
this.setState(prevState => {
return { isOn: !prevState.isOn };
});
};
}
export default Header;
// src/components/Header.spec.js
import React from 'react';
import { shallow } from 'enzyme';
import Header from './Header';
describe('Header component', () => {
it('renders without crashing', () => {
shallow(<Header />);
});
it('should render exactly one header element', () => {
const header = shallow(<Header />);
const headers = header.find('header');
expect(headers.length).toEqual(1);
// expect(shallow(<Header />).find('header').length).toEqual(1)
});
it('should render the title "Welcome to React" if no title provided"', () => {
const expected = 'Welcome to React';
const header = shallow(<Header />);
const h1 = header.find('.App-title');
expect(h1.text()).toEqual(expected);
});
it('should render provided title', () => {
const expected = 'I can test React Components!';
const props = { title: expected };
const header = shallow(<Header {...props} />);
const h1 = header.find('.App-title');
expect(h1.text()).toEqual(expected);
});
it('should render an h3"', () => {
const header = shallow(<Header />);
const h3 = header.find('.display');
expect(h3.length).toEqual(1);
});
it('should be off by default', () => {
const header = shallow(<Header />);
const instance = header.instance();
expect(instance.state.isOn).toEqual(false);
});
it('should render a button"', () => {
const header = shallow(<Header />);
const button = header.find('.toggle');
expect(button.length).toEqual(1);
});
it('toggleOn() should toggle the on/off state"', () => {
const header = shallow(<Header />);
const instance = header.instance();
expect(instance.state.isOn).toEqual(false);
instance.toggleOn();
expect(instance.state.isOn).toEqual(true);
instance.toggleOn();
expect(instance.state.isOn).toEqual(false);
});
it('clicking on the toggle button should toggle the on/off state"', () => {
const header = shallow(<Header />);
const instance = header.instance();
const button = header.find('.toggle');
expect(instance.state.isOn).toEqual(false);
button.simulate('click');
expect(instance.state.isOn).toEqual(true);
button.simulate('click');
expect(instance.state.isOn).toEqual(false);
});
it('clicking on the toggle button should show an alert"', () => {
window.alert = jest.fn(); // our mock/spy
const expected = 'toggle clicked!';
const header = shallow(<Header />);
const button = header.find('.toggle');
button.simulate('click');
button.simulate('click');
expect(window.alert).toHaveBeenCalledTimes(2);
expect(window.alert).toHaveBeenCalledWith(expected);
});
});

Day 3

Today

  • snapshot testing.
  • component testing.
  • fakes, stubs, and mock functions aka spies.

Snapshot Testing

  • sub-type of component testing.
  • very useful to spot regressions.
  • automatically generated by Jest.
  • a snapshot is a JSON based record of a component's output.
  • snapshots are saved to a __snapshots__ folder as a sibling of the tested component.
  • during testing, the components are automatically compared to their last recorded snapshot.
  • fails with any change to the component.
  • the snapshots are commited to source control.

Snapshot Workflow

  • first, install react-test-renderer if you haven't.
  • second import renderer from 'react-test-renderer';.
  • then import { TheComponent } from './TheComponent';.
  • create a tree.
  • run the assertion to match snapshot.
import renderer from 'react-test-renderer';
import { TheComponent } from './TheComponent';
const tree = renderer.create(<TheComponent />);

expect(tree.toJSON()).toMatchSnapshot();
  • the first time, the snapshot is created.
  • when the snapshot fails, the developer can decide to update the snapshot if the change was intended.
  • to update snapshot: jest TheComponent -u or --update, but, ain't nobody got time for that (if you used create-react-app the command is yarn test -u).

Snapshot Pros and Cons

  • fast and semi-automatic.

  • catches regressions that could be missed by humans.

  • works with any library that generates HTML components (Angular, Vue, React).

  • better than no tests at all to protect applications from regressions.

  • easy to override, save new snapshot.

  • protects only against regression.

  • easy to break, smallest change will fail test suite.

  • adds more files to the repository.

  • waste of time to have them while actively making changes to components.

Component Testing

  • appearance and functionality of a component.
  • highly sensitive to small changes.
  • great against regression.
  • verifies changes to component output resulting from changes to state.
  • does NOT verify interaction between components.

What do we test?

  • does it render?
  • does it render correctly?
  • are sub-components rendered, and how many of them.
  • are props handled correctly.
  • is state handled correctly.
  • does it handle events correctly?

What makes a good test?

  • should be independent.
  • test only what needs to be tested.
  • one test per behavior/functionality, multiple assertions are ok.
  • one code unit at a time.

Workflow

  • render component in asolation without any sub components. This is called shallow rendering.
  • we'll use a library called enzyme for shallow rendering.
  • insde the README.md file generated by create-react-app there are instructions on how to add enzyme to the project. Follow the instructions under

Install Enzyme

  • type: yarn add enzyme enzyme-adapter-react-16 react-test-renderer.
  • add a global setup file: src/setupTests.js.
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });
Using enzyme. Docs
  • add library.
  • add this code to App.test.js.
import { shallow } from 'enzyme';

it('shallow renders without crashing', () => {
  shallow(<App />); // notice this only renders App and will not render any child components
});

Mocks

  • a duplicate of a unit of code that has no implementation.

  • has the same API (method names, return value type and shape must be returned) of the original unit, but has no side effects.

  • prevent side effects.

  • reduce dependencies.

  • isolate tests to simplify testing.

  • add mocks to a __mocks__ folder, next to the mocked module.

  • to mock npm modules, just name your mock the same as the module and it will be loaded automatically.

    • folder must be at the root of the application, next to node_modules folder.
    • add a file inside the folder with the name of the module you want to mock.
  • to mock local files:

    • add the __mocks__ folder at the same level of the file to mock.
    • where the mock is needed (inside the test file) call jest.mock('path to code file to be mocked');.

Spies

Spies are functions that provide information about how they are used.

  • counts function calls.

  • records arguments passed to it when it is called.

  • we can return fake values in order to test specific scenarios.

  • to create spies: const jamesBond = jest.fn();. This creates a noop function.

  • passing a funcion as the first argument, wraps that function into a spy that then keeps track of useful information.

{
"name": "dashboard",
"version": "0.1.0",
"private": true,
"dependencies": {
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-scripts": "1.1.4",
"react-test-renderer": "^16.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom --verbose",
"eject": "react-scripts eject"
}
}
// src/setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment