목적과 의도에 따라 달라지는 인수 테스트 작성법
인수 테스트(Acceptance Test)를 처음 접하는 사람들이 자주 하는 질문들이 있다: "이게 인수 테스트가 맞나요?", "통합 테스트와 어떤 점이 다른가요?". 기존에 작성하던 테스트와 큰 차이가 없다고 느끼는 사람들에게는 당연한 궁금증일 수 있다. 이러한 느낌이 드는 이유는 인수 테스트와 통합 테스트가 가지는 유사성 때문이다. 둘 다 여러 컴포넌트나 시스템을 통합하여 테스트하는 점에서 기초적인 공통점이 있다.
테스트 코드를 볼 때 "이 테스트는 어떤 테스트인지?"를 고민하기 전에 "어떤 목적으로 작성되었는지?"를 먼저 고민해 보자. 차이는 '목적'과 '관점'에 있다. 인수 테스트는 사용자가 어떻게 시스템을 경험할지를 중심으로 테스트한다. 반면 통합 테스트는 기술적인 구성 요소가 제대로 작동하는지를 검증한다. 중요한 점은 테스트가 어떻게 작성되었는지 보다는 테스트의 목적을 명확히 하고 용어를 일관되게 사용하는 것이다.
이러한 테스트를 얘기할 때 '기능 테스트'나 '인수 테스트'라는 용어를 종종 들을 수 있다. 어떤 사람들은 이 두 테스트가 다르다고 주장하고, 때로는 이런 용어와 정의에 대해 끝없이 토론하기도 한다. 이런 토론은 종종 혼란의 원인이 되곤 한다.
하지만 중요한 건 이런 것들이 아니다. 결국엔 소프트웨어가 사용자 입장에서 제대로 작동하는지, 기술적인 면만 아니라 실제로 테스트해야 한다. 이 테스트를 무슨 이름으로 부르든 그게 문제가 아니다. 중요한 건 이런 테스트를 꼭 해야 한다는 것. 그러니까 용어 하나를 정하고, 그 용어를 일관되게 사용하면서 테스트를 작성해야 한다.
The Practical Test Pyramid - Acceptance Tests
목적에 따라서 테스트의 이름이 달라지다니, 아직 공감이 가지 않을 수 있다. 이 글에서는 주어진 상황과 목적에 따라 어떻게 인수 테스트를 구성하는지에 대한 과정을 소개할 것이다. 그래서, 인수 테스트가 뭐라고? 에서 이야기한 대로 "사용자 스토리 기반의 기능 테스트"라는 관점에서 시나리오를 정의하고 그 시나리오대로 기능이 잘 동작하는지를 확인하기 위해 인수 테스트를 작성하는 과정을 소개한다.
먼저 로또 요구사항을 만족하는 자바 기반의 콘솔 애플리케이션을 만드는 과정이다. 요구사항을 바탕으로 시나리오 형태의 요구사항으로 변환한 뒤 이 시나리오를 검증하는 인수 테스트를 만드는 순서로 진행한다.
- 콘솔 기반의 로또 애플리케이션을 만든다.
- 금액을 입력하면 금액에 맞는 개수의 로또를 구입한다.
- 지난주 당첨 번호를 입력하면 당첨 결과와 수익률이 계산된다.
다음은 요구사항을 시나리오형태로 바꿔보자. 실제로 이 애플리케이션을 동작시키는 사용자의 입장에서 어떤 행위를 하면 어떤 결과가 나타나는지를 명세하는 형식으로 작성한다.
- 10000원을 입력한다.
- 10장의 로또가 구매된다.
- 구입한 10장의 로또 번호가 출력된다.
- 지난주 당첨 번호로 1, 2, 3, 4, 5, 6을 입력한다.
- 당첨 통계와 수익률이 출력된다.
이 단계에서 기존 요구사항보다 구현할 방법이 조금은 더 명확해졌다고 느끼는 사람들도 있다. 정상적인 시나리오에 이어서 비정상적인 시나리오도 함께 작성한다.
- 로또 한 장의 가격보다 적은 금액 입력
- 100원을 입력한다.
- 로또를 구매할 수 없다.
- 유효하지 않은 로또 당첨 번호 입력
- 10000원을 입력한다.
- 10장의 로또가 구매된다.
- 지난주 당첨 번호로 1, 1, 1, 1, 1, 1을 입력한다.
- 당첨 통계와 수익률을 출력할 수 없다.
정의한 시나리오를 바탕으로 시나리오를 검증하는 인수 테스트를 만들어 보자. 테스트를 만들 때는 다른 코드에 영향을 덜 받는 요구사항부터 만드는 것을 추천한다. 이유는 먼저 만들어지는 테스트가 다음에 만들어지는 테스트를 작성하는데 도움이 되는 경우가 많기 때문이다.
시나리오의 각 단계를 수행하고 그 결과를 검증하는 인수 테스트를 작성했다. 이 테스트를 통해 시나리오 대로 기능이 잘 동작하는지를 검증할 수 있다. 특이한 점은 이 인수 테스트는 사용자 관점으로 작성되지 않은 것처럼 보인다. 로또를 구매하거나 로또 당첨 번호를 입력하는 행위를 실제 사용자가 수행하는 것 대신 메서드 호출을 사용했다.
테스트의 검증 대상으로는 입출력을 수행하는 콘솔의 부분은 제외했다. 콘솔 입출력 부분이 포함되어도 크게 달라지는 부분은 없다. insertMoney 메서드나 inesrtWinningLottoNumber 메서드를 호출하는 부분이 콘솔을 통해 수행되는 것 정도가 있을 것 같다.
다음은 지하철 노선도 관리 요구사항을 만족하는 API 애플리케이션을 만드는 과정이다. 진행 방법은 앞서 소개한 방식과 동일하게 요구사항을 확인하고 시나리오를 정의한 다음 시나리오를 검증하는 인수 테스트를 만드는 순서로 진행한다.
- 지하철을 관리하는 API를 개발한다.
- 기능으로는 "지하철 역 생성", "역 목록 조회"가 있다.
- 지하철 역 생성 요청을 한다.
- 지하철 역이 생성을 되었다.
- 지하철 역이 생성되어 있다.
- 지하철 역 목록 조회 요청을 한다.
- 지하철 역 목록이 응답되었다.
API 기능을 검증하는 인수 테스트는 API의 요청과 응답을 이용하여 기능을 검증하도록 작성했다. 실제 요청을 보내고 응답을 받아서 검증을 하기 위해 RestAssured를 사용했고, 요청을 처리하는 환경을 만들기 위해 @SpringBootTest와 webEnvironment 설정을 RANDOM_PORT 설정을 이용했다. 테스트 코드에서 사용한 코드의 기능에 대해서 알지 못해도 상관없다. 요구사항을 시나리오 형태로 정의한 후 이를 검증하는 테스트로서만 이해해 주길 바란다.
앞서 다룬 내용을 살펴보면, "이게 인수 테스트가 맞나요?"와 "통합 테스트와 어떤 차이점이 있나요?"라는 질문이 다시 생각날 수 있다. 두 테스트 케이스 모두 '인수 테스트'라고 소개했지만, 하나는 통합 테스트나 단위 테스트의 형태를 띠고 있고, 다른 하나는 API 테스트나 E2E 테스트와 비슷하다.
인수 테스트의 개념은 테스트 의도에 따라 정해지는 것이지 테스트를 어떻게 구현하는지에 따라 정해지는 것이 아니다. 유닛 레벨이나 통합 레벨, 사용자 인터페이스 레벨에서 인수 테스트를 적용할 수 있다. … 더 나아가, 인수 테스트를 유닛이나 컴포넌트가 의도한 동작을 하는지 확인하는 설계 검증 테스트로 사용할 수 도 있다. 어떤 경우든 인수 테스트는 사용자에게 애플리케이션이 인도될 수 있는 지를 확인한다.
린 애자일 기법을 활용한 테스트 주도 개발
테스트의 구분 방법보다는 테스트의 목적과 의도를 명확히 하는 것이 중요하다
간혹 인수 테스트는 사용자 인수 테스트를 연상하기 때문에 사용자의 행위 기반으로 검증을 해야 하고, 그렇기 때문에 프론트엔드 기반으로 검증해야 한다고 생각하는 사람이 있을 수 있다. 그래서 앞서 소개한 인수 테스트는 엉터리라고 말한다면... 존중은 한다. 하지만 테스트의 구현 방법에 따라서 테스트 종류를 잘 구분하는 것보다는 목적과 의도에 맞게 테스트를 잘 활용하여 개발하는데 도움을 받는 것이 더 중요한 거 같다고는 말해주고 싶다.