박종훈 기술블로그

테스트는 왜 해야하고 어떻게 해야할까

테스트를 왜 해야하고 어떻게 해야할까?
xUnit 테스트 패턴 - 제라드 메스자로스 - 0장


리팩터링 4장에서 마틴 파울러는 다음과 같이 이야기 했다.

프로그래머가 업무 시간을 어떻게 보내는지를 살펴보면 사실 코드를 짜는 시간은 얼마 되지 않는다는 사실을 알게 될 것이다. 뭐가 어덯게 돌아가야 하는지를 찾아보거나 설계를 하기도 하지만 업무 시간의 대부분은 디버깅으로 보낸다. 프로그래머라면 누구나 몇 시간, 아니 밤새 디버깅해본 경험이 있을테고 이런 힘든 디버깅에 대한 무용담쯤은 하나씩 늘어놓을 수 있다. 버그 수정이야 보통 금방이지만 버그 찾기는 죽음이다. 게다가 버그 하나 고치고나면 꼭 다른 버그가 생기기 마련이고, 한참 후에야 그걸 알게 된다. 그러면 또 그 버그를 찾느라 온종일을 날려먹게 된다.

피드백

피드백은 중요한 요소다. 우리가 들인 노력으로 기대한 효과가 나는지 알려준다. 테스팅은 소프트웨어에서 피드백을 받는 일이 전부다. 이런 이유로 “애자일”이나 “린” 소프트웨어 개발의 필수 요소 중 하나로 피드백이 포함된다.

개발 과정 안에 피드백 루프가 있으면 우리가 작성하는 소프트웨어에 대한 자신감을 가질 수 있다.

테스팅

‘테스팅’의 전통적인 정의는 품질 보증의 세계에서 유래했다. 소프트웨어에 버그가 있다는 걸 확신하므로 테스트를 한다. 테스트를 하고, 또 하고, 또 한다. 소프트웨어에 버그가 있는지 없는지 더 이상 밝혀낼 수 없을 때까지 반복한다.

고전적인 테스트 과정은 소프트웨어가 완성된 후에야 시작된다. 이런 이유로 테스팅은 소프트웨어의 품질을 측정할 뿐 소프트웨어의 품질을 높여주진 못했다.

이럴 경우에는 비 개발자가 테스트에 참여하게 된다. 이렇게 테스트 해 얻은 피드백은 굉장히 가치가 있지만 개발 주기에서 개발자는 피드백을 너무 늦게 받으므로 피드백의 가치가 떨어진다.

더구나 발견된 문제를 수정하고 그걸 또 테스트해야 하므로 개발 일정이 밀리는 끔찍한 일이 벌어진다.

이런 상황을 어떻게 개선할 수 있을까?

개발자 테스트

‘언제든지 한 번에’ 돌아가는 코드를 짤 수 있다고 믿는 프로그래머는 거의 없다. 오히려 한 번에 코드가 돌아가면 깜짝 놀라곤 한다. 그렇기 때문에 개발자도 테스트를 한다. 개발한 소프트웨어가 생각한 대로 돌아가는지 개발자도 확인하고 싶어한다. 대부분 개발자는 소프트웨어를 단위별로 따로 테스트하는 방식을 선호한다. 단위(unit)는 큰 규모의 컴포넌트일 수도 있고, 클래스나 메소드, 함수일 수도 있다.

이러한 테스트는 테스트 대상이 되는 단위가 고객의 요구사항이 아닌 소프트웨어의 설계 결과라는 점에서 테스터들이 작성하는 테스트와 다르다.

자동 테스트

‘깨지기 쉬운 테스트’ 문제

자동화된 테스트는 사소한 이유로 쉽게 실패하곤 한다. 테스트 자동화가 어떤 한계를 지니는지를 잘 이해해야 한다.

다음과 같은 한계가 있을 수 있다.

  • 동작에 민감함
  • 인터페이스에 민감함
  • 데이터에 민감함
  • 문맥에 민감함

우리는 이 네 가지 민감함을 극복하는 방법에 대해서 배워나갈 것이다.

동작에 민감함

시스템의 동작이 변경될 경우 (ex. 요구사항에 따라 시스템을 수정함) 변경된 부분을 실행하는 테스트를 실행하면 거의 실패한다. 이는 어떤식으로 테스트 자동화를 하더라도 피할 수 없다. 진짜 문제는 다시 시스템이 테스트를 시작할 수 있는 상태로 만들기 위해 해당 기능을 직접 실행해야 할 수도 있다는 점이다. 이로인해 미치는 영향이 생각보다 클 수 있다.

인터페이스에 민감함

테스트 대상 시스템(SUT)의 비즈니스 로직을 사용자 인터페이스로 테스트하겠다는 건 좋은 생각이 아니라. 인터페이스가 약간만 바뀌어도 테스트가 실패한다.

데이터에 민감함

모든 테스트는 테스트 픽스처(fixture)라는 시작 시점이 있다고 가정한다. 대부분 이런 테스트 픽스처는 시스템 데이터베이스를 통해 정의된다. 시스템 데이터 자체가 변경되는 경우에 대해 큰 노력을 들이지 않았다면 테스트가 실패한다.

문맥에 민감함

시스템의 동작은 시스템 외부 상태에 따라 영향을 받는다. 이러한 상황을 제어할 방법을 미리 마련해 놓지 않는 한 항상 성공한다고 장담할 수 없다.

자동 테스트 사용하기

회귀 테스트는 기존의 소프트웨어가 수정되면서 발생할 수 있는 의도하지 않은 버그를 잡아준 다는 점에서 매우 중요한 피드백을 제공한다.

