리액트에서 테스트를 위해서 많이 사용하는 도구 중 하나인 React Testing Library에 대한 포스트입니다.

테스트에 사용되는 라이브러리들이 많으니 각자 본인에게 맞는 것을 활용해 공부하며 적용해 보는 것이 좋을 것 같습니다.

아래에 리액트 공식 사이트에서 사용되는 테스트 유틸 및 라이브러리 예시를 확인해보세요.

  • Enzyme: React를 위한 JavaScript 테스트 유틸.
  • Jest: React를 포함한 JavaScript 테스트 프레임워크.
  • react-testing-library: 가벼운 React DOM 테스트 유틸.
  • React-unit: React를 위한 가벼운 단위테스트 라이브러리.
  • Skin-deep: 얕은 렌더링을 지원하는 React 테스트 유틸.
  • Unexpected-react: React 컴포넌트와 이벤트를 발생시켜주는 플러그인.

설치

yarn add @testing-library/react @testing-library/jest-dom

npm install --save react-testing-library @testing-library/jest-dom

npm 또는 yarn을 이용해 react-testing-library를 설치한다. react-testing-library@testing-library/react 로 변경되었으니 주의!

jest-dom@testing-library/jest-dom 으로 변경됐다고 한다.

react-testing-library는 '사용자 관점'에서 테스트를 진행한다. 보통 Enzyme를 이용한 테스트 방식은 상태값, 상태 변수에 대해 테스트를 한다. 하지만 전자는 상태 관리는 컴포넌트의 구현 세부사항일 뿐이다. 즉, 상태 변수가 언제든 다른 컴포넌트로 옮겨지거나, react에서 vue.js로 바뀌더라도 테스트에는 문제가 없어야한다.

다양한 쿼리

  • getBy* 쿼리 (ex. getByTestId, getByText, getByRole): 이 함수들은 동기적(synchronous)이며 그 요소가 현재 DOM 안에 있는지 확인한다. 즉, 현재상태만 확인한다. 그렇지 않으면 에러를 발생시킨다.
  • findBy* 쿼리 (ex. findByText): 이 함수들은 비동기적(asynchronous)이다. 그 요소를 찾을 때까지 일정 시간(기본 5초)을 기다린다. 만약 그 시간이 지난 후에도 요소를 찾을 수 없으면 에러를 발생시킨다.
  • queryBy* 쿼리: 이 함수들은 getBy* 처럼 동기적이다. 하지만 요소를 찾을 수 없어도 에러를 발생시키지 않는다. 단지 null 값을 리턴한다.

테스트 디버깅하기

render(<SomeComponent />);
screen.debug(); //원하는 실행 지점에서 DOM트리를 console에 출력해, 디버깅할 수 있다.

screen.debug() 는 브라우저의 개발 도구처럼 편리하고 상호작용적이진 않지만, 테스트 환경에서 어떤 일이 일어나고 있는지 확실히 알 수 있도록 도와준다.

const link = screen.getByRole('link', { name: /how it works/i });
screen.debug(link);

위와 같이 debug 함수에 파라미터를 전달하면 해당 요소만을 콘솔에 출력해준다.

Mocking

  • jest.fn: Mock a function
  • jest.mock: Mock a module → 자동적으로 모듈의 모든 함수를 mocking 해준다.
  • jest.spyOn: Spy or mock a function → 마찬가지로 모든 함수를 mocking 해주면서, 원래의 함수를 다시 복원할 수도 있다.

가장 기본적인 사용 방식은 함수를 mock 함수로 재할당하는 것이다. 재할당 된 함수가 쓰이는 어디서든지 mock 함수가 원래의 함수 대신 호출 될 것이다.

예시
  1. jest.fn() mocking
// 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);
// 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;
// app.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);
});

  1. jest.mock() mocking
jest.mock('./math.js');

위의 코드로 mocking하는 것은 본질적으로 아래 코드처럼 하는거랑 같다.

export const add      = jest.fn();
export const subtract = jest.fn();
export const multiply = jest.fn();
export const divide   = jest.fn();
// app.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);
});
반응형

참고로 이 글에서 말하는 테스트는 React환경에서 사용하기 위한 방법을 공부한 것을 정리했다.

우선, Jest를 설치한다.

npm install --save-dev jest

yarn add --dev jest

 

다음으로 간단한 예시로 sum.js 파일을 하나 만들고, 다음과 같이 작성한다.

function sum(a, b) {
  return a + b;
}

 

이제 위의 코드를 테스트하기 위한 sum.test.js 테스트 파일을 하나 생성한다. 

const { sum, sumOf } = require("./sum");

//test, it --> 새로운 테스트 작성
test("adds 1 + 2 to equal 3", () => {
	expect(sum(1, 2)).toBe(3);
});

 

