인수 테스트 알아보기

17 November 2021 — Written by Boorownie
#인수 테스트

전통적인 인수 테스트(Acceptance Test)

Wikipedia에서는 인수 테스트를 명세(specification) 또는 계약의 요구 사항이 충족되는지 확인하기 위해 수행되는 테스트라고 설명하고 있습니다.

In engineering and its various subdisciplines, acceptance testing is a test conducted to determine if the requirements of a specification or contract are met.

Wikipedia - Acceptance testing

소프트웨어 제작을 의뢰한 클라이언트가 결과물을 인수 받을 때 계약 명세에 따라 제대로 동작하는지 결정할 때 사용되는 테스트입니다. 수락, 받아들임이라는 뜻을 가지고 있는 Acceptance를 인수로 해석한 이유는 인수를 수락한다는 의미로 해석한 것이라고 생각할 수 있습니다.

사실 인수 테스트라는 용어를 찾아보면 소프트웨어 이외에 다른 분야에서도 찾아볼 수 있습니다. 네이버 국어사전에서는 다음과 같이 설명되어 있습니다.

[군사] 현장에 새로 설치된 장비를 실제로 운영하기에 앞서 기능과 성늘의 적합성 여부를 살펴보는일

네이버 국어사전 - 인수 테스트

이러한 용어들을 정리해 보면 요구사항에 따라 잘 동작하는지 최종적으로 검증하는 테스트라고 정리할 수 있습니다.

인수 테스트 in Extreme Programming

이 글에서 소개할 인수 테스트는 Extreme Programming(이하 XP)에서 사용되는 인수 테스트를 의미하며, 그 뜻은 XP의 프로세스에 영향을 받아 전통적인 사용자 인수 테스트와는 약간의 차이가 있습니다.

Acceptance testing is a term used in agile software development methodologies, particularly extreme programming, referring to the functional testing of a user story by the software development team during the implementation phase. The customer specifies scenarios to test when a user story has been correctly implemented.

Wikipedia - Acceptance testing (Acceptance testing in extreme programming)

XP에서 인수 테스트는 구현에 앞서 요구사항을 명세하는 테스트이며, 사용자 스토리를 기능적으로 검증하는 테스트를 의미합니다. 인수 테스트는 사용자 스토리로부터 만들어 집니다. 사용자 스토리가 정의되면 테스트할 시나리오가 구체적으로 지정되는데 인수 테스트는 지정된 시나리오로 동작했을 때 예상되는 결과가 나오는지를 검증합니다. 참고로 이러한 시나리오는 기능의 요구사항을 의미하는데 인수 테스트가 통과할 조건이라 해서 인수 조건이라 부릅니다.

img.png

전통적인 사용자 인수 테스트와 XP에서의 인수 테스트의 가장 큰 차이점은 테스트의 목적입니다. 전통적인 사용자 인수 테스트는 요구사항에 맞춰 잘 동작하는지 최종 단계에서 검증하지만 XP에서의 인수 테스트는 기능 구현에 앞서 요구사항을 명세하는 목적을 함께 가지고 있으며 그 요구사항을 정의할 때 고객(XP에서 고객은 요금을 지불하는 사람이 아니라 시스템을 실제로 사용하는 사람, 혹은 요구사항을 정의하는 사람을 의미)과 개발자, 테스터가 함께 합니다. (XP의 Whole Team과 관련된 내용은 이 글에서는 다루지 않고 ATDD를 다루는 글에서 다루도록 하겠습니다.)

인수 테스트 특징

인수 테스트의 성공은 기능 구현이 완료되었음을 나타냅니다. 기능을 구현할 때 인수 테스트를 작성하는 것으로 시작하고 이 테스트를 성공 시킬때 까지 기능 구현을 하기 때문에 기능 구현의 끝을 알 수 있습니다. 이러한 특징으로 인해 인수 테스트의 성공률은 전체 작업의 진척도를 측정하는 수단으로도 활용할 수 있습니다.

