프론트엔드에서 테스트코드를 작성해보자!

정윤규·2023년 9월 17일
10

softeer

목록 보기
1/1
post-thumbnail

서론

소프티어에서 프로젝트를 시작하기 앞서 프론트엔드 파트는 테스트 코드에 대한 생각이 일치했다. 둘 다 매우 긍정적이었고 시간의 여유가 있다면 테스트를 꼭 해보기로 했고 프로젝트 3주차에 테스트 코드를 도입할 수 있었다.
이전엔 테스트가 없이 리팩토링을 해오면서 코드 변경에 따른 사이드 이펙트를 수작업으로 검증해야 했고, 버그가 발생한 코드를 미처 눈치재지 못하고 배포하게 되는 대참사도 겪었다. 그래서 테스트 코드의 필요성을 뼈저리게 느끼고 있었고 꼭 해보고 싶었으나 막상 하려고 할 때마다 프론트에서 어떤 테스트를 해야 할 지 막막해 시도를 미루고 있었다.
사실 테스트 코드는 해보기 전엔 막막하지만 한번만 해보고나면 어느정도 감을 잡게되는 영역이라고 생각한다. 물론 여전히 어렵다.
이 글은 테스트 코드를 작성한 과정들을 정리한 개발일지이다.

프론트엔드 테스트 해보기

vitest + react-testing-library

테스트 프레임워크는 vitest를 채택했다. vitest를 선택한 이유는 다음과 같다.

  • vite 환경으로 react project를 구성했기 때문에 vitest가 호환이 잘 되었다.
  • jest도 있었으나 설정이 훨씬 복잡한데 비해 vitest는 간단해서 설정에 큰 시간이 소모되지 않았다.
    • jest로 테스트 환경을 설정하려면 babel과 관련된 dependency가 상당 필요하고 부가 설정을 해주어야 할게 많다.
    • vitest는 babel 관련 plugin들이 필요하지 않다.
  • vitest의 속도가 훨씬 뛰어났다.

확실히 jest와 대비해 reference가 적다는 단점이 있었지만 jest와 API가 비슷해 jest와 관련한 reference를 찾아보면 대부분의 문제를 해결할 수 있었다.

react-testing-library 는 리액트 컴포넌트를 테스트할 수 있게 도와주는 라이브러리이다. 이것을 통해 컴포넌트가 잘 렌더링 되었는지, 사용자 중심으로 컴포넌트를 테스트할 수 있도록 해준다.

테스트 환경 설정하기

본격적으로 테스트 환경을 설정해보자. (타입스크립트 환경 기준으로 설명한다.)
먼저 vitest, react-testing-library 관련 패키지를 설치한다.

// npm, pnpm을 사용한다면 그에 따라 설치 command를 바꿔주자
yarn add -D vitest
yarn add -D @testing-library/jest-dom @testing-library/react jsdom @testing-library/user-event

src/tests 폴더에 setup.tstest-util.tsx 파일을 다음과 같이 각각 만들어준다.

Setup | Testing Library

import '@testing-library/jest-dom';
import matchers from '@testing-library/jest-dom/matchers';
import { cleanup } from '@testing-library/react';
import { afterEach, expect } from 'vitest';

// expect가 가지는 matcher를 확장한다.
// ex) toBeInTheDocument() 같은 matcher를 쓸 수 있게 된다.
expect.extend(matchers);

// 각 테스트를 마칠 때 마다 cleanup 실행
afterEach(() => {
  cleanup();
});
import { PropsWithChildren, ReactElement } from 'react';
import { render } from '@testing-library/react';
import { ErrorBoundary } from '@/components/common';
import { SelectionProvider, SlideProvider, StyleProvider } from '@/providers';

// 모든 Provider를 wrapping한 컴포넌트
// 프로젝트에서 사용하는 provider들을 사용하면 된다.
function AllProviders({ children }: PropsWithChildren) {
  return (
    <StyleProvider>
      <SelectionProvider>
        <SlideProvider>
          <ErrorBoundary>{children}</ErrorBoundary>
        </SlideProvider>
      </SelectionProvider>
    </StyleProvider>
  );
}

// render를 Provider로 wrapping한 customRender를 만든다.
function customRender(ui: ReactElement, options = {}) {
  render(ui, { wrapper: AllProviders, ...options });
}

