Jest #2 - Application

Joshua Song / 송성현·2020년 3월 13일
4

백엔드

목록 보기
4/8
post-thumbnail

Introduction

이번 포스트는 저번에 작성한 두번째 포스트를 이은, Jest 사용의 확장이다. 저번 포스트에서는 기본 개념과 기본 셋업을 학습했다면, 이번엔 그래도 자주 사용하는 테스트와 좀 더 여러가지 기능을 학습하고 또 복습하는 것이 목적이다. 여러 예제를 통해 그래도 Jest안의 테스트 기능들을 보자!

Function.js

import axios from "axios";

const functions = {
    add: (num1, num2) => num1 + num2,
    isNull:() => null,
    checkValue: (x) => x,
    createUser: () => {
        const user = {firstName: "Joshua"};
        user["lastName"] = "Song";
        return user;
    },
    fetchUser: () => axios.get("https://koreanjson.com/users/1")
        .then(res => res.data)
        .catch(err => "error")
}

export default functions;

이번 포스트에서 계속 요긴하게 사용할 함수 목록이다. 큰 함수안에 여러가지 method들을 정의했다.

add 는 말 그래도 인자로 받는 두 숫자를 더해서 반환해 준다.
isNull은 함수를 실행해서 null 값이 나오게 하는 목적이다.
checkValue는 인자로 넣은 값이 제대로 반환이 되는지 확인한다.
createUser는 firstName과 lastName을 담은 객체를 생성해 반환한다.
fetchUser는 koreanjson이라는 api를 사용해 한 가상의 유저의 정보를 가져와 반환하다.

그렇다면 이제 적절한 테스트 코드를 보자~!

add 테스트

//* toBe
test("Adds 2 + 2 to equal 4", () => {
    expect(functions.add(2,2)).toBe(4);
});

//* not toBe
test("Adds 2 + 2 to NOT equal 5", () => {
    expect(functions.add(2,2)).not.toBe(5);
});

add method의 기능을 잘 확인하고, 또 관련된 테스트를 작성하였다. 코드들은 매우 직관적이지만 한번 간단히 설명하자면 첫번째 테스트는 2와 2를 인자로 넣었을 때 4라는 값이 나오는지 확인하는 것이고, 두번째는 2와 2를 인자로 넣었을 때 not.toBe(5)로 5가 반환되지 않는 것을 확인하기 위한 테스트이다. 두번째 테스트에서 볼 수 있듯 not을 앞에 넣어주기만 하면 그 반대의 의미를 테스트 할 수 있다.

isNull 테스트

//* to be Null
test("Should be null", () => {
    expect(functions.isNull()).toBeNull();
});

이번 테스트는 toBeNull()이란 기능을 실험해보기 위한 것인데 위에 설명했듯 isNull은 null 을 반환하는 함수이기에 그 함수를 실행히켰을 때 나오는 값이 null인지 확인해야 한다. 이름에서 알 수 있듯이 toBeNull()은 expect 안의 값이 null인지 아닌지 확인하는 역할을 한다.

checkValue 테스트

//* to be Falsy
test("Should be falsy", () => {
    expect(functions.checkValue(undefined)).toBeFalsy();
});

checkValue는 인자로 들어오는 값이 반환되는 함수인데 여기서 toBeFalsy()는 값이 falsy한지 아닌지를 확인한다. 이 경우 undefined를 인자로 집어 넣으면 falsy 한 값이 나올 것이기에 toBeFalsy를 적용해 테스트해볼 수 있다. toBeTruthy()는 값이 truthy한지 확인하는 기능이다. 참 쉽쥬?

createUser 테스트

//* toEqual
test("User should be Joshua Song", () => {
    expect(functions.createUser()).toEqual({
        firstName: "Joshua",
        lastName: "Song"
    });
});

createUser 함수는 이름을 가지고 있는 객체를 생성해 반환하는데 여기서 테스트는 toBe와 비슷한 toEqual을 사용한다. createUser를 실행하면 {firstName:"Joshua", lastName: "Song"}이라는 객체를 생성하기에 이 테스트는 통과할 것이다. 여기서 왜 toBe가 아닌 toEqual을 사용할까?

