소프트웨어 테스트 feat. react

2023. 5. 20. 21:48프론트엔드

소프트웨어 테스트

테스트란 무언가 동작하는지 확인하는 행위이다.

야 이 제품이 잘 돌아가는지 테스트 해봐~

 

소프트웨어 테스트란 소프트웨어가 우리가 의도한 대로 동작하는지 테스트하는 행위를 의미한다.

옛날에는 개발자는 개발만하고, 소프트웨어가 올바르게 동작하는지 확인하는 테스트팀이 별도로 존재했고, 개발자는 테스팅 과정에 큰 관여를 하지 않았다.

 

그러나 최근에는 개발자가 자동화 할 수 있는 소프트웨어 테스트를 구현하고 실행하는것이 보편화 되고 있다.

자동화된 테스트란?

컴퓨터를 통해서 소프트웨어를 테스트하는것을 의미합니다.

 

소프트웨어를 통해서 테스트를 할 경우…

  1. 컴퓨터를 통해서 실행하기에 사람이 실행하는 것보다 빠르다.
  2. 정해진 스크립트에 따라 일관성있게 동작하기에 사람이 테스트 할 경우 발생할 수 있는 휴먼에러를 기피할 수 있다.

소프트웨어 테스트를 개발자가 작성할 경우 얻을 수 있는 가장 큰 이점은 바로 “피드백을 빠른 주기로 개발 중에 받을 수 있다”

→ 개발자가 소프트웨어 테스트를 작성하고 이를 실행하며 개발을 할 경우 만들어진 소프트웨어가 정상적으로 동작하는지를 수시로 확인할 수 있으며, 이 피드백을 받는 주기가 굉장이 빠름

→ 소프트웨어의 동작에 대해서 피드백을 받으며 올바른 방향으로 수정해나갈 수 있고

→ ❗소프트웨어가 최소한 작성한 테스트 코드 안에서는 제대로 동작한다는 확신을 가질 수 있게 된다.

내가 이렇게 소프트웨어를 만들었는데, 당신이 의도한 대로 돌아가는지 테스트 코드로 다 확인도 해봤소!!

⚠️ 자동화된 테스트는 추후 CI/CD와 같은 프로세스에서도 해당 소스코드들이 정상적으로 동작하는지 확인하는 과정에서 사용할 수 있는 등 다방면으로 활용할 수 있습니다.

소프트웨어 테스트의 종류

소프트웨어 테스트는 테스트가 확인하고자 하는 범위, 복잡성에 따라서 크게 3가지 종류로 나눌 수 있습니다.

1) Unit Test

  • 유닛 테스트는 테스트중에서 가장 로우 레벨, 가장 작은 범위를 테스트 한다.
  • 유닛 테스트는 개별 함수, 메서드, 클래스, 컴포넌트 등의 동작을 테스트 한다.
  • 유닛 테스트는 제일 간단한 형태의 테스트로 실행하는데 가장 적은 비용이 든다. 따라서 유닛 테스트는 개발 과정에서 가장 빈번하게 수행할 수 있는 테스트이다.

2) Integration test

  • 통합 테스트는 두개 이상의 모듈(Unit)이 결합해서 동작을 잘 수행하는지에 대한 테스트
  • 컴포넌트 안에서 렌더링이 정상적으로 되는지만을 테스트 ⇒ unit test
  • 컴포넌트가 Redux등의 상태관리 라이브러리와 통합했을 때 두 모듈이 잘 어우러져서 최종적으로 의도한 결과를 도출하는지 테스트 ⇒ intergration test
  • 통합 테스트는 여러 모듈들을 통합하는 과정이 필요하기에 유닛 테스트보다는 많은 비용이 드는 테스트라고 할 수 있습니다.

3) End-to-End Test(E2E Test)

  • E2E 테스트는 실제 유저가 애플리케이션을 사용하는 것과 유사한 환경을 구축한 후 실제 유저의 동작을 흉내내서 테스트하는 것
  • 이는 실제 유저의 동작 흐름을 그대로 모방해서 테스트할 수 있다는 장점이 있지만 환경을 구축해야 한다.
  • 유저의 행동 시나리오를 구축해야 하기에 굉장히 비싼 테스트입니다.

실제 개발 환경에서 유닛테스트와 통합테스트처럼 소스코드에 변화가 있을때마다 빈번하게 수행할 수는 없으며, 대부분 핵심 기능에 대해서 E2E 테스트를 구축 한 후 확인이 필요한 순간에만 실행하는 것이 일반적

