트러블이슈

테스트 코드 도입기

Ahyeon, Jung 2024. 7. 17. 05:10

올해 초까지는 프론트엔드에서 왜 테스트 코드를 작성해야하는건지 이해하지 못했다. 브라우저에 띄워서 확인하면 되는걸 굳이 비용들여서 테스트 코드를 작성하는게 단순히 유행이라고만 생각했다. 하지만 이약저약에서부터 분업이 아닌 협업을 추구하면서 테스트 코드의 의미를 이해할 수 있었다.


테스트

프로그램을 실행하여 오류와 결함을 검출하고 애플리케이션이 요구사항에 맞게 동작하는지 검증하는 절차

내가 작성한 코드가 내가 의도한 대로 올바르게 동작하고 기능하는지 검증하는 것

 

=> 테스트 코드를 통해 프로덕트의 안정성과 유지 보수성을 향상시킬 수 있다

테스트 코드의 종류

종류 도구 설명
Static Test(정적 테스트) Typescript, eslint 등 구문오류와 타입오류를 감지해 알려줘서 런타임 에러를 방지할 수 있다.
Unit Test(단위 테스트) jest, mocha, react-testing-library 등 하나의 함수, 메소드, 클래스, 모듈 등이 의도한 대로 작동하는지 테스트
Integration Test(통합 테스트) react-testing-library, Enzyme 등 여러 개의 모듈, 컴포넌트 등이 상호작용하며 잘 동작하는지 테스트
E2E 테스트   사용자가 어플리케이션에서 경험할 것으로 예상되는 행동을 코드로 작성해 검증하는 테스트

 

UI 테스트

컴포넌트가 예상한 대로 화면에 그려지는지 테스트

Storybook, Bit, stylegudist 등

웹 접근성 테스트

장애인, 고령자, 저시력자, 색각 이상자 등 다양한 사용자 그룹이 웹사이트를 접근하고 이용할 수 있는 지 테스트

storybook accessibility addons, 스크린 리더, Wave

크로스 브라우저 테스트

다양한 브라우저에서 앱/웹이 동일하게 동작하는지 테스트

다양한 브라우저와 마우스&키보드, MDN 문서(Cross Browser Test)

F.I.R.S.T 원칙

  • Fast: 단위 테스트는 빨라야 한다.
  • Isolated: 단위 테스트는 외부 요인에 종속적이지 않고 독립적으로 실행되어야 한다.
  • Repeatable: 단위 테스트는 실행할 때마다 같은 결과를 만들어야 한다.
  • Self-validating: 단위 테스트는 스스로 테스트를 통과했는지 아닌지 판단할 수 있어야 한다.
  • Timely/Thorough: 단위 테스트는 프로덕션 코드가 테스트에 성공하기 전에 구현되어야 한다(TDD)/ 단위 테스트는 성공적인 흐름뿐만 아니라 가능한 모든 에러나 비정상적인 흐름에 대해서도 대응해야 한다.

DAMP(Descriptive And Meaningful Phrases)

테스트 코드를 서술적이고 의미 있게, 즉, 읽기 쉽고 이해하기 쉽게 작성하자

