본문 바로가기
매일 해내는 개발/Develog

JavaScirpt 테스팅 프레임워크 Jest 기초

by 해야지 2023. 5. 21.
반응형

기본 패턴

describe("테스트 그룹화", ()=>{
	test("테스트 설명", ()=>{
	    expect("검증 대상").toXxx("기대 결과");
	});
});

기본 개념

키워드

  • describe(): Jest에서 제공하는 테스트 스위트 함수, 테스트를 그룹화할 때 사용한다.
  • test() 또는 it(): 테스트를 작성할 때 사용한다. it()  test() 의 alias이다.
  • expect(): 실제 테스트를 수행할 때 예상되는 값을 검사한다. expect() 와 함께 다양한 Matcher함수를 사용하여 테스트를 작성할 수 있다.
  • beforeEach(): 각 테스트가 실행되기 전에 반복 실행될 코드를 작성한다.
  • afterEach(): 각 테스트가 실행된 후에 반복 실행될 코드를 작성한다.
  • beforeAll(): 모든 테스트가 실행되기 전에 한 번 실행될 코드를 작성한다.
  • afterAll(): 모든 테스트가 실행된 후에 한 번 실행될 코드를 작성한다.

Matcher

  • toBe() : 값이 정확하게 같은지 검사한다. 값의 타입까지 정확하게 검사해야하는 경우 사용한다,.
  • toEqual() : 값이 동등한지 검사한다. 객체 또는 배열과 같은 데이터 구조의 값들을 비교할 때 사용한다.객체의 경우,반면에, toEqual 함수는 두 개의 변수나 객체가 동일한 값을 가지는지를 확인하기 위해 사용된다. 이 함수는 객체 내부의 모든 속성 및 값이 일치하는 경우에 참으로 판별된다. 자바스크립트에서 == 연산자를 사용하여 비교하는 것과 유사하다.
    const obj1 = { a: 1, b: 2 };
    const obj2 = { a: 1, b: 2 };
    expect(obj1).toBe(obj2); // false
    expect(obj1).toEqual(obj2); // true
    
    toBe()로 비교했을 때 false인 이유는 두 객체가 메모리 상에서 서로 다른 위치에 저장되기 때문이다. 반면에 toEqual()로 비교했을 때 두 객체의 속성 값이 모두 같으므로 true를 반환한다. 예를 들어, expect({a: 1, b: 2}).toEqual({b: 2, a: 1})과 같이 객체의 프로퍼티 순서가 다르더라도 값이 일치하면 테스트가 통과된다.
    test('toBe test', () => {
      const a = [1, 2, 3];
      const b = [1, 2, 3];
      expect(a).toBe(b); // fail
    });
    
    test('toEqual test', () => {
      const a = [1, 2, 3];
      const b = [1, 2, 3];
      expect(a).toEqual(b); // pass
    });
    
    toBe()는 a와 b의 참조가 다르기 때문에 false를 반환하고 toEqaul()은 값을 비교하여 두 배열의 값이 같으므로 true를 반환한다.
  • </aside>
  • 배열의 경우에도 마찬가지이다.
  • 예를 들어, 다음과 같다.
  • toBe()는 두 개의 변수나 객체가 동일한 객체인지를 확인하기 위해 사용된다. 즉, 메모리 상에서 동일한 위치에 저장된 값을 가리키는 경우에만 참으로 판별된다. 자바스크립트에서 === 연산자를 사용하여 비교하는 것과 유사하다.
  • <aside> 💡 toBe()와 toEqual()의 차이 원시적인 타입(number, boolean, string, null)을 사용하면 큰 차이가 없다. 하지만 객체 또는 배열의 경우 차이가 있다. toBe()는 비교하는 두 자료의 참조를 비교하고 toEqual()은 비교하는 두 자료의 값들을 비교한다.
  • toStrictEqaul() : toEqual()과 같지만 특정 요소에 undefined가 포함되는 것을 허용하지 않는다.
  • not.toBe() : 값이 다른지 검사한다.
  • not.toEqual() : 값이 동등하지 않은지 검사한다.
  • toBeTruthy() : 값이 truthy한 값인지 검사한다.
  • toBeFalsy() : 값이 falsy한 값인지 검사한다.</aside>
  • <aside> 💡 자바스크립트 규칙에 의해 다음과 같은 값들은 falsy한 값으로 간주된다. false, 0, ''(빈 문자열), undefined, NaN 이 외 나머지 값들은 모두 truthy한 값으로 간주된다.
  • toThrow() : 예외를 던지는 함수를 검사한다.
  • toContain() : 값이 배열이나 문자열 안에 포함되어 있는지 검사한다.
  • not.toContain() : 값이 배열이나 문자열 안에 포함되어 있는지 검사한다.
  • toBeDefined() : 값이 정의되어 있는지 검사한다.
  • toBeUndefined() : 값이 정의되어 있지 않은지 검사한다.
  • toBeNull() : 값이 null인지 검사한다.
  • toBeGreaterThan() : 값이 주어진 값보다 큰지 검사한다.
  • toBeGreaterThanOrEqual() : 값이 주어진 값보다 크거나 같은지 검사한다.
  • toBeLessThan() : 값이 주어진 값보다 작은지 검사한다.
  • toBeLessThanOrEqual() : 값이 주어진 값보다 작거나 같은지 검사한다.
  • toHaveBeenCalledTimes() : 함수가 호출된 횟수를 검사한다.
  • toHaveBeenCalledWith() : 함수가 특정 인수와 함께 호출되었는지 검사한다.

