[Wanted]_Week4-2_소프트웨어 테스트

hanseungjune·2023년 7월 22일
0

Wanted

목록 보기
20/21
post-thumbnail

소프트웨어 테스트란?

소프트웨어를 통해서 테스트를 할 경우 아래와 같은 이점을 얻을 수 있습니다.

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

소프트웨어 테스트를 개발자가 작성할 경우 얻을 수 있는 가장 큰 이점은 바로 “피드백을 빠른 주기로 개발 중에 받을 수 있다”입니다. 이는 곧 개발자의 생산성 향상을 야기합니다. 그리고 자동화된 테스트는 추후 CI/CD와 같은 프로세스에서도 해당 소스코드들이 정상적으로 동작하는지 확인하는 과정에서 사용할 수 있는 등 다방면으로 활용할 수 있습니다.

소프트웨어 테스트의 종류

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

1) Unit Test

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

2) Integration test

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

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

E2E 테스트는 실제 유저가 애플리케이션을 사용하는 것과 유사한 환경을 구축한 후 실제 유저의 동작을 흉내내서 테스트하는 것입니다. 이는 실제 유저의 동작 흐름을 그대로 모방해서 테스트할 수 있다는 장점이 있지만 환경을 구축해야 하며, 유저의 행동 시나리오를 구축해야 하기에 굉장히 비싼 테스트입니다. 따라서 실제 개발 환경에서 유닛테스트와 통합테스트처럼 소스코드에 변화가 있을때마다 빈번하게 수행할 수는 없으며, 대부분 핵심 기능에 대해서 E2E 테스트를 구축 한 후 확인이 필요한 순간에만 실행하는 것이 일반적입니다. 프론트엔드에서의 E2E 테스트는 실제 브라우저와 유사한 환경을 구축 한 후, 거기서 실제로 여러가지 이벤트를 발생시킨 후 일련의 과정을 테스트하는 방식으로 진행됩니다. e.g., 회원가입 전체 플로우를 테스트

Jest를 활용한 JavaScript 테스트

소프트웨어 테스트는 현대 개발에서 필수적인 요소로 자리잡았기에 각 진영마다 소프트웨어 테스트를 위해서 사용할 수 있는 라이브러리들이 개발되어 있습니다. 그 중 자바스크립트 진영에서는 Jest, Mocha, chai 등의 테스트 라이브러리들이 대표적으로 사용되고 있습니다. 이중에서 Jest가 주간 약 1800만 다운로드의 압도적인 점유율을 가지고 있으며, CRA에서도 기본적으로 Jest를 포함해서 환경을 구성해주는 등 사실상 표준으로서 사용되고 있기에 이후 과정에서도 Jest를 기준으로 테스트코드를 작성하겠습니다.

Jest 사용법

Jest는 기본적으로 *.test.* 의 형태를 가진 파일을 테스트 파일로 인식하며, 해당 파일안에 있는 코드를 실행합니다. 우리가 일반적으로 소프트웨어를 테스트 하는 과정을 생각해보면

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

위와 같은 과정을 거칩니다.

테스트 코드를 작성하는 것도 마찬가지로 테스트를 하고자 하는 동작을 수행한 뒤 그 결과가 기대한 상황과 일치하는지를 검증하는 과정을 코드로 작성하게 됩니다.

Jest에서는 이를 기대한 상황과 일치하는지 판단하는 함수들을 matchers라고 표현합니다. 따라서 Jest의 코드는 아래와 같은 형태를 띄게 됩니다.

  1. 특정한 동작을 수행한다.
  2. matcher를 통해서 실제 결과와 기대값이 맞는지를 검증한다

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

실제 코드를 통해서 알아보겠습니다.

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 :

    • Object의 경우 참조값이 다르기에, toBe를 활용할 경우 실제 각 객체의 내용이 같더라도, 일치하지 않다고 판단되게 됩니다. 따라서 객체를 상호 비교할 때는 toEqual matcher를 활용해야 합니다. toEqual 은 객체의 각 요소들을 재귀적으로 검사하면서 두 객체가 동일한지 판단해줍니다.
    const obj = {hello:"world"};
    
    test("object equal", () => {
        expect(obj).toBe({hello:"world"}) // X
        expect(obj).toEqual({hello:"world"}) // O
    });
  3. toBeNull, toBeUndefined

  4. toBeGreaterThan, toBeGraterThanOrEqaul, toBeLessThan, toBeLessThanOrEqaul : 숫자값을 검증할 때 유용하게 사용할 수 있는 matcher입니다.

  5. toContain : Iterable한 객체들이 특정한 요소를 포함하고 있는지 검증할 때 사용할 수 있습니다.

const iterable = [1,2,3,4,5];

test("iterable contain 3", () => {
	expect(obj).toContain(3)
});
  1. not : matcher의 기대값을 반대로 변경해줍니다.
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).not.toBeUndefined();
});

