TDD 도입기

Shin·2023년 4월 12일
0
post-thumbnail

테스트 코드를 도입하기로 한 이유

1. 기존 서비스 코드의 리팩토링 시 기존 서비스에 영향을 주지 않게 하기 위해

기존 서비스 코드에서 작동하던 로직들을 수정할 일이 있거나, 리팩토링 해야할 일이 있을 때, 해당 코드를 수정했을 때 어디 부분까지 이 코드가 영향을 미치는지, 수정해도 이상이 없는지 여부 등을 체크하기 어렵고, 놓치게 되어 에러가 발생하는 문제가 있었기 때문에, 테스트 코드를 도입하여 수정 시 어디 부분에 영향을 미치고, 어떤 부분에서 에러가 일어나는지 확실하게 체크하여 유지보수 및 신기술 도입 등에 도움을 주기 위해 필요했습니다.

2. 코드의 구조 및 품질 향상을 위해

테스트 코드를 작성하기 위해서는 테스트 코드를 작성하기 쉽게 코드를 작성해야합니다.
테스트 코드를 작성하기 쉬운 코드란 , 한 함수에서는 하나의 로직만 수행해야하며, 많은 파라미터를 받아선 안되고, 제어할 수 없는 코드가 있으면 안된다 등이 있는데 이 것들은 모두 테스트코드 에서만이 아닌 개발 영역에서도 똑같은 이야기입니다.
즉 테스트하기 쉬운 코드라는건 서비스를 유지하기 좋은 품질 높은 코드라는 걸 의미하므로, 테스트 코드를 작성하면서 동시에 서비스를 이루고 있는 코드의 품질 또한 함께 향상시킬 수 있습니다.


JEST를 선택한 이유

중요하게 생각하는 점

  1. 현재 테스트코드를 작성해본 시니어 혹은 사수가 없는 환경이기 때문에 많은 사람들이 사용하고, 레포가 많은 라이브러리 일 것.
  2. 공식문서가 잘 만들어져있어, 다른 곳을 보지 않고 공식문서만 참고하더라도 테스트코드를 작성할 수 있을 것.
  3. Angular와 NestJs 두 곳 모두에서 환경을 구성할 때 문제가 없을 것.

위 3개를 중요 시 하게 보았으며, 그 중 1번을 가장 높은 우선 순위로 두고 고르기로 했습니다.

위 사항들을 토대로 비교군을 3개 [ Jest, Mocha, Jasmine ]를 지정했으며, 그 비교군들 중에 왜 JEST를 선택했는지 작성하겠습니다.

NPM 다운로드 수 비교


Jest > Mocha > Jasmine 순서대로 다운로드 수 가 많다.


