대체 자바스크립트 테스트가 뭘까요

대체 자바스크립트 테스트가 뭘까요

원문: Kent C. Dodds, "But really, what is a JavaScript test?"

소프트웨어를 테스트하는 이유? 너무 많은데요. 그중에서도 제가 생각하는 두 가지 이유는 아래와 같습니다.

  1. 테스트는 작업 속도를 높여 소프트웨어를 더 빠르게 개발하고

  2. 변경 사항을 적용할 때 기존 코드를 망가뜨리지 않는다는 것을 보장합니다.

그렇다면 여기서 몇 가지 질문을 던져 볼게요 (이 질문들은 트위터 투표입니다).

이 글의 목표는 여러분 모두가 마지막 질문에 "네"라고 대답할 수 있도록 만드는 것입니다. 따라서 자바스크립트에서 테스트가 실제로 무엇인지에 관하여 여러분이 기본적인 이해를 가지고 더 나은 테스트를 작성하도록 도울 것입니다.

이제 간단한 math.js 모듈이 내보내는 두 함수에 관하여 테스트를 작성해 보겠습니다.

const sum = (a, b) => a + b
const subtract = (a, b) => a - b

module.exports = {sum, subtract}

참고할 수 있는 깃허브 리포도 만들어 두었습니다 🐙😸

1단계

아래는 제가 생각하는 가장 기초적인 형태의 테스트입니다.

// basic-test.js
const actual = true
const expected = false
if (actual !== expected) {
  throw new Error(`${actual} is not ${expected}`)
}

테스트 코드를 실행하려면 node basic-test.js를 실행하세요! 이게 테스트입니다! 🎉

테스트는 무언가의 실제 결과가 예상 결과와 일치하지 않을 때 오류를 던지는 코드입니다. (브라우저 이벤트를 발생시키기 전 컴포넌트가 문서에 렌더링되어야 하거나 데이터베이스에 사용자가 존재해야 하는 등의) 먼저 설정되어야 하는 일부 상태에 의존하는 코드를 다루는 경우 테스트는 더 복잡해질 수 있습니다. 하지만 math.js 모듈에 있는 함수처럼 "순수 함수" (주어진 입력에 항상 동일한 결과를 반환하고 주변 환경의 상태를 변경하지 않는 함수)는 테스트하기에 비교적 용이합니다.

actual !== expected라고 나온 부분은 "어서션(assertion)"이라고 부릅니다. 어서션은 코드에서 어떤 것이 반드시 특정 값이어야 하거나 음... 특정 테스트를 통과해야 한다는 방식입니다. actual이 정규표현식과 일치하거나, 특정 길이의 배열이거나, 여러 다양한 경우도 어서션이 될 수 있어요. 핵심은 어서션이 실패하면 오류를 던진다는 것입니다.

math.js 함수를 위한 가장 기본적인 테스트는 아래와 같습니다.

// 1.js
const {sum, subtract} = require('./math')

let result, expected

result = sum(3, 7)
expected = 10
if (result !== expected) {
  throw new Error(`${result} is not equal to ${expected}`)
}

result = subtract(7, 3)
expected = 4
if (result !== expected) {
  throw new Error(`${result} is not equal to ${expected}`)
}

잘하셨어요! 테스트를 node로 실행하면 오류 없이 명령어가 종료됩니다. 이제 +-로 변경하여 sum 함수를 망가뜨리고 다시 실행해 보겠습니다.

$ node 1.js
/Users/kdodds/Desktop/js-test-example/1.js:8
  throw new Error(`${result} is not equal to ${expected}`)
  ^

Error: -4 is not equal to 10
    at Object.<anonymous> (/Users/kdodds/Desktop/js-test-example/1.js:8:9)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

멋집니다! 우리는 이미 기본 테스트로 이득을 보고 있어요! 자동화된 테스트를 망가뜨리지 않고는 sum 함수를 망가뜨릴 수 없어요! 성공입니다!

