Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
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
Archives
Today
Total
관리 메뉴

과거의 내가 미래의 나에게

프론트엔드 개발에서 테스트 전략 적용해보기(2) - 함수 단위 테스트 본문

카테고리 없음

프론트엔드 개발에서 테스트 전략 적용해보기(2) - 함수 단위 테스트

양바삭 2024. 9. 25. 21:14

오늘부터 본격적으로 테스트코드를 짜보면서 익혀가는 과정을 서술할 것이다. 책에서는 JEST라는 테스트러너를 메인으로 사용하고 필요에 따라 추가로 라이브러리를 이용해 볼 것이다.

학습 목차는 함수 단위 테스트-UI컴포넌트 별 단위테스트-UI컴포넌트 통합테스트-시각적회귀테스트-E2E테스트 순으로 진행될 것이고 중간에 스토리북을 통한 UI 컴포넌트 탐색기를 공부할 것 같다.

오늘은 함수 단위 테스트를 작성해보면서 JEST의 사용법을 천천히 익혀보자

 

 

1. 테스트 작성법과 테스트 그룹 작성 그리고 실행하기

 ■ test 함수

test 함수는 2개의 인자를 가지는데, 첫 번째는 테스트명을 작성하고 두 번째는 단언문을 작성한다. 

< 단언문 >
검증값이 기댓값과 일치하는지 검증하는 문이다. 일반적으로 디버깅이나 테스트 과정에서 사용된다. js에서는 단언문을 기본적으로 제공하진 않고 다만 비슷한 기능을 구현할 수 있을 뿐이다.

 

단언문 안에는 expect 함수와 이에 덧붙히는 매처(matcher) 함수로 구성되어있다. expect에서는 테스트할 값을 받고, 그 값이 특정한 조건이나 결과와 일치하는지를 다양한 매처 함수를 사용해 확인할 수 있다

test("1+2=3",()=>{
  //expect(검증값).tobe(기댓값)
    expect(add(1,1)).tobe(2)
})
// add() 함수는 외부에서 import하여 사용한다고 가정
// tobe() 등가 비교 매처

 

test 함수는 테스트 코드가 어떤 의도로 작성됐으며 어떤 작업이 포함됐는지를 테스트명으로 명확하게 표현해야한다.


 ■ describe 함수

연관성이 있는 테스트들을 그룹화할 때 사용하는 함수이다. 2개의 인자를 받고, 첫 번째는 테스트명 두 번째는 그룹 내의 함수들로 구성된다. describe는 describe 함수 내에 또 describe를 넣을 수도 있기에 더 깔끔하게 정리할 수도 있다.

describe("add 함수",()=>{
  test("1+1=2",()=>{
    expect(add(1,1)).tobe(2)
  });
  test("1+2=3",()=>{
    expect(add(1,2)).tobe(3)
  });
})

 

 

 ■ 테스트 실행하기 

실행방법은 package.json에 npm 스크립트를 추가하거나 제스트 러너 프로그램을 별도로 설치하여 작동 가능하다. 

npm 스크립트를 통해서 테스트하면 하나의 테스트를 할 때 그 경로를 직접 접근해야한다. 경로를 직접 서술하는 것은 다소 번거로울 수 있기에 하나씩 실행한다면 제스트 러너 프로그램을 추천한다 한다.

//npm 스크립트를 통한 실행법

// package.json
{
  "script": {
    "test": "jest"
  }
}

// bash
$ npm test
$ npm test 'src/test/index.test.ts'

 

 

 

2. 에지 케이스와 예외 처리

에지 케이스란 테스트 시 비정상적인 입력을 일으키는 것으로, 이러한 입력들은 보통 일반적인 상황에서는 잘 발생하지 않지만 시스템이 동작 오류를 막기 위해 꼭 필요한 케이스라 할 수 있다.

이러한 케이스를 나열한 후 이를 처리하는 것을 예외 처리라고 하는데 이러한 과정들은 모두 시스템을 견고하게 하는데 도움이 된다.

 

