Software Testing


소프트웨어 개발에서 테스트 자동화는 개발 생산성을 크게 향상시키고 유지보수에 매우 중요한 역할을 합니다. 때문에 적절한 테스트를 작성하는 것은 소프트웨어 품질과 직결되는 매우 중요한 부분입니다.

소프트웨어 테스트는 다양한 종류가 존재하는데요. 각 테스트는 목적과 방법 등에 따라 차이점을 가집니다.

Test Pyramid

위 그림은 파틴 파울러가 소개한 것으로도 유명한 테스트 피라미드입니다. 테스트 피라미드는 소프트웨어 테스트를 종류별로 그룹화하여 시각화한 그림으로써, 해당 그림을 통해 비용이 상이한 테스트 그룹의 비율을 적절히 조정할 수 있습니다.

이번 글에서는 소프트웨어 테스트에서 대표적인 Unit Test Integration Test Acceptance Test 의 개념에 대해 알아보겠습니다.

Unit Test, 단위 테스트


단위 테스트는 애플리케이션에서 테스트 가능한 가장 작은 소프트웨어를 검증하는 테스트입니다.

단위 테스트에서 테스트 대상 단위는 엄격하게 정해져 있지 않지만, 객체지향언어의 경우 일반적으로 클래스 또는 메서드 수준으로 정해집니다. 단위가 작을수록 복잡성이 낮아지기 때문에 단위 테스트를 활용하여 기능을 검증하기 더 쉬워집니다. 즉, 테스트 대상의 단위를 작게 설정하여 최대한 간단하고 디버깅하기 쉽도록 작성해야 합니다.

단위 테스트는 테스트 피라미드 가장 아래에 위치하는 테스트인 만큼, 독립적인 객체 테스트이기 때문에 테스트 비용이 가장 적습니다. 때문에 도메인 단위로 유효성 검증과 기능에 대한 책임을 적절히 부여하면 중요한 비즈니스 로직을 적은 테스트 비용으로 검증할 수 있습니다.

public class PersonTest {
 
    @Test
    void test() {
        Person actual = new Person(1L, "Quaritch");
 
        assertThat(actual.getName()).isEqualTo("Quaritch");
    }
}

프로그래밍 언어마다 단위 테스트에서 사용하는 프레임워크가 다른데요. Java 에선 주로 JUnit 으로 테스트 코드를 작성합니다.

public class ExampleControllerTest {
 
    @Mock
    private PersonRepository personRepository;
 
    @Test
    void test() {
        ExampleController exampleController = new ExampleController(personRepository);
        given(personRepository.findById(1L))
            .willReturn(Optional.of(new Person(1L, "Quaritch")));
 
        Person actual = exampleController.findPerson(1L);
 
        assertThat(actual.getName()).isEqualTo("Quaritch");
    }
}

그렇다고 단위 테스트가 도메인 로직에만 종속적인 것은 아닙니다. Spring 으로 애플리케이션을 개발하다 보면 ControllerService 에 로직이 포함되는 경우가 종종 있습니다. 해당 로직을 검증하기 위해 통합 테스트를 작성하는 것은 통합 테스트의 목적에서 크게 벗어날 수 있으며 비용적인 측면에서도 비효율적일 수 있습니다. 때문에 Mockito 등을 활용한 Mocking 기법을 통해 해당 로직만을 검증하는 방식을 사용하여 단위 테스트를 수행할 수도 있습니다.

Integration Test, 통합 테스트


통합 테스트는 단위 테스트보다 더 큰 동작을 달성하기 위해 여러 모듈을 모아 이들이 의도대로 협력하는지 확인하는 테스트입니다.

통합 테스트는 단위 테스트와 다르게 외부 라이브러리와 같이 개발자가 변경할 수 없는 부분까지 함께 검증할 때 사용합니다. 요청부터 DB 접근까지 전체 코드가 제대로 동작하는지 검증할 수 있지만, 그렇다고 통합 테스트가 애플리케이션이 완전하게 동작하는 것을 무조건 보장하진 않습니다.

