React Tac Toe is the result of a personal experiment I conducted when investigating how to write React tests in such a way that they pass even if I completely change the way the game's state is managed. That is, the exact same suite of tests should pass even if the underlying implementation of the game is changed. After all, since the external behaviour of the app does not change, should the tests?
Read more about The What, How, and Why of Nicely Testing React Components on my personal website!
All the cool stuff is in the ReactTacToe component! You will see that the Tic Tac Toe game is implemented with three different state management paradigms:
- React Hooks - Standard stateful fields on a component
- MobX Objects - Object-oriented approach using observables
- A Reducer - A pure function that returns new state upon actions being dispatched
Yet, all three of these implementations are tested using the same suite of tests in ReactTacToe.spec.jsx.
import * as ReactTacToeImplementations from '.';
describe('ReactTacToe', () => {
Object.keys(ReactTacToeImplementations).forEach(implementation => {
describe(`using ${implementation}`, () => {
// ...
});
});
});
This is achieved because the tests follow three simple principles:
- Test what the component does, not how it works
- Write tests from the perspective of the user
- Only expose the component's surface in the test
For example:
const clickSquare = n => fireEvent.click(wrapper.getByTestId(`tile-${n}`));
const squareMark = n => wrapper.getByTestId(`tile-${n}`).textContent;
it('should populate the first square clicked with an X', () => {
wrapper = render(<ReactTacToe />);
expect(squareMark(0)).toEqual('');
clickSquare(0);
expect(squareMark(0)).toEqual('X');
});
- The component's supposed to put an X where the user clicks, so let's test that, not whether an internal state object is holding the correct value.
- Instead of instance methods in the component, let's think about real user actions. In our case, the thing a user can do is click on a square.
- All the user can click on is a square, and all the user can see is whether there's a mark or not. That's the component's surface.
Feel free to explore a bit more (and suggest improvements in the form of pull requests)!
# Start the app
npm start
# Start and watch
npm run start:watch
# Run tests
npm test