Jest 기초 (23/04/24)

nazzzo·2023년 4월 25일
0

TDD & Jest


TDD(Test Driven Development)는 테스트 주도 개발이라는 소프트웨어 개발 방법론 중 하나입니다
일반적으로 TDD는 테스트 코드를 먼저 작성하고, 이를 통과하는 코드를 작성하는 과정을 거칩니다
이를 통해 코드가 어떻게 동작해야 하는지에 대한 명확한 이해를 가지게 되며, 이를 바탕으로 개발을 진행할 수 있습니 다

그리고 Jest는 메타(구 페이스북)에서 만든 TDD를 위한 프레임워크입니다
Jest는 자동화된 테스트를 지원하여, 개발자가 코드 변경 사항을 만들 때마다 테스트를 수행할 수 있도록 도와줍니다 이를 통해 개발 과정에서 코드 변경 사항이 예상대로 작동하는지에 대한 신뢰를 가질 수 있게 해줍니다



실행 명령어

npx jest

*jest는 기본적으로 [파일네임].test.[확장자]를 찾아서 실행시키도록 설정되어 있습니다


jest는 원래 브라우저의 자바스크립트를 테스트할 목적으로 만들어졌습니다
그리고 타입스크립트는 컴파일 과정을 거쳐야만 자바스크립트 파일로 바뀝니다
jest가 타입스크립트를 기본적으로 지원하지 않는 관계로, 둘을 함께 사용하려면 몇가지 설정이 필요합니다


1. 타입스크립트 설정

npm init -y
npm install -D typescript tsc-alias ts-node tsconfig-paths nodemon

tsconfig.json

{
    "compilerOptions": {
        "module": "CommonJS",
        "outDir": "./dist",
        "target": "ES6",
        "lib": ["ES6", "dom"],
        "esModuleInterop": true,
        "strict": true,
        "baseUrl": "./src",
        "paths": {},
    },
    "include": ["src/**/*"],
    "exclude": ["**/*.test.js"]
}

nodemon.json

{
    "watch": ["src/**/*"],
    "ext" : "ts",
    "exec": "ts-node -r tsconfig-paths/register ./src/index.ts"
}

package.json

  "scripts": {
    "dev": "nodemon",
    "build" : "tsc && tsc-alias"
    "test": "jest",
  },



2. jest 설정

npm install -D jest @types/jest ts-jest
  • @types/jest는 jest에서 사용하는 모든 타입을 제공하는 라이브러리입니다
    에디터가 jest 명령어를 읽을 수 있도록 해줍니다
    (tsconfig.json에서 테스트용 디렉토리를 include안에 포함시켜야 합니다)
  • ts-jest는 타입스크립트로 jest를 실행할 수 있도록 해줍니다

방법1) jest 실행

npx jest --preset ts-jest

방법2) package.json 파일을 수정합니다

  "scripts": {
    "dev": "nodemon",
    "build": "tsc && tsc-alias",
    "test": "npx jest"
  },
  "jest": {
    "preset" : "ts-jest",
    "testEnvironment": "node"
  },

( --testEnvironment node는 테스트 환경을 노드로)

jest 실행

npm run test
// or
npx jest

방법3) jest.config.json 파일을 생성해서 따로 관리할 수도 있습니다

{
    "preset" : "ts-jest",
    "testEnvironment": "node"
}



2-2. jest 구문


다음은 자주 사용하는 jest 구문에 대한 설명과 예제 코드입니다


describe()

describe()는 테스트 그룹을 묶는 역할을 합니다
it() or test() : it() 함수나 test() 함수는 하나의 테스트 케이스를 나타냅니다
첫 번째 인자는 테스트 케이스의 이름을 나타내고, 두 번째 인자는 실제 테스트를 수행할 콜백 함수입니다

expect()

expect() 함수는 테스트 결과를 검증하는 역할을 합니다
인자에 it 함수의 결과값을 할당하여 검증합니다
(expect(add(1, 2)).toBe(3))

matcher

matcherexpect() 함수 결과값에 사용할 수 있는 메서드입니다
Jest에서는 toBe()toEqual() 등 다양한 matcher 메서드를 제공합니다

beforeEach()

각각의 it() 함수가 실행될 때마다 항상 먼저 실행될 콜백함수입니다
(beforeEach() => it() => beforeEach() => it() ...)
beforeAll() : 모든 it() 함수가 실행되기 전에 단 한 번만 먼저 실행될 콜백함수입니다

mok()

mock() 함수는 실제로 동작하는 함수를 임시로 대체하는 역할을 합니다
예를 들어, 데이터베이스 연결을 테스트하려면 데이터베이스에 접속할 필요 없이
mock() 함수를 사용하여 가짜 데이터베이스를 만들어서 테스트를 수행할 수 있습니다


// user.controller

