소프트웨어개발론

유스케이스 기반 통합 테스트 작성

김민석(갈레, 페퍼) 2022. 9. 5. 10:12
반응형

✍️ 문제 상황

  • 테스트 작성 시, 단위 테스트를 모두 작성하고 통합 테스트는 모듈간의 연결 유무를 확인할 수 있는 최소한의 개수의 테스트만 작성했습니다. 하지만 통합 테스트의 정의와 필요성에 대해 재고하면 좋을 것 같다는 피드백이 있었습니다(Github PR review link)
 고생많으셨어요!
통합테스트를 주로 어떻게 작성하는지에 대해서는 한 번 찾아보시면 좋겠습니다.

 기본적으로 통합테스트는 단순히 서비스간 혹은 디비간의 통신이 잘 되는지만 확인하는게 아니라 모든 환경이 갖추어진 상태에서 API들이 원하는대로 동작하느냐를 테스트하기위함이고, 따라서 가급적 유저 입장에서 실행 가능한 많은 시나리오들을 통합테스트케이스에서 처리할 수 있도록 하는게 좋습니다.

 CI/CD를 설정하고 통합테스트에서 유저들의 usecase를 자동으로 수행해줌으로써 빌드/배포 과정을 사람이 테스트해볼필요없이 자동화하는데 목적이 있거든요. 따라서 통합테스트도 유저들이 실행할 수 있는 다양한 케이스들을 실행할 수 있도록 셋업하는게 좋습니다. (굳이 이 PR이 아니라 다른 PR로 만드셔도 좋구요.)

 

✍️ 접근 방법과 해결 과정

피드백 받기 전

  • 피드백을 받기 전 제가 이해한 통합 테스트는 ‘대부분 시스템이 프로세스 외부 의존성과 통합해 어떻게 작동하는지를 검증하는 테스트’였습니다. 단위 테스트를 꼼꼼히 작성한다면, 외부 API와의 통신 여부만 보장되면 원하는 Input 대비 Output을 얻을 수 있다고 생각했습니다. 이에 통합 테스트에서 프로세스 외부 의존성과의 상호작용을 확인하기 위해 가장 긴 주요 흐름 혹은, 외부 시스템과의 통신을 모두 확인하는 데 필요한 만큼 통합테스트를 작성하려고 했습니다.

 

피드백 받은 후

  • 통합 테스트의 정의와 의도에 대해 다시 학습해본 결과, 단위 테스트로는 가능한 많은 비즈니스 시나리오의 예외 상황을 확인하고, 통합 테스트는 주요 흐름(happy path)과 단위 테스트가 다루지 못하는 기타 예외 상황을 다룬다고 판단했습니다. 즉 통합 테스트는 모듈들이 통합하면서 발생할 수 있는 예상치 못한 동작과 외부 API의 변화에 대응할 수 있게 하는 수단이였습니다. 이후 안전한 모듈 통합과 외부 API 통신을 위해 다양한 유스케이스를 고려한 통합 테스트 작성이 필요하다고 생각했습니다.

 

유스케이스 템플릿 선정 및 작성

  • 유스케이스 기반한 테스트를 작성하기 위해 유스케이스의 정의와 작성 방법에 대해 알아봤습니다. 위키피디아에 따르면 유스케이스는 “목표를 달성하기 위한 액터와 시스템의 상호작용”입니다. 이를 구체화하는 방법은 UML, text 등 다양한 방법이 있지만, 저는 Cockburn style을 사용하기로 결정했습니다. Martin Fowler style도 있지만, Cockburn style이 유스케이스의 핵심 요소들을 잘 구조화할 수 있는 형태라고 생각했기 때문입니다. 유스케이스를 작성할 때 간결성과 가독성이 중요하기 때문에 Cokcburn style 중에 가장 light한 Casual 형태를 선택했습니다.
  • 물론 실제 개발 할 때는 유스케이스 상세를 작성할 일이 없을 수도 있습니다. 에디님의 기술 블로그에 따르면 “기획자의 요구사항이 명확하고 잘 정리된 "상세기획안"이 있다면, 굳이 유스케이스를 작성하지 않아도 된다.”라고 합니다. 이에 저 또한 동의합니다. 기획자와 게임 개발을 할 때를 생각하면, 요구사항이 명확하고 잘 정리된 상세 기획안을 보면 유스케이스가 필요 없었기 때문입니다. 아래는 제가 게임 개발할 때 기획자분께서 주셨던 상세 기획안입니다.

<그림1> 상세 기획서

 

✍️ 결과

  • 아래 유스케이스 상세를 바탕하여 통합테스트를 작성할 계획을 했습니다.

<그림2> 회원가입 유스케이스

 

  • 위 유스케이스의 주요 성공 시나리오3을 코드로 작성한 예입니다.
@SpringBootTest(classes = ResellPlatformApplication.class)
@AutoConfigureMockMvc
@Transactional
public class UserCreateIntegrationTest BasedTest {

    UserDTO userDTO;

    ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    UserServiceImpl userServiceImpl;

    @Autowired
    MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        userDTO = UserTestFactory.createUserDTOBuilder().build();
    }

    @DisplayName("성공 시나리오: 고객이 유저 서비스에 회원 가입을 요청한다.")
    @Test
    void userCreate_success() throws Exception {
        // given
        String body = objectMapper.writeValueAsString(userDTO);

        // when
        ResultActions resultActions = mockMvc.perform(post("/users/create")
                .contentType(MediaType.APPLICATION_JSON)
                .content(body)
                .with(csrf()));


        // then
        resultActions.andExpect(status().isOk());

        Optional<String> usernameFound = userServiceImpl.findUsername(userDTO.getPhoneNumber());
        assertThat(usernameFound).isNotEmpty();
        assertThat(usernameFound.get()).isEqualTo(userDTO.getUsername());
    }
    
    @DisplayName("실패 시나리오: 동일한 username이 이미 회원 가입돼있음.")
    @Test
    void userCreate_success() throws Exception {
        ...
    }
}

 

📙 레퍼런스

  • 단위테스트, p271~280, 플라디미르 코리로프 지음, 임준혁 옮김, 2021.10.20 발행
반응형