우리는 기능을 구현할 때, 만들고자 하는 기능을 수행하는 인수 테스트를 작성하는 것으로 시작한다. 인수 테스트가 실패하는 동안은 시스템이 아직 그 기능을 구현하지 않고 있다는 것을 보여준다; 인수 테스트가 통과되면, 기능 구현은 끝이다.

테스트 주도 개발로 배우는 객체 지향 설계와 실천

인수 테스트는 블랙박스 테스트의 특징을 가집니다. 인수 테스트는 코드 기반의 구현 방법이나 사용되는 기술에 대한 검증이 아니라 요구사항이 충족되는지를 검증해야합니다. 테스트를 구동하는 사람은 해당 프로그램이나 해당 기능에 대한 특정 지식이 필요하지 않고, 소프트웨어가 무엇을 해야 하는지만 알고 있습니다.

img_1.png

마지막으로 이전 반복주기에서 생성한 인수 테스트는 다음 반복주기에서 회귀 테스트로도 사용됩니다. 테스트 스위트가 증가하면 시스템을 변경할 때 회귀 실패에서 보호받을 수 있습니다.

인수 테스트의 구현과 형태

상황에 따라 다른 요구사항이 주어졌을 때 인수 테스트의 구현에 대해서 알아보겠습니다.

예시 1 - 콘솔 애플리케이션 개발을 위한 인수 테스트

요구사항
요구사항

- 콘솔 기반의 로또 애플리케이션을 만든다.
- 금액을 입력하면 금액에 맞는 갯수의 로또를 구입한다.
- 지난주 당첨 번호를 입력하면 당첨 결과와 수익률이 계산된다.
시나리오

요구사항대로 정상적으로 동작하는 것을 검증할 수 있는 시나리오를 만들겠습니다. 최초 시나리오는 정상적으로 애플리케이션이 동작하는 시나리오로 작성했고 다음으로는 비정상적인 시나리오를 입력합니다. (시나리오 기반의 인수 조건을 만드는 과정에서 사용자 스토리를 포함한 XP의 전체 프로세스는 생략했습니다.)

정상적인 시나리오

- 10000원을 입력한다.
- 10장의 로또가 구매된다.
- 구입한 10장의 로또 번호가 출력된다.
- 지난주 당첨 번호로 1, 2, 3, 4, 5, 6을 입력한다.
- 당첨 통계와 수익률이 출력된다. 
비정상적인 시나리오 1 - 로또 한장의 가격보다 적은 금액 입력

- 100원을 입력한다.
- 로또를 구매할 수 없다.
비정상적인 시나리오 2 - 유효하지 않은 로또 당첨 번호 입력

- 10000원을 입력한다.
- 10장의 로또가 구매된다.
- 지난주 당첨 번호로 1, 1, 1, 1, 1, 1을 입력한다.
- 당첨 통계와 수익률을 출력할 수 없다.
인수 테스트 구현

정의한 시나리오를 바탕으로 시나리오를 검증할 수 있는 인수 테스트를 만들겠습니다.

    @DisplayName("로또 한장의 가격보다 적은 금액 입력")
    @Test
    void lessPriceThanLotto() {
        LottoApplication lottoApplication = new LottoApplication();

        assertThrows(RuntimeException.class, () -> {
            lottoApplication.insertMoney(100);
        });
    }
    @DisplayName("유효하지 않은 로또 당첨 번호")
    @Test
    void invalidWinningLottoNumber() {
        LottoApplication lottoApplication = new LottoApplication();

        Message message = lottoApplication.insertMoney(10000);
        assertThat(message.getText()).isEqualTo("10장의 로또가 구매되었습니다.");

        assertThrows(RuntimeException.class, () -> {
            lottoApplication.insertWinningLottoNumber(1, 1, 1, 1, 1, 1);
        });
    }
    @DisplayName("정상 동작 시나리오")
    @Test
    void common() {
        LottoApplication lottoApplication = new LottoApplication();

        Message buyMessage = lottoApplication.insertMoney(10000);
        assertThat(buyMessage.getText()).isEqualTo("10장의 로또가 구매되었습니다.");

        Message winningMessage = lottoApplication.insertWinningLottoNumber(1, 2, 3, 4, 5, 6);
        assertThat(winningMessage.getText()).contains("당첨 통계");
        assertThat(winningMessage.getText()).contains("당첨 수익률");
    }