Jest와 RTL을 이용한 리액트 테스트

Jest를 통해서 순수한 자바스크립트 코드를 테스트할 수 있게되었지만, 리액트는 UI 라이브러리기에 리액트의 동작을 순수한 Jest만으로 테스트하기에는 다소 어려움이 있습니다. 따라서 UI를 렌더링하는 부분을 책임지는 react-dom 라이브러리에서 제공해주는 별도의 기능들과 결합하여 테스트를 수행해야합니다. 이러한 과정을 매 테스트마다 수행하기에는 다소 번거롭기에 이를 대신해서 리액트 컴포넌트를 테스트 할 때 사용할 수 있는 라이브러리들이 있습니다.

컴포넌트의 UI와 동작을 테스트 할 때 많이 사용되는 라이브러리로는 Enzyme와 React-Testing-Library(RTL)이 존재합니다. 이 중 Enzyme는 “구현"을 테스트하는 것에 초점이 맞춰져 있는 라이브러리이며 RTL은 “결과"를 테스트하는 것에 초점이 맞춰진 라이브러리입니다. 이 중 어떤 것을 사용해야한다는 정답은 존재하지 않으며, 테스트하고자 하는 목적에 따라서 두개를 적절하게 선택해서 사용하기도 합니다. 이번 시간에는 이 중에서 리액트 공식문서에서 권장하고 있으며, CRA에 기본 구성으로 포함된 점, npm 다운로드 수가 더 많은 점, “결과"를 테스트하는 방식을 활용하기 위해서 RTL 라이브러리를 기준으로 이후 과정을 진행하겠습니다.

React Testing Library

리액트 테스팅 라이브러리는 리액트 컴포넌트를 테스트 할 때는 내부에서 어떤식으로 세부적인 구현이 이루어졌는지를 테스트하는 것이 아니라, 행위에 대해서 어떤 결과가 나와야하는지에 초점을 두어야 한다는 철학을 기반으로 만들어졌습니다.

세부적인 구현을 기반으로 테스트를 한다는 것은 “특정 버튼을 클릭하면 컴포넌트의 state가 변한다. 그리고 이게 UI에 반영된다”처럼 동작을 기반으로 테스트를 구성하는 것입니다.

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

이처럼 결과를 중심으로 테스트를 작성하게 되면 컴포넌트의 겉보기 동작은 그대로 유지하며, 내부적인 구현은 얼마든지 변경할 수 있습니다. 예를들어 구현을 테스트 했을 경우 상태관리를 useState가 아닌 Recoil, Redux 등으로 변경했을 경우 테스트코드를 다시 작성해야 하지만, 결과를 중점으로 테스트 했을 경우 상태관리가 어떻게 바뀌든 최종적으로 버튼을 클릭했을 때 화면에 2라는 숫자가 나온다는 결과만 동일하다면 테스트코드를 변경할 필요가 없습니다.

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은 앞서 말했듯이 렌더링, 요소 접근 등의 기능을 수행해줍니다. 하지만 테스트를 위해서는 이 요소들이 DOM상에 존재하는지, 그리고 특정 프로퍼티를 가지고 있는지 등을 검사할 수 있어야 합니다. 이는 DOM에 관련된 기능이기에 jest에서는 이러한 기능을 수행할 수 있는 matcher들을 기본적으로 포함하고 있지는 않습니다. 이러한 matchers를 추가하기 위해서는 jest-dom 라이브러리를 사용해야 합니다(마찬가지로 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();
})

RTL의 기본적인 활용

  • screen: screen은 말 그대로 현재 렌더링이 진행되고 있는 화면을 의미합니다. DOM상에서는 document.body와 동일하다고 할 수 있습니다. DOM API와 마찬가지로 screen을 통해서 현재 화면에 렌더링된 요소들에 관련된 여러 메서드들을 확인할 수 있습니다.
  1. screen.debug

    • screen.debug 메서드는 테스트 과정에서 출력된 DOM을 확인하고 싶을 때 사용할 수 있습니다. 때때로 테스트 과정에서 원하는 결과가 나오지 않았는데, 어디서 잘못된지 파악하기 힘든 상황이 발생할 수 있습니다. 그런데 테스트는 실제 브라우저에서 실행되는 것이 아니기에 브라우저의 개발자도구를 통해서 DOM 트리를 확인하는 동작이 불가능합니다. 이럴 때 사용할 수 있는게 screen.debug 메서드입니다. 이 메서드를 호출하면 호출한 시점의 렌더링된 DOM tree를 확인할 수 있습니다.
  2. 요소를 가져오는 메서드들

    • DOM에서 제공하는 getElementBy~~~, querySelector 등의 API와 마찬가지로 RTL에서도 렌더링된 요소들에게 접근할 수 있는 메서드들이 존재합니다.
    • 이러한 메서드는 크게 3가지 종류로 구분됩니다.
      • getBy~~~ : 해당 요소가 현재 DOM상에 있는지 동기적으로 확인합니다. 만약 찾는 요소가 없을 경우 예외를 던집니다.
      • findBy~~~ : 해당 요소가 현재 DOM상에 있는지 비동기적으로 확인합니다. 해당 요소를 찾기 위해 일정 시간을 기다리며, 시간이 지난 후에도 찾을 수 없는 경우 예외를 던집니다.
      • queryBy~~~ : getBy와 동일하게 동작하지만 찾는 요소가 없을 경우 예외를 던지는 것이 아닌 null을 반환합니다.
  • 예시
    • getByRole
    • getByText
    • getByLabelText
    • getByPlaceholderText
    • getByDisplayValue
    • getByAltText
    • getByTitle
    • getByTestId
  1. userEvent

    • 실제 DOM상에서 유저처럼 이벤트를 발생시키기 위해서는 testing-library/user-event 라이브러리를 사용할 수 있습니다.
    • userEvent.이벤트명(엘리먼트) 의 형태로 활용할 수 있습니다.
    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 button = screen.getByText('Click me!')
    
        userEvent.click(button);
    })

