MSA Phase 6. TDD(2) - Integration Test

devty·2023년 9월 21일
1

MSA

목록 보기
8/14
post-thumbnail

본론

통합 테스트란?

  • 통합 테스트(Integration Test)는 소프트웨어 개발 과정에서 여러 시스템이 서로 상호작용하며 원활하게 작동하는지 확인하는 테스트 방법이다.
  • 이는 단위 테스트(Unit Test)가 개별 컴포넌트의 기능을 테스트하는 것과는 달리, 통합 테스트는 여러 컴포넌트가 함께 작동할 때 발생할 수 있는 문제를 찾아내기 위해 설계된다.

통합 테스트를 사용하는 이유?

  1. 단위 테스트에서 발견하지 못한 오류나 문제점들을 통합 테스트를 통해서 발견할 수 있다.
  2. 통합 테스트는 전체 시스템의 성능을 평가할 수 있는 중요한 단계입니다. 이를 통해 특정 서비스간의 통신 지연이나 병목 현상 등을 식별할 수 있다.
  3. 통합 테스트는 실제 사용자가 시스템을 사용할 때 발생할 수 있는 다양한 시나리오를 모방하여, 전체 시스템이 원활하게 작동하는지 확인하는 엔드-투-엔드 테스트의 일부로써의 역할을 수행한다.
  4. 통합 테스트를 통해 개별 서비스나 전체 시스템의 복구 시나리오를 테스트하고, 시스템의 안정성을 평가할 수 있다.
  5. 전체 시스템의 품질을 보장하기 위해서는 개별 서비스 뿐만 아니라, 서비스 간의 통합 부분도 테스트가 필요하다.
  6. MSA는 여러 개의 독립적인 서비스들이 복잡한 방식으로 상호 작용하는 구조이다. 통합 테스트는 이러한 서비스 간의 상호작용이 예상대로 작동하는지 검증한다.

통합 테스트 도구 및 프레임워크

  • 통합 테스트를 진행할 때는 여러 도구와 프레임워크를 활용하여 테스트 과정을 효율화한다.
    • JUnit : 자바 기반의 테스트 프레임워크로, 단위 테스트 뿐만 아니라 통합 테스트 작성에도 많이 사용된다.
    • TestNG : JUnit과 유사하나, 더 복잡한 테스트 시나리오와 병렬 실행이 가능하다.
    • Postman : API 테스트와 통합 테스트를 수행할 수 있는 도구다.
  • 이 중 우리는 JUnit을 가지고 통합 테스트를 진행할 예정이다. 선택한 이유는 아래와 같다.
    • JUnit은 Java 테스트 프레임워크 중 가장 인기가 많아 TestNG에 비해 커뮤니티가 엄청 크다.
      • 참고할 문서가 많다는 뜻
    • JUnit은 Postman와 다르게 코드 기반 테스팅을 제공하여, 테스트 케이스를 프로그래밍 코드로 작성할 수 있어 더욱 유연하고 강력한 테스팅이 가능하다.

통합 테스트 시나리오 설계

  • 통합 테스트 시나리오 설계에서는 아래와 같은 고려사항이 중요하다
    1. 경계 값 분석 : 서로 다른 시스템이나 컴포넌트의 경계에서 발생할 수 있는 문제를 식별하기 위해 경계 값들을 중심으로 테스트 케이스를 설계한다.
    2. 흐름 테스트 : 주요 비즈니스 로직이나 프로세스 흐름을 테스트하는 시나리오를 설계한다.
    3. 예외 처리 테스트 : 예외 상황과 오류 처리 메커니즘을 테스트하는 시나리오를 개발한다.
    4. 성능 테스트 : 통합 테스트 시나리오에서는 각 컴포넌트 간의 통신에서 발생할 수 있는 성능 이슈를 식별하기 위해 성능 테스트를 포함시킬 수 있다.

통합 테스트 자동화의 중요성

  • 테스트 자동화는 통합 테스트의 효율성과 정확성을 높이는 중요한 요소다.
    1. 빠른 피드백 : 테스트 자동화를 통해 개발자는 코드 변경에 대한 피드백을 더 빠르게 받을 수 있다.
    2. 재사용 가능 : 일단 작성된 테스트 케이스는 반복적으로 재사용될 수 있어, 이전에 작동하던 기능이 여전히 잘 작동하는지 확인하기 쉽다.
    3. 비용 절감 : 수동 테스트에 비해 테스트 자동화는 장기적으로 시간과 비용을 절감할 수 있다.