■ 타입스크립트를 통한 처리

함수의 매개변수에 타입 애너테이션을 붙여 다른 타입의 값이 들어오는 것을 빠르게 캐치할 수 있다.

 

■ 예외 처리 및 예외 처리 검증 테스트

1) 예외 처리

export function add(a:number, b:number) {
  if(a<10 || a>100) throw new Error("0~100 사이의 값을 입력해주세요")
  if(b<10 || b>100) throw new Error("0~100 사이의 값을 입력해주세요")
  
  const sum = a+b
  
  if(sum > 100) return 100
  return sum
}

 

위의 예씨에서 throw new Error 부분이 예외를 처리한 부분이다. 원하는 입력값 외의 것이 들어오면 Error라고 처리되게끔 코드를 구성한 것이다. 

이런 식으로 함수에 예외 처리를 추가하면 구현 중에 발생하는 문제를 빠르게 발견할 수 있다.

 

2) 예외 처리 검증 테스트

이러한 예외처리가 잘 처리되어있는지에 대한 테스트도 가능하다.

expect(() => add(-10, 110)).toThrow() // 예외 처리된 코드를 타므로 테스트는 성공
expect(() => add(10, 90)).toThrow() // 예외 처리된 코드를 타지 않으므로 테스트는 실패

 

예외 처리를 테스트하는 코드이므로 예외 처리 코드를 타지 않는다면 해당 테스트는 실패처리가 된다. 이 테스트는 원본 코드에서 예외 처리를 하고 의도한대로 해당 예외처리가 정말 잘 되었는지를 테스트하는 것이기에 헷갈리면 안될 것이다.

 

또한 toThrow 매처 안에 예외 처리로 발생한 메시지값(new Error("메시지!"))을 동일하게 넣어 해당 메시지가 동일하게 들어오는 지도 체크가능하다.

// add 함수의 예외 처리 일부
throw new Error("에러!")

// test
expect(()=>add(1,2)).toThrow("에러!") // 통과
expect(()=>add(1,2)).toThrow("에러가 났습니다!!!") // 실패

 

< 예외처리 시 expect 검증값에는 화살표 함수로! >
주의해야 할 점은 expect 내의 검증값을 화살표 함수로 써야한다는 것이다. 왜냐하면 화살표 함수가 아닌 함수를 그대로 적으면 이 함수는 즉시 실행되고 함수의 실행 결과가 expect로 넘어가게 된다. 그렇게 되면 expect가 처리하기도 전에 이미 에러가 발생해버려서 expect는 에러 발생 유무를 알 수 없게 된다. 여기서 중요한 것은 에러의 결과가 아닌 예외 처리가 정상적으로 작동하는지를 테스트하는 것이다. 그렇기에 화살표 함수를 이용하여 호출된 결과가 아닌 함수 자체를 전달받아야한다.

 

 

 

■ instanceof 연산자를 활용한 세부 사항 검증

instanceof 연산자는 객체가 특정 클래스나 생성자의 인스턴스인지 확인할 때 사용하는 연산자로 결과는 boolean으로 반환된다. instanceof를 이용하여 Error 클래스를 각 상황별로 인스턴스를 만들어 테스트 시 어떤 에러가 났는지 더 구체적으로 살펴볼 수 있게 한다.

export class RangeError extends Error{};

funtion checkRange(value: number){
 if(value < 0 || value > 100) throw new RangeError("0~100사이의 값을 입력해주세요");
};

export funtion add(a: number, b:number){
 checkRange(a);
 checkRange(b);
 const sum = a + b
 if(sum > 100) return 100
 return sum
};
import {add, RangeError} from ".";

test("인수가 0~100의 범위 밖이면 에러 발생", () => {
 expect(() => add(-10, 10)).toThrow(RangeError);
});

 

Error를 상속받아 태어난 RangeError를 toThrow 안에 적어주면 안에서 instanceof 연산자를 활용해 해당 에러가 이 곳에서 왔는지 확인할 수 있다. 

 

 

 

3. 용도별 매처

