An Interest In:
Web News this Week
- February 29, 2024
- February 28, 2024
- February 27, 2024
- February 26, 2024
- February 25, 2024
- February 24, 2024
- February 23, 2024
Writing Well-Structured Unit Test in TypeScript
The purpose of this post is to discover the implementation of writing unit test using Jest, a JavaScript testing Framework, in Sequelize and TypeScript project.
Setup Project
Let's create a new brand project using NPM and Git Versioning.
mkdir my-projectcd /my-projectgit initnpm init
Then we will install some dependencies, we will use babel for running Jest using TypeScript
npm install --save sequelize pg pg-hstorenpm install --save-dev typescript ts-node jest babel-jest @types/sequelize @types/jest @babel/preset-typescript @babel/preset-env @babel/core
As we use TypeScript, we need to create tsconfig.json
to indicate how transcript TypeScript files from src to dist folders.
//tsconfig.json{ "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "target": "es2017", "rootDir": "./src", "outDir": "./dist", "esModuleInterop": false, "strict": true, "baseUrl": ".", "typeRoots": ["node_modules/@types"] }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.test.ts"]}
Then, we need to add babel.config.js
in project folder, so we can run the unit test directly.
//babel.config.jsmodule.exports = { presets: [ ['@babel/preset-env', {targets: {node: 'current'}}], '@babel/preset-typescript', ],};
Okay, now let's start writing the code.
Write Code
We will follow a design pattern with a model, a repository, a database lib, and a service. It will be as simple as possible, so we could write simple unit test with full coverage. The project structure will be like this
my-project/src/| bookModel.ts| bookRepo.test.ts| bookRepo.ts| bookService.test.ts| bookService.ts| database.tsbabel.config.jspackage.jsontsconfig.json
Firstly, we need to create database.ts
, it is a database connection lib in Sequelize.
//database.tsimport { Sequelize } from 'sequelize';export const db: Sequelize = new Sequelize( <string>process.env.DB_NAME, <string>process.env.DB_USER, <string>process.env.DB_PASSWORD, { host: <string>process.env.DB_HOST, dialect: 'postgres', logging: console.log });
Now, let's define the model. Models are the essence of Sequelize. A model is an abstraction that represents a table in your database. In Sequelize, it is a class that extends Model. We will create one model using Sequelize extending Class Model representing Book Model.
//bookModel.tsimport { db } from './database';import { Model, DataTypes, Sequelize } from 'sequelize';export default class Book extends Model {}Book.init( { id: { primaryKey: true, type: DataTypes.BIGINT, autoIncrement: true }, title: { type: DataTypes.STRING, allowNull: false }, author: { type: DataTypes.STRING, allowNull: false }, page: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, publisher: { type: DataTypes.STRING }, quantity: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, created_at: { type: DataTypes.DATE, defaultValue: Sequelize.fn('now'), allowNull: false }, updated_at: { type: DataTypes.DATE, defaultValue: Sequelize.fn('now'), allowNull: false } }, { modelName: 'books', freezeTableName: true, createdAt: false, updatedAt: false, sequelize: db });
Cool, next we will create a repository layer. It is a strategy for abstracting data access. It provides several methods for interacting with the model.
//bookRepo.tsimport Book from './bookModel';class BookRepo { getBookDetail(bookID: number): Promise<Book | null> { return Book.findOne({ where: { id: bookID } }); } removeBook(bookID: number): Promise<number> { return Book.destroy({ where: { id: bookID } }); }}export default new BookRepo();
Then we will create a service layer. It consists of the business logic of the application and may use the repository to implement certain logic involving the database.
It is better to have separate repository layer and service layer. Having separate layers make the code more modular and decouple database from business logic.
//bookService.tsimport BookRepo from './bookRepo';import Book from './bookModel';class BookService { getBookDetail(bookId: number): Promise<Book | null> { return BookRepo.getBookDetail(bookId); } async removeBook(bookId: number): Promise<number> { const book = await BookRepo.getBookDetail(bookId); if (!book) { throw new Error('Book is not found'); } return BookRepo.removeBook(bookId); }}export default new BookService();
Alright, we have done with the business logic. We will not write the controller and router because we want to focus on how to write the unit test.
Write Unit Test
Now we will write the unit test for repository and service layer. We will use AAA (Arrange-Act-Assert) pattern for writing the unit test.
The AAA pattern suggests that we should divide our test method into three sections: arrange, act and assert. Each one of them only responsible for the part in which they are named after. Following this pattern does make the code quite well structured and easy to understand.
Let's write the unit test. We will mock the method from bookModel to isolate and focus on the code being tested and not on the behavior or state of external dependencies. Then we will assert the unit test in some cases such as should be equal, should have been called number times, and should have been called with some parameters.
//bookRepo.test.tsimport BookRepo from './bookRepo';import Book from './bookModel';describe('BookRepo', () => { beforeEach(() =>{ jest.resetAllMocks(); }); describe('BookRepo.__getBookDetail', () => { it('should return book detail', async () => { //arrange const bookID = 1; const mockResponse = { id: 1, title: 'ABC', author: 'John Doe', page: 1 } Book.findOne = jest.fn().mockResolvedValue(mockResponse); //act const result = await BookRepo.getBookDetail(bookID); //assert expect(result).toEqual(mockResponse); expect(Book.findOne).toHaveBeenCalledTimes(1); expect(Book.findOne).toBeCalledWith({ where: { id: bookID } }); }); }); describe('BookRepo.__removeBook', () => { it('should return true remove book', async () => { //arrange const bookID = 1; const mockResponse = true; Book.destroy = jest.fn().mockResolvedValue(mockResponse); //act const result = await BookRepo.removeBook(bookID); //assert expect(result).toEqual(mockResponse); expect(Book.destroy).toHaveBeenCalledTimes(1); expect(Book.destroy).toBeCalledWith({ where: { id: bookID } }); }); });});
Then, we will write unit test for service layer. Same as repository layer, we will mock repository layer in service layer test to isolate and focus on the code being tested.
import BookRepo from './bookRepo';import Book from './bookModel';class BookService { getBookDetail(bookId: number): Promise<Book | null> { return BookRepo.getBookDetail(bookId); } async removeBook(bookId: number): Promise<number> { const book = await BookRepo.getBookDetail(bookId); if (!book) { throw new Error('Book is not found'); } return BookRepo.removeBook(bookId); }}export default new BookService();
Alright, we have done writing the unit test.
Before running the test, we will add script test in our package.json as follows:
//package.json..."scripts": { "build": "tsc", "build-watch": "tsc -w", "test": "jest --coverage ./src"},...
Cool, finally we can run the test with this command in our terminal:
npm test
After running, we will get this result telling our unit test is success and fully coverage
Links:
- Sequelize Extending Model - https://sequelize.org/docs/v6/core-concepts/model-basics/#extending-model
- Difference between Repository and Service Layer - https://stackoverflow.com/questions/5049363/difference-between-repository-and-service-layer
- Unit Testing and the Arrange, Act and Assert (AAA) Pattern - https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80
Original Link: https://dev.to/arifintahu/writing-well-structured-unit-test-in-typescript-2hal
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To