React

We use Jest as a testing framework for React coding exercises.

Hello, World!

Before we go into details, let's see how you can use Jest to test the most basic React component: writing a paragraph that contains "Hello, World!".

This is what a student might write:

App.js

import React from 'react';
// don't change the Component name "App"
export default class App extends React.Component {
render() {
return (
<p>
Hello, World!
</p>
);
}
}

And this is an example of what you could write to test or evaluate the student's code:

App.spec.js

import React from 'react';
import App from './App';
import { shallow } from 'enzyme';
describe('App component', () => {
it('should show "Hello, World!"', () => {
const wrapper = shallow(
<App />
);
expect(wrapper.text()).toContain('Hello, World!');
});
});

Let's examine the above evaluation code. Besides the 3 functions that Jest provides, describe , it , and expect , the rest is just JavaScript and Enzyme code that helps with React testing.

describe

describe defines the name of the subject that you want to test. Its value can be any string that is relevant to your exercise. You can have multiple describe calls in your evaluation if you need to test different subjects.

it

Inside describe , you can define as many tests as you want that are related to that same subject, by calling the it function. The string argument that it receives describes how the subject is expected to behave.

Inside it and describe you have access to the React component. You can:

  • use shallow() to create a component without creating its child React components.

  • use mount() to create a component including its child React components.

  • access the React component through wrapper.

  • get other React components in App using wrapper.find().

  • get references to elements from the student's code and interact with them (for example: you can simulate a click on a button on the page).

Tip: Jest will connect the 2 strings passed to describe and it to give a meaningful description of the test. In the above example, if any of the tests fail, this is what the student will see:

  • App component should show "Hello, World!"

expect

Finally, the expect function is what actually decides whether a test fails or succeeds. Once you have a reference to a piece of the DOM (or other value) generated by student's code, you can assert whether it behaves as it should by calling:

expect(valueFromStudentCode).toEqual(expectedValue);

toEqual() is called a Jest "matcher" and there are many more that you can use, like:

  • toBeDefined()

  • toBeNull()

  • toBeLessThan()

You can read about the rest in Jest's documentation: expect.

A matcher can be prepended with not if you want a negative assertion.

expect(valueFromStudentCode).not.toEqual(expectedValue);

If any of the expect calls fail inside a test, that test will be marked as failed by Jest and reported back to the student. If you want to give the student more granular feedback about a failing assertion, in your own words, you can pass an additional string argument to the matcher.

expect(valueFromStudentCode)
.toEqual(expectedValue, 'Your code does not work because this and that.');

expect with a message

Another more robust alternative to give granular feedback to the user about a failing assertion is an improved expect() where you pass in a message. This is provided by the jest-expect-message library.

expect(valueFromStudentCode, 'Your code does not work because this and that.')
.toEqual(expectedValue);

Let's see some more examples of things you can test:

Example 1: CSS Styling with classes

Let's say you want to verify that a paragraph has proper CSS styling, say color "red". Use an external CSS file to define styles for your components:

App.js

import React from 'react';
import './App.css';
// don't change the Component name "App"
export default class App extends React.Component {
render() {
return (
<div>
<p className="hello">Hello, World!</p>
</div>
);
}
}

App.css

.hello {
color: red;
}

App.spec.js

import React from 'react';
import App from './App';
import { shallow } from 'enzyme';
describe('the paragraph', () => {
it('should be red', () => {
const wrapper = shallow(
<App />
);
// check for class
expect(wrapper.find('p').at(0).props().className).toEqual('hello');
});
});

Example 2: CSS Styling inline

Students can also implement the code by inlining the style. This is not recommended because inline styling isn't best practice for React:

App.js

import React from 'react';
// don't change the Component name "App"
export default class App extends React.Component {
render() {
return (
<p style={ {color: 'red'} }>
Hello, World!
</p>
);
}
}

App.spec.js

import React from 'react';
import App from './App';
import { shallow } from 'enzyme';
describe('the paragraph', () => {
it('should be red', () => {
const wrapper = shallow(
<App />
);
// check for style
expect(wrapper.find('p').prop('style').color).toEqual('red');
});
});

Students may also use hex colors (or em instead of px for lengths) so you'll have to be careful in your instructions or test cases. Another reason to stick with testing for classes.

Example 3: User interactions

How do you test whether your students handle events, like the click of a button? Let's say that a button click must change the color of a paragraph to "blue". This is what a solution could look like:

App.js

import React from 'react';
// don't change the Component name "App"
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
color: 'red',
};
this.onClick = this.onClick.bind(this);
}
onClick() {
this.setState({
color: 'blue',
});
}
render() {
return (
<div>
<p style={ {color: this.state.color} }>Some text</p>
<button onClick={this.onClick}>Change color</button>
</div>
);
}
}

And this is how you would test it:

App.spec.js

import React from 'react';
import App from './App';
import { shallow } from 'enzyme';
describe('"Change color" button', () => {
let button, wrapper;
beforeEach(() => {
wrapper = shallow(
<App />
);
button = wrapper.find('button');
});
it('should exist', () => {
expect(button).toBeTruthy();
});
it('should change the paragraph color to red', () => {
expect(wrapper.find('p').prop('style').color).toEqual('red');
button.simulate('click');
expect(wrapper.find('p').prop('style').color).toEqual('blue');
});
});

Example 4: Multiple files

Here’s a simple example with 2 files, one that implements a component Foo that is expected to return Foo , and another one with a function Bar that is expected to return Bar .

App.js