앞서 만든 sum함수를 import해온 다음 테스트 구문에 활용해 제대로 원하는 결과가 테스트되는지 확인하는 코드이다.

이제 테스트 코드를 실행해볼 차례인데, 다음과 같이 pakage.json파일에 추가해준다.

"scripts": {
	"test": "jest"
}

 

아래 명령어를 통해 테스트를 실행하도록 한다. 

npm run test

 

테스트 코드가 제대로 통과가 된다면, 다음과 같은 결과가 나타날 것이다. 참고로 아래 결과는 VSCode를 사용했을 때 나타난 결과이다.

PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

 

추가적으로 describe를 사용을 해보기 위해 sum.js파일에 새로운 함수를 추가해주자. 여기서 describe 메서드는 여러 테스트 케이스를 묶어주는 기능을 한다.

// sum.js

function sum(a, b) {
  return a + b;
}

// 배열의 합
function sumOf(numbers) {
  return numbers.reduce((acc, current) => acc + current, 0);
}

module.exports = { sum, sumOf };

 

그리고 테스트 코드 역시 수정해주고, 실행을 하게 되면 테스트 결과가 나타난다.

// sum.test.js
const { sum, sumOf } = require("./sum");

describe("sum", () => {
  it("calculates 1 + 2", () => {
    expect(sum(1, 2)).toBe(3);
  });

  it("calculates all numbers", () => {
    const array = [1, 2, 3, 4, 5];
    expect(sumOf(array)).toBe(15);
  });
});

test 대신 it을 사용해도 같은 기능이다. 다만 it을 사용하게 되면, 테스트 케이스 설명을 영어로 작성할 경우 "말이 되게" 매끄럽게 작성 가능해져 사용한다고 한다.

반응형

Boolean

let isDone: boolean = false;

 

Number

타입스크립트에서 숫자는 부동 소수점 값 또는 BigInteger 값이다. 16진수, 10진수, 8진수, 2진수 타입도 지원한다.

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;

 

String

자바스크립트와 똑같이 double quotes(""), single quotes('')로 문자열을 감싼다.

let color: string = "blue";
color = 'red';

마찬가지로 backtick (`${ expr }`) 형태로 사용하면 된다.

let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}.

I'll be ${age + 1} years old next month.`;



Array

let list: number[] = [1, 2, 3];

let list: Array<number> = [1, 2, 3]; //제네릭 타입방식

 

유니언 타입(다중 타입)의 ‘문자열과 숫자를 동시에 가지는 배열’도 선언할 수 있다.

// Union 이용한 배열
let array: (string | number)[] = ['Apple', 1, 2, 'Banana', 'Mango', 3];

let array: Array<string | number> = ['Apple', 1, 2, 'Banana', 'Mango', 3];

 

배열이 가지는 항목의 값을 단언할 수 없다면 any를 사용할 수 있다.

let array: any[] = [0, 1, {}, [], 'str', false];

 

인터페이스(Interface)나 커스텀 타입(Type)을 사용할 수도 있다.

interface IUser {
  name: string,
  age: number,
  isValid: boolean
}

let userArr: IUser[] = [
  {
    name: 'Jiho',
    age: 25,
    isValid: true
  },
  {
    name: 'Juliet',
    age: 42,
    isValid: false
  },
  {
    name: 'Evan',
    age: 52,
    isValid: true
  }
];

 

readonly 키워드나 ReadonlyArray 타입을 사용해 읽기 전용 배열을 생성할 수도 있다.

let arrA: readonly number[] = [1, 2, 3, 4];
let arrB: ReadonlyArray<number> = [0, 5, 5, 2];

arrA[0] = 123; // Error - TS2542: Index signature in type 'readonly number[]' only permits reading.
arrA.push(123); // Error - TS2339: Property 'push' does not exist on type 'readonly number[]'.

arrB[0] = 123; // Error - TS2542: Index signature in type 'readonly number[]' only permits reading.
arrB.push(123); // Error - TS2339: Property 'push' does not exist on type 'readonly number[]'.

 

Tuple

배열 요소의 수가 고정된 형태의 자료형이다. 각각의 요소별로 타입을 지정해주면 된다.

// Declare a tuple type
let x: [string, number];

// Initialize it
x = ["hello", 10]; // OK

// Initialize it incorrectly
x = [10, "hello"]; // Error

 

Enum

특정 값들의 집합을 의미하는 자료형이다. 타입스크립트에서는 문자형 이넘과 숫자형 이넘을 제공 한다.

enum Color {
  Red, //0
  Green, //1
  Blue, //2
}
let c: Color = Color.Green;

 

기본적으로 enum은 인덱스 번호가 0번부터 시작하지만, 따로 지정해 변경해줄 수 있다. 또는 모든 값에 수동으로 설정해주는 것도 가능하다. 이때 번호 대신 문자열도 지정 가능하지만, 모든 값에 설정해줘야만 한다는 것을 주의해야 한다.