테스트코드는 DRY(Don't Repeat Yourself) 원칙과 충돌할 때도 있으나, 중복이 발생하더라도 직관적이고 명확하게 이해되도록 테스트 코드를 작성하는 것이 좋다.

Given-When-Then

BDD(Behaviour Driven Development)의 중심인 사용자 행위를 기반으로 한 테스트 시나리오를 작성할 때, Given-When-Then 구조로 테스트를 구성한다면 명확한 시나리오 위에서 개발자가 코드를 쉽게 파악하고 이해할 수 있다. 

  • Given: 테스트를 하기 위해 세팅하는 주어진 환경
  • When: 테스트를 하기 위한 조건으로 프론트엔드에선 사용자와의 상호작용인 경우도 많음
  • Then: 예상 결과를 나타내며 의도대로 동작하는지 검증 및 확인할 수 있음

 

테스트 코드의 도입 계기

내 코드를 설명하고 안정성을 보장해야한다

 처음에는 단순히 유틸함수를 안정성있게 만들어줘야했기 때문에, 유닛 테스트 단위로 input값이 들어오면 어떤 output 값이 나오는지에 대한 테스트 코드를 작성했다. 케이스 상황을 생각하면서 함수가 좀 더 견고해진다는 장점이 있다. 뿐만 아니라 어떤 input이 들어오면 어떤 output 값이 나오는지를 명시해주기 때문에 내부 로직을 이해하지 못해도 믿고 가져다 쓸 수 있는 공통 함수로 넣어주기 좋다. 실제로 초기에 테스트 코드를 도입할 때 주석을 믿는 것보다 실행결과를 보고 판단해야한다고 생각해서 코드 문서화의 일종으로 도입했다. 도입을 해보니 함수를 먼저 작성하고, 시나리오를 설정하면서 함수를 더 안전하게 만들 수 있었다.

 

 추가적으로 다른 사람이 PR을 올릴 때 내 의도를 명확히 할 수 있고, 누군가 새로운 기능을 추가해도 내 코드는 기존의 동작대로 움직인다는 것을 확인할 수 있다.

 

 개인적으로 유닛 테스트의 장점은 컴포넌트와 로직을 분리해서 생각할 수 있다는 점이다. 함수형 프로그래밍과 결을 같이 하는데, 유닛 테스트를 작성하려고 생각하다보면 불필요하게 컴포넌트 내부에 포함되어서 중복 생성되는 함수들을 바깥으로 모듈화할 수 있다.

 

import { HeatmapProps } from "types";
import getMaxValue from "./getMaxValue";

describe("values가 들어오면 최댓값을 반환하는 getMaxValue", () => {
  it("빈배열이 들어오면 0을 반환한다.", () => {
    const result = getMaxValue({ values: [] });
    expect(result).toBe(0);
  });

  it("2개 이상의 요소가 들어오면 최댓값을 반환한다.", () => {
    const values: HeatmapProps["values"] = [
      { value: 10 },
      { value: 20 },
      { value: 5 },
    ];
    const result = getMaxValue({ values });
    expect(result).toBe(20);
  });

  it("1개의 요소가 들어오면 해당 value를 반환한다.", () => {
    const values: HeatmapProps["values"] = [{ value: 10 }];
    const result = getMaxValue({ values });
    expect(result).toBe(10);
  });
});

시각적 테스트가 필요하다

peerDependency. 즉, react-dom을 가지고 있음을 전제로 해놓고 도구를 만들기 때문에, 실제로 사용하는 것은 불가능했다. 개발 도구이자 렌더링한 결과를 보여주는 Storybook을 설치했다. 다양한 props를 스토리를 작성하여 전달할 수 있기 때문에 시각적 테스트를 할 때 유리하다.

 

통합테스트가 필요하다

스토리북으로 컴포넌트 개발을 진행할 수 있었지만, 실제로 브라우저에서 어떻게 사용될지를 알려주고 싶었다. 따라서 패키지 내부에 CRA를 통해 컴포넌트가 렌더링되는 페이지를 구성해주었다. 그러나 패키지 내부에 하나의 프로젝트를 더하는건 시간이 오래걸리고 복잡하다는 큰 단점이 있었다.

이에 cypress를 통해 테스트 코드를 통한 통합 테스트를 진행하였다. 가상으로 컴포넌트를 렌더링했을 때 각 요소가 존재하는지, 사용자와의 상호작용이 발생했을 때 원활하게 동작하는지에 대한 테스트 코드를 작성할 수 있다.

describe('CalendarHeatmap Component', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('CalendarHeatmap 컴포넌트 렌더링', () => {
    cy.get('h1').contains('Calendar Heatmap Demo');
    cy.get('.calendar-heatmap-container').should('exist');
    cy.get('.heatmap-container').should('exist');
  });

  it('Month and Week label 렌더링', () => {
    cy.get('.calendar-heatmap-container').within(() => {
      cy.get('.month-label').should('exist');
      cy.get('.week-label').should('exist');
    });
  });

  it('1월 1일 hovering 시 heatmap-hover-element 렌더링', () => {
    cy.get('.calendar-heatmap-container .calendar-heatmap')
      .children()
      .first()
      .trigger('mouseover')
      .then(() => {
        cy.get('.heatmap-hover-element').should('contain', '2024-01-01');
      });
  });

  it('heatmap elements의 색상 테스트', () => {
    const colorMap = {
      0: "#ccd6e3",
      10: "#FF5733",
    };

    cy.get('.calendar-heatmap-container .calendar-heatmap')
      .children()
      .each(($el, index) => {
        const value = [10, 20][index];

        if (value !== null && value !== undefined) {
          const expectedColor = colorMap[value];
          cy.wrap($el).should('have.css', 'background-color', expectedColor);
        }
      });
  });
});

Props에 따른 UI를 확인해야 한다

https://66968c0a991b747e3708caa9-uobtrmwlkx.chromatic.com/

 

storybook - Storybook

 

66968c0a991b747e3708caa9-uobtrmwlkx.chromatic.com

시각적 테스트의 맥락으로, 스토리북에서 props 별 스토리를 작성하면 스토리북을 통해 여러가지 케이스를 확인할 수 있다. 이건 테스트 코드라고 하긴 뭐하지만, chromatic 배포를 통해 디자이너와의 소통이 가능해지고 공통 컴포넌트를 활용하기 좋다. 

 

