- 1. Test cases
- 1.1. Single test case per scenario
- 1.2. Valid and short test case
- 1.3. Fill in the 'describe' block
- 1.4. Test case descriptions should follow a pattern:
- 1.5. Every single test case should explain what
should be done
. - 1.6. Use multiple describe blocks if you test different things
- 1.7. Backend: Use test factories instead of creating instances explicitly
- 1.8. Backend: avoid fetching values from database in tests using ORM
When you add a test ask yourself if this test is required and why. Is there any other test checking the same functionality? If you can't find a good reason for the test to exist - don't write it.
There should be only necessary and valid steps in a single test case. If a single test case contains too many test steps this may lose its aim.
// ❌ bad
it(`should work`, () => {
expect(sum(2, 3)).toBe(5);
expect(sum(0, 0)).toBe(0);
expect(sum(-1, 1)).toBe(0);
expect(sum(-1, 0)).toBe(-1);
})
// ✅ good
it(`should return a sum of positive numbers`, () => {
expect(sum(2, 3)).toBe(5);
});
it(`should return 0 when adding zeroes`, () => {
expect(sum(0, 0)).toBe(0);
});
it(`should return 0 when adding additive inverses `, () => {
expect(sum(-1, 1)).toBe(0);
});
it(`should return a negative number when adding negative and zero`, () => {
expect(sum(-1, 0)).toBe(-1);
});
Write a module name that is tested in the describe block for the unit and integration test cases.
// ❌ bad
describe('', () => {
it(`Function 'sum' should return 0 if adding zeroes`, () => {})
it(`Function 'sum' should have property 'age'`, () => {})
it(`Function 'sum' should return 0 if called without arguments`, () => {})
});
// ✅ good
describe(`Function 'sum':`, () => {
it(`should return 0 if adding zeroes`, () => {})
it(`should have property 'age'`, () => {})
it(`should return 0 if called without arguments`, () => {})
});
should [EXPECTED_RESULT] when [STATE]
. With filled in describe
block each test case description should start with lowercase.
// ❌ bad
it('Works without arguments', () => {})
// ✅ good
it('should return 0 when called without arguments', () => {})
// ❌ bad
it(`adds zeroes and returns 0`, () => {});
it(`has property 'age'`, () => {});
it(`returns 0 if called without arguments`, () => {});
// ✅ good
it(`should return 0 if adding zeroes`, () => {});
it(`should have property 'age'`, () => {});
it(`should return 0 if called without arguments`, () => {});
// ❌ bad
describe('', () => {
it('BaseRobot class should create a robot', () => {});
it('FlyingRobot class should create a flying robot', () => {});
});
// ✅ good
describe('BaseRobot class', () => {
it('should create a robot', () => {});
});
describe('FlyingRobot class', () => {
it('should create a flying robot', () => {});
});
Why? To be tool-agnostic. It's possible to update factories in one place instead of checking every test-case separately
💡 Note: If your project doesn't have factories infrastructure, time to create it
// ❌ bad
const user = await User.create();
// ✅ good
const user = await userFactory.create();
Why? Again, to be tool-agnostic and don't rely on existing infrastructure. One day ORM may be changed which may lead to refactoring the whole tests infrastructure
await client.signUp({ email: '[email protected]' });
// ❌ bad
const user = await UserModel.findOne({ where: { email: '[email protected]' }}) // direct DB call is prohibited
// ✅ good
const user = await client.getUser({ email: '[email protected]' }) // use API client to make sure user is created