// eslint-disable-next-line import/export
export * from '@testing-library/react';
// eslint-disable-next-line import/export
export { customRender as render };
export { default as userEvent } from '@testing-library/user-event';

render API는 옵션으로 provider를 wrapping해 줄 수 있는데 이 설정을 해주지 않으면 context를 사용하는 컴포넌트에서 Provider에 감싸져있지 않다고 무조건 오류가 나게 된다. 따라서 모든 Provider로 감싼 customRender를 render로 내보내 준다.
앞으로는 test-util 에서 내보낸 API들을 사용할 것이다.

다음으로 루트에 vitest.config.ts 파일을 만들고 아래와 같이 설정해주자. 원래 vite.config.ts 에 설정해도 된다고 공식문서에 되어있는데 test property가 계속 타입으로 잡히지 않아서 설정 파일을 분리했다. 찾아보니 stackoverflow에도 그냥 분리하는게 낫다고 되어있었다.

import { resolve } from 'node:path';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/tests/setup.ts',
  },
  resolve: {
    alias: [{ find: '@', replacement: resolve(__dirname, './src') }],
  },
});

위의 globals 속성은 vitest의 API를 global하게 사용할 수 있도록 해주는 속성인데 타입스크립트 환경에서 global API를 사용하기 위해 tsconfig에 설정을 추가해주어야 한다.

// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

msw로 api mocking 하기

1차적인 설정은 완료되었지만 api 요청이 있는 컴포넌트를 테스트해야할 수 있다. 기본적으로 테스트를 할 때엔 실제 API를 사용해선 안된다. 실제 API를 사용하게 되면 서버 환경에 따라 테스트 결과가 달라질 수 있어 테스트 환경에서는 api mocking server를 구축하고 실제 서버와의 의존성을 없애 안전한 테스트가 가능하는 것이 올바르다.

api mocking server는 msw 라이브러리로 간단하게 설정 가능하다.

Install - Getting Started

일단 msw를 설치하자

yarn add -D msw

src/mocks 아래에 handlers.tsserver.ts를 작성한다.

// src/mocks/handlers.ts
import { rest } from 'msw';

// Mock Data
export const posts = [
  {
    userId: 1,
    id: 1,
    title: 'first post title',
    body: 'first post body',
  },
  {
    userId: 2,
    id: 5,
    title: 'second post title',
    body: 'second post body',
  },
  {
    userId: 3,
    id: 6,
    title: 'third post title',
    body: 'third post body',
  },
];

// Define handlers that catch the corresponding requests and returns the mock data.
export const handlers = [
  // 여기서 api controller를 작성한다.
  // url에 일치하는 api 요청이 있을 때 mocking 한다.
  rest.get('/test', (req, res, ctx) => {
    console.log(req);
    return res(ctx.status(200), ctx.json(posts));
  }),
];
// src/mocks/server.ts
import { handlers } from './handlers';
import { setupServer } from 'msw/node';

// This configures a Service Worker with the given request handlers.
export const server = setupServer(...handlers);

setup.ts 도 msw server를 테스트 시 실행할 수 있게 수정해준다.

import '@testing-library/jest-dom';
import matchers from '@testing-library/jest-dom/matchers';
import { cleanup } from '@testing-library/react';
import { server } from '@/mocks/server';
import { expect } from 'vitest';

expect.extend(matchers);

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

afterEach(() => {
  cleanup();
  server.resetHandlers();
});

afterAll(() => server.close());

설정을 완료했다!

테스트 코드를 작성해보자!

테스트 코드는 크게 다음의 4가지에 대해 작성했다.

  • 유틸함수
  • 커스텀 훅
  • 단순 컴포넌트
  • 복잡한 컴포넌트(통합 테스트)

유틸함수 테스트 코드

프론트엔드에서 가장 작성하기 쉬운 테스트 코드는 유틸함수이다. 무엇을 테스트해야 할 지가 직관적이어서 작성하기 어렵지 않았다.

간단하게 함수가 각 기능을 제대로 수행하는지에 대한 테스트 코드를 작성해주었다.

import { setPriceFormat, toSeparatedNumberFormat } from '@/utils/number';

describe('toSeparateNumberFormat 유틸 함수 테스트', () => {
  it('숫자를 3자리마다 콤마(,)를 찍어서 반환한다.', () => {
    const number = 135135;
    const result = toSeparatedNumberFormat(number);

    expect(result).toBe('135,135');
  });
});

