TDD - stub 방식과 mock 방식

 

 

1. Stub 방식

const UserService = require('../user_service');
const StubUserClient = require('./stub_user_client');

describe('UserService - Stub', () => {
  let userService;

  beforeEach(() => {
    userService = new UserService(new StubUserClient());
  });

  it('should log in successfully', async () => {
    const data = await userService.login('testUser', 'password');
    expect(userService.isLogedIn).toBe(true);
    expect(data).toEqual({ userId: 'testUser', token: 'fake-token' });
  });
});

 

장점 :

- stub은 특정 메서드의 반환값을 미리 정의하여 간단하게 테스트할 수 있음. 실제로 API 호출을 하지 않으므로 테스트가 빠르다.

- stub에서 반환하는 값이 고정되어 있어, 테스트 결과가 항상 일관된다.

 

단점:

- stub은 주로 반환값을 설정하는 데에만 집중하므로, 메서드 호출 횟수나 호출 여부 같은 부분은 검증하가 어렵다.

 

2. Mock 방식

const UserService = require('../user_service');

describe('UserService - Mock', () => {
  let userService;
  let userClientMock;

  beforeEach(() => {
    userClientMock = {
      login: jest.fn().mockResolvedValue({ userId: 'testUser', token: 'fake-token' }),
    };
    userService = new UserService(userClientMock);
  });

  it('should log in successfully', async () => {
    const data = await userService.login('testUser', 'password');
    expect(userClientMock.login).toHaveBeenCalledWith('testUser', 'password');
    expect(userService.isLogedIn).toBe(true);
    expect(data).toEqual({ userId: 'testUser', token: 'fake-token' });
  });
});

 

장점:

- mock은 메서드 호출 횟수, 호출 파라미터 등을 검증할 수 있어, 테스트의 정확성을 높일 수 있다.

- 다양한 시나리오를 쉽게 시뮬레이션할 수 있어, 복잡한 비즈니스 로직을 테스트하는 데 유리하다.

 

단점:

- mock을 설정하는 과정이 stub보다 복잡할 수 있으며, 잘 못 설정할 경우 테스트가 실패할 수 있다.

 

3. 결론 - 어떤 방법을 선택할까?

- 간단한 테스트 : login 메서드의 기본적인 동작을 검증하는 경우, stub 방식이 더 간단하고 빠르다.

- 로그인 시도 횟수나 특정 조건에서의 동작을 검증하고 싶다면 mock 방식이 더 적합하다.

 

 


 

 

테스트를 작성할 때, 검사하는 메서드의 기능이 "어떻게" 작동하는지가 아니라 "무엇을"하는지에 집중해야 한다. 마치 우리가 휴대폰을 사용할 때, 휴대폰 내부의 복잡한 회로나 프로그램을 알 필요 없이 그저 버튼을 눌러 원하는 기능을 사용하는 것과 같다.

 

class Calculator {
  add(a, b) {
    // 내부 구현은 복잡할 수 있어요
    let result = 0;
    for (let i = 0; i < b; i++) {
      result += a;
    }
    return result;
  }
}
test('2 더하기 3은 5이다', () => {
  const calc = new Calculator();
  expect(calc.add(2, 3)).toBe(5);
});

test('0 더하기 0은 0이다', () => {
  const calc = new Calculator();
  expect(calc.add(0, 0)).toBe(0);
});

 

위의 테스트들은 계산기가 실제로 올바른 결과를 내는지만을 확인하고 있다. 결과야말로 인류 모두가 정말과 관심이 있는 부분이다....

 

왜 이렇게 해야 할까라고 하면,

 

1. 유연성 및 재사용 : 나중에 덧셈 함수의 내부 구현을 바꾸더라도 (예를 들어, 더 빠른 방법을 찾았다면), 테스트는 여전히 유효하다.

 

2. 명확성 : 테스트를 보는 사람이 그 기능이 무엇을 해야 하는지 쉽게 이해할 수 있다.

 

3. 유지보수 : 내부 구현이 바뀔 때마다 테스트를 수정할 필요가 없다.

 

결론적으로, 테스트를 작성할 때 "이 기능이 사용자에게 어던 결과를 제공해야 하는가?"에 초점을 맞춰야 한다.

 

좀 더 구체적으로 보면, 메서드가 "무엇을 하는지"와 "어떤 결과를 반환하는지"에 초점을 맞추는 것이다.

 

1. 입력 : 메서드에 어떤 값을 넣었는지

2. 출력 : 그 결과로 어떤 값이 나오는지

3. 동작 : 메서드가 어떤 일을 수행했는지 (예: 데이터베이스에 저장, 이메일 전송 등)

 

이런 접근 방식은 "블랙박스 테스팅"이라고도 불리며, 메서드의 외부에서 관찰 가능한 동작에만 집중한다.

이는 실제 사용자의 관점에서 소프트웨어를 테스트하는 것과 유사하다.