TDD/Jest

장여진·2022년 5월 1일
0
post-thumbnail

TDD란?

  • Test Driven Development로 테스트 주도 개발을 의미
  • 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현
  • 기능을 오픈하게 되면 테스트 코드는 반드시 필요!
    => 새로운 기능 추가 시 테스트를 위해

  • 코드 작성 시 작은 단위를 test하는 Unit Testing을 진행하며 완성된 로직은 Integration Testing을 진행

💡테스트코드의 중요성
코드의 변화가 있을 때마다, 플레이그라운드를 통해 직접 모든 기능을 체크해보는 것은 효율적이지 않고 빈틈이 있을 수 있으므로 테스트코드가 필요❗

테스트의 종류

1) unit test

  • 버튼 클릭처럼 개별 기능 단위테스트
    2) integration test
  • 여러 기능 한꺼번에 통합테스트(유닛들을 모아서 함께 테스트하는 것)
    3) e2e(end-to-end) test => 가장 중요✨
  • 접속해서 로그인하고 구매하는 등 시나리오가 있는 E2E테스트

Jest란?

  • 테스트 코드 작성을 위한 프레임워크
  • NestJS에서는 기본적으로 지원
  • Jest파일의 기본 구조

    beforeEach: Testing 이전에 실행되는 부분
    describe: 여러개의 테스트를 모아놓은 그룹단위
    it(test): 하나의 테스트 단위 (it들은 모두 독립적)

[하나의 테스트 예시]

// it(테스트 이름 , 실행 함수)
it('더하기 테스트', () => {
  const a = 1;

  const b = 2;

  expect(a + b).toBe(3); // expext()의 내용이 3이 될 것이라고 기대한다!!!
});

[여러개 묶음으로 테스트하는 예시]

// describe(테스트 이름, 실행할 함수들)
describe('나의 테스트 그룹', () => {
  it('더하기 테스트', () => {
    const a = 1;

    const b = 2;

    expect(a + b).toBe(3); //()의 내용이 3이 될 것이라고 기대한다!!!
  });

  it('곱하기 테스트', () => {
    const a = 1;

    const b = 2;

    expect(a * b).toBe(2); //()의 내용이 2이 될 것이라고 기대한다!!!
  });

💡 테스트 코드가 많으면 많을 수록 디테일하게 검증할 수 있지만 얼마나 자세하게 검증할지는 고려 필요(처음부터 너무 완벽하게 설계X, 점차적으로 추가하는 것이 좋음)

Mocking

  • Jest를 사용하면 별도의 라이브러리 설치 없이 바로 mock 기능 지원!
  • 단위 테스트 시 해당 코드가 의존하는 부분을 가짜(mock)로 대체하는 기법
    ex) 테스트 코드가 실제 데이터베이스에 접근하는 경우
  • Mocking을 이용하면 구체적으로 구현해야 하는 실제 객체 사용보다 훨씬 빠르고, 동일한 결과를 내는 테스트를 작성 가능
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  // 전역변수로  사용하기 위해 선언
  let appController: AppController; // 타입같이 선언
  let appService: AppService;
  // 사전 작업할 내용
  beforeEach(() => {
    // 직접 의존성 주입(AppService,AppController 연결)
    appService = new AppService();
    appController = new AppController(appService); // constructor주입 필요
  });

  // describe("api이름",() => {})
  describe('getHello', () => {
    it('이 테스트의 검증 결과는 Hello World를 리턴해야함!!', () => {
      const result = appController.getHello();

      expect(result).toBe('Hello World!'); // result가 Hello World니?
    });
  });
});

📌beforeEach에 app.module.ts와 같이 appModule이라는 TestingModule이용하기

  • yarn add @nestjs/testing 설치
    => 테스트에 사용되는 종속성만 선언해서 모듈을 만들고 해당 모듈로 Service,Repository를 가져올 수 있음!
import { Test } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppModule } from './app.module';
import { AppService } from './app.service';

describe('AppController', () => {
  // 전역변수로  사용하기 위해 선언
  let appController: AppController; // 타입같이 선언
  //let appService: AppService;
  // 사전 작업할 내용
  beforeEach(async () => {
    // Module을 통해 의존성 주입
    // nestjs testing라이브러리
    const appModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    // Module을 통해 AppController 가져오기
    appModule.get<AppController>(AppController);

    // AppService는 자동으로 주입
    //appService = new AppService();
    appController = appModule.get<AppController>(AppController);
  });

  // describe("api이름",() => {})
  describe('getHello', () => {
    it('이 테스트의 검증 결과는 Hello World를 리턴해야함!!', () => {
      const result = appController.getHello();

      expect(result).toBe('Hello World!'); // result가 Hello World니?
    });
  });
});

📌Unit Test시 실제 코드가 실행되는 환경과 같은 환경 조성 필요

  • NestJS는 Dependency Injection을 통해 각 Module을 캡슐화하여 서로의 의존성을 최대한 배제하고 주입하여 사용!
    => 따라서 NestJS의 Test 환경을 조성할 경우 의존성 주입을 하지 않고 의존성 자체를 Mocking
  • AppController를 Test했을때 실제로 AppController에 영향을 주는 AppService와 같은 Provider들의 의존성을 신경쓰지 않고, 실제 Test에 사용될 Mocking AppService를 사용하여 독립된 환경의 Controller를 테스트