tobe(), toThrow() 외에도 다양한 매처가 존재하는데 아래에서 살펴보도록 하겠다.

 

■ 진릿값

  • toBeTruthy(): 값이 참인지
  • toBeFalsy(): 값이 거짓인지
  • toBeNull(): 값이 Null인지
  • toBeUndefined(): 값이 undefined인지

참인 값 혹은 거짓인 값과 일치하는 매처이다. 각 매처 앞에 not을 추가하면 반전시킬 수 있다.

toBeFalsy에는 null과 undefined도 해당되는데, 이 두 개를 확인하려면 toBeNull이나 toBeUndefined를 사용하는 것이 제일 실하다.

expect(true)BeTruthy();
expect(false).not.toBeTruthy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();

 

 

  수치검증

  • toBe(): 값이 동일한지 
  • toBeGreaterThan(): 값이 큰 지
  • toBeGreaterThanOrEqual(): 값이 크거나 같은 지
  • toBeLessThan(): 값이 작은지
  • toBeLessThanOrEqual(): 값이 작거나 작은지
  • toBeCloseTo(): 소수계산 시 사용

등가 비교나 값끼리 비교할 때의 매처이다. 소수 계산에서는 부동소수점의 문제가 있기에 toBe가 아닌 toBeCloseTo를 사용하여 테스트를 올바르게 유도할 수 있다.

expect(1+4).toBe(5) 
expect(0.1+0.4).toBeCloseTo(0.5) 
expect(6).toBeGreaterThan(5) // 6은 5보다 크다(6>5)
expect(6).toBeGreaterThanOrEqual(5) // 6은 5보다 크거나 같다(6>=5)

 

< 부동소수점 >
부동소수점은 소수점이 있는 숫자(실수)를 컴퓨터에 저장하는 방식이다. 컴퓨터는 실수를 정확하게 표현한다. 실수를 저장할 때 약간 틀리게 저장하는데 그 이유는 컴퓨터가 숫자를 저장하는 방식은 이진수를 활용하기 때문이다. 소수점들을 이진수로 변경하게 되면 끝없는 숫자가 반복되게 되는데 컴퓨터는 무한의 숫자를 다 저장할 수 없기에 이를 임의로 끊다보니 근사값으로 저장된다. 그러다보니실수의 계산은 약간의 오차가 발생할 수도 있다. 

 

 

  문자열검증

  • toContain(): 값이 동등하거나 포함되었는지. 동등으로 toBe나 toEqual를 사용해도 된다
  • toMatch(): 정규표현식 검증
  • toHaveLength(): 길이가 맞는지
  • stringContaing(): 객체에서 포함된 문자열을 검증할 때(포함 유무)
  • stringMatching(): 객체에서 포함된 문자열을 검증할 때(정규표현식)
const msg = Hello world;
expect(msg).toContain("Hello");
expect(msg).not.toContain("Bye");
expect(msg).toMatch(/Hello/); 
expect(msg).toHaveLength("11");

const obj = {status: 200, message: msg};
expect(obj).toEqual({
 status: 200,
 message: expect.stringContaining("Hello")
});
expect(obj).toEqual({
 status: 200,
 message: expect.stringMatching(/Hello/)
});

 

 

  배열검증

  • toContainEqual(): 배열에 특정 객체가 포함되었는지
  • arrayContaining(): 인수로 배열을 넘겨주며 배열 안의 요소들이 전부 포함되어있어야 함

배열에 원시형인 값인 특정 값이 포함되어있는지 확인하려면 toContain을 활용하면 된다. 또한 배열 길이를 활용하려면 toHaveLength를 활용하면 된다.

const obj1 = {test:1};
const obj2 = {test:2};
const obj3 = {test:3};
const arr = [obj1, obj2, obj3];

expect(arr).toContain(obj1);
expect(arr).toContain([obj1, obj3]);

 

 

  객체검증

  • toMatchObject(): 부분적으로 프로퍼티가 일치하면 성공하고, 일치하지 않는 것이 있다면 실패한다. 
  • toHaveProperty(): 객체에 특정 프로퍼티가 존재하는지
  • objectContaining(): 객체 내 또다른 객체를 검증할 때