통합 테스트를 통해 단위 테스트에서 발견하기 어려운 버그를 찾을 수 있습니다. 예를 들어, 싱글 코어 CPU 에선 잘 동작하지만 멀티 코어 CPU 에서 잘 동작하지 않는 등의 환경 버그를 찾아낼 수 있습니다.

반면, 통합 테스트는 단위 테스트보다 더 많은 코드를 테스트하기 때문에 신뢰성이 떨어질 수 있으며 어디서 에러가 발생했는지 확인하기 쉽지 않기에 유지보수가 힘들다는 점도 존재합니다.

@SpringBootTest
public class ExampleServiceTest {
 
    @Autowired
    private ExampleService exampleService;
 
    @Autowired
    private PersonRepository personRepository;
 
    @Test
    void test() {
        personRepository.save(new Person(1L, "Quaritch"));
 
        Person actual = exampleService.findPerson(1L);
 
        assertThat(actual.getName()).isEqualTo("Quaritch");
    }
}

Spring 에서는 클래스 상단에 @SpringBootTest 를 붙여 통합 테스트를 수행할 수 있습니다.

Acceptance Test, 인수 테스트


인수 테스트는 애자일 개발 방법론에서 파생된 테스트로써, 사용자 시나리오 맞춰 수행하는 테스트이며, 주로 익스트림 프로그래밍에서 사용되는 용어입니다.

단위 테스트와 통합 테스트와 다르게 비즈니스 쪽에 초점을 두는 테스트이기 때문에 프로젝트에 참여하는 사람들이 토의를 통해 누가, 왜, 무엇을 하는지 에 대한 시나리오를 만들고, 개발자는 이에 의거해서 코드를 작성합니다. 개발자가 직접 시나리오를 작성할 수도 있지만, 기본적으로 다른 의사소통집단으로부터 시나리오를 인수받아 개발한다는 의미를 가지고 있습니다.

전체적인 코드를 테스트한다는 측면에서 통합 테스트와 비슷해 보이지만 시나리오가 정상적으로 동작하는지 테스트하기 때문에 통합 테스트와는 분류가 다릅니다. 실제 사용자 관점에서 테스트하기 때문에 주로 E2E, End-to-End 형식을 통해 확인합니다. 이런 형은 API 를 통해 드러나기 때문에 인수 테스트는 주로 이 API 를 확인하는 방식으로 이루어집니다.

즉, 인수 테스트의 주된 목적은 소프트웨어를 인수하기 전에 명세한 인수 조건대로 잘 동작하는지 검증하는 것입니다.

public TokenResponse Host_토큰을_요청한다() {
    return RestAssured
            .given().log().all()
            .when().post("/fake/login")
            .then().log().all()
            .extract()
            .as(TokenResponse.class);
}

Java 에서는 RestAssured MockMvc 같은 도구를 활용하여 인수 테스트를 작성할 수 있습니다.

RestAssured 를 사용하여 실제 HTTP 요청을 보내 인수 테스트를 진행합니다. 네트워크 응답 대기 시간이 소요되기 때문에 인수 테스트를 실행하는데 가장 오랜 시간이 걸립니다.

인수 테스트를 작성하다보면 통합 테스트에서 검증하는 테스트를 중복으로 검증하는 경우가 종종 발생합니다. 이는 곧 테스트 시간 증가로 이어지고 개발 생산성이 크게 저하될 우려가 있습니다.

인수 테스트의 목적은 인수 조건대로 잘 동작하는지 검증하기 위해서이기 때문에 예외 케이스 같은 테스트는 인수 테스트의 목적에 부합하지 않습니다.

따라서 인수 테스트에선 API 성공 사례들만 검증하는 것이 테스트 시간도 아끼고 목적에도 부합하는 적절한 테스트가 될 수 있습니다.

References