describe('setPriceFormat 유틸함수 테스트', () => {
  it('setPriceFormat이 값을 반환한다.', () => {
    const input = 100000;
    const result = setPriceFormat(input);

    expect(result).toBeTruthy();
  });

  it('input과 unit을 입력하면 input을 unit으로 나눈값을 반환한다.', () => {
    const input = 100000;
    const unit = 10000;

    const result = setPriceFormat(input, unit);
    expect(result).toBe(10);
  });

  it('나눈 값이 소수면 반올림을 하여 반환한다.', () => {
    const input = 100000;
    let unit = 10001;

    let result = setPriceFormat(input, unit);
    expect(result).toBe(10);

    unit = 3000000;
    result = setPriceFormat(input, unit);
    expect(result).toBe(0);
  });
});

expect의 여러가지 method들을 통해 반환값을 검증할 수 있다. 마스터님이 테스트 코드를 작성할 때 중요한 점으로 설명을 자세하게 적는 것도 포함된다고 하셨다. 따라서 최대한 직관적이고 구체적인 설명을 적으려 했다.

커스텀 훅 테스트

커스텀 훅의 경우 다른 모듈간의 의존성이 없고 단순한 기능을 하는 훅은 테스트하기 쉬웠다.

import { useCallback, useState } from 'react';

function useToggle(initialState: boolean) {
  const [status, setStatus] = useState(initialState);

  const toggle = useCallback(() => {
    setStatus((prev) => !prev);
  }, []);

  const setOn = useCallback(() => {
    setStatus(true);
  }, []);

  const setOff = useCallback(() => {
    setStatus(false);
  }, []);

  return { status, toggle, setOn, setOff } as const;
}

export default useToggle;

예를 들어 위의 useToggle 훅은 boolean값과 그 값을 제어하는 함수들을 반환해주는 훅이다. 이에 대한 테스트는 아래와 같이 간단하게 작성할 수 있다.

import useToggle from './useToggle';
import { act, renderHook } from '@/tests/test-util';

describe('useToggle 테스트', () => {
  it('initialState를 전달하면 status가 initialState와 같아야 한다.', () => {
    const { result } = renderHook(() => useToggle(false));

    expect(result.current.status).toBe(false);
  });

  it('setOn을 호출하면 status가 true가 된다.', () => {
    const { result } = renderHook(() => useToggle(false));
    const { setOn } = result.current;

    expect(result.current.status).toBe(false);
    act(() => setOn());
    expect(result.current.status).toBe(true);
  });

  it('setOff을 호출하면 status가 false가 된다.', () => {
    const { result } = renderHook(() => useToggle(true));
    const { setOff } = result.current;

    expect(result.current.status).toBe(true);
    act(() => setOff());
    expect(result.current.status).toBe(false);
  });

  it('toggle을 호출하면 status 값이 전환 된다.', () => {
    const { result } = renderHook(() => useToggle(true));
    const { toggle } = result.current;

    expect(result.current.status).toBe(true);
    act(() => toggle());
    expect(result.current.status).toBe(false);
  });
});
  • 반환하는 state가 있다면 값에 대한 검증을 수행한다.
  • 값을 제어하는 함수가 있다면 호출 시 변화하는 값에 대한 검증을 수행한다.

테스트 코드에서 hook을 바로 호출하면 훅을 사용할 수 없다고 오류가 발생하는데 이는 react-testing-libraryrenderHook 함수를 사용하여 훅을 호출할 수 있다.

renderHook 의 반환값중 result를 통해 훅의 반환값들을 참조할 수 있다. 만약 함수를 호출하고 싶다면 act 함수를 통해 콜백으로 호출해야 한다.

다음 커스텀 훅을 보자. Provider내에서 context를 사용했는지에 대한 검증을 하고 내부에서 사용했을 경우 context를 반환하고 아닐 시 오류를 반환하는 커스텀 훅이다.

import { useContext, type Context } from 'react';

function useSafeContext<T>(context: Context<T>) {
  const _context = useContext(context);

  if (!_context) {
    throw new Error('Context는 반드시 Provider로 감싸져야 합니다.');
  }

  return _context;
}

export default useSafeContext;

이런 훅은 어떻게 테스트를 작성해볼 수 있을까? 이는 크게 두 가지 분기에 대해서 테스트해야 한다고 생각했다.

  1. 오류 발생
  2. context 반환