프론트엔드에서의 E2E 테스트는 실제 브라우저와 유사한 환경을 구축 한 후, 거기서 실제로 여러가지 이벤트를 발생시킨 후 일련의 과정을 테스트하는 방식으로 진행됩니다.

e.g., 회원가입 전체 플로우를 테스트

저의 얕은 지식으로 E2E테스트를 할 수 있는 테스트 라이브러리에는 Cypress 가 있습니다. ( 유저의 행동을 코드로 정의 해서 테스트 가능 )

JEST 를 이용한 테스트 코드 작성해보기

우리는 FE를 공부하는 동료들이니까 JS 에서 사용하는 Jest를 알아보자.

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
Jest 는 간단함에 초점을 둔 자바스크립트 테스트 라이브러리 입니다.

It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!
요즘 사용하는 최신 기술에 같이 사용할 수 있어요~

blar blar~ : https://jestjs.io/docs/getting-started

  • Jest가 주간 약 1800만 다운로드의 압도적인 점유율을 가지고 있다.
  • CRA에서도 기본적으로 Jest를 포함해서 환경을 구성해주는 등 사실상 표준으로서 사용되고 있음.

Jest 사용법

Jest는 기본적으로 *.test.* 의 형태를 가진 파일을 테스트 파일로 인식하며, 해당 파일안에 있는 코드를 실행함.

우리가 일반적으로 테스트 하는 과정을 생각해보면

  1. 특정한 동작을 수행한다.
  2. 동작을 수행한 결과가 기대한 상황과 일치하는지 판단한다.

위와 같은 과정을 거칩니다. 이것을 소프트웨어 관점에서 생각해보면

  1. 특정한 동작을 수행한다.( Unit 단위로 )
  2. 동작을 수행한 결과가 기대한 상황과 일치하는지 판단한다. 근데 matchers를 이용해서

이때 하나의 특정한 동작을 수행하기 위해서 test() 또는 it() 함수를 활용해서 확인할 수 있다.

⚠️ *.test.* 파일에서 test, it, expect가 미리 정의되어 있기 때문에 오류가 나지 않는다.

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