통합 테스트 코드

  • 가장 기본이 되는 회원가입 API를 가져왔다. 해당 코드를 모키시스트로 작성할 예정이다.
  • UserRegisterController.java
    public class UserRegisterController {
    
        private final RegisterUserUseCase registerUserUseCase;
        private final UserResponseMapper userResponseMapper;
    
        @PostMapping("/register")
        public ResponseEntity<T> registerUser(
                @RequestBody RegisterUserRequest registerUserRequest
        ) {
            RegisterUserCommand command = RegisterUserCommand.builder()
                    .username(registerUserRequest.getUsername())
                    // 등등
                    .build();
    
            User user = registerUserUseCase.registerUser(command);
            RegisterUserResponse response = userResponseMapper.mapToRegisterUserResponse(user);
            
    				return ResponseEntity.status(HttpStatus.OK).body(response);
        }
    
    }
  • application-test.yml
    spring:
      datasource:
        url: jdbc:h2:mem:testdb;MODE=MySQL
        driver-class-name: org.h2.Driver
        username: sa
        password: password
      h2:
        console:
          enabled: true
      jpa:
        hibernate:
          ddl-auto: create-drop
          dialect: org.hibernate.dialect.MySQL5InnoDBDialect
        show-sql: true
        database-platform: org.hibernate.dialect.H2Dialect
    • url: jdbc:h2:mem:testdb;MODE=MySQL → H2 데이터베이스가 MySQL(MariaDB)의 문법을 따르게 된다.
      • 특정 쿼리가 MySQL의 기본모드에서는 작동하지만 H2에서는 작동을 안 할수도 있다.
      • 이 문제는 맨 밑에 결론(몰랐던 점)부분에서 다루겠다.
    • dialect: org.hibernate.dialect.MySQL5InnoDBDialect → MySQL 5 버전의 InnoDB 스토리지 엔진에 특화된 SQL 생성 및 데이터베이스 작업을 지원한다.
    • 나머지는 우리가 익히 알고 있는 설정들이라 넘어가겠다.
  • UserRegisterControllerTest.java (1)
    @Transactional
    @AutoConfigureMockMvc
    @ActiveProfiles("test")
    @SpringBootTest(classes = UserServiceApplication.class)
    @DisplayName("UserRegisterControllerTest 통합 테스트")
    public class UserRegisterControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @Autowired
        private ObjectMapper objectMapper;
    
    		// 밑에 이어집니다.
    }
    • @Transactional → 테스트 메서드가 트랜잭션 내에서 실행되도록 지정한다.
      • 테스트가 종료되면 트랜잭션이 롤백되어, 테스트에서 수행한 데이터베이스 변경이 실제 데이터베이스에 영향을 미치지 않게 됩니다.
      • 이는 테스트 간의 독립성을 보장한다.
      • 데이터베이스 상태를 초기화하여 다음 테스트에 영향을 미치지 않도록 한다.
    • @AutoConfigureMockMvc → MockMvc 인스턴스를 자동으로 구성합니다.
      • MockMvc는 Spring MVC의 테스트를 도와주는 클래스로, HTTP 요청을 DispatcherServlet에 전송하고 결과를 받아 검증할 수 있게 해줍니다.
    • @ActiveProfiles("test") → 위에서 만들어준 application-test.yml를 사용하기 위함.
    • @SpringBootTest(classes = UserServiceApplication.class) → classes 속성에 지정된 UserServiceApplication클래스를 사용하여 애플리케이션 컨텍스트를 로드합니다.
    • MockMvc → MockMvc를 사용하여 HTTP 요청을 시뮬레이션하고 응답을 검증할 수 있습니다.
    • ObjectMapper → ObjectMapper는 JSON과 자바 객체간의 변환을 담당하는 클래스입니다. 테스트에서는 주로 HTTP 요청 본문을 JSON 문자열로 변환하거나, HTTP 응답 본문의 JSON 문자열을 자바 객체로 변환하는데 사용됩니다.
  • UserRegisterControllerTest.java (2)
    public class UserRegisterControllerTest {
    
    		// 각종 빈 등록(MockMvc, ObjectMapper)
    
        @Test
        @DisplayName("회원가입 요청에 대한 성공 응답 반환")
        public void registerUserTest() throws Exception {
            RegisterUserRequest registerUserRequest = new RegisterUserRequest(
                    "testuser01",
                    "Test@1234",
                    "Test@1234",
                    "testnickname",
                    "010-1234-5678",
                    "testuser01@example.com"
            );
    
            mockMvc.perform(post("/users/register")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(registerUserRequest)))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true));
        }
    
    		// 밑에 이어집니다.
    }
    • 해당 테스트는 흐름 테스트이다.
    • RegisterUserRequest → 실제로 매개변수로 들어가는 객체를 생성해준다.
    • mockMvc.perform() → MockMvc 인스턴스를 사용하여 HTTP 요청을 시뮬레이션한다.
    • post("/users/register") → HTTP POST 메서드를 사용하여 "/users/register" 경로에 요청을 보낸다.
    • contentType(MediaType.APPLICATION_JSON) → 요청 본문의 Content-Type 헤더를 "application/json"으로 설정한다.
    • content(objectMapper.writeValueAsString(registerUserRequest)) → registerUserRequest 객체를 JSON 문자열로 변환하여 요청 본문으로 설정한다.
  • UserRegisterControllerTest.java (3)
    public class UserRegisterControllerTest {
    
    		// 각종 빈 등록(MockMvc, ObjectMapper)
    
        @Autowired
        private UserJpaRepo userJpaRepo;
    
        @BeforeEach
        public void setup() {
            UserJpaEntity duplicateUsernameEntity = new UserJpaEntity();
            duplicateUsernameEntity.setUsername("username");
            duplicateUsernameEntity.setPassword("Test@1234");
            duplicateUsernameEntity.setNickname("uniqueNickname");
            duplicateUsernameEntity.setPhone("010-1234-5670");
            duplicateUsernameEntity.setEmail("uniqueEmail@example.com");
            duplicateUsernameEntity.setRole(Role.USER);
    
            userJpaRepo.save(duplicateUsernameEntity);
        }
    
        @Test
        @DisplayName("회원가입 요청에 사용자 이름이 중복될 경우 실패 응답 반환")
        public void registerUserTest_UsernameDuplicate() throws Exception {
            RegisterUserRequest registerUserRequest = new RegisterUserRequest(
                    "username", // 중복 사용자 이름
                    "Test@1234",
                    "Test@1234",
                    "testnickname",
                    "010-1234-5678",
                    "testuser01@example.com"
            );
    
            mockMvc.perform(post("/users/register")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(registerUserRequest)))
                    .andDo(print())
                    .andExpect(status().isConflict());
        }
    
    }
    • 해당 테스트는 예외 처리 테스트이다.
    • UserJpaRepo → UserJpaRepo를 사용하기 위해 빈 등록을 해주었다.
    • @BeforeEach → 각 테스트 메서드 실행 전에 실행되는 메소드로 duplicateUsernameEntity를 userJpaRepo에 저장하였다. (username 중복 테스트를 위해 미리 저장해둠.)
    • mockMvc.perform(post("/users/register") → 아까와 동일하게 진행을 하였다.
    • andExpect(status().isConflict()) → 기대한 값은 409(conflict)이다.
  • UserRegisterControllerTest.java (4)
    public class UserRegisterControllerTest {
    
    		// 각종 빈 등록(MockMvc, ObjectMapper)
    
    		@Test
    		@DisplayName("회원가입 요청에 비밀번호 길이가 너무 짧을 경우 실패 응답 반환")
    		public void registerUserTest_ShortPassword() throws Exception {
    		    RegisterUserRequest registerUserRequest = new RegisterUserRequest(
    		            "testuser03",
    		            "short", // 길이가 너무 짧은 비밀번호
    		            "short",
    		            "testnickname",
    		            "010-1234-5678",
    		            "testuser03@example.com"
    		    );
    		
    		    mockMvc.perform(post("/users/register")
    		                    .contentType(MediaType.APPLICATION_JSON)
    		                    .content(objectMapper.writeValueAsString(registerUserRequest)))
    		            .andDo(print())
    		            .andExpect(status().isBadRequest());
    		}
    
    }
    • 해당 테스트는 경계 값 분석 테스트이다.
    • 경계 값 분석 테스트란
  • UserRegisterControllerTest.java (5)
    public class UserRegisterControllerTest {
    
    		// 각종 빈 등록(MockMvc, ObjectMapper)
    
    		@Test
    		@DisplayName("회원가입 API 성능 테스트")
    		public void registerUserTest_Performance() throws Exception {
    		    long startTime = System.currentTimeMillis();
    		    int testCount = 100; // 예: 100회의 테스트 반복
    		
    		    for(int i = 0; i < testCount; i++) {
    		        RegisterUserRequest registerUserRequest = new RegisterUserRequest(
    		                "testuser" + i,
    		                "Test@1234",
    		                "Test@1234",
    		                "testnickname" + i,
    		                "010-1234-5678",
    		                "testuser" + i + "@example.com"
    		        );
    		
    		        mockMvc.perform(post("/users/register")
    		                        .contentType(MediaType.APPLICATION_JSON)
    		                        .content(objectMapper.writeValueAsString(registerUserRequest)))
    		                .andExpect(status().isOk());
    		    }
    		
    		    long endTime = System.currentTimeMillis();
    		    System.out.println("Total Time for " + testCount + " requests: " + (endTime - startTime) + "ms");
    		}
    
    }
    • 해당 테스트는 성능 테스트이다.
    • 100회의 회원가입 요청을 보내고 전체 수행 시간을 측정한다.
    • 지금은 Low Level에 테스트를 진행하는거라 간단하게 작성했지만 JMeter 같은 도구를 이용하면 정교한 성능 테스트가 가능하다.

통합 테스트 완료

  • 이로서 위에 총 4개의 테스트 코드가 존재하는데 통합 테스트 시나리오에서 짠 4개 모두 테스트를 완료하였다.
  • 모든 API에 대해서 4개의 시나리오를 다 테스트할 필요는 없으니, 필요한 테스트만 진행해도 될 것 같다.
  • 비교적 간단한 예시로 들었으니 조금 더 커스텀해서 사용하는게 좋을 것이다.
  • 테스트는 언제나 다다익선같다.
    • 개발 리소스는 많이 들지만..그만큼 나중에 보상 받을 것이다.
  • 아래와 같이 모든 테스트가 성공하였다!

결론

몰랐던 점

  • url: jdbc:h2:mem:testdb;MODE=MySQL 아까 우리가 이 부분은 따로 보자고 했었는데 그 이유는 트러블 슈팅을 적기 위함이다.
  • 우리는 익히 url: jdbc:h2:mem:testdb;이렇게 사용하지 않는가?
  • 이렇게 사용하면 문제가 발생한다. 회원가입 요청에 대한 성공 응답 반환 테스트를 돌려보겠다.
    • 테이블을 찾을수 없다라는 문구가 나오는데 왜 못찾는지 찾아보다 통합 테스트는 User-Service에 대한 소스코드를 컴파일 하고 테스트를 하기에 컴파일하는 부분부터 확인을 해보았다.
    • 아니나 다를까 user table이 생성이 안 된 것이다.
  • 그 이유를 몇가지 파악하다 Table이 생성 안 되는 조건중에 예약어가 생각이 났다.
  • 찾아보니 H2에서는 user라는 단어가 예약어로 포함되어 생성이 불가능하였다.
  • 따라서 원인은 발견하였고 해결 방법으로 user → member로 수정을 하는 방법도 있지만 이 방법은 기존에 모토를 많이 바꿔야하므로 jdbc:h2:mem:testdb;MODE=MySQL로 해주어 H2 데이터 베이스를 MySQL(MariaDB) 모드로 실행하여 H2에서 MySQL(MariaDB)의 문법과 호환되게 설정해주었다.
  • 이렇게 되면 MySQL(MariaDB)에는 user가 예약어로 포함이 되지 않아 성공적으로 user table을 생성하였다.
    • 이제는 user table이 잘 생성이 되어 insert 되는 모습이다.👍👍👍

후기

  • 이로서 단위, 통합테스트까지 끝났다.
  • 이제 내가 짠 테스트 코드가 모든 클래스 및 분기 테스트를 잘 진행했는지 확인하기위에 Test Coverage를 확인하겠다.
  • Test Coverage를 확인하기 위해 Jacoco라는 툴을 사용해서 만들 예정이다.
  • 다음 블로그는 아마도..jacoco가 될 것 같다.
  • 다음 이시간에…To Be Continued
profile
지나가는 개발자

0개의 댓글