본문 바로가기

내용 복습/Node.js

Jest 공부 3일차

1. TDD란

테스트 주도개발(Test Driven Developmen)의 약자로 핵심 규칙중 하나는 먼저 테스트를 작성하고 그 다음 구현하는 것이다. 테스트 코드를 작성하면서 요구사항을 정확하게 인식하는 장점이 있다. 물론 대규모 프로젝트에 TDD를 적용한다고 해서 대규모의 테스트코드를 전부 작성하고 구현하는 코드를 작성하진 않는다. 기본적으로 작동하는 데 필요한 코드만 작성하고 테스트를 작성하는 방식으로 진행한다. 작동 상태를 먼저 구현하게 되면 어떤 기술, 디자인패턴을 사용할 지 아이디어가 생기고 나아가 무엇을 테스트 할지에 대한 개념이 생긴다. 프로젝트의 작동 상태가 확보되면 TDD 스타일로 코드를 작성할 수 있다. 즉, 테스트 주도 개발은 프로젝트의 기본 단계에서 시작하는 것이 아니라 이미 작동하는 기본 애플리케이션을 확장하고 싶을 때, 혹은 버그가 발생했을 때 유용하다. 

 

2. Test doubles in Jest

테스트 더블이라는 용어는 영화에서 스턴트 더블이 실제 배우 대신 사용되는 상황에서 유래되었다. 코드의 일부 유닛이나 외부 서비스, 이를테면 데이터베이스는 빠르게 접근할 수 접근할 수 없다. 실제 프로덕션 데이터는 회사의 VPN 뒤에 안전하게 있기 때문에 접근하기 어렵다. 이때 사용하는 것이 테스트 데이터베이스인데, 프로덕션 데이터베이스를 직접 참조하는 것이 아닌 Docker 컨테이너와 같은 테스트 데이터베이스를 가리키도록 할 수 있다. 또한 데이터베이스에 대한 호출을 모의 호출로 대체할 수도 있다. 쉽게 말해 테스트 더블은 실제 객체 대신 테스트 목적으로 사용할 수 있는 가짜 객체이다.

  • 더미 객체(dummy object) : 단순히 전달되지만 실제로 사용되지 않는 객체, 주로 메서드의 인수로 사용되며, 테스트에서 특정 객체가 필요하지만 그 객체의 기능이 필요치 않을 때 사용
  • 페이크 객체(fake object) : 실제로 동작하는 구현이지만, 복잡함을 줄이기 위해 간소화된 버전입니다. 예를 들어, 데이터베이스 대신 메모리 내에서 작동하는 간단한 구현을 사용할 수 있습니다.
  • 스텁(stub) : 특정 값을 반환하도록 미리 설정된 불완전한 객체입니다. 테스트 중에 특정 메서드가 호출되면 미리 정의된 결과를 반환하도록 설정할 수 있습니다. 검증(assertion)에 직접 사용하는 것이 아니라 주로 외부 의존성을 모의하기 위해 사용됩니다.
  • 스파이(spy) : 호출된 메서드에 대한 정보를 추적하는 객체입니다. 즉, 어떤 메서드가 호출되었는지, 몇 번 호출되었는지, 어떤 인수로 호출되었는지를 기록할 수 있습니다. 스파이는 주로 함수의 호출 여부를 확인하고, 호출된 인수를 검증하는 데 사용됩니다.
  • 목(mock) : 특정한 기대치와 함께 미리 프로그래밍된 객체입니다. 테스트 중에 메서드가 호출되면 예상된 동작을 수행하도록 설정할 수 있으며, 호출된 메서드가 예상한 대로 작동했는지를 검증할 수 있습니다. 목은 주로 복잡한 상호작용이 있는 경우에 유용합니다.

 