describe('user controller 검증', () => {
    beforeEach(() => { })

    it("create() 검증", () => {
        // req.body가 잘 들어오는지, service 메서드가 잘 작동하는지, res.send 혹은 res.json 응답객체가 잘 담겼는지
        const a = 1 + 1
        expect(1).toBe(a) // failed
    })

    it("create() 예외처리 검증", () => {
        // req.body를 강제로 다른 값으로 바꿔서 에러 발생시키기
        // catch문이 잘 발동되는지, next 함수가 잘 작동하는지...
        const a = 1 + 1
        expect(2).toBe(a) // passed
    })
})
  • it 메서드는 test()로 바꿔쓸 수도 있습니다



beforeEach() 예제

class UserController {
    public num: number = 0
    constructor() {}
}

describe('user controller 검증', () => {
    let user: UserController
    afterAll(() => { })
    afterEach(() => { })
    beforeAll(() => { })
    beforeEach(() => {
        // beforeEach에서 인스턴스를 생성하면 코드 중복을 막을 수 있습니다
        user = new UserController()
    })

    it("create() 검증", () => {
        user.num = 10
        expect(0).toBe(user.num) // failed
    })

    it("create() 예외처리 검증", () => {
        expect(0).toBe(user.num) // passed
    })
})



beforeAll() 예제

class UserController {
    public num: number = 0
    constructor() {}
}
describe('user controller 검증', () => {
    let user: UserController
    let result: { name: string} = {name: ""}
    afterAll(() => { })
    afterEach(() => { })
    beforeAll(() => { 
        result = { name: "hello world" }
    })
    beforeEach(() => {
        user = new UserController()
    })

    it("create() 검증", () => {
        // req.body가 잘 들어오는지, service 메서드가 잘 작동하는지, res.send 혹은 res.json 응답객체가 잘 담겼는지
        user.num = 10
        expect(10).toBe(user.num)
    })

    it("create() 예외처리 검증", () => {
        // req.body를 강제로 다른 값으로 바꿔서 에러 발생시키기
        // catch문이 잘 발동되는지, next 함수가 잘 작동하는지...
        expect(0).toBe(user.num)
    })
})



인터페이스만 잘 지킨다면 테스트 코드는 문제없이 잘 돌아갑니다
그런데 테스트코드의 역할은 까지 확인하는 것인데... DB를 연동할 수 없는데 값에 대한 확인은 어떻게 해야 할까요?

이것을 해결해주는 것이 위에서 설명한 Mock()함수의 역할입니다

아래 예제에서는 '인자값'(요청)이 잘 들어가는지, '리턴값'(응답)을 잘 뱉어내는지를 확인할 수 있어야 합니다
(다른 의존성 코드는 제쳐두고 서비스 코드의 로직이 잘 돌아가는지만 확인하면 됩니다)

Mock() 예제

interface BoardModel {
    email?: string
    username?: string
    subject?: string
    content?: string
    hashtag?: string
    category?: string
    images?: string
    thumbnail?: string

}


interface BoardRepository {
    getUserById: (email: string) => Promise<BoardModel>
}

// DTO
interface BoardWriteDTO {
    email: string
    subject: string
    content: string
    hashtag: string
    category: string
    images: string
    thumbnail: string
    tel1?: string
    tel2?: string
    tel3?: string
}

class BoardService {
    constructor(private readonly boardRepository: BoardRepository) { }

    // 리턴값은 BoardModel
    public async postWrite(data: BoardWriteDTO): Promise<BoardModel> {
        const { email, tel1, tel2, tel3 } = data
        const tel = `${tel1}-${tel2}-${tel3}`
        const { username } = await this.boardRepository.getUserById(email)
        return { username }
    }
}

class BoardController {
    constructor(private readonly boardService: BoardService) { }
    public write() {
        const data: BoardWriteDTO = {
            email: "",
            subject: "",
            content: "",
            hashtag: "",
            category: "",
            images: "",
            thumbnail: "",
        }
        this.boardService.postWrite(data)
    }
 }



 describe('Board Service', () => {
    let boardService: BoardService
    let boardRepository: BoardRepository
    // 인스턴스 생성을 위해
    beforeEach(() => {
        // const boardRepository: BoardRepository = {
        //     getUserById: (email: string) => {
        //         return {} as BoardModel
        //     }
        // }

        // Mock() ~ 실제 레포지토리 대신 실행될 테스트용 함수
        boardRepository = {
            // 함수가 호출되면 반환될 결과값 (web7722 in 프로미스 객체)
            getUserById: jest.fn().mockResolvedValue("web7722"),
        }
        boardService = new BoardService(boardRepository)
    })
    it('postWrite test', async () => {
        const dto: BoardWriteDTO = {
            email: "testAddress",
            subject: "a",
            content: "a",
            hashtag: "a",
            category: "a",
            images: "a",
            thumbnail: "a",
            tel1: "010",
            tel2: "1234",
            tel3: "5678"
        }
        const { username } = await boardService.postWrite(dto)

        // 실행결과 getUserById가 실행이 되는지를 검증
        expect(boardRepository.getUserById).toBeCalled() // passed

        // 인자값이 잘 들어가고 있는지를 검증
        expect(boardRepository.getUserById).toBeCalledWith(`${dto.tel1}-${dto.tel2}-${dto.tel3}`)
    })
})

0개의 댓글