Tech

Diary

Lecture

About Me

개발중

테스트 더블이란

JeongSeulho

2024년 01월 27일

준비중...
클립보드로 복사

0. 들어가며

테스트 더블에 대해 정리

1. 테스트 더블이란?

테스트를 위해 실제 구현을 대체하는 방법을 지칭하며 다양한 종류가 있음(모킹 등)

2. 테스트 더블 사용 이유

  • 실제 구현체를 사용하지 못하는 경우, 대체할 수 있는 방법이 필요
  • 테스트 코드와 외부 의존성을 분리하여 테스트 코드의 안정성을 높이기 위함
  • 예외 처리에 대한 상황을 재현하여 테스트를 진행하기 위함

3. Dummy

테스트 환경에서 특정 함수 또는 모듈이 필요하지만, 해당 모듈의 구현이나 실행은 필요 없는 경우 사용
즉, 테스트 환경에서 호출만 가능하면 되는 경우 사용
아래는 React Hook Form 테스트를 위한 컴포넌트

copy
const TestForm = props => {
  const methods = useForm({
    defaultValues: {
      name: '',
      address: '',
      phone: '',
      requests: '',
      coupon: NO_COUPON_ID,
      ...props,
    },
  });

  return (
    <FormProvider {...methods}>
      <ShippingInformationForm />
      {/* 아래에서 handleSubmit에 아무것도 없는 빈 콜백 () => {}을 전달하고 있다(Dummy) */}
      <button type="button" onClick={methods.handleSubmit(() => {})}>
        테스트 버튼
      </button>
    </FormProvider>
  );
};

4. Stub

모듈이 호출 시 정해진 값을 반환하도록 하는 방법이며 내부 로직을 구현하지는 않음 단, 아주 간단한 로직이 포함될 수 있음
고려된 케이스 이외의 경우는 대응 불가

copy
export const handlers = [
  ...[
    apiRoutes.users,
    apiRoutes.product,
    apiRoutes.categories,
    apiRoutes.couponList,
  ].map(path =>
    rest.get(`${API_DOMAIN}${path}`, (_, res, ctx) =>
      res(ctx.status(200), ctx.json(response[path])),
    ),
  ),
  rest.get(`${API_DOMAIN}${apiRoutes.products}`, (req, res, ctx) => {
    const data = response[apiRoutes.products];
    const offset = Number(req.url.searchParams.get('offset'));
    const limit = Number(req.url.searchParams.get('limit'));
    const products = data.products.filter(
      (_, index) => index >= offset && index < offset + limit,
    );

    return res(
      ctx.status(200),
      // 아주 간단한 로직이 포함된 Stub
      ctx.json({ products, lastPage: products.length < limit }),
    );
  }),
  rest.get(`${API_DOMAIN}${apiRoutes.profile}`, (req, res, ctx) => {
    return res(ctx.status(200), ctx.json(null));
  }),
  rest.post(`${API_DOMAIN}${apiRoutes.users}`, (req, res, ctx) => {
    if (req.body.name === 'FAIL') {
      return res(ctx.status(500));
    }

    return res(ctx.status(200));
  }),
  rest.post(`${API_DOMAIN}${apiRoutes.login}`, (req, res, ctx) => {
    if (req.body.email === 'FAIL@gmail.com') {
      return res(ctx.status(401));
    }

    return res(
      ctx.status(200),
      ctx.json({
        access_token: 'access_token',
      }),
    );
  }),
  rest.post(`${API_DOMAIN}${apiRoutes.log}`, (_, res, ctx) => {
    return res(ctx.status(200));
  }),
];

5. Spy

모듈의 호출 정보까지 기록하는 방법
주로 호출 횟수나 인자를 검증하는데 사용

copy
const navigateFn = vi.fn();

vi.mock('react-router-dom', async () => {
  const original = await vi.importActual('react-router-dom');
  return {
    ...original,
    // 호출 정보를 기록하는 Spy
    useNavigate: () => navigateFn,
    // 특정 값을 반환하는 Stub
    useLocation: () => ({
      pathname: 'pathname',
    }),
  };
});
copy
import Cookies from 'js-cookie';

describe('로그인이 성공한 경우', () => {
  it('전달된 access_token을 쿠키에 저장하는 메서드를 호출한다', async () => {
    const { user } = await render(<Forms />);
    const submitButton = screen.getByRole('button');

    // 함수 구현을 그대로 사용하면서 호출 정보를 기록기 위한 spyOn
    vi.spyOn(Cookies, 'set');

    await user.type(screen.getByLabelText('이메일'), 'email@gmail.com');
    await user.type(screen.getByLabelText('비밀번호'), 'password123');
    await user.click(submitButton);

    expect(Cookies.set).toHaveBeenCalled(1);
    expect(Cookies.set).toHaveBeenCalledWith('access_token', 'access_token');
  });
});

6. Mock

실제 모듈과 유사하게 행동하도록 만들어진 모의 객체
검증시 행동을 기반으로 검증

copy
const navigateFn = vi.fn();

// react-router-dom 모듈을 Mock
vi.mock('react-router-dom', async () => {
  const original = await vi.importActual('react-router-dom');
  return {
    ...original,
    // 호출 정보를 기록하는 Spy
    useNavigate: () => navigateFn,
    // 특정 값을 반환하는 Stub
    useLocation: () => ({
      pathname: 'pathname',
    }),
  };
});

프론트엔드에서는 스텁만 사용하여 상태를 검증하는 경우는 거의 없음 모의 객체 혹은 실제 모듈의 구현에 Stub이나 Spy를 주입하여 행동을 검증

7. Fake

테스트 전용으로 만들어진 단순한 모듈 또는 객체

copy
    const cart = {
      6: {
        id: 6,
        title: 'Handmade Cotton Fish',
        price: 100,
        description:
          'The slim & simple Maple Gaming Keyboard from Dev Byte comes with a sleek body and 7- Color RGB LED Back-lighting for smart functionality',
        images: [
          'https://user-images.githubusercontent.com/35371660/230712070-afa23da8-1bda-4cc4-9a59-50a263ee629f.png',
          'https://user-images.githubusercontent.com/35371660/230711992-01a1a621-cb3d-44a7-b499-20e8d0e1a4bc.png',
          'https://user-images.githubusercontent.com/35371660/230712056-2c468ef4-45c9-4bad-b379-a9a19d9b79a9.png',
        ],
        count: 3,
      },
      7: {
        id: 7,
        title: 'Awesome Concrete Shirt',
        price: 50,
        description:
          'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
        images: [
          'https://user-images.githubusercontent.com/35371660/230762100-b119d836-3c5b-4980-9846-b7d32ea4a08f.png',
          'https://user-images.githubusercontent.com/35371660/230762118-46d965ab-7ea8-4e8a-9c0f-3ed90f96e1cd.png',
          'https://user-images.githubusercontent.com/35371660/230762139-002578da-092d-4f34-8cae-2cf3b0dfabe9.png',
        ],
        count: 4,
      },
    };

    mockUseCartStore({ cart });