실습

천천히 따라치면서 익숙해져보아요

  1. initialize
  2. npm init -y
  3. Jest 라이브러리 설치
  4. npm i -D jest
  5. test 스크립트 수정
  6. "script": { "test": "jest" },
  7. Jest를 ES6에서 사용하려면 =>

babel 을 사용해서 ES6 문법을 이전 문법으로 바꿔주는 preset을 사용한다.

npm install @babel/preset-env
//(혹은)
yarn add @babel/preset-env

이후 현재 TDD를 진행하고 있는 디렉토리에서 ‘.babelrc’ 파일을 만들어준다.

해당 파일에 아래 코드를 추가하면 끝.

{
  "presets": ["@babel/preset-env"]
}

실습코드

  1. Matcher 기초 실습 코드
test('Matchers test', () => {
  // toBe
  expect(1 + 2).toBe(3);
  expect('Hello' + ' ' + 'World').toBe('Hello World');
  expect([1, 2, 3]).not.toBe([1, 2, 3]);
})

  // toEqual
	test('object assignment', () => {
	  const data = {one: 1};
	  data['two'] = 2;
	  expect(data).toEqual({one: 1, two: 2});
		expect({ name: 'John', age: 30 }).toEqual({ age: 30, name: 'John' });
	  expect([1, 2, 3]).toEqual([1, 2, 3]);
	  expect([1, 2, 3]).not.toEqual([1, 3, 2]);
	});
  
  // toBeNull
	test('null', () => {
	  const n = null;
	  expect(n).toBeNull();
	  expect(n).toBeDefined();
	  expect(n).not.toBeUndefined();
	  expect(n).not.toBeTruthy();
	  expect(n).toBeFalsy();
	  expect(undefined).not.toBeNull();
	});

	test('zero', () => {
	  const z = 0;
	  expect(z).not.toBeNull();
	  expect(z).toBeDefined();
	  expect(z).not.toBeUndefined();
	  expect(z).not.toBeTruthy();
	  expect(z).toBeFalsy();
	});
  
  // toBeUndefined
	test('undefined', ()=>{
	  let b;
	  expect(b).toBeUndefined();
	  expect(null).not.toBeUndefined();
	})

  // toBeTruthy / toBeFalsy
	test('truthy falsy', ()=>{
	  expect('hello').toBeTruthy();
	  expect('').toBeFalsy();
	  expect(0).toBeFalsy();
		expect("0").toBeTruthy();
	  expect(1).toBeTruthy();
	})

  // toContain
	test('contain',()=>{
	  expect('Hello World').toContain('World');
	  expect([1, 2, 3]).toContain(2);
	  expect([1, 2, 3]).not.toContain(4); 
	})

  // toMatch
	test('match', ()=>{
	  expect('hello@test.com').toMatch(/\\w+@\\w+\\.\\w+/);
	  expect('123-456-7890').toMatch(/\\d{3}-\\d{3}-\\d{4}/);
	  expect('Hello World').not.toMatch(/\\d+/);
	})
});
  1. toBe() 기본 실습 코드
