TypeScript로 잘 구성된 단위 테스트 작성

이 게시물의 목적은 JestSequelize 프로젝트에서 JavaScript 테스트 프레임워크인 TypeScript을 사용하여 작성 단위 테스트 구현을 발견하는 것입니다.

설정 프로젝트



NPMGit Versioning을 사용하여 새로운 브랜드 프로젝트를 만들어 봅시다.

mkdir my-project
cd /my-project
git init
npm init


그런 다음 일부 종속 항목을 설치하고 TypeScript를 사용하여 Jest를 실행하기 위해 babel을 사용합니다.

npm install --save sequelize pg pg-hstore
npm install --save-dev typescript ts-node jest babel-jest @types/sequelize @types/jest @babel/preset-typescript @babel/preset-env @babel/core


TypeScript를 사용할 때 TypeScript 파일을 src에서 dist 폴더로 전사하는 방법을 나타내기 위해 tsconfig.json를 생성해야 합니다.

//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"]
}


그런 다음 프로젝트 폴더에 babel.config.js를 추가해야 단위 테스트를 직접 실행할 수 있습니다.

//babel.config.js
module.exports = {
    presets: [
        ['@babel/preset-env', {targets: {node: 'current'}}],
        '@babel/preset-typescript',
    ],
};


자, 이제 코드 작성을 시작하겠습니다.

코드 작성



모델, 리포지토리, 데이터베이스 라이브러리 및 서비스로 디자인 패턴을 따를 것입니다. 가능한 한 간단하므로 전체 범위를 포함하는 간단한 단위 테스트를 작성할 수 있습니다. 프로젝트 구조는 다음과 같습니다.

my-project/
├──src/
|   ├──bookModel.ts
|   ├──bookRepo.test.ts
|   ├──bookRepo.ts
|   ├──bookService.test.ts
|   ├──bookService.ts
|   └──database.ts
├──babel.config.js
├──package.json
└──tsconfig.json


먼저 database.ts를 생성해야 합니다. 이것은 Sequelize의 데이터베이스 연결 라이브러리입니다.

//database.ts
import { 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
    }
);


이제 모델을 정의해 보겠습니다. 모델은 Sequelize의 핵심입니다. 모델은 데이터베이스의 테이블을 나타내는 추상화입니다. Sequelize에서는 Model을 확장하는 클래스입니다. Book Model을 나타내는 Sequelize 확장Class Model을 사용하여 하나의 모델을 생성합니다.

//bookModel.ts
import { 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
    }
);


좋습니다. 다음으로 리포지토리 레이어를 생성합니다. 데이터 액세스를 추상화하기 위한 전략입니다. 모델과 상호 작용하는 여러 가지 방법을 제공합니다.

//bookRepo.ts
import 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();


그런 다음 서비스 계층을 생성합니다. 애플리케이션의 비즈니스 로직으로 구성되며 저장소를 사용하여 데이터베이스와 관련된 특정 로직을 구현할 수 있습니다.
separate repository layer and service layer 있는 것이 좋습니다. 별도의 레이어를 사용하면 코드가 더 모듈화되고 비즈니스 로직에서 데이터베이스가 분리됩니다.

//bookService.ts
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();


자, 우리는 비즈니스 로직을 완료했습니다. 단위 테스트를 작성하는 방법에 집중하고 싶기 때문에 컨트롤러와 라우터를 작성하지 않습니다.

단위 테스트 작성



이제 리포지토리 및 서비스 레이어에 대한 단위 테스트를 작성합니다. 단위 테스트를 작성하기 위해 AAA (Arrange-Act-Assert) pattern을 사용합니다.
AAA 패턴은 우리가 테스트 방법을 배열, 행동 및 주장의 세 부분으로 나누어야 함을 시사합니다. 그들 각각은 그들이 이름을 딴 부분에 대해서만 책임이 있습니다. 이 패턴을 따르면 코드가 잘 구조화되고 이해하기 쉬워집니다.