import { Test } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppModule } from './app.module';
import { AppService } from './app.service';

// 가짜 AppService생성
class MockAppService {
  // 함수명은 동일해야함
  getHello() {
    // 가짜 DB로 접속하도록 코드 변경
    return 'Hello World!';
  }
}

describe('AppController', () => {
  // 전역변수로  사용하기 위해 선언
  let appController: AppController; // 타입같이 선언
  //let appService: AppService;
  // 사전 작업할 내용
  beforeEach(async () => {
    // Module을 통해 의존성 주입
    // nestjs testing라이브러리
    const appModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [
        {
          provide: AppService, // 원본
          useClass: MockAppService, // 나만의 AppService 주입하기 ( dependency injection의 장점 - 실제 DB에 접근하는 것을 막을 수 있음 )
        },
      ], // 원본으로 주입하면 실제 DB에 접근하기 때문에 가짜 AppService 생성해서 주입필요 = Mock
    }).compile();

    // Module을 통해 AppController 가져오기
    appModule.get<AppController>(AppController);

    // AppService는 자동으로 주입
    //appService = new AppService();
    appController = appModule.get<AppController>(AppController);
  });

  // describe("api이름",() => {})
  describe('getHello', () => {
    it('이 테스트의 검증 결과는 Hello World를 리턴해야함!!', () => {
      const result = appController.getHello();

      expect(result).toBe('Hello World!'); // result가 Hello World니?
    });
  });
});

Jest Service

🔎 Testing시 실제 Repository를 사용하여 동일한 환경을 만들어준다면 실제 DB에 데이터가 들어가게 되어 심각한 오류 발생
그렇다면 Service test시 DB에 접근하는 경우는 어떻게 해야할까?🤔
1. 실제 DB에 등록해도될까? => 사용XXXX
2. 가짜 DB를 만들어 놓고 거기다 등록?
=> test시간이 오래걸림 / 접속문제(네트워크 문제로 접속이 안될 수 있음)
3. 가짜 DB를 javascript로 만들고, 거기다 push해도 될까? => 가장 많이 사용❗❗❗

[예시]

  • TestingModule을 생성할 때 UserRepository를 mocking
  • UserRepository가 사용할 save method를 mocking
  • UserRepository가 사용할 findOne method를 mocking
    => findOne의 반환값을 상위에 mocking한 데이터로 지정
import { ConflictException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { UserService } from '../user.service';

class MockUserRepository {
  mydb = [
    { email: 'a@a.com', password: '0000', name: '짱구', age: 8 }, //
  ];
  findOne({ email }) {
    const users = this.mydb.filter((el) => el.email === email);
    if (users.length) return users[0];
    return null;
  }

  save({ email, password, name, age }) {
    this.mydb.push({ email, password, name, age });
    return { email, password, name, age };
  }
}
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>; // repository 타입
// keyof
// const IProfile = {
//   name: '철수',
//   age: 13,
// };
// const qqq: keyof IProfile; === const qqq: 'name' | 'age';  key만 뽑아줌

describe('UserService', () => {
  let userService: UserService;
  let userRepository: MockRepository<User>;
  beforeEach(async () => {
    const userModule = await Test.createTestingModule({
      providers: [
        UserService,
        //repository 등록
        {
          provide: getRepositoryToken(User),
          useClass: MockUserRepository,
        },
      ],
    }).compile();

    userService = userModule.get<UserService>(UserService);
    userRepository = userModule.get<MockRepository<User>>(
      getRepositoryToken(User),
    );
  });

  describe('create', () => {
    it('이미 존재하는 이메일 검증', async () => {
      const userRepositorySpyFindOne = jest.spyOn(userRepository, 'findOne'); //  몇번 시행되었는지 체크
      const userRepositorySpySave = jest.spyOn(userRepository, 'save');
      //userRepository만 mockRepository로 바뀌어서 진행
      const myData = {
        email: 'a@a.com',
        hashedPassword: '1234',
        name: '철수',
        age: 13,
      };
      try {
        await userService.create({ ...myData });
        console.log('AAAAA');
      } catch (error) {
        console.log('BBBBB');
        expect(error).toBeInstanceOf(ConflictException);
      }
      expect(userRepositorySpyFindOne).toBeCalledTimes(1);
      expect(userRepositorySpySave).toBeCalledTimes(0);
    });

    it('회원 등록 잘됐는지 검증!', async () => {
      const userRepositorySpyFindOne = jest.spyOn(userRepository, 'findOne'); //  몇번 시행되었는지 체크
      const userRepositorySpySave = jest.spyOn(userRepository, 'save');

      const myData = {
        email: 'bbb@bbb.com',
        hashedPassword: '1234',
        name: '철수',
        age: 13,
      };

      const myResultData = {
        email: 'bbb@bbb.com',
        password: '1234',
        name: '철수',
        age: 13,
      };

      const result = await userService.create({ ...myData });
      expect(result).toStrictEqual(myResultData); // 객체, 배열 비교 시 엄격하게 비교 필요

      expect(userRepositorySpyFindOne).toBeCalledTimes(1);
      expect(userRepositorySpySave).toBeCalledTimes(1);
    });
  });

  //describe('findOne', () => {});
});

공부하며 작성하고 있는 블로그입니다.
잘못된 내용이 있을 수 있으며 혹시 있다면 댓글 달아주시면 감사하겠습니다 😊

0개의 댓글