명세로서의 테스트

애자일 방법론의 중요 실천 방법 중 하나인 테스트 주도 개발(TDD)에서는 자동 테스트를 전혀 다르게 사용한다. 테스트 주도 개발에서는 자동 테스트를 회귀 테스트보다는 작성해야 하는 소프트웨어 기능을 명세로 만드는 데 쓴다. TDD의 장점은 소프트웨어 개발을 “무엇을 해야 하는가?”와 “어떻게 해야 하는가” 두 단계로 나눌 수 있다는 데 있다.

애자일 개발자는 단계별로 시스템을 기능에 따라 나눠 설계, 개발하고 개발한 소프트웨어가 제대로 돌아가는지 확인한 다음에야 다음 단계로 진행한다. 애자일 개발자가 설계를 미리 하지 않는다는 것이 아니라 ‘끊임없이 계속 설계’한다는 걸 의미한다. 이것을 극단적으로 적용한 것이 창발적 설계(Emergent Design, 설계를 미리 하지 않고 필요한 순간에 함)이다.

꼭 이런식으로 개발할 필요는 없다. 얼마든지 고차원 설계(혹은 아키텍처)를 미리 해놓은 다음, 상세 설계만 기능별로 필요할 때 해도 된다.

어떤 방법을 쓰든지 간에 클래스나 메소드를 어떻게 구현할까 고민하기 전에 실제 만들어질 소프트웨어가 어떻게 동작해야 하는지를 먼저 생각해 보는 편이 좋다.

먼저 테스트를 작성한 후 테스트를 성공시키는 데 집중한다. 이 과정에서 작업이 얼마나 진행됐는지도 알 수 있다. 코드를 작성해 나감에 따라 테스트가 하나하나 통과하는 걸 확인할 수 있다. 이전에 작성한 테스트들이 지금 수정한 것으로 인해 예상치 못한 부수효과(side effect)가 생기지 않았는지 회귀 테스트해준다. SUT의 기존 기능을 ‘고정시켜’ 실수로 변경되는 걸 막아주는 능력이야 말로 자동 단위 테스트가 진가를 발휘하는 부분이다.

패턴

패턴이란 ‘반복해서 나타나는 문제들의 해결책’이다.

이 책에서는 패턴을 세 종류로 나눴다.

  • 테스트 전략(Strategy) 패턴
    • 신선한 픽스처 (Fresh Fixture)
    • 공유 픽스처 (Shared Fixture)
  • 테스트 설계 패턴
    • 모의 객체 패턴
    • 테스트 대역 패턴
  • 관용 패턴
    • 코딩 관용구(Coding Idiom)

패턴의 좋은 점은 여러 대안 중 적절한 선택을 할 수 있게 충분한 정보를 제공한다는 데 있다.

추가적으로 안티패턴 이 있다.

리팩터링

리팩터링은 코드 동작의 변경 없이 설계를 변경하는 굉장히 엄격한 접근 방법이다. 재설계하는 동안 아무것도 깨트리지 않았음을 보장해주는 자동 테스트라는 안전망 없이 리팩터링하기란 굉장히 어렵기 때문에 리팩터링은 자동 테스트와 함께 사용된다.

테스트 리팩터링은 자동 테스트를 테스트하는 자동 테스트가 없다는 점에서 제품 코드 리팩터링과는 약간 다르다. 테스트를 리팩터링한 후에 테스트가 성공하더라도 이 테스트가 적당한 조건일 때 실패 한다고 장담할 수 있을까? 이런 문제가 발생할 가능성을 최소화 하기 위해 굉장히 보수적이고 안전한 리팩터링으로 하게 된다.

용어

이 책은 소프트웨어 개발과 소프트웨어 테스트라는 별개의 두 도메인에서 용어를 가져왔다. 덕분에 독자에 따라서 익숙하지 않은 용어도 보게 될 것이다. 모르는 용어는 찾아보면 된다. 하지만 몇 가지 용어는 이 책에서 다루는 내용 대부분을 이해하기 위해서 꼭 익숙해져야 하므로 여기에서 짚고 넘어간다.

테스트 용어

테스트 대상 시스템 (System Under Test, SUT) 은 ‘테스트하려는 대상’을 의미한다. 단위 테스트에서는 테스트하려는 클래스나 메소드 모두가 SUT다. 고객 테스트에서는 전체 애플리케이션이 SUT다.

개발 중인 애플리케이션이나 시스템 중에서 SUT에 포함되진 않지만 SUT에서 호출하거나 SUT를 실행할 때 필요한 데이터를 미리 설정해주기 위해 필요한 부분을 의존 컴포넌트(Depended-On Component, DOC)라고 한다. SUT와 DOC 둘 다 테스트 픽스처의 일부이다.

sut of tests

위 이미지는 테스트 별 SUT 범위를 표현한 예시이다. 애플리케이션, 컴포넌트, 단위는 각각 해당 테스트에 대해서만 SUT가 된다. 단위1 SUT는 단위2 테스트의 DOC(픽스처의 일부) 역할을 하면서 하면서 동시에 컴포넌트1SUT와 애플리케이션1 SUT의 부분이기도 하다.

마무리

책에 있는 패턴들보다 어쩌면 더 좋은 해결책이 있을 수도 있다. 이 책에 있는 해결책들은 그저 여러 사람들이 써 보았을 때 좋았던 해결책일 뿐이다. 조언을 항상 곧이곧대로 듣진 말자.

다음 글에서 실제적으로 어떻게 테스트 코드를 개선할지에 대한 예시를 같이 알아본다.