const obj1 = {name: "yang", emotion: "happy", miniObj: {mini: 1}};

expect(obj1).toMatchObject({name:"yang"});
expect(obj1).not.toMatchObject({test:1});

expect(obj1).toHaveProperty("name");

expect(obj1).toEqual({
 name: yang,
 emotion: "happy",
 miniObj: expect.objectContaining({mini: 1})
})

 

 

4. 비동기 처리 테스트

인수를 통해 대기시간을 넘겨주면, 대기시간만큼 기다린다음 경과시간을 반환해주는 함수가 있다. 이러한 비동기 처리가 포함된 함수를 테스트하는 여러 방법을 알아보겠다.

 

1. resolve 된 경우

export function wait(duration: number) {
 const pm = new Promise((resolve)=>{
  setTimeout(()=>{resolve(duration)}, duration)
 });
 return pm;
};


// then 메서드 사용
test('지정 시간 경과 후 경과시간이 resolve 됨', ()=>{
 return wait(50).then((resolve)=> expect(resolve).toBe(50);)
})

// resolves 매처 사용
test('지정 시간 경과 후 경과시간이 resolve 됨', ()=>{
 return wait(50).resolves.toBe(50);
})

// async/await 사용(resolve 매처 사용)
test('지정 시간 경과 후 경과시간이 resolve 됨', async ()=>{
 await expect(wait(50)).resolves.toBe(50)
})

// async/await 사용 (resolve 매처 비사용)
test('지정 시간 경과 후 경과시간이 resolve 됨', async ()=>{
 expect(await wait(50)).toBe(50)
})

 

2. reject 된 경우

export function wait(duration: number) {
 const pm = new Promise((_, reject)=>{
  setTimeout(()=>{reject(duration)}, duration)
 });
 return pm;
};


// catch 메서드 사용
test('지정 시간 경과 후 경과시간과 함께 reject 됨', ()=>{
 return wait(50).catch((duration)=> expect(duration).toBe(50))
})

// rejects 매처 사용
test('지정 시간 경과 후 경과시간과 함께 reject 됨', ()=>{
 return expect(wait(50)).rejects.toBe(50)
})

// async/await 사용(rejects 매처 사용)
test('지정 시간 경과 후 경과시간과 함께 reject 됨', async ()=>{
 await expect(wait(50)).rejects.toBe(50)
})

// try-catch문 사용
test('지정 시간 경과 후 경과시간과 함께 reject 됨', async ()=>{
 expect.assertions(1)
 try(){
  await wait(50)
 }catch(err){
  expect(err).toBe(50)
 }
})

 

< expect.assertion(n) >
test가 성공하는 방법은 단언문을 통과하는 것이다. 그 밖에도 통과하는 방법이 있는데 단언문 자체를 실행시키지 않고 테스트 함수를 완료하는 것이다. 하지만 이러한 테스트는 완전히 의미없는 케이스가 되어버릴 것이다. 이러한 실수를 줄이는 방법이 assertion 매처이다.
이는 테스트 시, 단언문이 괄호 속에 들어간 숫자만큼 실행되어야 한다는 것으로 해당 매처를 넣어놓으면 단언문이 실행되지 않고 넘어가는 일은 줄어들 것이다.

 

비동기 처리에서 어떤 곳에서는 return이 들어가 있고 어떤 곳에서는 사용되지 않는다. 

jest는 기본적으로 동기 방식의 테스트를 진행하기에 비동기를 사용하려면 jest에게 현재 비동기 함수를 실행할 것이란 것을 알려줘야한다. async/await는 그 자체로 promise 객체를 반환하기에 굳이 return이 없어도 jest가 비동기 함수임을 캐치하지만, 이를 쓰지 않은 경우는 직접 promise 객체를 return 해줌으로써 jest에게 기다리라는 제스쳐를 남겨야 하는 것이다.

 

Comments