결론적으로 JEST를 선택한 이유

  • 장단점

    1. Jest

    장점

    • 테스트 속도: Jest는 빠른 테스트 실행 속도를 제공합니다. 이는 병렬 테스트 실행과 효율적인 파일 변경 감지 덕분입니다.

    • 설정 용이성: Jest는 설정이 간단하며, 설정 파일을 통해 많은 옵션을 쉽게 구성할 수 있습니다.

    • 간편한 mocking: Jest는 기본 제공 mock 함수를 통해 간편하게 모듈, 함수 또는 타이머를 모의할 수 있습니다.

    • 코드 커버리지: Jest는 내장된 코드 커버리지 도구를 제공합니다. 이를 통해 테스트가 얼마나 많은 코드를 커버하는지 쉽게 파악할 수 있습니다.

    • NestJS와 Angular 호환성: Jest는 NestJS 및 Angular 와 잘 호환되며, NestJS에서 기본으로 제공하는 테스트 도구입니다.
      또한 Jasmine 을 기반으로 만들었기 때문에 Angular와의 호환성도 매우 높습니다.

    • 커뮤니티 및 지원: Jest는 큰 커뮤니티와 광범위한 지원 문서를 가지고 있어, 도움이 필요한 경우 쉽게 찾을 수 있습니다.

    • Immersive Watch Mode 를 사용하면 변경사항에 영향을 받는 파일만 테스트가 가능합니다.

      단점

    • Jasmine에 비해 Angular 프로젝트에서의 사용 경험이 상대적으로 적습니다.

      2. Jasmine

      장점

    • Angular와의 호환성: Angular는 기본적으로 Jasmine을 테스트 프레임워크로 사용하므로, Angular 프로젝트에서 사용하기에 아주 적합합니다.

    • 통합 테스트 환경: Jasmine은 단위 테스트와 통합 테스트를 모두 지원하며, 프론트엔드와 백엔드 모두에서 사용할 수 있는 통합 테스트 환경을 제공합니다.

      단점

    • 테스트 속도: Jest에 비해 Jasmine의 테스트 실행 속도가 다소 느릴 수 있습니다. 이는 Jest가 병렬 테스트 실행과 효율적인 파일 변경 감지를 제공하기 때문입니다.

    • 추가 도구 필요: Jasmine 자체에는 목(mock) 생성이나 스파이(spy) 기능이 내장되어 있지만, 추가 도구(예: Sinon)를 사용해야 할 수도 있습니다.

    • 코드 커버리지: Jasmine에는 내장된 코드 커버리지 도구가 없으므로, 별도의 도구(예: Istanbul)를 사용해야 합니다.

      3. Mocha

      장점

    • 유연성: Mocha는 매우 유연한 테스트 프레임워크로, 다양한 라이브러리와 플러그인과 함께 사용할 수 있습니다. 이는 원하는 기능과 도구를 추가하고 구성할 수 있는 큰 장점입니다.

    • 커뮤니티와 지원: Mocha는 오랫동안 존재해온 테스트 프레임워크로 인해 커뮤니티와 지원이 강력합니다. 따라서 문제가 발생했을 때 솔루션을 찾기가 더 쉬울 수 있습니다.

      단점

    • 복잡한 설정: Mocha를 사용하면 다양한 라이브러리와 플러그인을 함께 사용해야 하는데, 이는 설정이 복잡해질 수 있습니다. 예를 들어, Angular와 NestJS에서 Mocha를 사용하려면, 추가로 assertion 라이브러리(예: Chai), mocking 라이브러리(예: Sinon), 코드 커버리지 도구(예: Istanbul) 등을 설치하고 설정해야 합니다.

    • 기본적인 기능 제한: Mocha는 기본적인 기능만 제공하므로 필요한 도구를 직접 찾아서 추가해야 합니다. 이는 시간과 노력이 필요할 수 있습니다.

    • NestJS와 Angular 호환성:: NestJS와 Angular 둘 다 기본 테스트 프레임워크로 Mocha를 사용하지 않고 있기 때문에 Mocha를 사용하려면 추가적인 구성 작업이 필요할 수 있습니다.

위 처럼 각자의 장단점을 찾아 분석해본 결과, 현재 우리 개발 팀의 상황에서는 참고할 레포가 많고, 문제 상황이 생겼을 때 알아볼 수 있는 자료가 많은 JEST가 좋다고 생각하여 결정하게 되었습니다.

  • 위 중요하게 생각하는 점을 모두 충족하는 Jest.
    다운로드수의 경우 가장 최근 날짜 기준 [ 2023.03.31 ] 으로 Jest [21,544,753], Mocha [7,395,784], Jasmine [1,786,472 ] 로 압도적인 수로 가장 많습니다.
  • Angular 와 NestJs 두 환경에서 모두 사용이 가능하며, 호환성도 좋습니다.
  • 공식문서가 가장 보기 편하게 되어 있으며, Angular와 NestJs에 적용한 베스트 사례 블로그 글 또한 공식문서에 올라와 있습니다.

JEST 자주 쓰는 Matcher

Jest를 현 프로젝트에 적용해야하는데 어디부터 시작해야할지 고민이였지만,

가장 자주 보고, 수정하여 익숙한 Menu_master 부터 시작하기로 했습니다.

적용한 과정을 보여드리기 전 JEST에 대해 알고 보면 훨씬 이해가 빠를 것 같아 자주쓰는 matcher 등을 정리해놓았습니다.

toBe

toBe는 값이 일치하는지 확인합니다. 객체의 경우 참조가 같아야 일치로 간주됩니다.

test('2 + 2 는 4', () => {
  expect(2 + 2).toBe(4);
});

toEqual