//stringFunctions.js 파일 생성

function reverseString(str) {
  return str.split("").reverse().join("");
}

function capitalizeString(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export { reverseString, capitalizeString };
//stringFunction.test.js 파일 생성

import { reverseString, capitalizeString } from "./stringFunctions";

//describe로 전체 그룹화
describe("stringFunctions", () => {
	//describe로 소그룹 구분
  describe("reverseString", () => {
    test("reverse a string", () => {
      expect(reverseString("hello")).toBe("olleh"); //pass
    });

    test("reverse an empty string", () => {
      expect(reverseString("")).toBe(""); //pass
    });
  });

	//describe로 소그룹 구분
  describe("capitalize String", () => {
    test("capitalizes the first letter of a string", () => {
      expect(capitalizeString("hello")).toBe("Hello"); //pass
    });

    test("does not modify an already capitalized string", () => {
      expect(capitalizeString("Hello")).toBe("Hello"); //pass
    });

    test("capitalizes the first letter of a one-letter string", () => {
      expect(capitalizeString("h")).toBe("H"); //pass
    });

    test("does not modify an empty string", () => {
      expect(capitalizeString("")).toBe("");//pass
    });
  });
});
  1. beforeEach, afterEach, beforeAll, afterAll, toThrow 예제 코드
let counter = 0;

beforeAll(() => {
  console.log('beforeAll');
});

beforeEach(() => {
  counter++;
  console.log(`beforeEach ${counter}`);
});

afterEach(() => {
  console.log(`afterEach ${counter}`);
});

afterAll(() => {
  console.log('afterAll');
});

test('Counter should be incremented', () => {
  expect(counter).toBe(1);
});

test('Counter should be incremented again', () => {
  expect(counter).toBe(2);
});

test('toThrow test', () => {
  function throwError() {
    throw new Error('Error');
  }

  expect(throwError).toThrow();
});
//결과
beforeAll //모든 테스트 시작 전 한 번 실행
beforeEach 1 //테스트 케이스 실행시마다 시작 전 실행
  ✓ Counter should be incremented (1 ms)
afterEach 1 //테스트 케이스 종료시마다 종료 후 실행
beforeEach 2
  ✓ Counter should be incremented again (1 ms)
afterEach 2
  ✓ toThrow test (1 ms)
afterAll
  • ToThrow()와 ToThrowError()의 차이점toThrow() 메서드는 함수가 오류를 발생시키지 않으면 실패하고 toThrowError() 메서드는 함수가 Error 클래스의 인스턴스가 아닌 오류를 발생시키면 실패한다.
  • toThrow()와 toThrowError()는 모두 함수가 오류를 발생시킬 것인지 확인하는 데 사용되지만 toThrow()는 함수가 어떤 유형의 오류라도 발생시킬 것인지 확인하는 데 사용되는 반면 toThrowError()는 함수가 Error 클래스의 인스턴스를 발생시킬 것인지 확인하는 데 사용된다.

Mocking

Mocking이란?

Jest에서 가장 많이 사용되는 기능 중 하나로 테스트할 때 실제 객체 대신에 테스트용으로만든 가짜 객체를 사용하는 것을 의미한다. Moking을 사용하면 테스트에서 의존성을 제거하고 코드를 격리시켜서 테스트를 더 쉽고 빠르게 수행할 수 있다. 따라서 Mocking은 테스트의 효율성과 신뢰성을 높이는 데 유용한 기술이다.

Mocking의 목적

  1. 의존성이 있는 코드의 동작을 검증하고자 할 때 사용한다.테스트할 때, 코드가 의존하는 다른 코드나 외부 시스템 등에 문제가 발생하면 테스트가 실패하거나 테스트가 수행되지 않아 버그가 발생할 수 있다. 이러한 문제를 해결하리 위해 의존성을 대체하는 가짜 객체를 만들어 사용하면 테스트를 좀 더 견고하고 신뢰성 있게 수행할 수 있다.</aside>
  2. <aside> 💡 의존성 있는 코드란? 특정 코드 또는 모듈이 다른 코드 또는 모듈에 의존하여 동작하는 경우를 말한다. ex) 데이터베이스에 접근하는 코드, 외부 API를 호출하는 코드, 파일을 읽고 쓰는 코드 등은 모두 외부에 의존성이 있는 코드이다. 이러한 코드를 테스트할 때, 실제 데이터베이스나 외부 API를 사용하여 테스트를 수행하게 되면 테스트가 실패하거나, 실행시간이 느려지거나, 테스트 데이터를 준비하기 어려운 등의 여러가지 문제가 발생할 수 있다. 따라서 mocking을 통해 가짜 객체를 만들어 사용하면 테스트를 보다 쉽게 수행할 수 있다. mock 객체는 실제 의존성이 있는 코드와 비슷한 동작을 수행하면서도 외부 의존성을 대체하여 테스트를 수행하는 것이다.
  3. 해당 의존성을 대체하는 가짜 객체를 사용하여 테스트에 필요한 동작을 수행한다.
  4. 의존성을 가진 코드와 독립적으로 테스트를 수행할 수 있다.
  5. 의존성이 있는 코드의 동작에 영향을 주지 않고 테스트가 수행될 수 있게 해준다.
  6. 다양한 시나리오를 쉽게 테스트할 수 있다.
  7. ex) 특정 조건에서만 발생하는 오류 등

