글 작성자: beaniejoy

JPA 관련 테스트 어플리케이션을 개발하다가 JUnit을 사용하여 간단한 테스트 코드를 작성하게 되었습니다. 매번 어플리케이션 실행 후 Postman으로 api 확인하는 것도 귀찮더라구요. 개발 진행 후에 실제로 api가 잘 작동하는지 수시로, 빠르게 확인해보고자 JUnit을 도입하게 되었습니다.

여기서는 간단한 예시를 가지고 Controller 단의 단위테스트를 위한 구성과 Service에서 실제 DB까지 잘 적용이 되는지 확인해보고자 Service 단에서는 통합테스트로 구성하면서 겪은 내용들을 기록하고자 합니다.

 

📌 1. 기본적인 구성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.projectlombok:lombok:1.18.20'
    annotationProcessor 'org.projectlombok:lombok'

    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

테스트 환경을 위해 h2 db를 적용했고 JPA 관련 테스트다 보니 JPA도 같이 적용하였습니다.

spring:
  profiles:
    active: dev

//...

---

spring:
  profiles:
    active: test
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

JUnit 테스트 환경에서 통합 테스트를 위한 test profile 설정 내용을 따로 작성하였습니다. 아무래도 JUnit 테스트 시에는 h2 메모리베이스로 가볍게 사용해야 하기에 profile을 따로 설정하였습니다.

이렇게 함으로써 어플리케이션 실행시의 설정 내용과 JUnit 테스트 실행시의 설정 내용을 구분지을 수 있습니다.

 

📌 2. Application 코드

@RequiredArgsConstructor
@Service
public class CafeService {
    private final CafeRepository cafeRepository;

    private final CafeSearch cafeSearch;

    public Page<CafeResponse> getAllCafesWithNameOrAddress(String name, String address, Pageable pageable) {
        CafeParam cafeParam = CafeParam.builder()
                .name(name)
                .address(address)
                .build();

        Page<Cafe> result = cafeRepository.findAll(cafeSearch.toSpecification(cafeParam), pageable);
        List<CafeResponse> responses = result.getContent().stream()
                .map(CafeResponse::of)
                .collect(Collectors.toList());

        return new PageImpl<>(responses, pageable, result.getTotalElements());
    }
}

Service에서는 JPA를 이용한 기본적인 Cafe 전체 조회 로직을 담고 있습니다. 제가 하고자 했던 테스트는 JPA Criteria Specification 내용이었고 위 코드가 실제 DB와 연결되었을 때 잘 작동하는지 보고자 했습니다. 그렇기에 Service 테스트는 통합 테스트를 구성하게 되었습니다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/cafes")
public class CafeController {

    private final CafeService cafeService;

    @GetMapping("")
    public Page<CafeResponse> getAllCafesDynamicParam(
            @RequestParam("name") String name,
            @RequestParam("address") String address,
            @PageableDefault(page = 0, size = 20) Pageable pageable
    ) {
        return cafeService.getAllCafesWithNameOrAddress(name, address, pageable);
    }
}

Controller는 진짜 기본적인 전체 조회 api 하나만 적용해보았습니다. GET 요청시 parameter로 name, address를 받게 했습니다.

결국엔 Controller부터 Service까지 name, address 각각의 param 입력 유무에 따라 동적으로 JPA가 쿼리를 만들어 해당 조건에 맞는 Response를 반환하는 api를 테스트하는 것입니다.

 

📌 3. Test 구성

@WebMvcTest
class CafeControllerTest {

    @Autowired
    MockMvc mvc;

    @MockBean
    CafeService cafeService;

    @BeforeEach
    public void setup() {

        // 내용 생략...

    }

    @Test
    @DisplayName("Cafe 기본 전체조회 테스트")
    public void getAllCafesTest() throws Exception {

        // 내용 생략...

    }
}

Controller는 해당 클래스에 대해서만 단위 테스트를 진행하면 되기 때문에 Controller 이외의 다른 객체들은 Mock 객체로 만들어 테스트를 진행하였습니다.

@WebMvcTest@Controller, @RestController, @ControllerAdvice 같은 어노테이션이 붙은 Controller 관련 bean들을 대상으로 load해줍니다. 그렇기 때문에 Controller 이외의 Service에 대해서는 MockBean을 통해 가짜객체를 주입해야 합니다.

@SpringBootTest
class CafeServiceTest {
    @Autowired
    CafeService cafeService;

    @Autowired
    CafeRepository cafeRepository;

    @Test
    @DisplayName("기본적인 Spec 조건으로 Cafe 전체 조회 테스트")
    public void findAllTest() {

        // 내용 생략...

    }
}

Service는 통합 테스트로 구성하기 위해 @SpringBootTest를 설정하였습니다.

@SpringBootTest는 classes에 따로 application context load할 클래스 내용을 지정하지 않으면 @SpringBootConfiguration을 찾아서 load해줍니다.
(Spring Boot 프로젝트 생성시 기본으로 만들어지는 xxxApplication.java@SpringBootApplication 어노테이션에 붙어 있습니다.)

@SpringBootTest를 붙여주게 되면 기본적으로 xxxApplication.java 기준으로 해당 프로젝트의 등록된 모든 bean 객체들을 가져오게 됩니다.

 

📌 4. 주의점

주의점은 ServiceTest에만 @ActiveProfiles("test")를 지정하고 모든 테스트를 실행시키면 ApplicationTest contextLoads() 혹은 ServiceTest에서 에러가 발생하는데 ApplicationTest, ServiceTest에 둘다 @ActiveProfiles를 같이 적용하거나 빼야 합니다.

@SpringBootTest
//@ActiveProfiles("test") - ServiceTest에 지정되어 있다면 같이 설정해줘야 함
class SpringDataJpaApplicationTests {

    @Test
    void contextLoads() {
    }

}

 

테스트 All Clear