[Java] Test code 의 이해 (2)

한호성·2023년 4월 11일
0

Test code 작성기

목록 보기
2/2

Introduction

프로젝트 진행중에는 TDD(Test-Driven Development)를 시간적 여유가 없었습니다.
TestCode를 이해하기 위해, Test code의 목적과, 어떤 종류가 있는지 공부해보도록 하겠습니다.
[목차]

  • 테스트란?
  • 테스트의 종류
  • 각 종류에 대한 이해
  • Test code 직접 작성해보기

요번 글에서는, Test code를 작성하면서 겪었던 문제들에 대해서 간단하게 정리해보도록 하겠습니다.

저희는 단위테스트, 통합테스트 작성을 각각 하게 되었는데, Production의 품질을 (코드 품질보단..) 조금 더 급하다고 판단하셨는지, 통합테스트를 먼저 진행하라고 하셨습니다. (유닛 테스트도 분명 매우매우 중요하다고 생각합니다. )

통합테스트 코드

우선 통합테스트를 짜기 위해서 어떤 Library를 사용할지 고민하였습니다. 두가지 선택지가 있었는데 통합테스트에 자주 쓰이는,

  1. Rest-assured 라이브러리
  • testImplementation 'io.rest-assured:rest-assured:4.4.0'
  1. Spring-boot-test 라이브러리에 포함된 TestRestTemplate을 사용
  • testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.7'

간단하게 두 코드를 사용하여서 api call하는 모습을 살펴보겠습니다.

Rest-Assured

