Search

테스트에 대한 생각

프로젝트를 구성할 때, 테스트 코드를 반드시 만든다.
엄격한 TDD를 지향하진 않는다.
하지만 빠르고 안정적인 개발을 해야겠다는 생각이 들면 자연스레 테스트 코드부터 적는다.
주로 세 종류의 테스트를 만든다.
1.
유닛 테스트(unit test)
2.
기능 테스트(functional test)
3.
통합 테스트(integration test)
각 테스트가 어떤 역할을 하고, 테스트를 작성할 때 어떤 것에 중점을 두는지 얘기하겠다.

이상향

그런데 아주 잠깐만 이상적인 프로젝트에 대해 이야기 해보겠다.
세 종류의 테스트를 구성하는데 근거가 되는 주요 논리다.
아무것도 없는 빈 파일에 프로젝트를 처음부터 만든다고 해보자.
처음으로 순수 함수를 하나 만든다.
순수 함수란 사이드 이펙트(side effect)가 없고, 동일한 입력에 대해서 동일한 결과를 반환하는 함수다.
사이드 이펙트가 없다는 말은 항상 예측 가능하다는 의미다.
필요에 따라 함수를 계속 만들어간다.
반드시 모든 함수는 계속 순수 함수를 유지한다.
이제 함수가 제법 많아져서 각 함수의 논리적인 속성에 맞게 클래스로 묶는다.
높은 응집력, 낮은 결합도를 가진 이상적인 클래스를 만들었다고 해보자.
클래스 내의 모든 메소드가 순수 함수의 속성을 띄고, 논리에 맞게 잘 분리된 클래스를 편의상 순수 클래스라 부르겠다.
계속 코드를 추가한다.
이제 클래스를 묶어서 모듈을 만든다.
그리고 각 모듈 별로 역할과 책임을 명확히 분리했다고 해보자.
순수 클래스를 묶어서 만든 이상적인 모듈을 역시 순수 모듈이라고 부르겠다.
그럼 이제 프로젝트는 순수 모듈의 집합이 된다.
마지막으로 시스템의 목적에 따라서 순수 모듈을 조합한다고 하자.
각 모듈 별로 역할과 책임이 명확하고, 사이드 이펙트가 없기 때문에 예측 가능한 이상적인 시스템이 완성됐다.
순수 모듈의 조합으로 모든 기능을 버그 없이 구현할 수 있게 됐다.

유닛 테스트

유닛 테스트는 프로그램의 각 부분을 고립시켜서 단독적으로 기능을 검증하는 테스트다.
이상적인 프로그램 세계관에서 각 함수와 클래스가 정말 순수한지 검증하는 테스트다.
시스템 내의 중요한 모듈에 대해서는 모든 메소드에 대해 유닛 테스트를 작성하는 편이다.
위 세계관에서 가장 근간이 되는 순수 함수의 순수성이 무너지면, 나의 시스템은 붕괴하기 때문이다.

기능 테스트

기능 테스트는 테스트 케이스에 맞게 사용자 관점에서 시스템과 상호 작용하는 테스트를 말한다.
위키피디아 따르면 블랙박스 형식으로 작성한다고 하는데, 난 화이트 박스 형식으로 작성한다.
일단은 마땅한 용어가 없어서 기능 테스트라고 부른다.
기능 테스트의 주요 목적은 시스템이 입력값에 대해 요구 조건에 맞는 응답을 반환하는 지 검증하는 것이다.
시스템의 일관성을 확보하여 코드를 적극적으로 변경할 수 있도록 하는 것에 중점을 둔다.
잘 짜여진 테스트가 신뢰를 보증한다.
다만 기능 테스트에서 에러 케이스에 대한 테스트는 적극적으로 만들지 않는 편이다.
주로 주어진 입력값에 대해 적합한 응답을 내는 테스트 케이스만 작성한다.
입력 데이터 검증 테스트를 굳이 만들지 않는 이유는 생산성과 연관 있다.
단순히 테스트 케이스가 많다고 시스템의 안정성이 꼭 확보되는 것이 아니기 때문이다.
오히려 기계적으로 적는 무의미한 테스트코드는 거추장스럽고 개발 속도만 늦춘다.
꼭 필요한 부분을 적절하게 테스트 하는 것이 훨씬 중요하다.
애초에 테스트를 작성하는 목표가 100%의 커버리지를 달성하는 것이 아니다.
대신 모든 성공 케이스에 대한 테스트 케이스와 Null Check 같은 검증은 꼼꼼하게 한다.
테스트는 일종의 카나리 같은 역할이다.
코드 변경으로 인해 발생하는 문제를 배포 전에 인식하는 수단이다.
테스트 케이스 개수와 시스템 안정성은 단순 비례하지 않는다.
또 가급적 검증 받은 라이브러리를 사용하여, 일정 부분은 라이브러리에게 테스팅을 위임한다.
잘 만들어진 라이브러리라면 이미 테스트가 꼼꼼하게 작성되어 있을 것이다.
사용자가 라이브러리를 신뢰할 수 없어서 라이브러리까지 직접 검증해야 한다면, 그 라이브러리는 쓰면 안 된다.
기능 테스트에서는 모킹(mocking)을 적극적으로 활용한다.
앞선 유닛 테스트를 통해 각 모듈의 순수성이 검증 되었다면, 실제로 모듈을 호출하지 않아도 충분하기 때문이다.
이어서 살펴볼 통합 테스트에서도 기능 테스트 대신 모듈 간의 인터페이스를 일정 부분 검증한다.
또 모킹을 사용하는 이유는 모든 것을 실제로 호출하면 테스트 동작 시간이 불필요하게 길어지기 때문이다.
경험적으로 봤을 때 테스트가 느려지면 개발자들은 점점 테스트를 안하려고 한다.
이상적인 세계관의 관점에서 보면, 시스템의 요구 조건에 맞게 각 모듈이 조합 됐는지 검증하는 절차다.

통합 테스트

통합 테스트는 모듈 간 상호작용을 검증하는 테스트다.
3rd party 라이브러리나 직접 작성하지 않은 코드와 인터페이스를 맞춰보는 것을 중점으로 둔다.
의도대로 외부 시스템(혹은 다른 모듈)과 통신할 수 있는지 검증하는 절차다.
대표적으로 ORM을 통해 실제로 데이터베이스에 데이터를 저장하는 것 같은 테스트다.
실제로 네트워크를 사용하거나 I/O를 발생시키는 테스트다.
기능 테스트에서 대부분 모킹했기 때문에 통합 테스트에서는 외부 인터페이스를 반드시 한 번 이상 실제 호출한다.
다르게 말하면, 통합 테스트에서 한 번씩 검증했기 때문에 기능 테스트에서는 모킹을 한다고 이야기 할 수도 있다.

결론

세 종류의 테스트는 서로 상보적인 관계를 갖는다.
유닛 테스트와 통합 테스트의 신뢰성을 바탕으로 기능 테스트를 작성하기 때문이다.
따라서 세 종류의 테스트가 전부 있어야 테스트를 작성하는 목적을 온전히 달성할 수 있다. (끝)