import React from 'react';
import Foo from './Foo';
import Bar from './Bar';
// don't change the Component name "App"
export default class App extends React.Component {
render() {
return (
<div>
<Foo /><Bar />
</div>
);
}
}

Foo.js

import React from 'react';
export default class Foo extends React.Component {
render() {
return (
<span>
Foo
</span>
);
}
}

Bar.js

import React from 'react';
export default class Bar extends React.Component {
render() {
return (
<span>
Bar
</span>
);
}
}

The evaluation file will be very similar to the ones we write for HTML:

App.spec.js

import React from 'react';
import App from './App';
import Foo from './Foo';
import Bar from './Bar';
import { mount } from 'enzyme';
describe('App component', () => {
it('should render Foo and Bar', () => {
const wrapper = mount(
<App />
);
expect(wrapper.find(Foo)).toBeTruthy();
expect(wrapper.find(Bar)).toBeTruthy();
expect(wrapper.text()).toEqual('FooBar');
});
});

Example 5: Exceptions

You can test for exceptions in your component. Here's a component that intentionally throws an exception while initializing.

App.js

import React from 'react';
// don't change the Component name "App"
export default class App extends React.Component {
componentDidMount() {
throw "failing in init";
}
render() {
return <p>Hi</p>;
}
}

App.spec.js

import React from 'react';
import App from './App';
import { shallow } from 'enzyme';
describe('component initialization', () => {
it('should throw a failure on init', () => {
expect(() => {
self.wrapper = shallow(
<App />
);
}).toThrow();
});
});

The more edge cases you can think of to test, the more helpful it will be for the students to help them write a correct solution.

Example 6: Spying on console.log()

This is a function that prints to standard output via console.log() :

App.js

import React from 'react';
// don't change the Component name "App"
export default class App extends React.Component {
search() {
console.log(2);
console.log('abc', 3);
}
render() {
return <p>Hi</p>;
}
}

It can be tested by using spies like this:

App.spec.js

import React from 'react';
import App from './App';
import { shallow } from 'enzyme';
describe('App component', () => {
it('prints to standard output', () => {
let stdout = '';
spyOn(console, 'log').and.callFake((...args) => {
stdout += args.join(' ') + '\n';
});
const wrapper = shallow(
<App />
);
wrapper.instance().search();
expect(stdout).toEqual('2\nabc 3\n');
});
});

Example 7: Hooks

Hooks allow you to use state without writing a class.

App.js

import React from 'react';
// don't change the Component name "App"
export default function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

App.spec.js

import React from 'react';
import App from './App';
import { shallow } from 'enzyme';
describe('App component', () => {
it('should increment a count', () => {
const wrapper = shallow(
<App />
);
let button = wrapper.find('button');
expect(wrapper.find('p').text()).toEqual('You clicked 0 times');
button.simulate('click');
expect(wrapper.find('p').text()).toEqual('You clicked 1 times');
});
});

Example 8: async / await

You can use modern async / await syntax to handle asynchronous code like delays or API requests.

App.js

import React from 'react';
const {useEffect, useState} = React;
const fetchTodos = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos', {
mode: 'cors',
});
const data = await res.json();
return data;
};
const App = () => {
const [todos, setTodos] = useState([]);
useEffect(() => {
const getTodos = async () => {
const data = await fetchTodos();
setTodos(data);
};
getTodos();
}, [setTodos]);
return (
<ul>
{todos.map(todo => {
return <li key={todo.id}>{todo.title}</li>;
})}
</ul>
);
};
export default App;

The code above can be tested with Jest async spec.

App.spec.js

import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import App from './App';
const data = [
{
id: 1,
title: "todo1",
},
{
id: 2,
title: "todo2",
},
];
const delay = async (duration = 100) => {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
};
describe('Todos <App />', () => {
const _saveFetch = window.fetch;
beforeEach(() => {
window.fetch = jest.fn().mockImplementation((...args) => {
return new Promise((resolve) => {
resolve({
json: () => {
return new Promise((res) => {
res(data);
});
},
});
});
});
});
afterEach(() => {
window.fetch = _saveFetch;
});
it('shows correct number of list elements', async () => {
const wrapper = mount(<App />);
await delay();
wrapper.update();
expect(wrapper.find('li').length).toEqual(2);
});
});

What else is possible?

Jest's documentation presents more advanced functionality that you can use, like:

  • asynchronous testing via the async/await directives doc

  • mocks: jeset.spyOn doc

  • beforeEach , afterEach doc

You can have your students include any external JavaScript from CDNs, as long as they are served over https. You can include:

  • jQuery

  • Underscore

  • anything else

For React coding exercises you can only define 1 App.js file, and as many Component and CSS files as you want.

Technical specs

  • We use Jest version >= 25.1.

  • We use Enzyme >= 3.11.0.

  • When previewing, all CSS files that you define will be included automatically (by us) at the end of the <head> tag.

  • When previewing, all JavaScript files that you define will be included automatically (by us) at the end of the <body> tag.

  • CSS Modules don't work in our Jest test harness. The CSS is not imported properly so you can't run getComputedStyle() to fetch CSS values. Stick to classNames and verify those instead.

Limitations

  • There is no webpack in the preview environment, so import/export are stripped out before rendering.

  • Students cannot define variables or functions that have the same name as the ones defined by Jest (describe , it , expect , fail and so on), or that are reserved (full list here).

  • When previewing, we load all your JavaScript files in the context of an HTML page. This means that files will be loaded and executed in order, one after the other. You cannot call a function in file A if the function is defined in file B and file B is loaded after file A. Also, your evaluation file will always be the last one.