enum Color {
  Red = 1,
  Green, //2
  Blue, //3
}
let c: Color = Color.Green;

enum Color {
  Red = 1,
  Green = 2,
  Blue = 4,
}
let c: Color = Color.Green;

enum Color {
  Red = "red",
  Green = "green",
  Blue = "blue",
}

 

리버스 매핑(Reverse Mapping) - 숫자형 이넘에만 존재하는 특징이다. enum의 키(key)로 값(value)을 얻을 수 있고 값(value)으로 키(key)를 얻을 수도 있다. enum 요소에 해당하는 번호를 통해 값을 얻을 수 있다.

enum Color {
  Red = 1,
  Green,
  Blue,
}
let colorName: string = Color[2];

// Displays 'Green'
console.log(colorName);

 

 

Void

일반적으로 값을 반환하지 않는 함수에서 사용한다. : void 위치는 함수가 반환 타입을 명시하는 곳이다.

값을 반환하지 않는 함수는 실제로는 undefined를 반환한다.

function hello(msg: string): void {
  console.log(`Hello ${msg}`);
}
const hi: void = hello('world'); // Hello world
console.log(hi); // undefined

 

Object

object는 원시 타입이 아닌 타입을 나타낸다. number, string, boolean, bigint, symbol, null, 또는 undefined 가 아닌 나머지를 의미한다.

declare function create(o: object | null): void;

create({ prop: 0 }); // 성공
create(null); // 성공

create(42); // 오류
create("string"); // 오류
create(false); // 오류
create(undefined); // 오류

 

 

타입 단언(Type assertions)

컴파일러에게 개발자가 명확하게 타입에 대해서 알려주는 방법이다. 예를 들어 타입스크립트의 타입인 string가 아닌 조금 더 명확한 "some_type"이라고 하는 명확한 타입이라고 알려주는 경우다.

 

다른 언어의 타입 변환(형 변환)과 유사하지만, 다른 특별한 검사를 하거나 데이터를 재구성하지는 않는다. 이는 런타임에 영향을 미치지 않으며, 온전히 컴파일러만 이를 사용한다. 타입 스크립트는 개발자가 필요한 어떤 특정 검사를 수행했다고 인지한다.

 

타입 단언의 사용법은 2가지 경우가 있다.

 

- Angle Braket 형태

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

 

- as 형태

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

위 두 예제는 동일하며 어떤 것을 사용할지는 주로 선호에 따른 선택이다. 하지만 TypeScript를 JSX와 함께 사용할 때는, as-스타일의 단언만 허용된다.

 

 

 

Union

2개 이상의 타입을 지정해주고자 할 때 사용하며, OR연산자 | (vertical bar)를 를 사용해 타입들을 지정한다. 유니언 타입은 여러 타입 중 하나가 될 수 있는 값을 의미한다.

let union: (string | number);
union = 'Hello type!';
union = 123;
반응형

 

 

10866번: 덱

첫째 줄에 주어지는 명령의 수 N (1 ≤ N ≤ 10,000)이 주어진다. 둘째 줄부터 N개의 줄에는 명령이 하나씩 주어진다. 주어지는 정수는 1보다 크거나 같고, 100,000보다 작거나 같다. 문제에 나와있지

www.acmicpc.net

 

 

<내 코드>

import sys

class Deque:
    def __init__(self):
        self.result = list()

    def push_front(self, num):
        self.result.insert(0, num)

    def push_back(self, num):
        self.result.append(num)

    def pop_front(self):
        if(self.empty()):
            return -1
        else:
            return self.result.pop(0)

    def pop_back(self):
        if(self.empty()):
            return -1
        else:
            return self.result.pop()

    def front(self):
        if(self.empty()):
            return -1
        else:
            return self.result[0]

    def back(self):
        if(self.empty()):
            return -1
        else:
            return self.result[-1]

    def size(self):
        return len(self.result)

    def empty(self):
        if(self.size() == 0):
            return 1
        else:
            return 0


N = int(sys.stdin.readline())
deque = Deque()

for _ in range(N):
    input_split = sys.stdin.readline().split()
    oper = input_split[0]

    if(oper == 'push_front'):
        deque.push_front(int(input_split[1]))
    elif(oper == 'push_back'):
        deque.push_back(int(input_split[1]))
    elif(oper == 'pop_front'):
        print(deque.pop_front())
    elif(oper == 'pop_back'):
        print(deque.pop_back())
    elif(oper == 'front'):
        print(deque.front())
    elif(oper == 'back'):
        print(deque.back())
    elif(oper == 'empty'):
        print(deque.empty())
    elif(oper == 'size'):
        print(deque.size())

 

반응형

+ Recent posts