TDD란?

TDD는 Test-Driven-Development의 약어로서 소프트웨어를 개발하는 여러 방법론 중 하나입니다.

TDD의 핵심은 기존에는 테스트 코드를 먼저 작성하고, 그 후에 실제 코드를 작성하는 것입니다. 일반적인 개발 흐름은 코드작성 → 테스트코드 작성의 흐름을 따르지만, TDD는 실제 코드를 작성하기도 전에 테스트 코드부터 작성을 시작합니다.

TDD는 크게 Red-Green-Blue 3가지 단계를 거칩니다.

  1. Red: 실제 구현을 하기 전에, 먼저 실패하는 테스트 코드를 작성한다. 그 후 테스트를 실행한다. 실제 코드가 작성되지 않았기에 테스트 코드는 당연히 실패한다.
  2. Green: 테스트를 통과하기 위해 가장 간단한 형태로 코드를 작성한다. 그 후 테스트를 실행한다. 테스트는 실제 구현이 되었기에 통과한다.
  3. Blue: Green 단계의 코드를 더 좋은 형태로 리팩터링한다. 이 과정에서 지속적으로 테스트를 실행해서 테스트가 통과하는지 확인한다.

이러한 방식으로 개발을 하게 되면 아래와 같은 이점을 얻을 수 있습니다.

  1. 코드 작성 과정에서 확신 및 자신감을 얻을 수 있게 된다.
    • 코드의 동작에 대한 테스트가 작성되어 있으며 이를 충족하는 것을 실시간으로 피드백 받으며 진행하기에 코드가 제대로 동작할것이란 확신을 얻을 수 있다.
  2. 구현을 잘못 한 경우 바로 확인할 수 있다.
    • 실제 개발을 하다보면 한참 코드를 작성했는데 실제 브라우저에서 테스트했을 때 원하는 대로 동작하지 않아 어느 지점부터 잘못됬는지 파악하기 위해서 전체 개발과정을 돌아봐야 하는 경우는 꽤나 자주 발생하는 상황입니다, 하지만 테스트 코드가 준비된 상황에서 개발을 하게 되면 특정 지점에서 잘못된 동작이 발생하면 바로 알 수 있기에 바로 구현을 수정할 수 있습니다. 즉 디버깅 과정이 용이해진다는 장점이 있습니다. i
  3. 코드의 동작이 명확해진다.
    • 어떻게 코드를 짜야할 지가 아닌 무슨 코드를 짜야할 지 부터 고민하게 된다.”
    • 테스트 코드를 작성하기 위해서는 이 코드가 어떤식으로 동작해야하는지를 먼저 생각해야합니다. 기존의 방식대로 일단 코드부터 작성하게 되면 나중에 이 코드에서 하고자하는 역할과, 구현해야 되는 동작들이 뒤죽박죽 섞이게 되는 상황이 발생할 수 있지만. 테스트 코드를 미리 작성한다면 이 과정에서 자연스럽게 이 코드가 해야하는 동작과 어떤 인터페이스가 갖춰줘야하는지를 생각하고 표현하게되므로 자연스럽게 코드의 동작과 관심사의 분리가 비교적 잘 이루어진다는 장점이 있습니다,
    • 즉, 어떻게 구현할지에 대한 생각보다 사용자의 관점에서 어떻게 사용할지에 초점을 맞춰서 생각할 수 있게 도와주기에 코드의 세부사항에 빠져들지 않고, 높은 수준의 목표를 항상 염두에 두면서 개발을 할 수 있게 해준다는 장점이 있습니다. 따라서, 내가 무엇을 만들어야 할지는 알지만, 어떻게 만들어야 할 지 모르는 상황일 때 적용하기 좋습니다
profile
필요하다면 공부하는 개발자, 한승준

0개의 댓글