위의 1번 라이브러리를 사용하여서 rest api call 하는 방법입니다.

    public static ExtractableResponse<Response> getResponseExtractableResponse(TemplateDto requestTemplateDto, Header header) {
        final ExtractableResponse<Response> response = RestAssured.given().log().all()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .header(header)
                .body(requestTemplateDto)
                .when()
                .post("/api/admin/templates")
                .then()
                .log().all().extract();
        return response;
        
        //리스폰스를 활용하여 원하는 값 얻기
        //1. objectMapper를 통해 객체로 string 을 parsing 하는방법
             TemplateDto responseDto = objectMapper.readValue(response.body().asString(), TemplateDto.class);
       //2. jsonPath 함수를 활용하여서 해당하는 값 가져오는 방법
       String madeTemplateId = makeTemplateResponse.body().jsonPath().getString("id");

        

TestRestTemplate

위의 2번 기본제공되는 개체를 써서 사용하면 다음과 같이 됩니다.


   HttpEntity<String> request = new HttpEntity<>(body, headers);

            //when
            ResponseEntity<String> responseEntity =
                    rt.exchange("/api/companies", HttpMethod.POST, request, String.class);


//responseEntity 활용하기
		   1. CompanyDto responseCompanyDto = objectMapper.readValue(responseEntity.getBody(), CompanyDto.class);
           
           2. cumentContext dc = JsonPath.parse(responseEntity.getBody());
           String errorTitle = dc.read("$.title");

위의 2가지를 다 사용해보고 팀원들과 상의해본 결과, 2번째 spring -test- starter 기본 제공되는 test class를 사용하기로 하였습니다. 그 이유로는 비슷한 사용방식, 큰 차이를 못느꼈음 --> 의존성이 최대한 없는 쪽으로 가자고 결론 내렸습니다.

통합테스트 주의해야할 점

통합테스트를 짜면서, 중복된 코드 및 동작을 최소화하고자 하였는데, 통합테스트 환경에 대해 완벽하게 인지하지 못해 하루를 홀랑 날려버린 이야기를 써보겠습니당...

테스트 격리 & @Transcational

통합 테스트를 진행하려다 보니, 여러가지 case 들이 많이 존재하였고, 각각의 테스트 마다 기본적으로 세팅 되어야 할 DB 데이터 값들이 필요하였습니다.

이 때, 모든 테스트 코드에 적용시키기 위해서, @BeforeEach annotation을 활용하여서 테스트 코드를 짰습니다.
코드 예시를 보겠습니다.

   @BeforeEach
    void insertData() {
        admin = User.builder()
                .userName(adminName1)
                .userEmail(adminEmail1)
                .userPassword(passwordEncoder.encode(adminPassword1))
                .userRole(UserRole.ADMIN)
                .build();
        User savedAdmin = userRepository.save(admin);
    }

이런식으로 유저정보가 기본 세팅값으로 필요한 경우, 데이터를 미리 갖고 작업을 시작하는 방식을 진행하였습니다.
그렇다면, 테스트가 끝나고 데이터들을 어떻게 처리해야할 까요? 그냥 내버려두면, 테스트 갯수만큼 user가 증가하게 되고 Primary key 값이 관리가 안될 것입니다. 이것이 굉장히 문제였습니다.
(보통 Entity PK 값을 id를 ( @GeneratedValue(strategy = GenerationType.IDENTITY) 방식을 사용합니다.)

이 문제를 해결하기 위해 단순히 @Transacational을 걸면되지 않을까? 라고 생각해서 @Transcational걸고, test를 진행하였습니다.

근데 왠걸, 통합 테스트에서 api call을 할 경우, 내가 저장한 Data들을 가져오지 못하고 있는 상황을 목격합니다. 이 문제를 다른 결의 문제라 생각하고, 이곳저곳 고치게 됩니다. ( 문제가 뭔지 모르는게 얼마나 무서운지..) 다른 문제들이라 생각했던 것들을 전부 점검하고 확인하고, 다시 천천히 생각해보았습니다.

문제를 해결한 중요 포인트는

  1. 통합테스트 환경 ( SpringBoot 를 띄우고, 직접 api call() 을해서 결과값을 검증하는 방식)
  2. @Transcational 에 대한 이해도

였습니다.

지금 문제의 상황을 알아보겠습니다.

  1. Transacation 시작
  2. @beforeEach 필요한 데이터들 Save() 호출 (아직 DB에 commit이 되지 않은 상태)
  3. api call () -> (controller -> service -> repository 거쳐 원하는 값 반환)
  4. @beforeEach 로 Db에 저장한 데이터 commit 되지 않았으니 당연히 데이터가 있을리 없다.
  5. Test Code에서의 @Transcational 은 저장한 데이터들 테스트 후, rollback

이 과정을 생각하지 않은체, 왜 DB에서는 가져오지 못하지..? 라는 엉뚱한 생각을 하게 되었습니다.. 만약 같은 Transacation에서 진행했더라면, 원하는 값을 가져와 사용하였을 텐데..( 영속성 컨텍스트에 존재하기 때문)

이러한 문제를 인지하고 DB에 직접 저장하고, @AfterEach annotation을 활용하여 직접 지워주는 작업을 해야겠다고 생각하였습니다.

    @Transactional
    public void truncateEachTable(String tableName, String pkName) {
        entityManager.flush();
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();


        entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
        entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN " + pkName + " RESTART WITH 1").executeUpdate();


        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
    }

참조하는 값 때문에 DB에 데이터가 잘 안지워주는 것을 고려하여, REFERENTIAL_INTEGRITY를 FALSE 로 한 후, DB Truncate 했습니다. (DB Sequence 정보까지 같이 지우기 때문에 꼭 필요합니다..) // 테이블을 다 찾아온후, 지우는 방법도 있었지만, table 명을 일정한 규칙없이 만들어놔서..어려움이 있어서, 각각의 table을 truncate하는 방식을 취했습니다.

정리

통합 테스트 코드를 짤 때에는, DB연동까지의 Test를 진행하기 때문에 그와 관련해서, 데이터들을 어떻게 세팅하고, 테스트간의 격리를 시킬지가 중요합니다. 우리가 편히 사용하는 @Transcational에 대해 구체적으로 공부하여, DB와 Application 간의 컨트롤을 잘 할 수 있도록 해야겠습니다.

profile
개발자 지망생입니다.

0개의 댓글