단점은,, 처음부터 계획한 도입이 아니었기 때문에 atom, molecule, organism 외에는 다 비즈니스 로직이 섞여 들어가서 스토리를 작성하기 어려움이 있다는 거다. 이번 기회에 Headless UI에 대해 더 명확한 이해를 할 수 있었다.

디자이너와 소통해야한다

애니메이션같은 경우 디자이너는 피그마를 통해서 하나씩 설정해주고 화면 녹화를 해줘야해서 뭔가 더 편한 소통 방법이 필요했다. 그래서 일단 빠르게 mock으로 구현해서 스토리북으로 보여주고 크로마틱에 배포해서 애니메이션 가능하다고 바로 전달드렸다. 

사실 이 애니메이션이 묘하게 서로 전문이 아니어서 뭐가 되는거고 뭐가 안되는건지, 어떻게 작동할지 서로 애매하게의문인 경우가 많았는데, 이를 해소할 수있어서 좋았다.  

 

로직을 내가 제어해서 케이스를 확인해야 한다

 초반에는 Headless UI가 의존성이 너무 강해서 선호하지 않았다. 이후 비즈니스 로직과 스타일의 분리가 재사용에 효과적인 것을 이해하고 자주 사용하였다. 하지만 재사용하지 않아도 되는 컴포넌트의 경우 그냥 하나의 컴포넌트로 만들었다. 문제는, 비즈니스 로직이 섞여 들어가니 상황별 케이스에 따른 UI를 내가 제어할 수 없었다.

가장 전역에서 만나는 케이스로, 헤더 컴포넌트 내에서 localStorage를 확인한 후에 조건에 따라 렌더링을 했기 때문에 내가 케이스를 제어할 수 없었다. 따라서 UI와 로직을 분리하여 로그인 상태 판단을 외부에서 하고 들어왔고, 각각의 케이스를 스토리로 작성하여 확인할 수 있었다.

서버에 의존하지 않고 케이스를 확인해야 한다

API를 불러온 후의 값에 따라 UI가 변경될 때 역시, 케이스를 제어해서 보여줄 수 있어야 한다. 이건 일반적으로 mocking을 통해해결할 수 있다.

import { AxiosError } from 'axios';
import axiosInstance from '../../api';
import getMemberList from './getMemberList'; 

jest.mock('../../api'); 

describe('getMemberList', () => {
  it('api 연결이 성공하여 데이터를 가져온다', async () => {
    const mockData = {
      data: {
        data:  [
        	{ memberId: 1, nickname: '김영원', profileImage: 'CABBAGE', part: 'PM' },
       	    { memberId: 1, nickname: '안재윤', profileImage: 'TOMATO', part: 'DESIGN' },
        	{ memberId: 1, nickname: '최정흠', profileImage: 'BLUEBERRY', part: 'BACKEND' },
        	{ memberId: 1, nickname: '박소현', profileImage: 'CUCUMBER', part: 'FRONTEND' },
        	{ memberId: 1, nickname: '정아현', profileImage: 'CARROT', part: 'FRONTEND' },
        	{ memberId: 1, nickname: '임정우', profileImage: 'STRAWBERRY', part: 'BACKEND' },
      	],
      },
    };
    axiosInstance.get.mockResolvedValue(mockData);

    const result = await getMemberList(1);
    expect(result).toEqual(mockData.data.data);
  });

 

기능정의서를 코드에 반영해두고 싶다

스토리북을 통해서 시각적 테스트 및 개발도구가 중요하다는걸 깨닫고 웬만하면 설치하는 편인데, E2E 테스트에 대해서는 여전히 의문이었다. 그러다가 기능정의서가 1.7 버전으로 올라와서 전부 다시 확인하고 있는 걸 보면서 깨달았다. 이래서 E2E 테스트를 하고, TDD를 하는군나..

 

아직 통합 테스트는 진행 중이어서 다른 포스트로 작성하겠다.


웹 접근성 테스트 

개인적으로 lighthouse를 사용할 때 기다리고 페이지를 하나씩 검사해야하다보니 불편함을 느꼈다. 이런 웹 접근성도 CI/CD 및 시각화 도구를 통해 자동화할 수 있다.

 

axe DevTools - Web Accessibility Testing - Chrome 웹 스토어 (google.com)

 

axe DevTools - Web Accessibility Testing - Chrome 웹 스토어

Accessibility Checker for Developers, Testers, and Designers in Chrome

chromewebstore.google.com

 

 


https://techblog.woowahan.com/17404/

'트러블이슈' 카테고리의 다른 글

XSS 공격을 막아보자  (0) 2024.08.08
테스트로 기능정의서 두번씩 안보기로 했다  (0) 2024.07.19
시도를 해보자  (0) 2024.07.08
프로젝트 구조 변경하기  (0) 2024.07.06
Lighthouse로 성능 개선하기  (0) 2024.07.04