What is Better Tests?
Better Tests is a collection of Javascript testing best practices inspired by BetterSpecs.org to improve your coding skills and level up your test suite.
Note: These recommendations use Jest syntax as the basis for testing Javascript. Make considerations when using a different framework.
Describe the object
Be clear about what function, class constructor, or component you are
describing. It’s best to follow examples on
MDN for how to describe Javascript
objects and their usage. For example, the function parseInt()
is represented
with open parentheses and no parameters. Check the documentation for
framework-specific examples.
Bad
describe('a function to transform data', () => {});
describe('a class to track analytics', () => {});
describe('a React component to render dates', () => {});
describe('a Vue component to render dates', () => {});
describe('a web component to render dates', () => {});
Good
describe('transformData()', () => {});
describe('Analytics()', () => {});
describe('<Dates/>', () => {});
describe("Vue.component('dates')", () => {});
describe('<dates>', () => {});
Nest describe blocks for context
Avoid nesting when testing user flows. Describe blocks are better at providing additional context.
Bad
describe('<ProductPage />', () => {
it('displays the user as a guest when not logged in', () => {});
it('prompts the user to login/signup in cart when not logged in', () => {});
it('allows the user to proceed as a guest to checkout when not logged in', () => {});
});
Good
describe('<ProductPage/>', () => {
describe('when user is a guest', () => {
it('displays the user as a guest', () => {});
it('prompts the user to login/signup in cart', () => {});
it('allows the user to proceed as a guest', () => {});
});
});
Describe expected behavior
Describe the expected behavior as clearly as possible, not the implementation details.
Bad
it('responds with a 302 if the user is not logged in', () => {});
it('transforms data correctly', () => {});
it('calls onSubmit when the user clicks the button', () => {});
Good
it('redirects guest users', () => {});
it('retrieves column ids from columns', () => {});
it('submits the form', () => {});
Single expectation tests
Make only one assertion for each test. One test assertion helps find errors by displaying the exact failing test and makes your code readable. In isolated tests, you want each example to specify one, and only one, behavior. Multiple expectations result in unfocused and confusing tests.
Bad
it('redirects guests to the home page and prompts them to sign in', () => {
...
expect(isUserLoggedIn).toBe(false);
expect(isHomePage).toBe(true);
expect(hasSigninPrompt).toBe(true);
});
Good
it('redirects guests to the home page', () => {
...
expect(isHomepage).toBe(true);
});
it('redirects new accounts to the home page', () => {
...
expect(hasSigninPrompt).toBe(true);
});
Don't use 'should'
“Should” is repetitive and doesn’t provide useful or meaningful information about expected behavior. Use actionable, present tense language in descriptions.
Bad
it('should sign up a user', () => {});
it('should prompt a guest to sign up', () => {});
it('should add to cart', () => {})
Good
it('signs up a user', () => {});
it('prompts a guest to sign up', () => {});
it('adds to cart', () => {})
Don't use mocks
Mocks are a code smell. Mocks are tempting but often test too much implementation and not enough behavior. Instead, find a way to stub the environment the way JSDOM stubs a browser DOM. You may want to use different tools in the future, and testing their implementation details will only stop you from refactoring.
Recomended libraries:
Bad
jest.mock('axios');
it('signs up a new user', () => {
const {getByTestId} = render(<UserForm />);
fireEvent.click(getByTestId('form-signup'));
expect(axios.post).toHaveBeenCalledWith(
'http://localhost.com/user/new',
expect.anything()
);
expect(getByTestId('form-signup-success')).toBeTruthy();
});
Good
it('signs up a new user', () => {
nock('http://localhost.com/user')
.post('/new')
.reply(200);
const {getByTestId} = render(<UserForm onSubmit={onSubmit} />);
fireEvent.click(getByTestId('form-signup'));
expect(getByTestId('form-signup-success')).toBeTruthy();
});
Create only the data you need
It’s tempting to set up all the data required to avoid errors. However, this makes testing in isolation difficult and doesn’t allow you to address situations with missing data. Instead, add data for specific contexts or individual tests only when necessary.
Bad
describe('<AccountPage/>', () => {
beforeEach(signInUser);
beforeEach(generateOrderHistory);
beforeEach(generateUserPreferences);
...
});
Good
describe('<AccountPage/>', () => {
it('redirects users who are not signed in', () => {});
describe('when a user is signed in', () => {
beforeEach(signInUser);
it('displays a message when the order history is empty', () => {});
it('displays the order history', () => {
generateOrderHistory();
...
});
it('sets the users preferences', () => {});
it('displays the users previous preferences', () => {
generateUserPreferences();
....
});
});
});
Test what the user sees
Rather than test implementation details, it’s better to test what the user sees. In front-end development, that usually means testing DOM behavior. Use libraries like: