TIL 116 - Test

김영현·2025년 1월 10일
0

TIL

목록 보기
126/129

The Difference Between Mocks and Stubs

테스트를 진행할때는 보통 소프트웨어의 한 요소에 집중한다. 이를 일반적으로 단위테스트(Unit Test)라 부른다.
그러나 단위테스트가 제대로 작동하려면, 종종 다른 단위(Unit, module, funtion...)이 필요하다.
예를들어 저장소같은 것을 대체할 수단이 필요하게 된다.

용어의 혼란

저장소를 대체할때 Stub, MOck, Fake, Dummy같은 용어가 혼용되어 용어의 혼란이 생기기 쉽다.
여기서는 Gerard Meszaros의 책에 나온 용어를 따른다.

Meszaros는 "Test Double"이라는 용어를 실제 객체 대신 사용하는 모든 가짜 객체의 포괄적인 용어로 정의한다. 이는 영화의 스턴트 더블(Stunt Double) 개념에서 유래했다. 또한 그는 Test Double을 다섯 가지로 분류했다.

  1. Dummy 객체
    • 단순히 매개변수를 채우기 위해 사용되며, 실제로는 사용되지 않습니다.
  2. Fake 객체
    • 실제로 동작하는 구현을 가지지만, 프로덕션에서 사용하기에는 적합하지 않습니다. (예: 메모리 기반 데이터베이스)
  3. Stub
    • 테스트 중 특정 호출에 대해 미리 정의된 응답을 제공합니다. 프로그래밍된 응답 외에는 다른 요청에 반응하지 않습니다.
  4. Spy
    • Stub이면서 호출된 정보를 기록합니다. 예를 들어, 이메일 서비스가 얼마나 많은 메시지를 보냈는지 기록할 수 있습니다.
  5. Mock
    • 호출을 받을 것으로 기대되는 메서드를 미리 정의한 객체로, 예상된 호출이 이루어졌는지 검증합니다

Mock vs Stub : 상태검증 vs 동작 검증

Mock은 행동 검증(Behavior Verification)을 고집하지만, 다른 Double들은 상태 검증(State Verification)을 사용한다. Mock도 실행 단계에서는 다른 Double처럼 동작하지만, 설정(Setup)과 검증(Verification) 단계에서 차이가 있다.
=> 일단 이해하기로는 Mock은 결과를 검증, 다른 가짜객체들은 상태를 검증한다. 정확히 와닿지 않으니 예제로 살펴보자.

예제: 이메일 서비스 테스트

실제 객체 대신 Test Double을 사용하는 일반적인 경우는 테스트 시 실제 객체가 다루기 어려울 때다. 예를 들어, 주문이 처리되지 않았을 경우 이메일 메시지를 보내야 한다고 가정해보자. 이 경우 테스트 중에 실제 고객에게 이메일을 보내고 싶지 않으므로, 이메일 시스템의 Test Double을 만든다.

즉, 이메일 시스템의 가짜객체를 생성한다.

원본 예시는 java코드이나 js로 변환해보았음!

// emailService.js
class EmailService {
  send(message) {
    throw new Error('Method not implemented');
  }
}

class EmailServiceStub extends EmailService {
  constructor() {
    super();
    this.messages = [];
  }

  send(message) {
    this.messages.push(message);
  }

  numberSent() {
    return this.messages.length;
  }
}

module.exports = { EmailService, EmailServiceStub };

// order.js
class Order {
  constructor(product, quantity) {
    this.product = product;
    this.quantity = quantity;
    this.mailer = null;
    this.isFilled = false;
  }

  setMailer(mailer) {
    this.mailer = mailer;
  }

  fill(warehouse) {
    ...
  }
}

module.exports = Order;

stub 사용 예

const { EmailServiceStub } = require(...);

test('이메일 전송 여부를 확인한다', () => {
  const order = new Order('Product1', 10); // 주문 생성
  const emailServiceStub = new EmailServiceStub(); // Stub 객체 생성

  order.setMailer(emailServiceStub); // 이메일 서비스로 Stub 설정
  order.fill(warehouse); // 주문 처리. warehouse는 가짜객체가 아니라 진짜 객체임

  expect(emailService.numberSent()).toBe(1); // Stub의 상태(전송된 메시지 개수) 검증
});

stub사용시 상태를검증한다. 즉, 전송된 메시지의 개수가 내가 기대한 값과 같은지를 검증한다.

Mock 사용 예

const emailServiceMock = {
  send: jest.fn(), // Mock 함수로 선언
};

const warehouseMock = {
	hasInventory:() => false;
};

// 테스트 코드
test('이메일 전송 함수가 호출되었는지 확인한다', () => {
// 주문 객체 생성 및 Mock 설정
  const order = new Order('TALISKER', 51);
  order.setMailer(emailServiceMock); 
  order.fill(warehouseMock);

  // 행동 검증: 'send' 메서드가 한 번 호출되었는지 확인
  expect(emailServiceMock.send).toHaveBeenCalledTimes(1);

  // 행동 검증: 'hasInventory' 메서드가 한 번 호출되었는지 확인
  expect(warehouseMock.hasInventory).toHaveBeenCalledTimes(1);

  // 행동검증: 'hasInventory' 메서드가 적절한 인자로 호출되었는지 확인
  expect(warehouseMock.hasInventory).toHaveBeenCalledWith('TALISKER', 51);
});

