Introduction
Mục đích của bài viết này là đưa ra một ví dụ đơn giản mà tôi đã sử dụng để viết unit test cho Tiki Tini App. Hi vọng bài viết giúp các bạn phần nào trong việc tìm tài liệu tham khảo, chọn lựa một đáp án phù hợp với quá trình kiểm thử và phát triển ứng dụng của các bạn.
Bài viết mang tính chia sẻ, hi vọng các bạn có thể đóng góp ý kiến của mình ở github repo issues.
Dưới đây là phần code của custom component mà chúng ta sẽ cùng test:
// tiki-tiniapp-with-unit-tests/app/src/components/the-button/index.js
import QRCode from "qrcode";
import Utils from "qrcode/lib/renderer/utils";
const createComponent = async () => {
const componentConfig = ({
data: {
link:"https://developers.tiki.vn",
size: 0,
},
onInit() {
this.generateQR = this.generateQR.bind(this)
},
methods: {
async tapButton() {
return await this.generateQR(this.data.link)
},
generateQR(url) {
return new Promise((resolve,reject) => {
try {
const canvas = my.createCanvasContext("qr-code");
const opts = Utils.getOptions({});
const qrData = QRCode.create(url);
const size = Utils.getImageWidth(qrData.modules.size, opts);
this.setData({ size, qrData });
canvas.getImageData({
width: size,
height: size,
success:(image)=> {
try {
Utils.qrToImageData(image.data, qrData, opts);
canvas.putImageData(image,0,0);
this.setData({ size, image: image.data });
resolve()
} catch (error) {
reject(error)
}
},
});
} catch (error) {
console.error("generateQR", error)
// I dont want to handle error here, just want to show the log
throw error;
}
})
}
}
});
Component(componentConfig);
return componentConfig
}
if (!my.TEST ) {
createComponent();
}
export default createComponent;
Table of contents
- Introduction
- Table of contents
- About unit test in Tiki Tini App
- Environment setup
- Example tests
- Source code
- Conclusion
About unit test in Tiki Tini App
Unit testing là phương pháp kiểm thử trên từng đơn vị của source code (class, function,..) và được coi là phương pháp cơ bản nhất mà một developer cần biết. Vì vậy sự có mặt của unit test trong dự án phần mềm của bạn thường là một điều hiển nhiên và khá quan trọng.
Đối với các Tini App, hiện tại phía Tiki chưa cung cấp cụ thể một lựa chọn nào để tích hợp Unit testing vào quá trình phát triển Tini app. Bạn cần tự setup phương án sử dụng unit test phù hợp với ứng dụng của bạn.
Environment setup
// jest.config.js file
module.exports = () => {
return {
verbose: true,
transformIgnorePatterns: ["node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)"],
automock: false,
setupFiles: ["./mocking/setupJestMock.js"],
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.ts?$",
collectCoverageFrom: ["app/**/*.{js,ts}"],
};
};
Source code structure
Cấu trúc dự án được sắp đặt đơn giản như sau:
├── __tests__
│ ├── component.the-button.test.ts
│ └── page.home.test.ts
├── app # Tini App source code
│ ├── package.json
│ # các dependencies phục vụ cho product development
│ ├── src
│ │ ├── app.js
│ │ ├── app.json
│ │ ├── app.tcss
│ │ ├── components
│ │ │ └── the-button
│ │ │ ├── index.js
│ │ │ ├── index.json
│ │ │ ├── index.tcss
│ │ │ └── index.txml
│ │ ├── home
│ │ │ └── index.js
│ │ │ ├── index.json
│ │ │ ├── index.tcss
│ │ │ └── index.txml
│ └── yarn.lock
├── babel.config.js
├── jest.config.js
├── mocking
│ └── setupJestMock.js
├── package.json
│ # các dependencies phục vụ cho local development
└── yarn.lock
Setup test dependencies
Trong bài chia sẻ này tôi sẽ dùng jest và babel. Các bạn có thể tham khảo dev dependencies dưới đây mà tôi sử dụng:
// dev dependencies in ./packages.json
{
"@babel/core": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/preset-typescript": "^7.15.0",
"@types/jest": "^27.0.2",
"babel-jest": "^27.2.5",
"husky": "^7.0.2",
"jest": "^27.2.5",
"jest-canvas-mock": "^2.3.1",
"typescript": "^4.4.4"
}
Example tests
Mocking
Ý tưởng ở đây là khi mock các entities, chúng ta sẽ intercept global function Component
hay Page
để lấy các config, init data, methods và lifecycle methods sau đó map lại references(binding for the this
context of methods) và trigger các unit mà ta cần test. Thông qua việc này ta có thể lấy được outputs cần test.
Các global JSAPI đơn giản là api của Tini App cung cấp nên chúng ta không cần test lại chúng, hãy giả định outputs của chúng luôn đúng và chỉ tập trung phần code của bạn.
Với usecase của tôi ở ví dụ này, tôi cần mock các api liên quan đến canvas nên tôi sẽ sử dụng canvas từ dom hỗ trợ bởi package jest-canvas-mock
để thực hiện nghiệm vụ này. Phần code dưới đây khá đơn giản nên tôi không muốn diễn giải lại.
Và good code documents itself:
Previews code:
import 'jest-canvas-mock';
import createComponent from "../app/src/components/the-button";
// typings
type AsyncReturnType<T extends (...args: any) => any> =
T extends (...args: any) => Promise<infer U> ? U :
T extends (...args: any) => infer U ? U :
any
type PureConfig = AsyncReturnType<typeof createComponent>;
type ComponentMethodsType = Pick<PureConfig, 'methods'>
type ComponentType = Omit<PureConfig, 'methods'> & ComponentMethodsType["methods"]
let Instance: ComponentType = {} as unknown as ComponentType;
// mocking
const componentTargetMocker = {
async handler(ConfigObj) {
Instance = this;
// intercept setData to get the render's data
Instance.data = ConfigObj.data;
Instance.setData = async (data, cb) => new Promise((resolve) => {
Object.assign(Instance.data, data)
resolve(Instance.data);
cb(Instance.data)
if (ConfigObj.didUpdate) ConfigObj.didUpdate();
});
// mocking lifecycles and doing reference binding
Instance.onInit = ConfigObj.onInit.bind(Instance);
if (ConfigObj.didMount) Instance.didMount = ConfigObj.didMount.bind(Instance);
if (ConfigObj.didUpdate) Instance.didUpdate = ConfigObj.didUpdate.bind(Instance);
if (ConfigObj.methods) Object.entries(ConfigObj.methods).forEach(([key, method]) => {
if (typeof method === 'function') {
ConfigObj.methods[key] = ConfigObj.methods[key].bind(Instance);
Instance[key] = method;
}
})
// trigger life cycle
await Instance.onInit();
await Instance?.didMount?.();
}
}
componentTargetMocker.handler = componentTargetMocker.handler.bind({});
const mockComponentCreator = jest.fn(componentTargetMocker.handler);
globalThis.my.createCanvasContext = (id) => {
const canvas = document.createElement('CANVAS') as HTMLCanvasElement;
canvas.id = id;
const ctx = canvas.getContext('2d');
const _getImgData = ctx.getImageData;
ctx.getImageData = (opt: any) => {
return opt.success(_getImgData(0, 0, opt.width, opt.height))
}
return ctx
}
globalThis.Component = mockComponentCreator;
Tests
Dựa vào phần mocking ở phía trên, chúng ta đã có thể thêm và pass các bài test cơ bản hay gặp mà không gặp quá nhiều khó khăn:
// stupid tests
describe("Myapp: common stupid test cases:", () => {
beforeAll(() => {
createComponent();
})
test("Component() has been called", (done) => {
expect(mockComponentCreator).toHaveBeenCalledTimes(1)
done();
});
test("tab generate QR with expected size", (done) => {
Instance.generateQR("https://developers.tiki.diferent-here");
expect((Instance.data.size)).toEqual(148)
done();
});
test("component data matches snapshot", (done) => {
Instance.tapButton()
expect((JSON.stringify(Instance.data, null, 2))).toMatchSnapshot()
done();
});
});
Source code
Các bạn có thể xem source code từ bài viết này tại github.com/cute-me-on-repos/tiki-tiniapp-with-unit-tests
Conclusion
Tuân thủ nguyên tắc của unit test là test theo đơn vị, các phần còn thiếu thì chúng ta có thể sử dụng mocks. Cá nhân tôi thấy nhiều bạn thường muốn test quá nhiều thứ trong unit test và đôi khi nhầm lẫn với integration testing. Việc có nên tách biệt hai khái niệm này hay không không quá quan trọng, và việc quyết định mức độ test coverage bao nhiêu là đến từ developer với những test cases được chính developer đó định đoạt chứ không phải hoàn toàn dựa vào những thứ như jest --coverage
. Phía reactjs cũng có đọan ghi chú sau vào ngay trang chủ của họ về tài liệu kiểm thử:
With components, the distinction between a “unit” and “integration” test can be blurry. If you’re testing a form, should its test also test the buttons inside of it? Or should a button component have its own test suite? Should refactoring a button ever break the form test? ( * ⓘ - reactjs/docs/testing)
Hi vọng bài viết giúp bạn có thêm tài liệu tham khảo khi áp dụng unit test cho dự án Tini App của bạn.
thien.ly from the cute me on repo team