it('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});
  • 테스트는 test(”테스트 이름", callback) 의 형태를 띄게 되며
  • callback 안에서 원하는 동작을 수행, expect(실제 결과 값).matcher() 의 형태
  • 하나의 콜백 안에서 여러 expect를 수행할 수 있으며, 그 중 하나라도 기대값과 일치하지 않을 경우, 해당 테스트는 실패!
const sum = (x,y) => x + y;

test('sum', () => {
  expect(sum(2,2)).toBe(4); // 통과
    expect(sum(3,1)).toBe(5); // 실패, sum test 실패
});

Jest에서 주로 사용되는 matcher

  1. toBe : expect의 인자가 toBe의 인자와 일치하는지를 검사합니다.
  2. toEqual : 참조형 변수를 비교 toEqual 은 객체의 각 요소들을 재귀적으로 검사하면서 두 객체가 동일한지 판단해줍니다.
  3. const obj = {hello:"world"}; test("object eqaul", () => { expect(obj).toBe({hello:"world"}) // X expect(obj).toEqual({hello:"world"}) // O });
  4. toBeNull, toBeUndefined : Null, Undefined인지 검사
  5. toBeGreaterThan, toBeGraterThanOrEqaul, toBeLessThan, toBeLessThanOrEqaul : 숫자값을 검증할 때 유용하게 사용할 수 있는 matcher
  6. toContain : Iterable한 객체들이 특정한 요소를 포함하고 있는지 검증할 때 사용
  7. const iterable = [1,2,3,4,5]; test("iterable contain 3", () => { expect(obj).toContain(3) });
  8. not : matcher의 기대값을 반대로 변경해줍니다.
  9. test('null', () => { const n = null; expect(n).toBeNull(); expect(n).not.toBeUndefined(); });

더 다양한 matcher를 보려면..

Using Matchers · Jest

react에서의 테스트

😒 리액트에서는 테스트를 어케 해요?!?

리액트에서는 컴포넌트를 주로 개발하기 때문에 우리가 테스트를 해야 하나? 라는 의심을 가진적이 없었을 수도 있다. 어차피 알아서 리액트가 랜더링 해주니까~

리액트 테스팅 라이브러리는 컴포넌트의 행위에 대해서 어떤 결과가 나와야 하는지 초점을 두어야 한다는 철학을 기반으로 만들어 졌다.

동작을 기반으로 테스트를 구성 ? 이는 어차피 리액트( 개발하는 사람 포함 )에서 책임을 질 일이다.

  • “특정 버튼을 클릭하면 컴포넌트의 state가 변한다. 그리고 이게 UI에 반영된다”

결과에 대해서 테스트를 한다는 것

  • “특정 버튼을 클릭하면 화면에 2라는 숫자가 나와야 한다"처럼 최종적으로 유저가 어떤 UI를 볼 수 있어야 하는지에 초점을 두고 테스트를 하는 것

→ 결과를 중심으로 테스트를 작성하게 되면 컴포넌트의 겉보기 동작은 그대로 유지하며, 내부적인 구현은 얼마든지 변경할 수 있다. ( 조금은 개발의 구현 방식에 대해 열어둬서 리펙토링을 얼마든지 할 수 있게 한다는것! )

React Test Library

RTL은 이러한 철학에 기반을 두었기에 리액트 컴포넌트를 렌더링하고, 특정 요소에 접근할 수 있게 하는 기능을 제공

testing-library/user-event 의 경우 유저의 행동과 마찬가지로, 특정 엘리먼트에서 이벤트를 발생시키는 기능을 제공

import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import App from './App'

test('App rendering', () => {
  render(<App />)

    const header = screen.getByText('Hello World')
    const button = screen.getByText('Click me!')

    userEvent.click(button);
})

RTL은 통상 jest-dom 라이브러리와 함께 사용됩니다. RTL은 앞서 말했듯이 렌더링, 요소 접근 등의 기능을 수행한다.

jest-dom : DOM상에 존재하는지, 그리고 특정 프로퍼티를 가지고 있는지 등을 검사할 수 있는 matcher를 답고 있는 라이브러리 ( CRA에 포함 되어 있음 )

import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import App from './App'

test('App rendering', () => {
  render(<App />)

    const header = screen.getByText('Hello World')
    const button = screen.getByText('Click me!')

    userEvent.click(button);

    expect(header).toBeInTheDocument();
    expect(button).toBeDisabled();
})

React Test Libray의 기본적인 활용

screen

현재 렌더링이 진행되고 있는 화면, DOM상에서는 document.body와 동일함.

DOM API와 마찬가지로 screen을 통해서 현재 화면에 렌더링된 요소들에 관련된 여러 메서드들을 확인 가능함.

  1. screen.debug이 메서드를 호출하면 호출한 시점의 렌더링된 DOM tree를 확인할 수 있다.
  2. 출력된 DOM을 확인하고 싶을 때 사용
    테스트는 실제 브라우저에서 실행되는 것이 아니기에 브라우저의 개발자도구를 통해서 DOM 트리를 확인하는 동작이 불가능 → 이럴 때 사용할 수 있는게 screen.debug
  3. 요소를 가져 오는 메서드
    • DOM에서 제공하는 getElementBy~~~, querySelector 등의 API와 마찬가지로 RTL에서도 렌더링된 요소들에게 접근할 수 있는 메서드들을 제공 크게 3가지 종류로 구분
      • getBy~~~ : 해당 요소가 현재 DOM상에 있는지 동기적으로 확인합니다. 만약 찾는 요소가 없을 경우 예외를 던집니다.
      • findBy~~~ : 해당 요소가 현재 DOM상에 있는지 비동기적으로 확인합니다. 해당 요소를 찾기 위해 일정 시간을 기다리며, 시간이 지난 후에도 찾을 수 없는 경우 예외를 던집니다.
      • queryBy~~~ : getBy와 동일하게 동작하지만 찾는 요소가 없을 경우 예외를 던지는 것이 아닌 null을 반환합니다.
    • 예시
      • getByRole : 시멘틱태그를 가져옴
      • getByText : text를 가져옴
      • getByLabelText : label을 기준으로 가져옴
      • getByPlaceholderText
      • getByDisplayValue : 입력한 input value로 가져옴
      • getByAltText : alt 속성으로 찾는것
      • getByTitle : title 속성으로 찾는 것
      • getByTestId
  4. userEvent
    • 실제 DOM상에서 유저처럼 이벤트를 발생시키기 위해서는 testing-library/user-event 라이브러리를 사용할 수 있다.
    • userEvent.이벤트명(엘리먼트)

타입스크립트로 jest를 작성하는 것은 조금 trouble shooting을 겪어야 할 것 같다.

Property 'toBeInTheDocument' does not exist on type 'Matchers'

https://github.com/testing-library/jest-dom