toEqual은 객체 또는 배열이 동일한 구조와 값을 가지고 있는지 확인합니다.

test('객체 동일성 확인', () => {
  const data = { one: 1, two: 2 };
  expect(data).toEqual({ one: 1, two: 2 });
});

toBeNull, toBeUndefined, toBeDefined, toBeTruthy, toBeFalsy

이 Matcher들은 각각 값이 null, undefined, 정의되어 있는지, 참으로 간주되는지, 거짓으로 간주되는지를 확인합니다.

test('null 확인', () => {
  const value = null;
  expect(value).toBeNull();
});

test('undefined 확인', () => {
  const value = undefined;
  expect(value).toBeUndefined();
});

test('정의 확인', () => {
  const value = 'defined';
  expect(value).toBeDefined();
});

test('참 확인', () => {
  const value = true;
  expect(value).toBeTruthy();
});

test('거짓 확인', () => {
  const value = false;
  expect(value).toBeFalsy();
});

**toBeGreaterThan, toBeGreater, ThanOrEqual, toBeLessThan, toBeLessThanOrEqual**

이 Matcher들은 숫자 값의 대소 관계를 확인합니다.

test('숫자 비교', () => {
  const value = 10;
  expect(value).toBeGreaterThan(5);
  expect(value).toBeGreaterThanOrEqual(10);
  expect(value).toBeLessThan(15);
  expect(value).toBeLessThanOrEqual(10);
});

toContain, toContainEqual

toContain은 배열이 특정 항목을 포함하는지 확인합니다. 객체의 경우, toContainEqual을 사용하여 동일한 구조와 값을 가지는 객체가 포함되어 있는지 확인할 수 있습니다.

test('배열 포함 확인', () => {
  const array = ['apple', 'banana', 'orange'];
  expect(array).toContain('banana');
  expect(array).not.toContain('grape');
});

test('객체 배열 포함 확인', () => {
  const array = [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' },
  ];
  expect(array).toContainEqual({ id: 1, name: 'John' });
  expect(array).not.toContainEqual({ id: 3, name: 'Jake' });
});

toHaveLength

toHaveLength는 문자열 또는 배열의 길이를 확인합니다.

test('길이 확인', () => {
  const string = 'hello';
  const array = [1, 2, 3];

  expect(string).toHaveLength(5);
  expect(array).toHaveLength(3);
});

toHaveProperty

toHaveProperty는 객체가 특정 속성을 가지고 있는지 확인합니다.

test('객체 속성 확인', () => {
  const object = { name: 'John', age: 30 };

  expect(object).toHaveProperty('name');
  expect(object).toHaveProperty('name', 'John');
  expect(object).not.toHaveProperty('address');
});

toBeInstanceOf

테스트에서 객체가 특정 클래스의 인스턴스인지 확인하는 데 사용합니다.

test('Object should be an instance of the specified class', () => {
  class MyClass {
    constructor() {}
  }

  const myObject = new MyClass();
  expect(myObject).toBeInstanceOf(MyClass);
});

jest.fn()

Jest에서 가장 기본적인 mocking 기능을 제공하는 함수입니다.

이 함수는 새로운 mock 함수를 생성하며, 호출 횟수, 전달된 인수 등과 같은 정보를 추적합니다.
이를 사용하여 함수를 대체하거나 테스트 동안 해당 함수의 동작을 제어할 수 있습니다.

const mockFn = jest.fn();
mockFn('Hello');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('Hello');

jest.spyOn()

이 함수는 객체의 메소드에 대한 spy를 생성합니다.
spyOn을 사용하면 원본 함수의 동작은 유지하면서 호출 횟수, 전달된 인수 등과 같은 정보를 추적할 수 있습니다. 선택적으로 원본 함수를 mock 구현으로 대체할 수도 있습니다.

const myObject = {
  sayHello: (name) => `Hello, ${name}!`
};

const spy = jest.spyOn(myObject, 'sayHello');
const result = myObject.sayHello('John');

expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith('John');
expect(result).toBe('Hello, John!');

mockReturnValue(value)

이 함수는 mock 함수가 호출될 때 반환할 값을 설정합니다. 이를 사용하여 테스트 동안 함수의 반환값을 제어할 수 있습니다.