toBe는 원시 타입을 비교할 때 사용하고 toEqual은 객체 혹은 배열을 비교할 때 사용한다.

숫자, 불리언, 스트링 같은 원시타입은 toBe로 일치하는지 확인하는데에 비해 toEqual은 객체 및 배열이 일치하는지 확인하는데 쓰인다.

Other Tools

위에 보여진 테스트 툴 이외에도 사용 가능한 많은 테스트들이 있다. 공식문서 혹은 열심히 검색을 하면 자신이 필요한 툴을 찾아 사용할 수 있다. 간단한 코드를 통해 알아보자.

Number

test("Should be under 1600", () => {
    const load1 = 800;
    const load2 = 800;
    expect (load1 + load2).toBeLessThanOrEqual(1600);
    expect (load1 + load2).toBeLessThan(1700);
    expect (load1 + load2).toBeGreaterThanOrEqual(1600);
    expect (load1 + load2).toBeGreaterThan(1500);
});

이 함수에 따르면, 인자로 들어가는 800과 800을 더했을 때 1600이라는 값이 나오는데 여기서 사용하는 툴은 일반적으로 숫자를 테스트할 때 사용한다. 툴의 기능은 이름을 따라가고 간단하기에 넘어가겠다.

test('adding floating point numbers', () => {
        const value = 0.3 + 0.4;
        expect(value).toBeCloseTo(0.7);
    });

Float값 테스트 시 toBe가 아닌 toBeCloseTo를 사용한다.

String

//* Regex
test("There is no I in team", () => {
    expect("team").not.toMatch(/I/);
})

test("There is no I in team", () => {
    expect("team").toMatch(/a/);
})

이번 테스트는 스트링으로 주어진 단어가 있을 때 그 안의 특정 알파벳이 들어가는 가 확인하는 테스트이다. Match와 관련된 툴을 사용하기에 스트링에 거의 사용하고 상당히 직관적이다. 첫번째 테스트는 "team"이라는 단어에 I가 안들어가는 것을 테스트 하는 것이고 두번째 테스트는 단어안에 알파벳 a가 있는지 확인하는 테스트이다. Straightforward하다.

Array

//* Arrays
test("Admind should be in usernames", () => {
    const usernames = ["David", "Karen", "John", "Admin"];
    expect(usernames).toContain("David");
})

이 테스트는 여러가지 배열이 있을 때 그 배열안에 특정 요소를 가지고 있는지 확인한다. toContain이라는 툴을 사용해 테스트를 한다. 이것도 매우 직관적이기에 넘어간다.

Promise (비동기)

이번엔 비동기 함수를 테스트하는 것을 보자.

  fetchUser: () => axios.get("https://koreanjson.com/users/1")
        .then(res => res.data)
        .catch(err => "error")

fetchUser를 기억하는가? 이 함수는 주어진 주소로부터 get을 해와 그 데이터 값을 반환한다. 그렇다면 이걸 어떻게 테스트할까..

//* Async Await
test("User fetched name should be 이정도", async () => {
    expect.assertions(1);
    const data = await functions.fetchUser();
    expect(data.name).toEqual("이정도");
})

음 일단 주어진 비동기 함수를 실행해 테스트를 진행해야 하기 때문에 테스트도 비동기를 처리할 수 있도록 해줘야 한다. 개인적으로 나는 ES6를 누리고자 async/await을 적용해주었다.
테스트의 두번째 라인을 보면 expect.assertions(1) 부분이 있는 것을 볼 수 있는데 이것은 몇개의 함수가 호출되었는지 숫자로 나타내주는 것이다. 비동기 함수를 처리할 때 유용한데 1이라는 숫자는 fetchUser라는 하나의 비동기 함수가 실행됬다는 것을 의미한다. 그 이후는 fetchUser로 값을 가져온 후 toEqual을 통해 값이 일치하는지 확인한다. 아래는 async/await을 사용하지 않은 버전이다.

//* Working with async data
 test("User fetched name should be 이정도", () => {
     expect.assertions(1);
     return functions.fetchUser().then(
     data => expect(data.name).toEqual("이정도")
     )
 })

Describe && Others

Describe

테스트들은 describe를 사용해 묶어줄 수 있다.

