티스토리 뷰

[ 소프트웨어 테스트 ]

# 소프트웨어 테스트란?

테스트란 무언가 동작하는 지를 확인하는 행위, 그렇다면 소프트웨어 테스트란 소프트웨어가 의도한 대로 동작하는지 확인하는 행위를 의미한다고 볼 수 있다.

 

자동화된 테스트란 사람이 손수 테스트하는 것이 아닌 컴퓨터를 통해서 소프트웨어를 테스트하는 것을 의미한다.

소프트웨어를 통해서 테스트를 수행하면 어떤 점이 좋을까?

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

또한 개발자가 소프트웨어 테스트를 작성할 경우에는 피드백을 빠른 주기로 개발 중에 받을 수 있다는 점이다.

즉 테스트를 작성하고 수시로 테스트를 동작하여 소프트웨어가 정상적으로 동작하는지 확인할 수 있으며, 바로바로 피드백을 받을 수 있어 올바른 방향으로 수정할 수 있다. 또한 소프트웨어가 최소한 작성한 테스트 코드 안에서는 제대로 동작한다는 확신을 가질 수 있다. 이는 곧 개발자의 생산성 향상을 야기한다. 자동화된 테스트는 추후 CI/CD와 같은 프로레스에서도 해당 소스코드들이 정상적으로 동작하는지 확인하는 과정엣 사용할 수 있는 등 다방면으로 활용할 수 있다.

 

# 소프트웨어 테스트의 종류

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

## 1) Unit Test ⭐⭐

단위 테스트, 테스트 중에서 가장 로우 레벨로 가장 작은 범위 테스트이다.

개별 함수, 메서드, 클래스, 컴포넌트 등의 동작을 테스트, 딱 한 가지 범위 내에서 테스트하는 것.

즉 제일 작고 간단한 테스트로 비용 또한 적게 든다.

따라서 유닛 테스트는 개발 과정에서 가장 빈번하게 수행할 수 있는 테스트이다.

## 2) Integration test ⭐⭐

통합 테스트, 두 개 이상의 모듈을 결합해서 테스트.

특정 컴포넌트 내에서 렌더링 여부를 확인하는 것은 유닛 테스트.

 

이 컴포넌트가 Redux 등의 상태관리 라이브러리와 통합했을 때 두 모듈이 잘 어우러져서 최종적으로 의도한 결과를 도출하는지 테스트하는 것은 통합 테스트.

유닛 테스트 보다 조금 비용이 많이 든다.

 

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

실제 유저가 사용하는 환경을 구축한 후 실제 유저 동작을 예측하고 테스트하는 것.

브라우저와 비슷한 환경에서 애플리케이션을 테스트하며, 실제로 여러 가지 이벤트를 발생시킨 후 일련의 과정을 테스트

가장 안정적이다. 왜냐 실제 유저의 동작 흐름을 그대로 녹여냈기 때문에,

그러나 비용이 많이 드는 테스트. 그렇기 때문에 대부분 핵심 기능에 대해서 E2E 테스트를 구축한 후 확인이 필요한 순간에만 실행하는 것이 일반적이다.

 

## + Manual Text (사람이 직접 수동으로 테스트하는 것)

# Jest를 활용한 JavaScript 테스트

그렇다면 어떻게 테스트를 하는 걸까? 

소프트웨어 테스트는 필수적인 요소이다. 그중 자바스크립트 진영에서는 Jest, Mocha, chai 등의 테스트 라이브러리들이 대표적으로 사용되고 있다. 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(내가 어떤 값을 또는 결과값을 기대하는 지, 콜백함수)
                                           	└ expect(동작함수).toBe(동작함수의 결과값)

 

테스트는 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 실패
});

1. toBe : expect의 인자가 toBe의 인자와 일치하는지를 검사한다.

2. toEqual :

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

test("object eqaul", () => {
	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)
});

6. not: matcher의 기댓값을 반대로 변경해다.

test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).not.toBeUndefined();
});

 