따라서 Test용 mock provider를 만들어 오류 발생 case에선 provider를 감싸지 않았고, context 반환 case에선 TestProvider를 render의 wrapper로 설정해주었다.

작성한 테스트 코드는 아래와 같다.

import { type PropsWithChildren, createContext } from 'react';
import useSafeContext from './useSafeContext';
import { renderHook } from '@/tests/test-util';

const TestContext = createContext<string | null>(null);

function TestProvider({ children }: PropsWithChildren) {
  const value = 'test';
  return <TestContext.Provider value={value}>{children}</TestContext.Provider>;
}

describe('useSafeContext hook 테스트', () => {
  it('Provider로 감싸지 않은 경우 에러를 던진다.', () => {
    expect(() => {
      renderHook(() => useSafeContext(TestContext));
    }).toThrowError(/Context는 반드시 Provider로 감싸져야 합니다./);
  });

  it('Provider 감싼 경우 context값을 정상적으로 전달받는다.', () => {
    const { result } = renderHook(() => useSafeContext(TestContext), {
      wrapper: TestProvider,
    });

    expect(result.current).toBe('test');
  });
});

단순 컴포넌트 테스트

컴포넌트 테스트의 경우 가장 어려웠다. 어떤 것을 테스트해야 할지 가장 감이 잡히지 않았던 것 같다.

여기서 말하는 단순한 컴포넌트라 함은 모듈간 의존성이 거의 존재하지 않는 공통 컴포넌트와 같은 범용성 높은 컴포넌트에 해당한다. 이러한 컴포넌트를 테스트할 때는 크게 아래의 것들에 대해서 테스트했다.

  1. 렌더링이 잘되는지?
  2. 핸들러가 있다면 핸들러가 컴포넌트 제어시 잘 호출되는지?
  3. props에 대해 특정 스타일이나 동작이 있다면 의도한대로 동작하는지?
  4. 스냅샷 테스트

예를 들어 버튼 컴포넌트에 대한 테스트는 다음과 같이 작성하였다.

import CTAButton from './CTAButton';
import { render, screen, userEvent, waitFor } from '@/tests/test-util';

describe('CTAButton 컴포넌트 테스트', () => {
  it('버튼이 정상적으로 렌더링된다.', () => {
    render(<CTAButton>버튼</CTAButton>);

    expect(screen.getByRole('button')).toBeInTheDocument();
  });

  it('버튼의 onClick 핸들러가 클릭시 정상적으로 호출된다.', async () => {
    const mockOnClick = vi.fn();
    render(<CTAButton onClick={mockOnClick}>버튼</CTAButton>);

    userEvent.click(screen.getByRole('button'));
    await waitFor(() => expect(mockOnClick).toBeCalledTimes(1));
  });

  it('버튼을 다중 클릭할 시 핸들러가 한번만 호출된다.', async () => {
    const mockOnClick = vi.fn();
    render(<CTAButton onClick={mockOnClick}>버튼</CTAButton>);

    userEvent.tripleClick(screen.getByRole('button'));
    await waitFor(() => expect(mockOnClick).toBeCalledTimes(1));
  });

  it('버튼이 disabled인 경우 클릭할 수 없다.', async () => {
    const mockOnClick = vi.fn();
    render(
      <CTAButton disabled onClick={mockOnClick}>
        버튼
      </CTAButton>,
    );

    const button = screen.getByRole('button');
    expect(button).toBeDisabled();

    userEvent.click(button);
    await waitFor(() => expect(mockOnClick).not.toBeCalled());
  });

  it('버튼의 isFull 속성이 true일 경우 width가 100%이다.', () => {
    render(<CTAButton isFull>버튼</CTAButton>);

    expect(screen.getByRole('button')).toHaveStyle('width: 100%');
  });

  describe('CTAButton 스냅샷 테스트', () => {
    it('size 속성이 small일 때 스냅샷이 일치한다.', () => {
      render(<CTAButton size='small'>버튼</CTAButton>);

      expect(screen.getByRole('button')).toMatchSnapshot();
    });

    it('size 속성이 medium일 때 스냅샷이 일치한다.', () => {
      render(<CTAButton size='medium'>버튼</CTAButton>);

      expect(screen.getByRole('button')).toMatchSnapshot();
    });

    it('size 속성이 large일 때 스냅샷이 일치한다.', () => {
      render(<CTAButton size='large'>버튼</CTAButton>);

      expect(screen.getByRole('button')).toMatchSnapshot();
    });
  });
});
  • 컴포넌트와 같은 UI의 경우 유저 이벤트를 발생시켜야 할 수 있기 때문에 userEvent 를 사용해서 특정 이벤트를 컴포넌트에 발생시킬 수 있다. 이 때 중요한점은 유저 이벤트는 비동기이기 때문에 waitFor 함수를 통해 비동기 이벤트를 기다려주어야 한다.
  • onClick과 같은 핸들러함수는 테스트 시 함수에 대한 mock을 대신 만들어 주입시켜줄 수 있다. 이는 간단하게 vitest가 지원하는 vi.fn() 함수를 통해 생성할 수 있다. 이를 대신 주입하고 버튼을 클릭할 시 mocking된 함수가 잘 호출되는지 확인하면 된다.
  • 스냅샷 테스트는 UI가 DOM에 렌더링 되어있는 스냅샷을 찍어놓고 이것이 변경되면 테스트가 실패하는 방식으로 동작한다. 스냅샷의 경우, 컴포넌트를 더 이상 변경할 필요가 없을 때 의도치 않은 변경을 방지하기 위한 프리징의 용도로 사용했다. 이 때 emotion같은 css-in-js를 사용한다면 실제 스타일이 아닌 className에 hash값이 담긴 dom에 대한 스냅샷이 찍히게 되는데, 실제 스타일에 대한 스타일을 찍고 싶다면 plugin을 추가적으로 설치해주어야 한다. emotion의 경우 공식에서 지원하는 plugin을 설치해 해결할 수 있었다. Emotion – @emotion/jest