const mockFn = jest.fn().mockReturnValue('Hello, World!');
const result = mockFn();
expect(result).toBe('Hello, World!');

mockResolvedValue(value)

이 함수는 mock 함수가 호출될 때 반환할 Promise를 설정합니다. 반환값이 resolve된 Promise로 감싸집니다. 이를 사용하여 테스트 동안 비동기 함수의 반환값을 제어할 수 있습니다.

const mockFn = jest.fn().mockResolvedValue('Hello, World!');
const result = await mockFn();
expect(result).toBe('Hello, World!');

mockRejectedValue(value)

이 함수는 mock 함수가 호출될 때 반환할 rejected Promise를 설정합니다. 반환값이 reject된 Promise로 감싸집니다. 이를 사용하여 테스트 동안 비동기 함수의 반환값을 제어할 수 있습니다.

const mockFn = jest.fn().mockRejectedValue(new Error('An error occurred'));
try {
  await mockFn();
} catch (error) {
  expect(error).toBeInstanceOf(Error);
  expect(error.message).toBe('An error occurred');
}

mockImplementation(fn)

이 함수는 mock 함수의 구현을 대체합니다. 이를 사용하여 원래 함수와 다른 동작을 하는 함수로 대체할 수 있습니다.

const mockFn = jest.fn().mockImplementation((name) => `Hello, ${name}!`);
const result = mockFn('John');
expect(result).toBe('Hello, John!');

toHaveBeenCalled()

이 Matcher는 함수가 호출되었는지 확인하는 데 사용됩니다.
테스트에서 Mock이나 Spy가 호출되었는지 확인할 수 있습니다.
호출되지 않았다면 테스트는 실패합니다.

const mockFunction = jest.fn();
mockFunction();
expect(mockFunction).toHaveBeenCalled();

toHaveBeenCalledTimes(count)

이 Matcher는 함수가 정확한 횟수만큼 호출되었는지 확인하는 데 사용됩니다.
함수 호출 횟수가 다르면 테스트가 실패합니다.

const mockFunction = jest.fn();
mockFunction();
mockFunction();
expect(mockFunction).toHaveBeenCalledTimes(2);

toHaveBeenCalledWith(arg1, arg2, ...)

이 Matcher는 함수가 주어진 인수로 호출되었는지 확인하는 데 사용됩니다.
인수가 다르면 테스트가 실패합니다.

const mockFunction = jest.fn();
mockFunction(1, 'hello');
expect(mockFunction).toHaveBeenCalledWith(1, 'hello');

toHaveBeenLastCalledWith(arg1, arg2, ...)

이 Matcher는 함수의 마지막 호출이 주어진 인수로 이루어졌는지 확인하는 데 사용됩니다. 마지막 호출에 사용된 인수가 다르면 테스트가 실패합니다.

const mockFunction = jest.fn();
mockFunction(1, 'hello');
mockFunction(2, 'world');
expect(mockFunction).toHaveBeenLastCalledWith(2, 'world');

toHaveBeenNthCalledWith(nthCall, arg1, arg2, ...)

이 Matcher는 함수의 n번째 호출이 주어진 인수로 이루어졌는지 확인하는 데 사용됩니다.
n번째 호출에 사용된 인수가 다르면 테스트가 실패합니다.

const mockFunction = jest.fn();
mockFunction(1, 'hello');
mockFunction(2, 'world');
expect(mockFunction).toHaveBeenNthCalledWith(1, 1, 'hello');

objectContaining

사용자가 지정한 속성들과 일치하는 속성을 가진 객체와 일치하는지 확인합니다.
매칭되는 객체에 다른 속성이 있어도 상관없습니다.

예를 들어, 아래와 같이 objectContaining 매처를 사용하여 객체에 특정 속성이 있는지 확인할 수 있습니다.

const receivedObject = {
  name: 'John',
  age: 25,
  city: 'New York',
};

expect(receivedObject).toEqual(
  expect.objectContaining({
    name: 'John',
    age: 25,
  })
);

resolves

resolves는 Promise가 성공적으로 resolve되었을 때, 예상되는 값을 검증하는 데 사용됩니다.
예를 들어, 다음과 같이 사용할 수 있습니다.