예시 2 - API 개발을 위한 인수 테스트

요구사항
요구사항

- 지하철을 관리하는 API를 개발한다.
- 지하철 역 생성, 역 목록 조회, 역 삭제
시나리오
지하철 역 생성 시나리오

- 지하철 역 생성
- 지하철 역 생성 요청을 한다.
- 지하철 역이 생성을 되었다.
지하철 역 목록 조회 시나리오

- 지하철 역이 생성되어 있다.
- 지하철 역 목록 조회 요청을 한다.
- 지하철 역 목록이 응답되었다.
인수 테스트 구현
@DisplayName("지하철역 관련 기능")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class StationAcceptanceTest {

    @LocalServerPort
    int port;

    @Autowired
    private DatabaseCleanup databaseCleanup;

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
        databaseCleanup.execute(); // DB 초기화 로직
    }

    @DisplayName("지하철 역 생성")
    @Test
    void createStation() {
        Map<String, String> params = new HashMap<>();
        params.put("name", "강남역");

        // when
        ExtractableResponse<Response> response = RestAssured
            .given().log().all().body(params)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .when().post("/stations")
            .then().log().all().extract();

        //then
        assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
    }

    @DisplayName("지하철 역 목록 조회")
    @Test
    void show() {
        Map<String, String> params = new HashMap<>();
        params.put("name", "강남역");

        RestAssured
            .given().log().all().body(params)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .when().post("/stations")
            .then().log().all();

        ExtractableResponse<Response> response = RestAssured
            .given().log().all()
            .accept(MediaType.APPLICATION_JSON_VALUE)
            .when().get("/stations")
            .then().log().all().extract();

        assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
        List<StationResponse> stations = response.jsonPath().getList(".", StationResponse.class);
        assertThat(stations.size()).isEqualTo(1);
    }
}

(API 레벨의 인수 테스트 구현 방법에 대서는 다음글에서 조금 더 구체적으로 알아보겠습니다.)

여기서 한가지 이상한점이 느껴지시나요? 인수 테스트인데 어떤건 단위 테스트이고 어떤건 API 테스트로 보이거나 E2E 테스트로 보일 수 있습니다. 그러면 여태 내가 끄덕이고 있던 인수 테스트는 E2E 테스트 일까요? API 테스트 일까요? 아니면 통합 테스트나 단위 테스트의 종류일까요? 인수 테스트는 테스트가 검증하고자 하는 대상과 테스트로 검증하려는 로직의 영역에 따라 달라집니다.

인수 테스트의 개념은 테스트 의도에 따라 정해지는 것이지 테스트를 어떻게 구현하는지에 따라 정해지는 것이 아니다. 유닛 레벨이나 통합 레벨, 사용자 인터페이스 레벨에서 인수 테스트를 적용할 수 있다. … 더 나아가, 인수 테스트를 유닛이나 컴포넌트가 의도한 동작을 하는지 확인하는 설계 검증 테스트로 사용할 수 도 있다. 어떤 경우든 인수 테스트는 사용자에게 애플리케이션이 인도될 수 있는 지를 확인한다.

린 애자일 기법을 활용한 (인수) 테스트 주도 개발

내가 구현할 시스템(애플리케이션)의 종류에 따라 다를 수 있고 해당 시스템을 활용하는 고객이 누군지, 요구사항이 어떤 것인지에 따라 인수 테스트의 형태는 달라질 수 있습니다.