const nameCheck = () => console.log("Checking names...");

describe("Checking Names", () => {
  
    test("User is Josh", () => {
        const user = "Josh";
        expect(user).toBe("Josh");
    });

    test("User is Karen", () => {
        const user = "Karen";
        expect(user).toBe("Karen");
    });

})

이렇게 describe 로 테스트를 묶어주면, 나중에 에러가 났을 때 어떤 테스트에서 났는지 확인할 수 있어 조금 더 정리하기 간편하다.

Before and After

자 이번엔 beforeEach와 afterEach, 그리고 beforeAll과 afterAll을 알아보자.
말 그대로 beforeEach와 afterEach는 테스트 각각을 실행하기 전과 후에 적용할 수 있는 함수이고 beforeAll과 afterAll은 테스트 전체를 통으로 실행하기 전과 후에 적용할 수 있는 함수이다. 예제를 통해 좀 더 보자.

const nameCheck = () => console.log("Checking names...");

describe("Checking Names", () => {
    beforeEach(() => nameCheck());

    test("User is Josh", () => {
        const user = "Josh";
        expect(user).toBe("Josh");
    });

    test("User is Karen", () => {
        const user = "Karen";
        expect(user).toBe("Karen");
    });

    test("User is David", () => {
        const user = "David";
        expect(user).toBe("David");
    });

    afterEach(() => nameCheck());
})

이 테스트에 앞서 나는 nameCheck() 라는 간단한 함수를 만들었고 테스트 전에 beforeEachafterEach 를 적용해주었다. 이후 yarn test로 테스트를 실행한다면,

위와 같은 결과가 나온다. beforeEach는 각 테스트 전에, afterEach는 각 테스트 후에 nameCheck를 실행시켜 3개의 테스트를 실행했을 때 총 6번의 "Checking names..."를 보여준다. 그렇다면 beforeAll과 afterAll 은 어떻게 나올까?

const nameCheck = () => console.log("Checking names...");

describe("Checking Names", () => {
    beforeAll(() => nameCheck());

    test("User is Josh", () => {
        const user = "Josh";
        expect(user).toBe("Josh");
    });

    test("User is Karen", () => {
        const user = "Karen";
        expect(user).toBe("Karen");
    });

    test("User is David", () => {
        const user = "David";
        expect(user).toBe("David");
    });

    afterAll(() => nameCheck());
})

간단하게, 위와 거의 같은 코드이다. 하지만 beforeEach를 beforeAll로, afterEach를 afterAll로 바꿔준 후 실행시켜 준다.

이 경우, 모든 테스트 실행 전 한번, 실행 후 한번, 총 두번 함수를 실행시키기에 콘솔에는 원하는 메시지가 두번 밖에 뜨지를 않는다.

실제 테스트 코드에서는 보통 데이터베이스에 연결해 필요한 값을 찾아와 값이 맞는지 확인하는데 이런 툴을 사용하면 매우 유용하다. 이외에도 다른 method들이 아주 많지만, 여기서 소개한 것들이 가장 흔히 사용하는 툴들 중 하나이다. 제스트를 하나 더 한다면 아마 좀 더 소개할 것 같다.

TypeScript

Jest는 타입스크립트를 사용해서도 작성할 수 있다. 개인적으로 타입스크립트가 자바스크립트보다 더 강력하고 꼼꼼하다고 생각하기에 타입스크립트로도 작성하는 방법을 알고 싶었다.
먼저 TypeScript를 설치해준다.

npm i -D typescript @types/jest

이미 jest는 설치되었기에 굳이 다시 안해주었지만 타입스크립트로 바로 시작할 경우 꼭 해줘야 한다. 이후 Jest에서 타입스크립트 실행을 위해 ts-jest를 설치해준다.

npm i -D ts-jest

yarn으로 설치하고 싶으면 yarn으로 해도 된다.

Package.json

필요한 부분들을 설치한 후, package.json에 jest라는 항목을 만들어 jest를 설정해준다.

"jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "testRegex": "\\.test\\.ts$",
    "moduleFileExtensions": [
      "ts",
      "js",
      "tsx",
      "json"
    ],
    "globals": {
      "ts-jest": {
        "diagnostics": true
      }
    }
  },