test('async test with resolves', async () => {
  const promise = Promise.resolve('Success');
  await expect(promise).resolves.toBe('Success');
});

rejects

rejects는 Promise가 거부되었을 때, 예상되는 에러를 검증하는 데 사용됩니다.

예를 들어, 다음과 같이 사용할 수 있습니다.

test('async test with rejects', async () => {
  const promise = Promise.reject(new Error('Error'));
  await expect(promise).rejects.toThrow('Error');
});

스파이와 모의(Mock)의 차이

스파이와 모의의 주요 차이점은 대체 여부입니다. 스파이는 기존 함수를 대체하지 않고 호출을 추적하며, 모의는 기존 함수나 모듈을 완전히 대체합니다. 스파이는 기존 구현을 사용하고 호출을 추적하는 반면, 모의는 기존 구현을 사용하지 않고 테스트에서 지정한 동작을 수행합니다.

  • 스파이를 사용하는 경우:
    • 함수가 호출되었는지 확인하고 싶을 때
    • 함수가 특정 인자로 호출되었는지 확인하고 싶을 때
    • 기존 구현을 유지하면서 함수 호출을 추적하고 싶을 때
  • 모의를 사용하는 경우:
    • 기존 구현을 완전히 대체하고 싶을 때
    • 특정 동작이 발생할 때 특정 값을 반환하도록 설정하고 싶을 때
    • 외부 서비스, API 호출, 파일 시스템 작업 등의 부작용을 가진 함수를 테스트하고 싶을 때

testdescribeit는 Jest 테스트 프레임워크에서 사용되는 함수들입니다.

  1. test: 이 함수는 개별 테스트 케이스를 정의하는 데 사용됩니다. test 함수는 첫 번째 인수로 테스트 케이스의 설명을 문자열로 받고, 두 번째 인수로 실행할 테스트 코드를 함수로 받습니다.

예시:

test('숫자 2와 3을 더하면 5가 나온다', () => {
  expect(2 + 3).toBe(5);
});
  1. describe: 이 함수는 관련된 테스트 케이스들을 그룹화하는 데 사용됩니다.
    describe는 첫 번째 인수로 그룹의 설명을 문자열로 받고, 두 번째 인수로 그룹 내부의 테스트 케이스들을 정의하는 함수로 받습니다.
    describe 블록 내부에서는 test 또는 it 함수를 사용하여 테스트 케이스를 정의할 수 있습니다.

예시:

describe('덧셈 테스트', () => {
  test('숫자 2와 3을 더하면 5가 나온다', () => {
    expect(2 + 3).toBe(5);
  });

  test('숫자 4와 6을 더하면 10이 나온다', () => {
    expect(4 + 6).toBe(10);
  });
});
  1. it: 이 함수는 test와 동일한 역할을 합니다. it은 BDD(Behavior-Driven Development) 스타일의 테스트 프레임워크에서 사용되는 함수이며, Jest에서도 지원됩니다.
    it 함수는 테스트 케이스의 설명을 자연스럽게 작성할 수 있도록 돕습니다.

예시:

describe('덧셈 테스트', () => {
  it('should return 5 when adding 2 and 3', () => {
    expect(2 + 3).toBe(5);
  });

  it('should return 10 when adding 4 and 6', () => {
    expect(4 + 6).toBe(10);
  });
});

결론적으로, testit는 개별 테스트 케이스를 정의하는 데 사용되며, 기능적으로 동일합니다.
두 함수 중 선호하는 것을 선택하여 사용할 수 있습니다.
describe는 테스트 케이스를 그룹화하는 데 사용되며, 테스트 코드의 구조를 명확하게 표현할 수 있습니다.


JEST Hook

beforeAll

이 훅은 테스트 스위트의 모든 테스트 케이스가 실행되기 전에 한 번만 호출됩니다.
beforeAll에서는 일반적으로 테스트 전에 필요한 리소스를 설정하거나 초기화하는 작업을 수행합니다.
예를 들어, 데이터베이스 연결을 설정하거나 테스트에 필요한 데이터를 생성할 수 있습니다.

beforeAll(() => {
  // 이 부분은 테스트 스위트가 실행되기 전에 한 번만 실행됩니다.
});

afterAll

