목차

toBe

방식 : 엄격한 동일성 검사(strict equality)

  • Object.is를 사용하여 검사
    • +0-0을 구분
    • NaN === NaN 은 false 지만 Object.is(NaN, NaN) 은 true

용도

  • 원시 값(primitive value) 비교 - 숫자, 문자열, null, undefined 등
  • 동일한 객체 참조 확인 - 객체나 배열이 같은 메모리 주소를 가리키는지 확인
  • 배열의 요소들이 순서와 내용까지 동일한지 확인

특징 : 값이 같더라도 서로 다른 객체나 배열인 경우 실패

 

예시

 expect(10).toBe(10); // 통과
 expect('hello').toBe('hello'); // 통과

 const obj1 = { a: 1 };
 const obj2 = { a: 1 };
 expect(obj1).toBe(obj2); // 실패 (서로 다른 참조)
 
const obj3 = obj1;
expect(obj3).toBe(obj1) // 통과 (서로 같은 참조)

expect([1,2,3].toBe([1,3,2]) // 실패
expect([1,2,3].toBe([1,2,3]) // 성공

 

toEqual

방식: 깊은 비교 (Deep Equality)

  • 객체나 배열의 구조와 내용을 비교
  • 부모 클래스에서 상속된 속성은 무시

용도

  • 객체나 배열의 구조와 내용을 비교할 때
  • 부모 클래스에서 상속된 속성은 무시해도 괜찮을 때.

특징

  • 참조를 무시하고 값만 비교
  • 객체 내부에 중첩된 값도 비교

 

const obj1 = { a: 1 };
const obj2 = { a: 1 };
expect(obj1).toEqual(obj2); // 통과 (내용이 같음)

const array1 = [1, 2, 3];
const array2 = [1, 2, 3];
expect(array1).toEqual(array2); // 통과 (배열의 내용이 같음)

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
expect(obj1).toEqual(obj2); // 통과 (내용이 동일)

toStrictEqual

방식 : 객체의 구조(프로퍼티와 값) 그리고 타입(클래스 여부) 가 동일한지 비교 검사, 객체가 직접 소유한 프로퍼티만 비교

용도 

  • 더 엄격한 객체 검증이 필요할 때
    • 객체의 키와 값이 같고, 구조가 완전히 동일한지 확인
    • 명시되지 않은 속성(undefined)까지 검사해야 할때
    • 단순히 {} 생성된 객체인지 클래스 인스턴스인지 비교가 필요한 경우
    • 배열의 요소들이 순서와 내용까지 동일한지 확인

특징

  • 객체의 추가적인 프로퍼티나 순서의 차이도 확인
  • 상속받은 프로퍼티도 비교 대상에서 제외
  • 상속받은 프로퍼티를 포함하려면 객체를 복사하거나 평탄화(flatten)필요

 

예시

const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
expect(obj1).toStrictEqual(obj2); // 통과 (내용 동일)

const obj3 = { a: 1, b: 2, c: 3 };
expect(obj1).toStrictEqual(obj3); // 실패 (추가된 프로퍼티 c)

const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
const arr3 = [1, 3, 2];
expect(arr1).toStrictEqual(arr2); // 통과 (배열 내용 동일)
expect(arr1).toStrictEqual(arr3) // 실패


/* 상속 프로퍼티 */
// 부모 클래스 정의
class Parent {
  constructor() {
    this.parentProp = 'parent';
  }
}

// 자식 클래스 정의
class Child extends Parent {
  constructor() {
    super();
    this.childProp = 'child';
  }
}

const obj1 = new Child();
const obj2 = { childProp: 'child', parentProp: 'parent' };

// `toStrictEqual`은 obj1의 상속 프로퍼티(parentProp)를 비교하지 않음
expect(obj1).toStrictEqual(obj2); // 실패

// obj1의 모든 프로퍼티를 명시적으로 포함한 객체와 비교
const obj1Flattened = Object.assign({}, obj1);
expect(obj1Flattened).toStrictEqual(obj2); // 통과

 

* 평탄화 추가 설명

원래 obj1 객체 (상속 포함):

Child {
  childProp: 'child',
  __proto__: Parent {
    parentProp: 'parent',
    __proto__: Object
  }
}

 

Object.assign({}, obj1)로 평탄화한 객체

{
  childProp: 'child',
  parentProp: 'parent'
}

 

 

차이점 요약

  toBe toEqual toStrictEqual
비교 방식 Object.is를 사용하여 참조와 값을 비교 구조와 내용의 깊은 비교
- toEqual의 기능 + 더 엄격한 검증
- 객체의 타입(인스턴스 여부)까지 검증
- 스파스 요소까지 포함
원시 값 비교 값이 같으면 true 값이 같으면 true 값이 같으면 true
객체 비교 참조가 같아야 true 객체의 내용이 같으면 true 객체의 내용 + 타입이 같아야 true
배열 비교 참조가 같아야 true 배열의 내용과 순서가 같으면 true 배열의 내용, 순서, 스파스 요소가 같아야 true
클래스 인스턴스 비교

동일 참조일 때만 true 구조가 같으면 true 구조와 클래스 타입이 같아야 true

스파스 요소 비교하지 않음 스파스 요소와 undefined를 동일하게 간주 스파스 요소와 undefined를 다르게 간주
사용 사례 - 원시 값 비교
- 객체 참조 확인
- 동일한 메모리 위치인지 확인
- 객체의 구조나 내용을 검증할 때
- 배열, 객체의 내용 확인
- 객체의 구조와 타입까지 검증해야 할 때
- 배열의 스파스 요소 확인

목차

Test Fixture 란

소프트웨어 테스트에서 테스트 환경을 설정하고 관리하기 위한 코드 집합을 의미합니다. 테스트가 정확하고 일관성 있게 실행될 수 있도록 준비하는 과정입니다. 이는 테스트를 위해 필요한 모든 상태나 환경을 포함합니다. 예를 들어, 데이터베이스를 설정하거나, 필요한 객체를 생성하거나, 특정 상태로 초기화하는 작업이 포함됩니다.

Test Fixture를 구성하는 기본 원칙

  • 독립적인 테스트 환경 구성: 이전 테스트의 실행이 이후 테스트에 영향을 주지 않도록 하기 위해, beforeEach 또는 afterEach를 사용해 환경을 초기화하고 정리
  • Setup과 Teardown 구분 
    • SetUp : beforeAll, beforeEach 메서드를 사용하여 필요한 초기 설정
    • Teardown : afterAll, afterEach를 사용해 자원을 정리
  • 재사용 가능한 코드 작성: 공통된 설정이나 데이터 준비 작업을 별도의 헬퍼 함수나 클래스로 추출하여, 필요할 때 재사용할 수 있게 작성

Test Fixture를 사용하는 이유

  1. 테스트가 항상 동일한 조건에서 실행되며, 테스트의 독립성과 신뢰성을 보장
  2. 사용하면 코드가 간결해지고, 유지보수성이 높아지며, 자동화된 테스트 실행이 용이

예제 상황

  1. 데이터베이스 테스트: 데이터베이스를 사용하는 애플리케이션의 경우, 각 테스트 전에 데이터베이스를 특정 상태로 초기화하고, 테스트 후에 정리하는 작업을 Test Fixture로 구성
  2. HTTP 요청 테스트: API 테스트에서는 각 요청에 대한 환경 설정(예: 모의 데이터 또는 인증 토큰 설정)을 Test Fixture로 구성

NestJS에서의 Test Fixture

NestJS에서 테스트를 작성할 때, beforeEach, afterEach, beforeAll, afterAll을 사용하여 테스트 픽스처를 구성할 수 있습니다. 주로 TestingModule을 사용하여 애플리케이션 모듈의 인스턴스를 생성하고, 의존성을 주입하는 작업이 포함

 

구성 요소 설명

  • TestingModule: NestJS의 테스트 모듈을 생성하여 의존성을 주입하고, 각 컴포넌트가 올바르게 동작하는지 확인합니다.
  • beforeAll: 테스트 스위트 내에서 한 번만 실행되는 초기화 코드입니다. 여기서 TestingModule을 컴파일하고 컨트롤러와 서비스를 인스턴스화합니다.
  • beforeEach: 각 테스트가 실행되기 전에 실행되어, 테스트 상태를 초기화하거나 Jest의 mock을 재설정합니다.
  • afterAll: 테스트가 모두 종료된 후 한 번 실행되어, 테스트 환경을 정리하거나 자원을 해제합니다.

NestJS와 Jest를 사용하여 테스트 픽스처를 구성하는 방법을 보여주는 예제

import { Test, TestingModule } from '@nestjs/testing';
import { MyService } from './my.service';
import { MyController } from './my.controller';

describe('MyController', () => {
  let controller: MyController;
  let service: MyService;

  beforeAll(async () => {
    // 모든 테스트 전에 실행되는 setup 작업 (한 번만 실행)
    const module: TestingModule = await Test.createTestingModule({
      controllers: [MyController],
      providers: [MyService],
    }).compile();

    controller = module.get<MyController>(MyController);
    service = module.get<MyService>(MyService);
  });

  beforeEach(() => {
    // 각 테스트 전에 필요한 초기화 작업 수행
    jest.clearAllMocks(); // Jest의 모든 mock 초기화
  });
  
  afterEach(() => {
    // 각 테스트 후 자원 해제나 초기화 작업
  });
  
  // 테스트
  it('should return expected data', async () => {
    const result = 'expected result';
    jest.spyOn(service, 'getData').mockImplementation(() => result);

    expect(await controller.getData()).toBe(result);
  });
  

  afterAll(() => {
    // 모든 테스트 후에 실행되는 teardown 작업 (한 번만 실행)
    // 테스트 모듈 정리 또는 자원 해제
  });
});

 

 

반복되는 코드를 모두 Fixture를 만들어서 해야 할까?

 

경우에 따라 다르겠지만, 테스트 상황에서 기본적으로 몰라도 되는 확인해보지 않아도 되는 코드는  Fixture를 생성해서 구현하는 것이 좋아 보인다. 예를 들어, 데이터 베이스 초기화같은 것이 있겠다.

 

하지만, 유저 생성, 토큰생성 등을 beforeAll 이나 beforeEach 에 구현해 버리면 각 테스트 마다 확인하기 위해 코드의 위 아래를 오가야하므로 오히려 비효율적이라고 생각한다.

 

차라리, 해당 기능들을 함수나 클래스를 만들어서 각 테스트에서 호출하여 사용하는 방법이 해당 테스트를 더 빨리 이해할 수 있을 것 같다.

  

 

'TDD > 개념' 카테고리의 다른 글

[ TDD ] 런던파 vs 고전파  (0) 2024.12.02

순서

1. 데이터를 저장할 때 에러가 나는 부분을 파악

2. 에러에 해당되는 부분에 대한 단위 테스트 작성

3. 테스트에 대응하는 실제 코드 작성

 

(테스트 상황) 비동기 요청에 대한 에러발생

비동기 요청에 대한 결과값

  • 성공 : Promise.resolve(value)
  • 에러 : Promise.reject(reason) 

에러 상황 구현

1. 데이터베이스에서 처리하는 부분은 문제가 없다.

2. 비동기 요청시 에러 발생

// express + jest
const productController = require("../../controller/products");
const productModel = require("../../models/Product");
const httpMocks = require('node-mocks-http');
const newProduct = require('../data/new-product.json');

productModel.create = jest.fn();

let req, res, next;
beforeEach(() =>{
    req = httpMocks.createRequest();
    res = httpMocks.createResponse();
    next = jest.fn(); // 미들웨어 함수를 목 함수로 대체
})

describe('Products Controller', ()=>{
    beforeEach(() =>{
        req.body = newProduct;
    });
    
    it('Should handle errors', async () => {
        const errorMessage = {message: 'Description property missing'};
        const rejectedPromise = Promise.reject(errorMessage);
        productModel.create.mockReturnValue(rejectedPromise) // 에러상황을 생성
        await productController.createProduct(req, res, next);
        expect(next).toBeCalledWith(errorMessage);
     });
});

 

- next 로 에러를 처리하는 이유 : express에서 비동기 요청에서 발생하는 에러를 받으면 서버가 망가진다. 이 에러를 next를 이용하면 처리가 가능하다.

 

테스트에 대응하는 실제 코드 작성

const productModel = require('../models/Product');

exports.createProduct = async (req, res, next) => {
    try{
        const createdProduct = await productModel.create(req.body);
        res.status(201).json(createdProduct);
    }catch (error) {
        next(error); // next 에 에러값을 넣어주면 비동기 요청에 대한 에러를 잘처리 할 수 있는 곳으로 보내준다.
    }
};

jest 를 적용하기 위해서 필요한 설정

1. Jest 설치 확인

npm install jest --save-dev

 

2. pacakge.json 에 있는 테스트 스크립트 변경/추가

 "scripts": {
    "test": "jest" 
  },

 

3. 기본적인 설정 파일을 생성 : jest.config.js 생성

$ npx jest --init

   - Would you like to use Typescript for the configuration file?   → No 선택

목차

 

테스트 우선하기

많은 개발자들은 빨리 구현해야 한다는 압박에 코드를 충분히 테스트하지 않고 기능을 구현한다.

 

압박 → 스트레스 테스트 안함 

 

시간이 흐르면

  • 코드가 복잡해짐( 가독성 저하)
  • 소프트웨어 품질 저하
  • QA 를 거치면서 버그 발생 
  • 버그 수정으로인한 버그 발생

TDD 를 적용!!

 

이점

  • 회귀테스트로 사용 할 수 있다.
    • 코드를 수정하거나 추가할 때 앞서 작성한 테스트 코드를 사용하여 다른 기능에 문제가 없는비 바로 확인
    • 변경한 코드로 인해 소프트웨어가 비정상적으로 동작하는 것을 사전에 막아줌
  • 버그 수정도 쉬워진다.
    • 버그가 발생하는 상황에 대한 테스트 코드를 추가하고 이를 통과시키면된다.
    • 버그를 고치는 과정에서 발생할 수 있는 새로운 버그를 놓치지 않을까 걱정할 필요가 없어진다.

TDD 전파하기

TDD 가 익숙해지면 오는 효과

  • 결함 감소
  • 스트레스 감소
  • 빠른 피드백

먼저 TDD 에 익숙해 지기위해서 "개인 프로젝트" 를 통해 TDD 연마 필수

 

레거시 코드에 대한 테스트 추가 방법

  • 레거시 코드에 대한 테스트를 많이 해보자
    • 어떻게 테스트 코드를 만들어야 할지 감이 잡힌다.
  • 테스트 코드를 만들기 힘든 부분은 일부 코드를 리팩토링해서 구조 변경
    • 테스트 하고 싶은 일부 코드부분을 분리하여 별도 클래스로 구현 : 범위가 작을수록  수월
    • 분리하려 만든 코드를  테스트 코드로 작성 : 점진적으로 코드를 분리 및 테스트 작성

TDD와 개발 시간

전체 개발 시간을 줄이려면 코딩 시간뿐만 아니라 테스트 시간과 디버깅 시간을 줄여야 한다.

  • 테스트 시간을 줄이는 방법 = 테스트 자동화 ▶ TDD 를 활용한 테스트 자동화
  • 디버깅 시간을 줄이는 방법  = 빠른 버그 발견 ▶ TDD 를 활용하여 초기에 버그 발견
  • 코딩 시간을 줄이는 방법 = 리팩토링을 통한 코드 구조와 가독성 개성 ▶ TDD 를 하면서 리팩토링
    • 리팩토링은 코드의 구조와 가독성을 개선한다. 즉, 미래에 코드 추가나 수정을 쉽게 해준다. 이는 곧 미래의 코딩 시간을 줄여준다.

# 하나의 기능을 구현할 때 테스트를 작성하여 통과할 때까지 구현을 하면 개별 요소에 대한 개발 시간을 단축 할 수 있다. 또한 브라우저를 실행해서 기능을 확인하는 하고 수정하는 과정도 줄일 수 있다. 

+ Recent posts