Better Unit Testing
Unit tests
Unit = Unit of work
This could involve multiple methods and classes invoked by some public API that can:
- Return a value or throw an exception
- Change the state of the system
- Make 3rd party calls
Basic
- Test case
- Test suit
- Stub
- Mock
- Coverage
Test case
A test case is the smallest unit of testing. It checks for a specific response to a particular set of inputs.
You may do these things in a test case:
- Prepare/mock input data
- Apply action
- Check the response
Format
it('should [expected behaviour] when [scenario/context]', () => {
// Your test logic
});
Example
it("should return 2 when 1+1", () => {
expect(add(1, 1)).toBe(2);
});
Some times you need to split it into multiple lines
:(
it("should return all matched person", async () => {
expect(await personService.getPersonsByIds([1, 2, 3])).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }
]);
});
:)
it("should return all matched person", async () => {
const result = await personService.getPersonsByIds([1, 2, 3]);
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
});
But keep in mind that split code into multiple lines is the last choice, put them in same line when possible.
Test suit
A test suite is a collection of test cases, test suites, or both. It is used to aggregate tests that should be executed together.
Testing a function
Format
describe("[functionName()]", () => {
it("should [expected behavior] when [scenario/context]");
});
Example
describe("add()", () => {
it("should work with int");
it("should return NaN when add a NaN");
it("should concern about preciseness");
});
Testing a class
Format
describe('[ClassName]', () => {
describe('[functionName()]', () => {
it('should [expected behaviour] when [scenario/context]');
}
});
Example:
describe("Calculator", () => {
describe("add()", () => {
it("should work with int");
it("should return NaN when add a NaN");
it("should concern about preciseness");
});
});
Mocking
Mocking is one particlar technique to allow testing of a unit of code with out being reliant upon dependencies.
Mock could do these for you:
- Erasing the actual implementation of a function
- Capturing calls to the function
- Capturing instances of constructor functions when instantiated with
new
- Allowing test-time configuration of return values.
- Boost test speed.
Ensure function called
import EventEmitter2 from 'eventemitter2';
class UploadManager extends EventEmitter2 {
on(type, listener) {
super.on(type, listener);
}
emit(type, ...arg) {
super.emit(type, ...arg);
}
}
it('should call the event handler', () => {
const handler = jest.fn();
uploadManager.on('1', handler);
uploadManager.emit('1');
expect(handler).toHaveBeenCalled();
});
Configure return values
import PostAPI from 'sdk/api/glip/post';
export default class PostService extends BaseService {
async getPostsFromRemote({ groupId, postId, limit, direction }) {
// ...
const requestResult = await PostAPI.requestPosts(params);
// ...
return result;
}
}
describe('getPostsFromRemote()', () => {
it('should return posts of the group from remote', async () => {
PostAPI.requestPosts.mockResolveValue({ data: { posts: postsOfGroup3 } });
const result = await postService.getPostsFromRemote({ groupId: 3 });
expect(result.posts).toBe(postsOfGroup3);
});
});
Replace dependecies module with mock
// utils.js
export const fn1 = () => {};
// doSomeThing.js
import { fn1 } from './utils';
export default function doSomeThing() {
// ...
fn1();
// ...
}
// doSomeThing.test.js
import { fn1 } from './utils';
import doSomeThing from './doSomeThing';
jest.mock('./utils');
it('should call fn1', () => {
doSomeThing();
expect(fn1).toHaveBeenCalled();
});
Coverage
test coverage is a measure used to describe the degree to which the source code of a program is executed when a particular test suite runs.
Coverage report
Coverage detail
Best Practice
Unit tests are isolated and independent of each other
- Any given behaviour should be specified in one and only one test
- The execution/order of execution of one test cannot affect the others
:(
const store = new Store();
it('should append object', () => {
store.append({id: 1});
expect(store.getById(1)).toBe({id: 1});
});
it('should remove object', () => {
store.remove(1);
expect(store.getById(1)).toBeNull();
});
:)
it('should append object', () => {
const store = new Store();
store.append({id: 1});
expect(store.getById(1)).toBe({id: 1});
});
it('should remove object', () => {
const store = new Store([{id: 1}]);
store.remove(1);
expect(store.getById(1)).toBeNull();
});
Unit tests are code too.
- They must meet the same level of quality as the code being tested.
- They can be refactored as well to make them more maintainable and/or readable.
- They should be review first when applying a code review.
Unit tests are lightweight tests
- Repeatable
- Fast
- Consistent
- Easy to write and read
Make your tests:
- Readable
- Maintainable
- Reliable
:(
expect(result === obj).toBe(true)
:)
expect(result).toBe(obj)
:(
expect(result.length).toBe(2)
:)
expect(result).toHaveLength(2)
:(
const result = store.getById(1);
expect(result.id).toBe(1);
expect(result.name).toBe('Object 1');
expect(result.group).toBe(2);
expect(result.type).toBe(3);
:)
const result = store.getById(1);
expect(result).toBe(obj1);
:(
store.append({id: 1, name: 'Object 1', group: 2, type: 3});
store.append({id: 2, name: 'Object 2', group: 2, type: 3});
:)
store.append({id: 1});
store.append({id: 2});
:(
store.append(obj);
expect(store.items.find(o => o.id === 1)).toBe(obj);
:)
store.append(obj);
expect(store.getById(1)).toBe(obj);
:(
it('should return 100 if type is 1 and 200 if type is not 1', () => {
const obj1 = {id: 1, type: 1};
const obj2 = {id: 2, type: 2};
const obj3 = {id: 3, type: 3};
[obj1, obj2, obj3].forEach(o => {
if(o.type === 1) {
expect(getPrice(o)).toBe(100);
} else {
expect(getPrice(o)).toBe(210);
}
});
});
:)
it('should return 100 when type is 1', () => {
expect(getPrice({id: 1, type: 1})).toBe(100);
});
it('should return 200 when type is not 1', () => {
expect(getPrice({id: 1, type: 2})).toBe(200);
});
Test Driven
Test first.
life-cycle
Why use TDD?
- Change the way of thinking.
- Setup a tiny enviorment for development and debug.
- Don't have to run entire application to verify code.
Always use TDD?
NO
Don't use TDD unless it makes you feel good and confident.
CI
Tools
- VS Code - Jest Plugin
- VS Code - Wallaby.js Plugin
- ESLint plugin for Jest
- Jenkins/Gitlab