단위 테스트를 작성해 봅시다. 우리는 외부 종속성의 동작이나 상태가 아닌 테스트 중인 코드를 격리하고 집중하기 위해 bookModel의 메서드를 조롱할 것입니다. 그런 다음 우리는 같아야 하고, 여러 번 호출되어야 하고, 일부 매개변수와 함께 호출되어야 하는 경우와 같은 일부 경우에 단위 테스트를 주장할 것입니다.

//bookRepo.test.ts
import 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
                }
            });
        });
    });
});


그런 다음 서비스 계층에 대한 단위 테스트를 작성합니다. 리포지토리 레이어와 동일하게 서비스 레이어 테스트에서 리포지토리 레이어를 모의하여 테스트 중인 코드를 격리하고 집중합니다.

//bookService.test.ts
import BookService from './bookService';
import BookRepo from './bookRepo';

describe('BookService', () => {
    beforeEach(() =>{
        jest.resetAllMocks();
    });

    describe('BookService.__getBookDetail', () => {
        it('should return book detail', async () => {
            //arrange
            const bookID = 1;
            const mockResponse = {
                id: 1,
                title: 'ABC',
                author: 'John Doe',
                page: 1
            };

            BookRepo.getBookDetail = jest.fn().mockResolvedValue(mockResponse);

            //act
            const result = await BookService.getBookDetail(bookID);

            //assert
            expect(result).toEqual(mockResponse);
            expect(BookRepo.getBookDetail).toHaveBeenCalledTimes(1);
            expect(BookRepo.getBookDetail).toBeCalledWith(bookID);
        });
    });

    describe('BookService.__removeBook', () => {
        it('should return true remove book', async () => {
            //arrange
            const bookID = 2;
            const mockBookDetail = {
                id: 2,
                title: 'ABC',
                author: 'John Doe',
                page: 1
            };
            const mockResponse = true;

            BookRepo.getBookDetail = jest.fn().mockResolvedValue(mockBookDetail);
            BookRepo.removeBook = jest.fn().mockResolvedValue(mockResponse);

            //act
            const result = await BookService.removeBook(bookID);

            //assert
            expect(result).toEqual(mockResponse);

            // assert BookRepo.getBookDetail
            expect(BookRepo.getBookDetail).toHaveBeenCalledTimes(1);
            expect(BookRepo.getBookDetail).toBeCalledWith(bookID);

            //assert BookRepo.removeBook
            expect(BookRepo.removeBook).toHaveBeenCalledTimes(1);
            expect(BookRepo.removeBook).toBeCalledWith(bookID);
        });

        it('should throw error book is not found', () => {
            //arrange
            const bookID = 2;
            const mockBookDetail = null;
            const errorMessage = 'Book is not found';

            BookRepo.getBookDetail = jest.fn().mockResolvedValue(mockBookDetail);

            //act
            const result = BookService.removeBook(bookID);

            //assert
            expect(result).rejects.toThrowError(errorMessage);
            expect(BookRepo.getBookDetail).toHaveBeenCalledTimes(1);
            expect(BookRepo.getBookDetail).toBeCalledWith(bookID);
        });
    });
});


좋습니다. 단위 테스트 작성을 완료했습니다.
테스트를 실행하기 전에 다음과 같이 package.json에 스크립트 테스트를 추가합니다.

//package.json
...
"scripts": {
    "build": "tsc",
    "build-watch": "tsc -w",
    "test": "jest --coverage ./src"
},
...


좋습니다. 마침내 터미널에서 다음 명령으로 테스트를 실행할 수 있습니다.

npm test


실행 후, 우리는 단위 테스트가 성공했고 완전히 적용된다는 것을 알려주는 결과를 얻을 것입니다 🎉


아름다운! ✨

연결:


  • Sequelize 확장 모델 - https://sequelize.org/docs/v6/core-concepts/model-basics/#extending-model
  • 리포지토리와 서비스 계층의 차이점 - https://stackoverflow.com/questions/5049363/difference-between-repository-and-service-layer
  • 단위 테스트 및 AAA(Arrange, Act 및 Assert) 패턴 - https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80
  • 좋은 웹페이지 즐겨찾기