테스팅 프레임워크 (또는 어서션 라이브러리)에서 가장 중요한 부분 하나는 오류 메시지가 얼마나 유용한지입니다. 테스트가 실패할 때 우리는 보통 먼저 오류 메시지를 봅니다. 오류 메시지에서 근본적인 문제가 무엇인지를 알 수 없다면 코드를 몇 분 동안 살펴보아야만 잘못된 이유를 이해할 수 있습니다. 사용 중인 프레임워크에서 제공하는 어서션을 얼마나 잘 이해하고 사용하는지에 따라 오류메시지의 질이 많이 달라집니다.

2단계

위에서 우리가 했던 것처럼 어서션을 만들기 위한 assert 모듈이 실제로 노드에 존재한다는 것을 알고 있나요 🤔? 노드에서 제공하는 모듈을 사용하도록 테스트를 리팩터링해 봅시다!

// 2.js
const assert = require('assert')
const {sum, subtract} = require('./math')

let result, expected

result = sum(3, 7)
expected = 10
assert.strictEqual(result, expected)

result = subtract(7, 3)
expected = 4
assert.strictEqual(result, expected)

좋아요! 여전히 테스트 모듈이네요. 우리가 이전에 했던 것과 기능적으로 동일합니다. 유일한 차이는 오류 메시지예요.