이 훅은 테스트 스위트의 모든 테스트 케이스가 완료된 후에 한 번만 호출됩니다.
afterAll에서는 일반적으로 테스트 후에 정리해야 하는 작업을 수행합니다.
예를 들어, 데이터베이스 연결을 종료하거나 생성된 데이터를 삭제할 수 있습니다.


afterAll(() => {
  // 이 부분은 테스트 스위트가 완료된 후에 한 번만 실행됩니다.
});

beforeEach

이 훅은 각 테스트 케이스가 실행되기 전에 호출됩니다.
beforeEach에서는 일반적으로 각 테스트 케이스 전에 필요한 설정 작업을 수행합니다.
예를 들어, 테스트에 필요한 초기 상태를 설정하거나 목(mock) 객체를 생성할 수 있습니다.

beforeEach(() => {
  // 이 부분은 각 테스트 케이스가 실행되기 전에 호출됩니다.
});

afterEach

이 훅은 각 테스트 케이스가 완료된 후에 호출됩니다.
afterEach에서는 일반적으로 각 테스트 케이스 후에 정리해야 하는 작업을 수행합니다.
예를 들어, 테스트에서 변경된 상태를 복원하거나 목 객체를 초기화할 수 있습니다.

afterEach(() => {
  // 이 부분은 각 테스트 케이스가 완료된 후에 호출됩니다.
});

적용 과정 [ Menu_master_service]

1. describe 를 통해 테스트 케이스 그룹화 및 TestingModule 을 통한 테스트 환경 격리

describe('MenuMasterService', () => {
  let service: MenuMasterService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MenuMasterService,
        MenuMasterSchema
      ]
    }).compile();

    service = module.get<MenuMasterService>(MenuMasterService);
    jest.clearAllMocks();
  });

위 코드에서 TestingModule은 아래와 같은 이유로 설정해주어야 합니다.

NestJS 애플리케이션은 여러 모듈과 서비스로 구성되어 있기 때문에 전체 애플리케이션을 실행하면, 모든 모듈과 서비스가 함께 로드되어 실행됩니다.
그러나 테스트를 진행할 때는 전체 애플리케이션을 실행할 필요가 없습니다. 대신 특정 모듈이나 서비스에 집중하여 해당 부분만 테스트하는 것이 바람직합니다.

NestJS에서 제공하는 TestingModule을 사용하면, 전체 애플리케이션 대신 테스트 대상 모듈이나 서비스와 관련된 부분만 로드할 수 있습니다.
즉, 애플리케이션의 일부분만 테스트하기 위한 격리된 환경을 생성하는 것입니다.
이렇게 함으로써 테스트의 실행 속도를 높이고, 다른 모듈이나 서비스로 인한 부작용을 최소화할 수 있습니다.

예를 들어 위와 같이, MenuMasterService를 테스트할 때 전체 애플리케이션을 로드하는 대신 MenuMasterService와 관련된 의존성만 로드하여 서비스의 동작을 테스트할 수 있습니다.
이는 테스트의 목적과 범위를 명확히 하고, 필요한 리소스만 사용하여 효율적으로 테스트를 수행할 수 있도록 합니다.

2. Provider에 의존성 주입하기

  1. MenuMasterService와 관련된 의존성을 providers 배열에 추가합니다. 이를 통해 MenuMasterService가 필요로 하는 의존성을 제공하고 테스트 환경에서 사용할 수 있습니다.
    `테스트에 사용할 서비스인MenuMasterService` 를 주입합니다.
  2. MenuMasterSchema 의존성 주입

3. 테스트 모듈을 컴파일

compile함수를 호출하여 테스트 모듈을 컴파일합니다.

4. 인스턴스 가져오기

get 함수를 사용하여 테스트 모듈에서 MenuMasterService 인스턴스를 가져옵니다. 이 인스턴스는 각 테스트 케이스에서 사용됩니다.

[ beforeEach에서 선언했기 때문 ]

5. Mock 함수 초기화

jest.clearAllMocks 함수를 호출하여 모든 Jest mock 함수의 호출 정보를 초기화합니다.
이렇게 하면 각 테스트 케이스가 독립적으로 실행되어 서로 영향을 주지 않도록 할 수 있습니다.