Mock 객체 생성 방법

  1. jest.fn()함수 사용 
    //app.js
    import * as math from './math.js';
    
    export const doAdd      = (a, b) => math.add(a, b);
    export const doSubtract = (a, b) => math.subtract(a, b);
    export const doMultiply = (a, b) => math.multiply(a, b);
    export const doDivide   = (a, b) => math.divide(a, b);
    
    //mock.test.js
    import * as app from "./app";
    import * as math from "./math";
    
    math.add = jest.fn();
    math.subtract = jest.fn();
    
    test("calls math.add", () => {
      app.doAdd(1, 2);
      expect(math.add).toHaveBeenCalledWith(1, 2);
    });
    
    test("calls math.subtract", () => {
      app.doSubtract(1, 2);
      expect(math.subtract).toHaveBeenCalledWith(1, 2);
    });
    
  2. //math.js export const add = (a, b) => a + b; export const subtract = (a, b) => b - a; export const multiply = (a, b) => a * b; export const divide = (a, b) => b / a;
  3. 가장 기본적인 전략으로 함수를 mock 함수로 재할당하는 것이다. 재할당 된 함수가 쓰이는 어디서든지 mock 함수가 원래의 함수 대신 호출된다.
  4. jest.mock() 함수 사용jest.mock('./math.js')를 하게되면 다음과 같이 설정한 것과 같다.**jest.mock() 예제 코드**하지만 이 방식은 모듈의 원래 구현에 접근하기가 어렵다는 것이다. 이런 경우 spyOn을 사용할 수 있다.
  5. //mock.test.js import * as app from "./app"; import * as math from "./math"; // Set all module functions to jest.fn jest.mock("./math.js"); test("calls math.add", () => { app.doAdd(1, 2); expect(math.add).toHaveBeenCalledWith(1, 2); }); test("calls math.subtract", () => { app.doSubtract(1, 2); expect(math.subtract).toHaveBeenCalledWith(1, 2); });
  6. export const add = jest.fn(); export const subtract = jest.fn(); export const multiply = jest.fn(); export const divide = jest.fn();
  7. jest.mock()을 사용하게 되면 exports하는 모든 함수들을 자동으로 mocking해준다.
  8. jest.spyOn()은 안 메소드가 실행되는 것을 살펴보는 것 뿐만 아니라 기존의 구현이 보존되길 바랄 때 사용한다. 구현을 mocking하고 차후에 원본을 복원할 수 있다.jest.spyOn()은 jest.fn()의 syntactic Sugar이기 때문에 아래와 같이 표현할 수 있다.
    import * as app from "./app";
    import * as math from "./math";
    
    test("calls math.add", () => {
      // store the original implementation
      const originalAdd = math.add;
    
      // mock add with the original implementation
      math.add = jest.fn(originalAdd);
    
      // spy the calls to add
      expect(app.doAdd(1, 2)).toEqual(3);
      expect(math.add).toHaveBeenCalledWith(1, 2);
    
      // override the implementation
      math.add.mockImplementation(() => "mock");
      expect(app.doAdd(1, 2)).toEqual("mock");
      expect(math.add).toHaveBeenCalledWith(1, 2);
    
      // restore the original implementation
      math.add = originalAdd;
      expect(app.doAdd(1, 2)).toEqual(3);
    });
    
  9. 기존의 구현을 저장하고 mocking했다가 기존 구현을 재할당 하는 방식으로 구현된다.
  10. import * as app from "./app"; import * as math from "./math"; test("calls math.add", () => { const addMock = jest.spyOn(math, "add"); //sypOn(spying할 객체, 메소드이름) // calls the original implementation expect(app.doAdd(1, 2)).toEqual(3); // and the spy stores the calls to add expect(addMock).toHaveBeenCalledWith(1, 2); });
  11. jest.fn()과 jest.mock()

Mocking 메서드

  1. mockReturnValue(value)
    // 변수를 mock함수로 만들기
    const mockFn = jest.fn();
    
    // mock는 빈 함수이기 때문에 기본적으로 undefined
    mockFn(); // undefined
    mockFn(1); // undefined
    mockFn([1, 2], { a: "b" }); // undefined
    
    // mock 리턴값 지정하기
    mockFn.mockReturnValue("I am a mock!"); // I am a mock!
    
    mockFn.mockReturnValue(42);
    mockFn(); // 42
    
    mockFn.mockReturnValue(63);
    mockFn(); // 63
    
  2. 함수가 호출될 때마다 반환 값을 지정한다.(단순 리턴값)
  3. mockImplementation(value)
    const mockFn = jest.fn();
    
    // 동작하는 모크 함수를 하나 만든다.
    mockFn.mockImplementation( (name) => `I am ${name}!` ); 
    
    console.log(mockFn("Dale")); // I am Dale!
    
    ------또는
    
    const mockFn = jest.fn( (name) => `I am ${name}!` );
    
    console.log(mockFn("Dale")); // I am Dale!
    
  4. 원래의 목 함수는 기본적으로 아무런 동작이나 리턴을 하지 않지만 해당 메서드를 통해 즉석으로 동작하는 목함수를 만들 수 있다.(함수)

Mocking을 사용하는 상황 예시

  1. 함수가 호출된 횟수와 어떤 인자가 전달되었는지 확인하는 예
  2. const calculator = { add: jest.fn(), }; calculator.add(2, 3); expect(calculator.add).toHaveBeenCalledTimes(1); expect(calculator.add).toHaveBeenCalledWith(2, 3);
  3. 함수가 특정 값을 반환하도록 모킹하는 예
  4. const calculator = { add: jest.fn(), }; calculator.add.mockReturnValue(5); const result = calculator.add(2, 3); expect(result).toBe(5);
  5. 함수가 오류를 발생하도록 모킹하는 예
  6. const calculator = { add: jest.fn(), }; calculator.add.mockImplementation(() => { throw new Error("Error message"); }); try { calculator.add(2, 3); } catch (error) { expect(error).toBeInstanceOf(Error); expect(error.message).toBe("Error message"); }
  7. 모듈을 모킹하는 예
  8. const axios = jest.mock("axios"); axios.get("<https://www.google.com>").mockResolvedValue({ data: { name: "Google", }, }); const response = await axios.get("<https://www.google.com>"); expect(response.data.name).toBe("Google");
반응형

댓글