nest에서 jest 사용

Featured image

TDD란

TDD란 테스트 주도 개발로 코드를 작성하기 전에 테스트를 먼저 작성하고 그 테스트를 통과할 수록 하는 코드를 구현하는 개발 방법론이다

TDD를 사용하게 되면 코드의 품질이 높아지고 테스트가 이미 작성되어 있으므로 수정시 기능이 손상되지 않는다

또한 테스트를 먼저 작성해 반복적인 테스트와 코드 작성을 통해 소프트웨어의 안전성과 품질을 높일 수 있다

또한 테스트만으로 모든 경우를 커버하기 어렵다

테스트 종류

  1. 유닛 테스트 코드의 개별 모듈이나 함수가 올바르게 동작하는지 검증하는 테스트로 각 함수나 메서드의 단위테스트이다

각 기능의 동작을 세부적으로 확인 할 수 있다

  1. 통합테스트 데이터베이스나 api등 외부 시스템과의 상호작용을 확인하는 테스트로 실제 환경과 가까운 상황에서 동작을 테스트 한다

3.E2E테스트 실제 시나리오를 시뮬레이션해 애플리케이션 전체가 잘 작동하는지 검증하는 테스트로 시스템의 통합성을 확인 할 수 있다

  1. 회귀테스트 코드 수정이나 기능 추가 후 기존의 기능이 제대로 동작하는지 확인하는 테스트

nest에서 jest사용하기

npm install --save-dev jest @types/jest ts-jest

각 테스트 시작전에

import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { Repository } from 'typeorm';
import { TokenBlacklist } from './entities/tokenBlacklist';
import { JwtService } from '@nestjs/jwt';
import * as argon2 from 'argon2';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User, userType } from './entities/user.entities';
import { createUserDTO } from './DTO';

describe('UserService', () => {
  let service: UserService;
  let tokenBlacklistRepository: jest.Mocked<Repository<TokenBlacklist>>;
  let jwtService: JwtService;
  let userRepository: jest.Mocked<Repository<User>>;
  //테스트가 실행되기 전에 실행되는 설정코드
  beforeEach(async () => {
    //createTestingModule를 사용해 해당 의존성을 가져온다
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        //의존성 주입
        UserService, //테스트 대상으로 의존성들이 모킹된 상태로 테스트 할 수 있게 함
        {
          provide: getRepositoryToken(User), //주어진 엔티티에 대한 리포지토리 토큰을 생성
          useValue: { findOne: jest.fn(), create: jest.fn(), save: jest.fn() }, //함수 호출을 감지하고 모킹해 사용한다
        },
        {
          provide: getRepositoryToken(TokenBlacklist),
          useValue: { save: jest.fn() },
        },
        {
          provide: JwtService,
          useValue: {
            signAsync: jest.fn(), //모킹 함수로 대체
            decode: jest.fn(),
          },
        },
      ],
    }).compile(); //테스트 모듈 생성, 컴파일

    service = module.get<UserService>(UserService); //해당의존성 을 가져온다
    userRepository = module.get(getRepositoryToken(User)); //jest.Mocked<Repository을 사용해 모킹한다
    tokenBlacklistRepository = module.get(getRepositoryToken(TokenBlacklist));
    jwtService = module.get<JwtService>(JwtService);
  }); //유저 서비스 인스턴스 가져와서 service에 할당
  //정의 되어 있는지 확인

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
  describe('validateUser', () => {
    //제대로 됐는지 확인
    //개별로 테스트를 정의하는 부분으로 설명과 함께 정의
    it('비밀번호가 맞는지 확인하는 validateUser 함수 test', async () => {
      const email = 'test@test.com';
      const password = 'password';
      //사용할 입력값
      //목업객체는 데이터 베이스에서 조회된 데이터를 모방한 목업 객체
      const mockUser = {
        userId: 1,
        email,
        password: await argon2.hash(password),
      }; //목업객체

      userRepository.findOne.mockResolvedValue(mockUser as User);
      //mockReturnValue함수는 findOne이 mockUser를 반환하게 함
      jest.spyOn(argon2, 'verify').mockResolvedValue(true);
      // jest.spyOn은 특정모듈의 메서드 감시(호출 가로채고 모킹 할 수 있게함)
      //mockResolvedValue함수는 비동기 함수의 반환값을 설정한다(true)
      //그러니까 목업 함수는 항상 user가 되게
      const result = await service.validateUser(email, password);
      expect(result).toEqual(mockUser);
    });

    it('비밀번호가 맞지 않을 경우 validateUser 함수 test', async () => {
      const email = 'test@test.com';
      const password = 'password';
      const user = {
        userId: 1,
        email,
        password: await argon2.hash('wrong'),
      };
      userRepository.findOne.mockResolvedValue(user as User);
      jest.spyOn(argon2, 'verify').mockResolvedValue(false);
      const result = await service.validateUser(email, password);
      expect(result).toBeNull();
    });
  });

  //createUserService
  describe('createUserService', () => {
    it('createUserService가 제대로 작동하는지', async () => {
      const userDto: createUserDTO = {
        email: 'test@gmail.com',
        password: '12345',
        username: 'test',
      };
      const saveUser: User = {
        ...userDto,
        userId: 1,
        password: await argon2.hash('123245'),
        status: userType.MEMBER,
        createdAt: new Date(),
        updatedAt: null,
        deletedAt: null,
        posts: [],
        userFavorites: [],
        comments: [],
        likes: [],
        friends: [],
      };

      userRepository.create.mockReturnValue(saveUser);
      userRepository.save.mockResolvedValue(saveUser);
      const result = await service.createUserService(userDto);
      expect(result).toEqual(saveUser);
    });
  });
});
  1. describe 테스트에서 사용되는 함수로 테스트할 대상을 그룹화해 구성한다.