3. spy

  • 기본적으로 jest.spyOn()으로 감시된 함수는 원래 동작을 그대로 유지하지만 필요에 따라 해당 함수의 값을 수정하거나 동작을 모킹할 수도 있다.
  • 호출 횟수, 어떤 인자와 함께 호출되었는지, 어떤 값을 반환하였는지 추적할 수 있다.
  • 테스트가 끝난 후 jest.restoreAllMock()이나 mockRestore()을 사용해 감시된 함수의 상태를 원래대로 복원할 수 있다.
  • toHaveBeenCalled()호출 여부를 확인할 수 있고, toBeCalledWith()은 toHaveBeenCalledWith()으로 변경되어 특정 인자로 호출되었는지 확인할 수 있다.
  • mockImplementation()이나 mockReturnValue()로 원래 함수의 동작을 변경할 수 있다.
  • spy의 대상함수는 반드시 실제로 존재해야 하며, 원래 함수 동작에 의존할 경우 부작용 테스트에 주의하여 필요하다면 mock도 병행하여 사용해야한다.
test('spy with mock implementation', () => {
  const spy = jest.spyOn(module, 'myFunction');
  
  // 원래 함수 동작 대신 모킹된 동작 정의
  spy.mockImplementation(() => 'mocked value');

  const result = myFunction();
  expect(result).toBe('mocked value');
  
  spy.mockRestore();
});
spy는 특정함수의 호출을 감시해 동작의 테스트나 검증에 사용되며 jest.spyOn() 메서드를 사용해서 생성한다.

 

 

 

4. mock

mock은 함수, 모듈, 또는 의존성을 대체하여 테스트 환경에서 사용하기 위한 기능이다. 복잡한 동작을 단순화하거나 테스트를 위한 특정 시나리오를 시뮬레이션 하는 데 유용하다.

1) 함수 모킹

기본적으로 jest.fn()을 통해 생성할 수 있고 아래와 같이 반환값이나 동작을 제어할 수 있다.

const mockFn = jest.fn();

mockFn('arg1', 'arg2'); // 호출
expect(mockFn).toHaveBeenCalled(); // 호출 여부 확인
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2'); // 호출된 인자 확인

// 반환값 설정
const mockFn = jest.fn().mockReturnValue('mocked value');
expect(mockFn()).toBe('mocked value');

// 호출순서에 따라 다른 동작 설정
const mockFn = jest.fn()
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('second call');

 

2) 모듈 모킹

jest.mock()을 사용해서 모듈의 모든 함수와 메서드를 자동으로 모킹할 수 있다. 예를 들어 uuid를 생성하는 코드를 test하기 위해선 uuid를 직접 import 하면 안 된다. 실제함수로 생성한 uuid와 test에서 생성한 uuid가 불일치할 것이기 때문이다. 대신, uuid 의존성을 모킹하고 결과값을 toBe()로 특정하여 의존성 동작에 상관없이 대상 코드의 로직 검증에 초점을 맞춰 테스트를 예측가능하게 할 수 있다.

const mockFn = jest.fn()
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('second call');

 

  • mockReturnValue(value): 항상 특정 값을 반환하도록 설정.
  • mockReturnValueOnce(value): 특정 호출에서만 특정 값을 반환하도록 설정.
  • mockImplementation(fn): 모킹 함수의 동작을 커스터마이징.
  • mockImplementationOnce(fn): 특정 호출에서만 동작을 커스터마이징.
  • mockResolvedValue(value): 비동기 함수가 특정 값을 resolve하도록 설정.
  • mockRejectedValue(value): 비동기 함수가 특정 값을 reject하도록 설정.

 

5. 테스팅 스타일(London vs chicago)

테스트 스타일은 mock 객체를 사용하는 방법에 따라 런던과 시카고(고전파) 스쿨 스타일로 나뉜다. 이것을 나누는 큰 기준은 코드베이스에서 유닛이란 무엇인 지를 생각해보는 것이다. 시카고 스타일은 낮은 모의 객체 접근 방식을 취하며 유닛을 여러 조각의 집합으로 다룬다. 복잡하며 넓은 관점에서 생각하여 여러 스타일의 구성요소가 함께 테스트된다는 것을 의미하여 mock 객체를 거의 쓰지 않는다.

반면, 런던 스타일은 mock 객체를 많이 사용하며 이 스타일에서 유닛을 클래스를 뜻한다. 클래스는 의존성을 갖고 있으며 특정 클래스 내의 모든 의존성을 mocking해야 한다고 주장하는 스타일이다. 이 두가지를 절충하여 쓰는 것이 좋고 유닛은 요구사항으로 생각하면 쉽다.