// src/modules/menu_master/menu_master.service.spec.ts
beforeAll(async () => {
  before_data = await MenuMasterSchema.findOne({ where: { menu_master_idx: 1 }, logging: false });
});

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
		providers: [MenuMasterService, MenuMasterSchema]	
		}).compile();

    service = module.get<MenuMasterService>(MenuMasterService);
    jest.clearAllMocks();
  });

 afterAll(async () => {
	const { menu_name, menu_img_url, menu_origin_price } = before_data;
  await MenuMasterSchema.update(
    { menu_name, menu_img_url, menu_origin_price },
    { where: { menu_master_idx: before_data.menu_master_idx }, logging: false }
  );
  await MenuMasterSchema.destroy({ where: { menu_master_idx: delete_idx }, logging: false });
  jest.restoreAllMocks();
  await sequelize.close();
});

beforeAll

beforeAll은 테스트 파일 내의 모든 테스트 케이스가 실행되기 전에 한 번만 실행되는 함수입니다.
여기서는 테스트를 시작하기 전에 MenuMasterSchema에서 menu_master_idx가 1인 데이터를 찾아 before_data 변수에 저장하고 있습니다.
이 데이터는 테스트가 끝난 후 원래 상태로 복구할 때 사용됩니다.

beforeEach

beforeEach는 각 테스트 케이스가 실행되기 전에 매번 호출되는 함수입니다.
여기서는 테스트 모듈을 생성하고 컴파일하며, MenuMasterServiceMenuMasterSchema를 프로바이더로 등록합니다.
그리고 MenuMasterService 인스턴스를 가져와 service 변수에 저장하고, 모든 Jest 목(mock) 함수의 호출 정보를 초기화합니다.
이렇게 하면 각 테스트 케이스가 독립적으로 실행되도록 보장할 수 있습니다.

afterAll

afterAll은 테스트 파일 내의 모든 테스트 케이스가 실행된 후 한 번만 실행되는 함수입니다.
여기서는beforeAll에서 저장한 before_data를 사용하여 원래의 MenuMasterSchema 데이터를 복구하고, 테스트에서 생성된 데이터를 삭제합니다.
그리고 모든 Jest 목(mock) 함수의 복원을 시도하고, 마지막으로 Sequelize 연결을 닫습니다.
이렇게 하면 테스트 실행 이후 환경을 정리할 수 있습니다.


clearMocks

clearMocks 메서드는 Jest에서 생성한 모든 목(mock) 함수의 호출 정보를 초기화합니다.
즉, 이 메서드를 호출한 이후로 목 함수가 얼마나 호출되었는지, 어떤 인자와 함께 호출되었는지 등의 정보가 초기화됩니다.
이를 통해 각 테스트 케이스가 서로 영향을 주지 않고 독립적으로 실행될 수 있도록 합니다.
일반적으로 beforeEach 훅에서 호출되어, 각 테스트 케이스 시작 전에 목 함수의 상태를 초기화합니다.

restoreAllMocks

restoreAllMocks 메서드는 Jest에서 생성한 모든 목(mock) 함수를 원래의 구현으로 복원합니다.
이 메서드는 테스트 실행 이후에 호출되어, 테스트 도중 변경된 목 함수를 원래 상태로 되돌립니다.
이를 통해 다른 테스트 파일이나 테스트 외부의 코드에서 목 함수를 사용할 때, 테스트로 인해 변경된 상태가 영향을 주지 않도록 합니다.
일반적으로 afterAll 훅에서 호출되어, 모든 테스트 케이스가 실행된 후에 목 함수의 상태를 복원합니다.


실제 적용한 테스트 코드 살펴보기

it('MenuMasterService 가 정상적으로 등록되어있는지 확인', () => {
    expect(service).toBeDefined();
  });

위 코드는 연결 한 menu_master.service가 정상적으로 등록이 되었는지 toBeDefined() 로 확인하는 테스트입니다.
이때 인스턴스가 생성되지 않아 service에 할당되지 않았다면 undefined 이기 때문에 에러가 발생합니다.