안에는 여러 테스트(it 메서드)가 들어갈 수 있다

  1. beforeEach 각 테스트가 실행되기 전에 반복적으로 실행되는 설정이 담긴 코드를 작성하는 곳으로 모든 테스트가 실행되기 전에 반복적으로 실행된다

안에 여러 모듈과 jest.fn()을 사용해 함수 호출을 감지하고 모킹한다

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; //테스트할 인스턴스 정의
import * as request from 'supertest'; //http 요청 테스트
import { AppModule } from './../src/app.module';
import { JwtService } from '@nestjs/jwt';

describe('AppController (e2e)', () => {
  let app: INestApplication; //테스트할 애플리케이션 인스탄스를 자징할 변수
  let jwtService: JwtService;
  let token: string;

  beforeAll(async () => { //테스트할 환경 설정
  //테스트할 모듈을 생성한다
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    jwtService = moduleFixture.get<JwtService>(JwtService);
    token = jwtService.sign({
      userId: 1,
      status: 'normal',
    }); //사용할 토큰 생성
    await app.init();
  });

  it('should register a new user', async () => {
    const response = await request(app.getHttpServer())
      .post('/user')
      .send({
        username: 'testuser',
        email: 'test@example.com',
        password: 'testpassword',
      })
      .expect(201); //201 기대
      //응답 본문에서 해당 속성 확인
    expect(response.body).toHaveProperty('userId');
    expect(response.body).toHaveProperty('username', 'testuser');
  });

  it('should log in the registered user', async () => {
    const response = await request(app.getHttpServer())
      .post('/user/login')
      .send({
        email: 'test@example.com',
        password: 'testpassword',
      })
      .expect(200);
    expect(response.body).toHaveProperty('accessToken');
  });

  it('should upload favorite', async () => {
    const response = await request(app.getHttpServer())
      .post('/categories/1/subCategories/1/subSubCategories/1/favorite/1/posts')
      .set('Authorization', `Bearer ${token}`)
      .field('title', 'testPostTitle')
      .field('description', 'test descriptiondescription')
      .expect(201);
    expect(response.body).toHaveProperty('favoriteId');
    expect(response.body).toHaveProperty('title', 'testPostTitle');
    expect(response.body).toHaveProperty(
      'description',
      'test descriptiondescription',
    );
  });

  //모든 테스트가 완료된 후  할 작업 정의
  afterAll(async () => {
    await app.close();
  });
});