** 대부분 파일 하나에 하나의 테스트 코드를 작성하는데 describe로 묶어서 공통적인 요소의 테스트 가능

test("", () => {});

describe("counterReducer", () => {
//it은 counterReducer를 가리킨다.
	it("should return 2 when prevState is 1", () => {});
	it("should return prevState when action is not provided", () => {});
})

it을 사용하면 들여쓰기가 된다.
결과값은 다음과 같다.

counterReducer
	should return 2 when prevState is 1
	should return prevState when action is not provided

** 테스트 코드가 너무 길고, 다량의 컴포넌트를 테스트할 때, __test__ 폴더 생성 후 폴더 내 테스트 파일을 생성하여 테스트 실행 가능

 

**지난 시간에 배운 thunk를 테스트해보자**

export const thunk = (store) => (next) => (action) => {
	typeof action === "function"
	? action(store.dispatch, store.getState)
	: next(action)

test 코드

// store, next
// action === function
// action with store.dispatch and store.getState

text("thunk should call action with dispatch and getState when action is function", () => {
    const sotre = {
        dispatch: () => {},
        getState: () => {},
    };
    const next = () => {};

    const thunkMiddleware = thunk(store)(next);

	// mockFun
    // const action = () => {};
    const action = jext.fn(); 
    // default: nothing 아무것도 안하지만, 호출 되었는지에 대한 matcher를 가지고  있다.
    // call, matcher
    
    thunkMiddleware(action);
    
    expect(action).toHaveBeenCalled();// 호출 여부 확인
    
    // action call with store.dispatch, store.getState
    
    //인자를 받았는지 확인 matcher
    expect(action).toHaveBeenCalledWith(store.dispatch, store.getState);// 인자와 함께 호출 여부 확인

}

 

// action !== function
// next call with action

test("thunk should call next with action when action is object", () => {
    const sotre = {
        dispatch: () => {},
        getState: () => {},
    };
    
    const next = jext.fn();

    const thunkMiddleware = thunk(store)(next);

    const action = {};

	thunkMiddleware(action);
    
    expect(next).toHaveBeenCalledWith(action);   

});

 

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

리액트는 UI 라이브러리이기에 순수 Jest 만드로 테스트하기에는 다소 어려움이 있다.

이를 react-dom 라이브러리에서 제공하는 여러 가기 기능들을 결합하여 테스트를 수행해야 한다.

이러한 과정을 매 테스트마다 수행하기에는 번거로우며 이러한 문제점을 해결해 줄 수 있는 테스트 라이브러리로 "Enzyme"와 "React Testing Library"이 등장했다.

 

Enzyme는 "구현"을 테스트. (무슨 함수를 호출하고, 무슨 메서드가 호출되어 무슨 값이... )

RTL은 "결과"를 테스트.

 

## React Testing Library

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

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

최종적으로 유저가 어떤 UI를 볼 수 있어야 하는지에 초점을 두고 테스트를 하는 것

 

이처럼 결과를 중심으로 테스트를 작성하게 되면 컴포넌트의 겉보기 동작은 그대로 유지하며, 내부적인 구현은 얼마든지 변경할 수 있다.

 

"구현"에 초점을 두고 테스트를 한다면, useState로 사용하다가 redux, recoil로 변경해서 사용했다면, 결과는 동일하되 내부 실행이 오류가 발생할 수 있다. 그렇기 때문에 최종적으로 실행 결과가 예측과 동일하다면 테스트 코드를 변경할 필요가 없어진다.

 

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);
})

위의 코드는 단순히 이벤트 만들 구성하는 테스트 코드이다. 어떤 결과 값이 나왔는지, 예측하는 코드는 없다.

matcher가 없기에 jest-dom 라이브러리와 함께 사용한다. DOM에 대한 matcher가 필요하기 때문에 jest-dom을 사용.

 

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은 말 그대로 현재 렌더링이 진행되고 있는 화면을 의미한다.

 