$ node 2.js
assert.js:42
  throw new errors.AssertionError({
  ^

AssertionError [ERR_ASSERTION]: -4 === 10
    at Object.<anonymous> (/Users/kdodds/Desktop/js-test-example/2.js:8:8)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

우리가 던진 에러가 더 이상 코드의 어떤 내용도 포함하지 않는다는 것을 알 수 있네요. 유감이지만... 😦 계속 진행해 보겠습니다.

3단계

자, 이제 우리가 직접 간단한 테스팅 "프레임워크"와 어서션 라이브러리를 작성합시다. 어서션 라이브러리부터 시작할게요. 노드에 내장된 assert 모듈 대신에 expect라는 이름의 라이브러리를 만들어 볼 거예요. 아래는 변경 사항에 맞게 리팩터링된 테스트입니다.

// 3.js
const {sum, subtract} = require('./math')

let result, expected

result = sum(3, 7)
expected = 10
expect(result).toBe(expected)

result = subtract(7, 3)
expected = 4
expect(result).toBe(expected)

function expect(actual) {
  return {
    toBe(expected) {
      if (actual !== expected) {
        throw new Error(`${actual} is not equal to ${expected}`)
      }
    },
  }
}

멋지네요. 이제 우리는 반환된 객체에 (toMatchRegextoHaveLength 같은) 다수의 어서션을 추가할 수 있습니다. 오! 이제 다시 오류 메시지가 나오네요.

$ node 3.js
/Users/kdodds/Desktop/js-test-example/3.js:17
        throw new Error(`${actual} is not equal to ${expected}`)
        ^

Error: -4 is not equal to 10
    at Object.toBe (/Users/kdodds/Desktop/js-test-example/3.js:17:15)
    at Object.<anonymous> (/Users/kdodds/Desktop/js-test-example/3.js:7:16)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

그래요. 상황이 좋아 보입니다.

4단계

그런데 말이죠. 여기도 문제가 있어요 😖... 어떻게 이 오류 메시지로 sum 함수가 망가졌다는 것을 알 수 있죠? subtract 모듈일 수도 있잖아요. 게다가 테스트 소스가 테스트를 (시각적으로나 다른 방면으로) 잘 분리하지 못하는군요.

분리가 잘 이루어지도록 헬퍼 함수를 작성해 봅시다.

// 4.js
const {sum, subtract} = require('./math')

test('sum adds numbers', () => {
  const result = sum(3, 7)
  const expected = 10
  expect(result).toBe(expected)
})

test('subtract subtracts numbers', () => {
  const result = subtract(7, 3)
  const expected = 4
  expect(result).toBe(expected)
})

function test(title, callback) {
  try {
    callback()
    console.log(`✓ ${title}`)
  } catch (error) {
    console.error(`✕ ${title}`)
    console.error(error)
  }
}

function expect(actual) {
  return {
    toBe(expected) {
      if (actual !== expected) {
        throw new Error(`${actual} is not equal to ${expected}`)
      }
    },
  }
}

이제 주어진 테스트와 관련된 모든 내용을 "test" 콜백 함수 안에 넣고 해당 테스트에 이름을 붙일 수 있습니다. 그런 다음 test 함수를 사용하여 더 유용한 오류 메시지를 제공할 뿐만 아니라 (첫 번째 오류가 발생해도 중단되지 않고) 파일의 모든 테스트를 실행할 수 있습니다! 이제 결과는 아래와 같아요.

$ node 4.js
✕ sum adds numbers
Error: -4 is not equal to 10
    at Object.toBe (/Users/kdodds/Desktop/js-test-example/4.js:29:15)
    at test (/Users/kdodds/Desktop/js-test-example/4.js:6:18)
    at test (/Users/kdodds/Desktop/js-test-example/4.js:17:5)
    at Object.<anonymous> (/Users/kdodds/Desktop/js-test-example/4.js:3:1)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
✓ subtract subtracts numbers

좋습니다! 이제 오류 메시지 자체와 테스트 제목을 볼 수 있어서 어떤 것을 고칠지 알 수 있겠네요.

5단계

이제 남은 할 일은 모든 테스트 파일을 찾아서 실행할 CLI 도구 작성뿐입니다! 처음에는 상당히 간단하지만 이 위에 추가할 수 있는 것이 엄~청 많답니다. 😅

여기서 우리는 테스팅 프레임워크와 테스트 러너를 구축하고 있는데요. 다행히도 이미 구축된 다수의 테스팅 도구들이 있습니다! 수많은 도구들을 사용해 보았고 모두 훌륭했습니다. 그럼에도 제스트(Jest) 🃏만큼 제 사용 사례와 가장 부합하는 도구는 찾지 못했습니다. 제스트는 놀라운 도구입니다 (제스트를 더 알아보세요).

따라서 자체 프레임워크를 구축하는 대신에 테스트 파일이 제스트와 함께 작동하도록 변경하려고 하는데... 우연히도 이미 그렇게 설정되어 있네요! 우리는 testexpect의 자체 구현만 제거해 주면 됩니다. 왜냐하면 제스트가 이들을 테스트에 전역 객체로 포함하기 때문이죠! 이제 아래와 같은 모습으로 보입니다.

// 5.js
const {sum, subtract} = require('./math')

test('sum adds numbers', () => {
  const result = sum(3, 7)
  const expected = 10
  expect(result).toBe(expected)
})

test('subtract subtracts numbers', () => {
  const result = subtract(7, 3)
  const expected = 4
  expect(result).toBe(expected)
})

해당 파일을 제스트로 실행하면 결과는 아래와 같은 모습입니다.

$ jest
 FAIL  ./5.js
  ✕ sum adds numbers (5ms)
  ✓ subtract subtracts numbers (1ms)

● sum adds numbers

expect(received).toBe(expected)

    Expected value to be (using Object.is):
      10
    Received:
      -4

      4 |   const result = sum(3, 7)
      5 |   const expected = 10
    > 6 |   expect(result).toBe(expected)
      7 | })
      8 |
      9 | test('subtract subtracts numbers', () => {

      at Object.<anonymous>.test (5.js:6:18)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.6s, estimated 1s
Ran all test suites.

텍스트로는 구별하기가 어렵지만 결과는 컬러 코딩됩니다. 아래는 결과 이미지입니다.

제스트는 컬러 코딩으로 관련 부분을 식별하는 데 정말 도움이 됩니다 😀 오류가 발생한 코드도 보여 주죠! 이제야 오류 메시지가 유용해지네요!

결론

그래서 자바스크립트 테스트가 뭘까요? 자바스크립트 테스트란 단지 어떤 상태를 설정하고, 어떤 행동을 수행하고, 새로운 상태를 위한 어서션을 만드는 코드입니다. 여기서는 beforeEachdescribe 같은 일반적인 프레임워크 헬퍼 함수는 다루지 않았고 toMatchObjecttoContain 같이 추가할 수 있는 어서션도 훨씬 더 많습니다. 그렇지만 이 글을 통하여 여러분이 자바스크립트 테스팅의 기본 개념에 관한 발상을 얻기를 바랍니다.

도움이 되기를 바라며! 행운을 빌어요! 👍