Mock사용시 내가 기대한 행동이 일어났는지를 검증한다. 즉, send메서드가 몇 번 호출되었는지, 어떤 인자로 호출되었는지 를 검증한다. Stub와 Mock은 상태,행동 검증이라는 차이가 있다.또한 stub 사용시 테스트를 위해 추가적인 메서드 구현이 필요할 수도 있다.

Meszaros는 Stub 중에서 동작 검증을 사용하는 것을 Test Spy라고 부르기도 한다.

출처 : https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs


Unit Test

단위테스트는 모든 부분을 분리하여 테스트한다. 즉, 하나의 모듈만 테스트한다.

describe("get todady!", () => {
  it("should return today like YYYY-MM-DD-HH:MM:SS", () => {
    const getToday = require("./getToday");
    const today = getToday();
    const regex = /^\d{4}-\d{2}-\d{2}-\d{2}:\d{2}:\d{2}$/;

    expect(today).toMatch(regex);
  });
});

그림으로 나타내면 이렇다.


이미지 출처 : https://dariuszwozniak.net/posts/kurs-tdd-2-testy-jednostkowe-a-testy-integracyjne

단일메서드를 테스트하기에 단위테스트는 나머지 소프트웨어가 올바르게 동작한다는 가정(종종 잘못된)에 의존한다.
이는 모든 종속성을 명시적으로 모의하기 때문이다.

따라서 특정 기능의 단위테스트가 올바르게 작동한다 해도, 해당 기능이 제대로 작동한다는 것을 의미하지 않는다.

굉장히 아이러니한 일이 아닐 수 없다. 왜 그런건지는 조금 이따 살펴보자.


Integration Test

통합테스트에서는 시스템의 여러 모듈을 테스트한다.


이미지 출처 : https://dariuszwozniak.net/posts/kurs-tdd-2-testy-jednostkowe-a-testy-integracyjne

왜 통합테스트가 필요한걸까?

만약 단위테스트만 사용하면 아래와 같은 참사가 일어난다.


이미지 출처 : http://softwaretestingfundamentals.com/integration-testing/

따라서 단위테스트와 통합테스트를 적절히 행해야 한다.

Unit Test, Integration Test단락의 출처 : https://stackoverflow.com/questions/10752/what-is-the-difference-between-integration-and-unit-tests


Why need Integration Test?

대충 이런 메서드가 있다고 가정해보자.

class Human {
  //...
  
  DoBreath: (air) => {
      console.log('스읍!');
      const carbonDioxide = purificate(air);
      console.log('휴~!');
      return carbonDioxide;
  }
}

누가봐도 아주 중요한 메서드다!🤣

이 메서드의 단위테스트를 작성해보자.



describe('breath unit test', () =>{
	it('Inhale oxygen and exhale carbon dioxide', () => {
    	const result = DoBreath(cleanAir);
      	expect(result).toBe(carbonDixoide);
    })
})

테스트에 통과하면, 작동하는 기능을 제공한다고 단언할 수 있다.

단위테스트를 작성하려면 나머지 클래스와 메서드가 작동하는 것을 가정하고 기능이 작동하는지 확인해야한다.
위 메서드에서는 purificate()가 무조건 올바르게 작동한다고 가정해야한다. 즉, 철저하게 다른 모듈에서 격리시켜야한다.

그러나 purificate()에 버그가 있다고 가정해보자. 다행히도 개발자가 통합테스트코드를 작성해놓아서, 버그를 찾을수 있었다.
만약 purificate()가 100군데서 사용되고 있다면 100개의 기능이 실패할 것이다. 하지만 통합테스트 코드를 작성해놓았기에 문제를 드러낼 수 있게되었다.

하지만 DoBreath()에 대한 단위테스트는 성공한다.

만약 B라는 클래스에 버그가 있다고 가정해보자.

해당 클래스에 의존하는 다른 클래스들의 통합 테스트는 실패할 것이다.

그러나 단위테스트는 단 하나의 테스트(B클래스에 대한)만 실패하게 된다.

단위 테스트는 Test Dobule위에서 진행된다. 따라서 다른 모듈이 실패할 경우를 고려하지 않는다.

결론

통합테스트는 무엇이 작동하지 않는지알려준다. 그러나 문제가 어디있는지추측하는데는 아무 소용이 없다.
단위테스트는 버그가 정확히 어디있는지 알려준다. 그러나 무엇이 작동하지 않는지알려주지는 않는다.

따라서 두가지 테스트를 적절히 행할 필요가 있다.

출처 : https://stackoverflow.com/a/7876055/23726403
https://arialdomartini.wordpress.com/2011/10/21/unit-tests-lie-thats-why-i-love-them/


Testing Pyramid

단위테스트는 하나의 모듈만을 테스트하지만, 통합 테스트는 여러 모듈을 테스트한다.
프로젝트의 규모가 커질 수록 모든 코드를 테스트하기란 어려운 법이다.

따라서 복잡한 테스트일수록 더 적게, 간단한 테스트일수록 더 많이 작성한다. 이것이 테스팅 피라미드의 정의다.

일반적으로 단위테스트는 자주실행한다.

통합테스트는 여러 모듈을 합한 테스트다. 따라서 브랜치 병합, 기능 변경사항 통합이후에 실행해야한다.

E2E 테스트는 End-to-End테스트라 하며 사용자 인터페이스 부터 데이터베이스에 이르기까지 전체 서비스를 시작부터 끝까지 테스트한다.
따라서 제일 적게 실행한다.

출처 : https://circleci.com/blog/testing-pyramid/


profile
모르는 것을 모른다고 하기

0개의 댓글