서비스 테스트
‘서비스’라는 용어는 사람에 따라 의미가 모호할 수 있지만, 일반적으로 서비스 테스트라는 것은 UI 등 사용자 인터페이스를 우회해서 서비스들을 직접 테스트하는 것을 말합니다. 여러 서비스로 구성된 시스템 대상으로 단일 서비스만 테스트할 경우, 독립적으로 문제를 검증하기 위해 다른 서비스는 스텁으로 만들어서 테스트할 서비스의 범위를 제한하게 됩니다.
일부 작은 서비스 테스트는 빠르게 끝나겠지만, 실제 DB 또는 네트워크를 통해 테스트한다면 테스트 시간은 늘어날 것입니다. 단위 테스트보다 더 많은 범위를 다루므로 테스트 실패 시 문제의 원인을 찾기가 더 어려울 것입니다.
통합 테스트
서비스 테스트 중 하나인 통합 테스트는 화면의 박스 부분처럼 구성 요소 간의 네트워크 경로 및 상호 작용을 검증하여 인터페이스 연동 성공과 오류 경로를 다루는 것이 목표입니다. 즉, 서브시스템을 통한 네트워크 경로를 연계하여 각 모듈이 서로 상호 작용하여 의도된 대로 같이 동작하는지 점검합니다.
이는 개별 동작을 면밀히 테스트하는 단위 테스트와 구별되는 부분입니다. 구성 요소 또는 모듈을 통합하는 테스트는 마이크로 서비스 아키텍처에서 임의의 크기로 만들 수 있지만 일반적으로 통합 코드 계층과 통합 할 외부 구성 요소 간의 상호 작용을 검증하는 데 사용됩니다.
외부 구성 요소의 예로는 다른 마이크로 서비스, DB, 캐시 등이 있습니다. 통합 테스트는 연계 부분과 관련된 로직을 리팩토링하거나 확장할 때 빠른 피드백을 받는 데 좋습니다. 다만 경계를 종합적으로 검증하기 위해 단위, 계약 테스트를 병행하는 것이 좋습니다.
통합 테스트 - Gateways
Gateway 통합 테스트는 HTTP 헤더 누락 정보, SSL 정상 처리 여부, 요청/응답 바디 정보 일치 여부 등 프로토콜 수준의 정밀 테스트를 하는 것입니다.
하지만 외부 구성 요소의 시간 초과, 느린 응답과 같은 비정상적인 테스트는 재현하기 어려운 편으로, 스텁을 사용하기도 합니다. 테스트 시 특정 데이터가 필요하기 때문에 테스트를 진행하면서 상태 관리가 어려울 수 있습니다.
다양한 상황에서 사용할 수 있는 테스트 세트를 미리 준비하는 게 필요합니다.
통합 테스트 - Persistence
DB 관련 테스트는 코드의 스키마와 DB 스키마 간 일치 여부를 확인하게 됩니다. 현재 ORM 기술은 많이 발전하여 캐싱과 플러싱을 지원하기 때문에 쉽게 테스트할 수 있습니다.
하나의 테스트 셋트가 잘 실행될 수 있게 사전 데이터 준비, 테스트, 결과 비교의 구조화가 중요합니다. 대부분의 DB는 네트워크를 통해 연결되므로 시간 초과 및 네트워크 오류가 발생할 수 있는데, 테스트 시 이런 오류를 잘 처리하는지 확인이 필요합니다.
구성 요소 테스트
구성 요소, 즉 컴포넌트는 전체 시스템 중 캡슐화되어 일관적이고 독립적으로 교체 가능한 일부분을 말합니다. 마이크로 서비스 아키텍처에서 컴포넌트는 서비스 자체입니다. 컴포넌트 테스트는 소프트웨어의 테스트 범위를 전체 시스템 중 일부분으로 정해서 내부 코드 인터페이스를 통해 시스템을 조작하며 테스트 더블을 사용하여 테스트 중인 코드를 다른 구성 요소와 분리할 수 있습니다.
즉, 테스트 더블인 인 메모리 DB를 통해 네트워크에 사용하지 않고 컴포넌트 테스트를 작성하여 테스트 실행 시간을 획기적으로 단축시키고, 빌드 복잡성을 줄일 수 있습니다. 구성 요소 테스트에서 요청을 발송하고 응답을 검색 할 수 있도록 내부 인터페이스를 통해 마이크로 서비스와 통신합니다.
미리 구축 된 라이브러리가 있지만 사용자가 직접 작성해서 테스트하기도 합니다. 이런 구성 요소 진입점 테스트는 실제 네트워크 상호 작용의 추가 오버 헤드 없이 서비스에 대한 제 HTTP 요청을 가능한 한 비슷하게 처리 할 수 있습니다.
외부 서비스와 마이크로 서비스를 분리하기 위해 실제 프로토콜 수준 클라이언트 대신 테스트 더블을 사용하도록 게이트웨이를 구성 할 수 있습니다. 내부 리소스를 사용하여 이러한 테스트 더블은 특정 요청이 일치 할 때 미리 정의 된 응답을 반환하도록 프로그래밍 할 수 있습니다.
이 테스트 더블은 다른 마이크로 서비스가 타임아웃 또는 느린 응답 등의 상황을 가정해 에뮬레이션하는 데에도 사용하여 오류 테스트를 할 수 있습니다.
외부 데이터 저장소를 H2 같은 메모리 DB로 대체하여 에뮬레이션하게 되면, 테스트 성능이 크게 향상 될 수 있습니다. 이렇게 하면 실제 데이터 저장소가 테스트 경계에서 제외되지만 데이터 영속성보다는 도메인 로직에 보다 집중할 수 있는 장점이 있습니다. 경우에 따라 Cassandra나 ElasticSearch 같은 일부 DB는 실제 연결하여 테스트하기도 합니다.
프로세스 내 Acceptance 테스트를 작성할 때 테스트 더블과 테스트 데이터를 직접 구성 할 수도 있지만, 권한 있는 내부 리소스를 통해 모든 요청을 라우팅하게 되면 서비스를 블랙박스 형태로 테스트 할 수 있습니다.
이를 통해 컴포넌트 테스트 스위트에 영향을 미치지 않고 DB 관련 또는 외부 서비스 연동을 변경할 수 있게 되는 장점이 있습니다. 실제 Resources는 URL 명명 규칙 또는 다른 네트워크 포트로 노출하여서 방화벽 레벨의 접근 제한을 통해 외부와 내부를 구분하여 설정하게 됩니다.
이럴 경우 모니터링, 유지 보수, 디버깅, 테스트 등 많은 장점을 가지게 됩니다. 내부 리소스에는 로그, 사용 가능한 기능 API, 데이터 접근, 시스템 메트릭, 간단한 Ping 리소스 등이 있어 해당 마이크로 서비스에 대한 상태, 종속성, 주요 트랜잭션 타이밍, 설정 파라미터에 대한 정보를 알 수 있게 되어 다른 마이크로 서비스와 상호작용하기 쉬워지게 됩니다.
별도의 프로세스로 배포된 마이크로 서비스를 대상으로 컴포넌트 테스트를 할 경우, 테스트 범위가 커지게 되고 실제 네트워크 통신과 DB의 사용으로 테스트가 더 오래 걸리게 됩니다. 하지만 여러 마이크로 서비스가 복잡하게 통합되거나, DB 저장, 논리적인 시작 순서 등을 가질 경우 독립 프로세스 방식이 더 나을 경우도 있습니다.
단위, 통합, 구성 요소 테스트를 결합하여 마이크로 서비스를 구성하는 모듈에 대해 넓게 검증할 수 있습니다. 하지만 외부와 의존성이 있는 계약 테스트나 엔드-투-엔드 비즈니스 플로우를 점검하는 테스트가 빠져있을 수 있습니다.
계약 테스트
계약 서비스는 Consumer, 즉 소비자가 컴포넌트 공개 API/인터페이스를 사용하여 입/출력 데이터 구조, 에러, 성능, 동시성 등에 대한 서비스의 기대치를 검증하기 위해 외부 서비스의 경계에서 점검하는 것입니다. 구성 요소 테스트처럼 서비스의 동작을 깊게 테스트하지는 않지만, 서비스 호출 후 입력/출력에 필수 속성이 포함되어 있는지, 응답 대기 시간과 처리량이 허용 범위에 있는지 확인하게 됩니다.
이에 제공자인 Producer 컴포넌트는 각 소비자와의 계약을 만족시키는 것이 중요하고, 변경이 일어날 경우 변경된 필드 등의 정보를 통보해야 합니다.
각 소비자는 계약 테스트를 통해 제공자의 API를 신뢰할 수 있습니다. 이는 마이크로 서비스의 API 디자인 및 운영할 때보다 더 중요합니다.
Test Coverage
이제 관련 도구와 샘플을 살펴보겠습니다. 코드 커버리지 개념은 여기서는 생략합니다. 상세한 내용은 홈페이지를 확인하시기 바랍니다. JaCoCo는 EclEmma 팀이 개발한 코드 커버리지 오픈소스 라이브러리로 탐색한 코드 경로를 검색해서 보고서를 작성하는 JVM 에이전트를 내장한 DevOps 코드 품질 도구입니다. 엔터프라이즈 애플리케이션에서 데이터베이스가 비중을 많이 차지하는 편입니다.
DB 관련 단위 테스트를 지원하는 프레임워크로 DBUnit이 있고, 이는 Spring Test와 연동하여 사용하기도 합니다. 메서드를 테스트하기 전에 테이블 데이터를 미리 준비하거나 테이블 변경 내용을 검증할 때 사용하면 좋습니다.
XML 파일 등으로 테스트 데이터 사전 로딩과 미리 설정한 테스트 DB 초기화 후 테스트할 객체를 수행합니다. DB에서 초기화된 결과를 조회 후 XML 파일의 기대 결과와 비교합니다. DBUnit으로 DB 관련 테스트를 할 때 DBMS를 공유할 경우 데이터 간섭이 있을 수 있기 때문에 개인 PC에 설치된 DB 등 독립된 테스트 DB를 사용하는 것이 좋습니다.
Spring Boot는 계속 발전하면서 변경되고 있습니다. 상세한 내용은 홈페이지를 참고하시기 바랍니다. 테스트 중심으로 살펴볼 것이기 때문에 Anemic Domain Model 형태입니다. 또한 테스트에 필요한 데이터는 SQL로 작성 후 사전 테스트 시 사용하게 됩니다.
Repository Test
코드 안의 스프링부트 @DataJpaTest은 JPA 관련 테스트 설정만 로딩하여 기본적으로 인메모리 엠베디드 DB 생성 및 @Entity 클래스를 스캔합니다. JPA를 통한 데이터 C/R/U/D 테스트가 가능하고, 매 테스트 종료 시마다 자동으로 테스트 데이터를 Rollback하기 때문에 실제 데이터 변경 여부를 걱정하지 않아도 됩니다.
또한 TestEntityManager를 사용해 Persist, Flush, Find 등 기본적인 JPA 테스트를 할 수 있고, @JdbcTest, @DataMongoTest 등의 어노테이션으로 다른 형태의 DB 테스트를 할 수도 있습니다.
Service Test
대응되는 JUnit 4 기준의 테스트 코드는 상세 내용을 가져오는 메서드와 상세 내용을 가져 오지 못했을 때의 메서드, 2가지로 작성되어 있습니다. 데이터 접근 영역인 Repository는 Mock 객체 종류 중인 하나인 Mockito를 사용했습니다.
JUnit 5는 테스트 코드 작성 시 변경된 패키지를 Import해야 되고, Annotation, Extension Model 등 변경 사항에 주의해서 작성해야 합니다.
코드 안의 @RestController는 전통적인 MVC 패턴의 View를 리턴하는 @Controller 어노테이션에 @ResponseBody 어노테이션이 합쳐진 개념으로 데이터를 리턴합니다. 코드 안의 스프링부트 @WebMvcTest 어노테이션은 웹 요청 및 응답 관련 MVC, 특히 웹에서 테스트하기 힘든 컨트롤러 테스트를 지원합니다. 시큐리티, 필터까지 자동으로 테스트하며 수동으로 추가/삭제까지 가능합니다.
WebMvcTest에 명시한 컨트롤러명이 MockMvc에 주입되고, @MockBean 어노테이션이 Service를 가짜 객체로 대체하게 됩니다. 참고로 @Service 어노테이션은 @WebMvcTest 적용 대상이 아니고, Controller와 Service 간에 인터페이스를 두고 연결되어야 합니다.
Application Context Test
@SpringBootTest 어노테이션은 애플리케이션 설정을 바꾸면서 테스트를 할 수 있고, 여러 단위 테스트를 묶은 통합 테스트를 수행할 수 있습니다. 애플리케이션 컨텍스트를 로드하여 테스트하기 때문에 다양한 테스트를 할 수 있지만, 애플리케이션에 설정된 빈을 모두 로드하므로 애플리케이션 규모가 크면 클수록 구동이 느려지는 단점이 있습니다.
SpringBootTest 어노테이션을 사용하려면 SpringJUnit4ClassRunner를 상속받은 SpringRunner.class 또는 SpringExtension.class를 함께 사용해야 합니다. 테스트에서 @Transactional을 사용하면 테스트 후 수정된 데이터가 롤백 되지만, 다른 스레드에서 테스트가 실행되면 WebEnvironment의 RANDOM_PORT나 DEFINED_PORT를 사용하여 테스트해도 롤백 되지 않습니다.
MockMvc는 Servlet Container를 사용하지 않고 서버 입장에서 API를 통해 테스트를 하고, TestRestTemplate은 Servlet Container를 통해 클라이언트 입장에서 RestTemplate을 사용하듯이 테스트를 합니다.
'SW > DevOps' 카테고리의 다른 글
K-MOOC 강좌 후기 : 오픈소스를 활용한 DevOps 환경 이해 (0) | 2019.12.21 |
---|---|
DevOps : End-to-End Test 개념, 종류, 방법 (0) | 2019.12.20 |
DevOps : UnitTest와 JUnit 개념 및 사용법 (0) | 2019.12.17 |
DevOps : UnitTest 개요, 방법 (0) | 2019.12.16 |
DevOps : SW 테스트 개요 (0) | 2019.12.15 |