복잡한 컴포넌트 테스트

복잡한 컴포넌트란 모듈간 의존성이 얽혀있는 컴포넌트에 대한 테스트이다. 여러 의존성이 있기 때문에 적절한 mocking 과정들이 필요하다.

  • 예를 들어 BottomHMGData 컴포넌트는 API 요청이 있다. 테스트 환경에선 실제 API 호출을 하면 안된다고 했다. 따라서 msw로 호출하는 API를 가로채 mock API를 사용해야 했다.
  • 테스트 환경에서 사용하는 jsdom은 브라우저의 DOM이 아니기 때문에 브라우저 API의 존재에 대해 알지 못한다. data fetch를 캐시하기 위해 사용한 Cache Storage 를 알지 못해 API 호출중 오류가 발생했다. 이를 해결하기 위해서 cache storage를 mock으로 만들어 테스트 환경에 주입해주어야 했다.
    class Cache {
      cache: Map<string, Response>;
      constructor() {
        this.cache = new Map();
      }
    
      // ...
    }
    
    // mock cache storage
    export class CacheStorageMock {
      caches: Map<string, Cache>;
      constructor() {
        this.caches = new Map();
      }
    
    	 // ...
    }
    
    // setup.ts
    // global에 cache API를 mocking한다.
    vi.stubGlobal('caches', new CacheStorageMock());
  • 이외에도 특정 Provider에 의존하는 경우 mock Provider로 wrapping 해주는 과정이 필요하다.

작성한 테스트 코드는 아래와 같다.

import BottomHMGData from './BottomHMGData';
import { render, screen, waitForElementToBeRemoved } from '@/tests/test-util';