1. screen.debug 

  • screen.debug 메서드는 테스트 과정에서 출력된 DOM을 확인하고 싶을 때 사용
  • 테스트는 실제 브라우저에서 실행되는 것이 아니기에 브라우저의 개발자도구를 통해서 DOM 트리를 확인하는 동작이 불가능
  • 브라우저의 개발자도구를 통해서 DOM 트리를 확인할 수 있도록 해주는 메서드

 

2. 요소를 가져오는 메서드들

DOM에서 제공하는 getElementBy~~~, querySelector 등의 API와 마찬가지로 RTL에서도 렌더링 된 요소들에게 접근할 수 있는 메서드들이 존재한다.

  • getBy~~~ : 해당 요소가 현재 DOM상에 있는지 동기적으로 확인합니다. 만약 찾는 요소가 없을 경우 예외를 던진다.
  • findBy~~~ : 해당 요소가 현재 DOM상에 있는지 비동기적으로 확인한다. 해당 요소를 찾기 위해 일정 시간을 기다리며, 시간이 지난 후에도 찾을 수 없는 경우 예외를 던진다.
  • queryBy~~~ : getBy와 동일하게 동작하지만 찾는 요소가 없을 경우 예외를 던지는 것이 아닌 null을 반환한다. (코드 계속 진행)

3. 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. 구현을 잘못한 경우 바로 확인할 수 있다.
    • 실제 개발을 하다 보면 한참 코드를 작성했는데 실제 브라우저에서 테스트했을 때 원하는 대로 동작하지 않아 어느 지점부터 잘못됐는지 파악하기 위해서 전체 개발과정을 돌아봐야 하는 경우는 꽤나 자주 발생하는 상황이다, 하지만 테스트 코드가 준비된 상황에서 개발을 하게 되면 특정 지점에서 잘못된 동작이 발생하면 바로 알 수 있기에 바로 구현을 수정할 수 있다. 즉 디버깅 과정이 용이해진다는 장점이 있다.
  3. 코드의 동작이 명확해진다.
    • 어떻게 코드를 짜야할지가 아닌 무슨 코드를 짜야할지부터 고민하게 된다.”
    • 테스트 코드를 작성하기 위해서는 이 코드가 어떤 식으로 동작해야 하는지를 먼저 생각해야 한다.
    • 기존의 방식대로 일단 코드부터 작성하게 되면 나중에 이 코드에서 하고자 하는 역할과, 구현해야 되는 동작들이 뒤죽박죽 섞이게 되는 상황이 발생할 수 있지만. 테스트 코드를 미리 작성한다면 이 과정에서 자연스럽게 이 코드가 해야 하는 동작과 어떤 인터페이스가 갖춰줘야 하는지를 생각하고 표현하게 되므로 자연스럽게 코드의 동작과 관심사의 분리가 비교적 잘 이루어진다는 장점이 있다.

출처

https://younuk.notion.site/8dfd44b6e25a4189ab3c05f049f84122


인사이트

React Testing은 사실 처음 접해보아서 어려움이 조금 많았다. 실제 4주 차 기업 과제에서 React Testing을 구현하는 데에 어려움이 많았다. 물론 그 어려움이 단순 Jest 문법이 아닌 Vite 환경에서 Vitest로 변경해서 테스트를 진행했었기에 추가적인 환경 설정이 조금 많이 어려웠다.

"프론트엔드가 무슨 테스트야. QA도 아니고...". 이런 생각 안 해본 것은 아니다😂.

지금 당장의 기능 구현도 급한데 테스트까지 진행해야 한다니.. 그래도 테스트를 구현하면 실제 서비스 배포 이전에 간단한 오류 수정 및 디버깅이 수월해질 수 있다는 특징이 있다니 실무에 있어서 간단한 화면 구현(사용자 이벤트)들은 프론트에서 테스트하는 방법 또한 업무 프로세스에 효율성을 줄 수 있을 것 같아 자주 이용해 보아야겠다 :)

댓글
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
최근에 올라온 글