Before diving into organizing test suites, it's so important to get your head around how your application is laid out. Most well-structured applications separate concerns into different sections. Typically, you'd have things like:
When it comes to testing JavaScript, you’ve got some pretty popular options:
For this guide, let's stick with Jest and React Testing Library for component testing.
Keeping a tidy directory structure can be a lifesaver. Try to mirror your source code structure with something like this:
src/
components/
Header.js
Header.test.js
Footer.js
Footer.test.js
services/
apiService.js
apiService.test.js
utils/
helper.js
helper.test.js
models/
userModel.js
userModel.test.js
test/
integration/
userFlow.test.js
e2e/
login.test.js
setupTests.js
jest.config.js
These tests ensure individual components do what they should. With Jest and React Testing Library, you can write tests like this:
// src/components/Header.js
import React from 'react';
const Header = ({ title }) => {
return <header>{title}</header>;
};
export default Header;
// src/components/Header.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Header from './Header';
test('renders the header with title', () => {
const title = 'Welcome to My App';
render(<Header title={title} />);
const headerElement = screen.getByText(title);
expect(headerElement).toBeInTheDocument();
});
Check that your services and utilities function correctly and handle any hiccups smoothly:
// src/services/apiService.js
export const fetchData = async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
return data;
};
// src/services/apiService.test.js
import { fetchData } from './apiService';
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'foo' }),
})
);
test('fetchData returns data when response is ok', async () => {
const data = await fetchData('/test-url');
expect(data).toEqual({ data: 'foo' });
});
test('fetchData throws error when response is not ok', async () => {
global.fetch.mockImplementationOnce(() => Promise.resolve({ ok: false }));
await expect(fetchData('/test-url')).rejects.toThrow('Network response was not ok');
});
Integration tests check how various parts of your application fit together. This might involve rendering multiple components or calling many services:
// test/integration/userFlow.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from '../../src/App';
test('user can complete a task flow', () => {
render(<App />);
fireEvent.click(screen.getByText(/start/i));
fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'John Doe' } });
fireEvent.click(screen.getByText(/submit/i));
expect(screen.getByText(/thank you, John Doe/i)).toBeInTheDocument();
});
E2E tests make sure your entire app works from a user's standpoint. Tools like Cypress and Selenium come in handy for these tests:
// test/e2e/login.test.js
describe('Login flow', () => {
it('should allow a user to login', () => {
cy.visit('/login');
cy.get('input[name="username"]').type('myUsername');
cy.get('input[name="password"]').type('myPassword');
cy.get('button[type="submit"]').click();
cy.contains('Welcome myUsername').should('be.visible');
});
});
Setting up the right test configuration ensures tests run smoothly and consistently. For Jest, you might have a file like this:
// test/jest.config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/test/setupTests.js'],
testEnvironment: 'jsdom',
moduleDirectories: ['node_modules', 'src'],
};
You'll also want to create a setup file:
// test/setupTests.js
import '@testing-library/jest-dom/extend-expect';
To make sure tests always run and pass before making any changes, set up continuous integration with services like GitHub Actions, Travis CI, or CircleCI. Here’s a GitHub Actions example:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install
- run: npm test
By following this structure and approach, you'll keep your test suites in order, making sure your JavaScript application stays top-notch and easy to maintain.