ts-jest.diagnostics는 컴파일 시 에러가 있을 경우 무시하지 않고 테스트를 실패하게 하는 옵션이고, testRegex는 테스트할 파일명을 정규표현식으로 찾는다. 이후 tsconfig.json을 추가한 후 필요한 설정을 해준다. 난 사람들이 자주 사용하는 config를 찾아 설정해주었다.

ES6

ES6를 타입스크립트에서 사용하기 위해 필요한 dependency들을 다 받았다. 타입이 설치되어 있지 않으면 사용하지 못하고, 또 tsconfig에서 적절한 설정에 되어있지않으면 빨간줄이 쳐지니 유의해야 한다.

//package.json
  "devDependencies": {
    "@babel/core": "^7.8.7",
    "@babel/preset-env": "^7.8.7",
    "@babel/preset-typescript": "^7.8.3",
    "@types/jest": "^25.1.4",
    "babel-jest": "^25.1.0",
    "jest": "^25.1.0",
    "supertest": "^4.0.2",
    "ts-jest": "^25.2.1",
    "typescript": "^3.8.3"
  },
  "dependencies": {
    "@types/es6-promise": "^3.3.0",
    "@types/node": "^13.9.1",
    "axios": "^0.19.2",
    "tslib": "^1.11.1"
  }
}

tslib설치, 까먹지 말자! ES6를 사용 설정해주어야 async/await의 사용이 가능하다.

Transfer

이제 이미 작성되있는 파일들을 js에서 ts로 바꿔주고 타입을 지정해주자! 난 reversestring이라는 함수로 예를 들겠다.

reversestring.ts && .test.ts

const reverseString = (str:string):string => 
  str
    .toLowerCase()
    .split("")
    .reverse()
    .join("");

export default reverseString;

이 함수는 스트링이 들어오면 그 값을 반대로 바꿔주는 것인데 음...코드 보면 먼저 스트링을 받아 소문자로 바꿔준 후 스플릿으로 배열로 바꿔 리버스로 순서를 바꿔준다. 이후 join을 해서 다시 스트링으로 변환한다. 여기서 타입 지정은 인자에 한번, 함수의 결과값에 한번 해줬다.

import reverseString from "../src/reversestring";

test ("reverseString function exists", () => {
    expect(reverseString).toBeDefined()
});

test ("String reverses", () => {
    expect(reverseString("hello")).toEqual("olleh")
})

test ("String reverses with uppercase", () => {
    expect(reverseString("Hello")).toEqual("olleh")
})

테스트 안 코드는 이 함수가 잘 작동하는지 확인하는 코드이다. 아, 첫번째 테스트에서 toBeDefined는 이 함수가 존재하는지 확인하는 테스트이다. 이후 toEqual을 사용해 작동을 확인한다. 타입스크립트를 위한 설정을 다 해주었기에 yarn test를 실행하면 타입스크립트로 잘 테스트가 돌아간다. 예~~

Conclusion

실제 Jest를 이용한 간단한 테스트케이스를 작성해보았다. 이렇게 함수 돌아가는 것을 테스트 해보니, 별로 복잡하고 긴 함수는 아니었어도 신기했다. 또 조금 더 안정적인 코드 작성에 한발 더 나아가는 듯해 좋았다. 조금 힘들었던 건 타입스크립트 설정이었다. ES5에 만족했다면 더 금방 했을텐데...난 async/await을 사용하고 싶었다.

이제 어느정도 기본을 익혔으니 본격적으로 테스트케이스 작성을 연습해야겠다!

참조
https://yonghyunlee.gitlab.io/temp_post/jest/
https://gongzza.github.io/javascript/learning-typescript-with-jest/
https://jestjs.io/docs/en/api
https://velog.io/@yesdoing/JavaScript-Testing-Tool-Jest-opjocpva77
https://www.youtube.com/watch?v=7r4xVDI2vho

profile
Grow Joshua, Grow!

2개의 댓글

comment-user-thumbnail
2020년 7월 9일

잘 보구 가용~~

답글 달기
comment-user-thumbnail
2021년 8월 18일

jest 글 재미있네요, 잘 보고 갑니다

(TDD 초보)

답글 달기