describe('BottomHMGData 컴포넌트 테스트', () => {
  const mockPowerTrain = {
    id: 1,
    name: '가솔린 2.0',
    price: 0,
    image: '',
  };

  const mockDriveTrain = {
    id: 1,
    name: '2WD',
    price: 0,
    image: '',
  };

  it('컴포넌트가 렌더링되고 데이터 fetching이 발생하면 로딩 UI가 표시된다.', () => {
    const mockFn = vi.fn();
    render(<BottomHMGData powerTrain={mockPowerTrain} driveTrain={mockDriveTrain} setTechnicalSpec={mockFn} />);

    expect(screen.getByText('데이터를 불러오는 중입니다...')).toBeInTheDocument();
  });

  it('컴포넌트가 정상적으로 렌더링된다.', async () => {
    const mockFn = vi.fn();
    render(<BottomHMGData powerTrain={mockPowerTrain} driveTrain={mockDriveTrain} setTechnicalSpec={mockFn} />);

    await waitForElementToBeRemoved(() => screen.queryByText('데이터를 불러오는 중입니다...'));
    expect(mockFn).toHaveBeenCalledTimes(1);

    expect(screen.getByText('배기량과 평균연비입니다.')).toBeInTheDocument();
  });

  it('컴포넌트에 전달된 powertrain과 drivetrain의 이름을 표시한다.', async () => {
    const mockFn = vi.fn();
    render(<BottomHMGData powerTrain={mockPowerTrain} driveTrain={mockDriveTrain} setTechnicalSpec={mockFn} />);

    await waitForElementToBeRemoved(() => screen.queryByText('데이터를 불러오는 중입니다...'));
    expect(mockFn).toHaveBeenCalledTimes(1);

    expect(screen.getByText('가솔린 2.0')).toBeInTheDocument();
    expect(screen.getByText('2WD')).toBeInTheDocument();
  });

  it('컴포넌트에 전달된 powertrain과 drivetrain에 따른 배기량과 연비정보를 표시한다.', async () => {
    const mockFn = vi.fn();
    render(<BottomHMGData powerTrain={mockPowerTrain} driveTrain={mockDriveTrain} setTechnicalSpec={mockFn} />);

    await waitForElementToBeRemoved(() => screen.queryByText('데이터를 불러오는 중입니다...'));
    expect(mockFn).toHaveBeenCalledTimes(1);

    expect(screen.getByText('3,778cc')).toBeInTheDocument();
    expect(screen.getByText('9.23km/L')).toBeInTheDocument();
  });
});

테스트 자동화하기

husky

테스트를 수동으로 실행하는 것보단 자동화하는 것이 좋을 것이다. 우리의 경우는 기본적으로 husky를 설정해놓아서 commit 전에 lint, prettier, type check를 검증하고 문제가 있으면 commit이 되지 않도록 설정해놓았었다.

여기에 더해 테스트도 함께 검증하도록 했다. 이 때 모든 테스트를 돌리게 되면 commit에 시간이 굉장히 많이 소요될 수 있으므로 related 속성과 lint-staged로 현재 staging된 파일들과 관련된 테스트만 실행하도록 하였다.

{
  "src/**/*.{ts,tsx}": ["yarn lint:fix --cache", "bash -c 'yarn typecheck'", "vitest related --run"]
}

Github actions

추가적으로 github actions를 통한 test workflow도 설정했다. 원래 처음엔 husky로 충분하다고 생각했으나 결국 husky는 로컬에서 설정을 끄고 github에 변경사항을 push할 수 있다고 생각되어 설정을 추가했다.
yaml 파일은 아래와 같이 구성했다. PR시 test를 검증하도록 했다.

name: front test

on:
  pull_request_target:
    branches: ["main", "dev-fe"]
    paths:
      - "frontend/**"

jobs:
  front-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Setting node js
        uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Setting environment variables
        run: |
          echo "VITE_API_BASE_URL=${{ secrets.VITE_API_BASE_URL }}" >> .env
        working-directory: ./frontend

      - name: Install Dependencies
        run: yarn install --frozen-lockfile
        working-directory: ./frontend

      - name: Run test
        run: yarn test
        working-directory: ./frontend

step 별로 보면 아래 순서로 실행한다.

  1. source code checkout
  2. node 설정
  3. env 파일 생성(env가 필요없다면 안해도 된다)
  4. dependency install
  5. 테스트 실행

만약 test가 실패하면 actions가 실패하고 github repository 설정을 통해 merge가 불가능하게 방지할 수 있다.

결론

프론트엔드 테스트 코드 굉장히 어렵다.. 무엇을 테스트해야 할 지에 대한 생각들을 할 수 있었고, 테스트 가능한 코드 작성에 대해 깊은 고민들을 할 수 있었다.

어려움이 있었으나 결과적으로 보다 안전한 코드 수정이 가능해졌고 지속 가능한 코드를 작성할 수 있게 되었다. 앞으로도 테스트 코드를 통해 관리하기 쉬운 코드를 작성할 수 있도록 노력해보려한다.

관련링크

테스트를 작성하기 위해 참고했던 좋은 글들이다.

profile
FE 개발자를 꿈꾸고 있습니다 :)

2개의 댓글

comment-user-thumbnail
2023년 11월 7일

최근 본 테스트 라이브러리 포스트중에 제일 도움이 됬습니다. 감사합니다!

1개의 답글