Unit Test Basic

1. Unit Test?

유닛 테스트(unit test)는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차다. 즉, 모든 함수와 메소드에 대한 테스트 케이스(Test case)를 작성하는 절차를 말한다(출처-위키백과).

좀 더 개발자의 입장에서 와닿게 이야기 하자면, 각 개별 Function / Method(이하 함수 로 줄입니다)에 대한 테스트 케이스를 작성하여 검증하고, 지속적인 테스트를 수행하여 기초단계에서 부터 오류를 줄이는 행위이다. 언뜻생각하면 개발자가 제품을 직접 테스트하면 될텐데, 왜 제품 개발에도 부족한 시간을 쪼개어 테스트 코드를 작성해야 할까?

2. Unit Test를 왜 하는가

개인적인 경험에 의하면 제품 소스에 Unit Test code가 존재했을 때 이런 부분들이 편했던 것 같다.

3. 테스트 코드 작성

글쓴이가 선호하는 의사코드는 다음과 같다 (mocha 기준).

describe('[파일 이름]', () => {
  describe('[함수 이름]', () => {
    context | describe('[케이스 설명]', () => {
      it('[케이스 설명]', () => {
        // arrange
        const userId = 14
        // act
        const res = fakeReturn(getUser(14))
        // assert
        expect(res.fakeUserName).is.equal('lucas')
        expect(res.fakeUserId).is.equal(14)
      })
    })

    context | describe('...', () => {
      ...
    })
  })
})
  1. 맨 처음은 파일 이름을 작성한다. 모든 테스트를 실행할 시 알아보기 편하다.
  2. 다음은 해당 파일 내에서 테스트하려는 함수의 이름을 작성한다. 파일 이름을 적는 이유와 동일하다.
  3. context | describe는 각자의 취향에 관한 부분인데, 아래에서 조금 설명하도록 하겠다. 이곳에는 테스트하려는 케이스에 대한 명세를 한다. 길어도 좋고, 짧아도 좋다. 쉽게 알아 볼 수만 있으면 된다.
  4. it에는 이 함수에게 기대하는 동작을 명세한다. 보통 should로 시작하는 문장을 적어주는 것이 일반적이다.
  5. it 내부에는 실제 함수가 동작을 하기위한 조건을 구성해주고(arrange), 실행하고(act), 실행의 결과를 검증한다(assert). 글쓴이의 경우 반드시 이렇게 세 부분으로 나눌 필요가 없는 간단한 함수(e.g. getSum) AAA원칙을 따르기 보다는 아래처럼 읽기 쉽게 한줄에 작성해 버린다.
expect(getSum(2, 3)).is.equal(5)
  1. 테스트 케이스 작성 시 arrow function을 사용하면 Mocha context에 접근할 수 없는 문제가 발생한다. lifecycle hook을 사용하거나 this에 접근할 일이 있다면 function declaration을 사용할 것.
describecontextitbeforeafter
BDDdescribecontextitbeforeafter
TDDsuite---testsuiteSetupsuiteTeardown

4. Test Double

단위 테스트 케이스는 테스트 대상이 의존하는 것에 대해 독립적으로 작성 되어야 한다. 대상이 의존하는 것에 대한 독립성이라는 언뜻 이해가 가지 않는 개념에 대해서, 실용주의 프로그래머를 위한 단위 테스트 with JUnit이라는 책에서는 이렇게 설명하고 있다.

Independent(독립적)
테스트는 깔끔함과 단정함을 유지해야 한다. 즉, 확실히 한 대강에 집중한 상태여야 하며, 환경과 다른 개발자들(명심하라. 다른 개발자들이 동시에 같은 테스트를 실행해 볼 수도 있다)에게서 독립적인 상태를 유지해야 한다.
또한 독립적이라는 것은 어떤 테스트도 다른 테스트에 의존하지 않는다는 것을 의미한다. 어느 순서로든, 어떤 개별 테스트라도 실행해 볼 수 있어야 한다. 처음 것을 실행할 때 그 밖의 다른 테스트에 의존해야 하는 상황을 원하지는 않을 것이다.

모든 테스트는 섬이어야 한다.

이 말이 의미하는 것은 언제, 어디에서, 누군가가 테스트를 실행하는것에 상관없이 테스트 수행이 가능해야한다는 뜻이다. 그렇다면 이런 독립성을 방해하는 요소가 무엇이 있을까?
글쓴이가 겪은 몇가지 케이스는 다음과 같다.

DB, 파일 I/O에 의존한 테스트는 DB에 연결할 수 없거나(DB점검, 보안 이슈, 네트워크 등의 문제로), 필요한 파일이 정확하게 포함되어 있지 않은 경우 문제를 일으킨다. 특히 DB에 의존하는 경우 db.save()등의 method가 실제로 작동하면서 DB에 필요없는 데이터가 insert되거나, 파일에 의존하는 경우 단지 테스트만을 위해 프로젝트 내에 불필요한 파일이 필요하게 되는 문제점이 발생한다.

네트워크 연결 역시 문제가 있다. 특정 서버가 특정 IP대역에서만 접근이 가능한 경우가 그렇다. 접근 가능한 IP대역을 추가하는 등의 방법으로 해결이 가능하지만, 단지 테스트때문에 보안 이슈나 정책을 무시할 수는 없다.

그렇기 때문에 진짜 DB나 파일에 접근하지 않고 이런 문제들을 해결해 줄 대역들이 자연스레 필요하게 된다(API를 호출하는 함수의 테스트 코드를 작서한다면 더욱!).

5. Stub, Fake, Spy, Mock

위의 문단 마지막에 대역(Stunt Double)이 필요하다고 했었는데, Unit test를 위한 대역들도 존재한다. 이를 테스트 더블(Test Double)이라고 한다. 테스트 더블은 보통 개념적으로 아래의 종류로 나눠볼 수 있다.

테스트 더블의 사용에 대해 설명하면 길어질 것이 뻔하기 떄문에, 다음 글에서 설명하는 것이 좋을 것 같다. 지금은 이런것이 있다는 것을 알고 지나가도록 하자.

6. Unit Test code 작성 시 생각하면 좋은 것들

뭔가 Unit test에 대한 설명을 급하게 마무리 하는 느낌이 있지만, 어렴풋이 개념을 잡기에는 충분하다고 본다. 이 글에서는 Unit test 코드의 필요성과 작성 요령, 기본적인 개념에 대해서만 알게 되어도 성공적이라고 생각한다.


참고