describe('create 테스트', () => {
    it('함수 등록 확인', () => {
      expect(service.create).toBeDefined();
    });

    it('원하는 데이터로 create 됐는지 확인', async () => {
      const data: InputMenuMasterSchema = {
        menu_name: 'Create Test Menu Name',
        menu_origin_price: 10000,
        menu_img_url: 'https://~~~'
      };
      const result = await service.create(data);
      expect(result).toBeInstanceOf(MenuMasterSchema);
      delete_idx = result.menu_master_idx;
    });
  });

위 코드는 MenuMasterService에서 create 함수를 테스트 하는 코드입니다.
위에서 아까와 같이 현재 service에 create 라는 함수가 있는지 확인하기 위해 toBeDefined Matcher로 확인합니다.
그 후 함수의 결과 값이 MenuMasterSchema와 일치 하는지 확인 후, 나중에 DB에서 생성한 값을 삭제하기 위해 생성한 idx 값을 저장해둡니다.

describe('search_seller_menu_list_v3 테스트', () => {
    it('함수 등록 확인', () => {
      expect(service.search_seller_menu_list_v3).toBeDefined();
    });
    it('함수 반환 값이 MenuMasterSchema 배열을 반환하는지 확인', async () => {
      const result = await service.search_seller_menu_list_v3({ take: 1, skip: 0 });
      expect(result).toBeInstanceOf(Array);
      if (result.length > 0) {
        expect(result[0]).toBeInstanceOf(MenuMasterSchema);
      }
    });
  });

위 코드는 search_seller_menu_list_v3 함수를 테스트하는 코드입니다.
search_seller_menu_list_v3 함수의 반환 값은 MenuMasterSchema 배열인데 이때, toBeInstanceOf 를 통해 반환 값이 배열의 형태인지 확인하고, mock 데이터로 하는게 아닌 실제 DB 값을 가져오는 것 이기 때문에 데이터가 없는 경우도 있어, 현재 가져온 배열의 길이를 체크 한 후, 값이 있는 경우에 그 값이 MenuMasterSchema 형태인지 확인합니다.

describe('find_include_brand_by_idx 테스트', () => {
    let brand: BrandSchema;
    it('함수 등록 확인', () => {
      expect(service.find_include_brand_by_idx).toBeDefined();
    });

    it('함수 반환 값이 MenuMasterSchema 타입인지 확인', async () => {
      const result = await service.find_include_brand_by_idx(before_data.menu_master_idx);
      brand = result?.brand;
      expect(result).toBeInstanceOf(MenuMasterSchema);
    });

    it('반환 값에 BrandSchema 가 포함되어 있는지 확인', () => {
      expect(brand).toBeInstanceOf(BrandSchema);
    });
  });

위 코드는 menu_master_idx를 통해 MenuMaster테이블에서 원하는 row를 받아오는 함수입니다.
이때, include 를 통해 BrandSchema도 같이 가져오는데, 이를 확인하기 위해 받아온 값에서 brand 값을 한번 더 체크해주도록 하였습니다.


적용 후기

Jest로 MenuMasterService와 MenuMasterController에 테스트코드를 작성하며 Covrage를 100%를 달성하였다.

테스트코드를 작성하면서 기존에 있던 함수들도 리팩토링을 조금 더 테스트하기 쉽게, 서로간의 역활을 명확하게 하고, 한 로직 당 한가지 행위를 수행하게 하기 위해 수정을 진행하며, 기존에는 수정하고, 직접 그걸 graphql로 불러서 확인하는 과정이 있었지만 이젠 수정한걸 편하게 테스트 코드를 돌려서 확인할 수 있다는게 엄청 편하게 느껴졌다.

특히 테스트코드를 작성하고 나서 리팩토링을 진행하니 어떤 부분들이 나뉘어져야하는지, 어떤 함수가 많은 양의 책임을 가지고 있었는지가 눈에 보여 더 쉽게 진행했었던거 같다.

지금은 이미 만들어져있는 코드를 가지고 테스트코드를 작성했지만, 앞으로 진행할 내용에서는 테스트 코드를 먼저 작성한 후, 그 테스트 코드에 맞춰서 로직을 작성하게 될 것이니, 더 유지보수하기 편한 코드를 작성할 수 있을 것 같아 기대가 된다.

profile
누군가의 선택지가 될 수 있는 사람이 되자

0개의 댓글