Skip to content

Driven Adapter(Query) ‐ Test Code영역

sweetykr7 edited this page Mar 13, 2025 · 1 revision

Driven Adapter

Introduction

Hexagonal Architecture(헥사고날 아키텍처)에서 Driven Adapter는 외부 시스템과의 상호작용을 담당하며, 데이터베이스, 메시지 브로커 등의 외부 리소스와 연결됩니다. 이번 backend-sample-hexagonal-simple-crud 프로젝트에서는 QueryDSL을 활용하여 Persistence Adapter의 Query를 구현하였습니다.

Driven Adapter(Query)의 코드 구현과 테스트 구현을 각각 분리하여 진행하였습니다.

항목 작업자
Driven Adapter(Query) 코드구현 wowddok99
Driven Adapter(Query) 테스트구현 sweetykr7

테스트 코드

필수 Annotation

: 테스트코드에서도 bean등록이 필요하다. 따로 해주지 않게 되면 테스트 하고 싶은 대상들이 제대로 주입이 되지 않는 현상이 발생한다. test에서 bean등록을 위해 @SpringBootTest@DataJpaTest 가 존재한다. 불필요한 빈의 로드를 피하고 보다 가볍게 테스트를 하기 위해서 DataJpaTest 가 채택되었다.

  • SpringBootTest
    • 애플리케이션의 전체 컨텍스트(Application Context)를 로드하여 통합 테스트를 수행할 때 사용된다
    • Spring Boot의 모든 빈을 로드하여 전체 애플리케이션을 실행하는 것과 유사한 환경에서 테스트를 진행하기 때문에 무거워진다.
    • 사용 시점
      • Spring의 모든 기능(예: Service, Repository, Controller 등)을 통합적으로 테스트할 때
      • 실제 애플리케이션과 유사한 환경에서 동작을 검증할 때
      • 외부 종속성(예: DB, 메시지 브로커, 파일 시스템 등)을 포함한 테스트가 필요할 때
  • DataJpaTest
    • JPA 관련 컴포넌트(Repository 등)만 로드하여 데이터 계층을 집중적으로 테스트할 때 사용된다.
    • @Entity, @Repository, JpaRepository 관련 빈만 로드하며, 불필요한 빈(Service, Controller 등)은 제외된다.
    • 내장 데이터베이스(H2 등)와 함께 사용되며, 테스트 후 자동으로 롤백하여 DB 상태를 유지할 필요가 없다
    • @Transactional이 자동으로 적용되므로, 데이터가 테스트 후 초기 상태로 돌아간다.
    • 사용 시점
      • Repository 및 JPA 관련 로직을 단위 테스트할 때.
      • 애플리케이션의 전체 컨텍스트를 로드할 필요 없이, 빠르고 가벼운 테스트 환경을 원할 때.
      • 특정 엔티티의 CRUD 동작이 제대로 수행되는지 확인할 때.

QueryDSL관련 설정

: driven adapter에서는 queryDSL을 사용한다. queryDSL에서는 QuerydslRepositorySupport 이 사용되는데, QueryDSL을 쉽게 사용할 수 있도록 지원하는 Spring Data JPA 클래스이다. 테스트시에도 QuerydslRepositorySupport 가 정상적으로 JPAQueryFactory 를 생성하도록 해야하는데, 이때 필요한 것이 EntityManager 이다. DataJpaTest를 사용하면 EntityManager를 자동으로 주입시켜주지 않기 때문에 주입시켜주는 절차가 필요하다.(SpringBootTest에서는 Spring이 자동으로 EntityManager를 주입시켜준다.) 이와 같이 entityManger를 가져온 후에 해당 adapter에 entityManger를 설정해줘야 한다. 그래야 정상적으로 QuerydslRepositorySupport 이 작동된다.

    class BoardQueryAdapterSpringBootTest (
            @Autowired private val boardJpaRepository: BoardJpaRepository,
            @Autowired private val boardEntityMapper: BoardEntityMapper,
            @Autowired private val entityManager: EntityManager,
    ) : FreeSpec({
        val boardQueryAdapter = BoardQueryAdapter(boardEntityMapper);
        boardQueryAdapter.setEntityManager(entityManager)    

beforeEach delete설정

: beforeEach는 하위에 설정한 테스트들이 각각 실행될때마다 한번씩 실행된다. 테스트마다 다른 설정이 필요한 경우가 있는데, 이때 기존에 저장한 DB를 지우고 다른 데이터를 집어 놓으려고 아래와 같이 설정하였다. 이렇게 하면 테스트마다 해당 데이터를 지우게 된다.

    beforeEach {
        boardJpaRepository.deleteAll()
    }

테스트 코드

  • 단건 조회하기 : 조회된 글이 존재하는지 검증 후에 불러온 board가 저장한 boardEntity와 동일한지 검증한다.
    "[Read] 게시글 단건 조회" - { // RootNode
        "[정상] 게시글이 존재할 때" - {
            // When: 게시글을 조회
            val fetchedBoard = boardQueryAdapter.findById(1L)

            "[검증1] 조회된 게시글이 존재하는지 검증" {
                fetchedBoard.isPresent() shouldBe true
            }

            "[검증2] 불러온 board가 저장한 boardEntity와 동일한지 검증" {
                val board = fetchedBoard?.get()!!
                board.title shouldBe boardEntity.title
                board.content shouldBe boardEntity.content
                board.status shouldBe boardEntity.status
            }
        }
    }
  • 다수건 조회하기 : 다수글을 조회하기 위해서 해당 테스트가 실행되기 전에 데이터를 여러개 넣어준다. 넣은 데이터들이 존재하는지 검증을 하고, 넣은 갯수만큼 데이터가 존재하는지 수를 검증한다.
"[Read] 게시글 다수건 조회" - { // RootNode
        // Given: 여러 게시물들을 저장
        boardJpaRepository.saveAll(
                listOf(
                        BoardEntity.builder()
                                .title("제목1")
                                .content("내용1")
                                .status(BoardStatus.ACTIVE)
                                .createdAt(Instant.now())
                                .updatedAt(Instant.now())
                                .build(),
                        BoardEntity.builder()
                                .title("제목2")
                                .content("내용2")
                                .status(BoardStatus.ACTIVE)
                                .createdAt(Instant.now())
                                .updatedAt(Instant.now())
                                .build()
                )
        )
        "[정상] 게시글이 존재할 때" - {
            // When: 게시글 전수 조회
            val pageable: Pageable = PageRequest.of(0, 10)
            val fetchedBoards = boardQueryAdapter.findAll(pageable)

            "[검증1] 게시글들이 존재하는지 검증" {
                fetchedBoards.hasContent() shouldBe true
            }

            "[검증2] 조회된 게시글 수를 검증" {
                fetchedBoards.content.size shouldBe 2
            }
        }
    }
  • 특정 상태를 필터링하여 조회하기 : 특정 상태로 필터링 조회를 제대로하는지 확인하기 위하여 테스트를 시작하기전 관련된 상태를 넣어서 DB에 저장한다. 저장한 게시글 수를 검증하고, 해당 게시글의 상태를 검증한다.
"[Read] 특정 상태로 필터링하기" - {
        // Given: 특정 상태에 해당하는 게시글 저장
        boardJpaRepository.saveAll(
            listOf(
                BoardEntity.builder()
                        .title("게시글 1")
                        .content("내용 1")
                        .status(BoardStatus.ACTIVE)
                        .createdAt(Instant.now())
                        .updatedAt(Instant.now())
                        .build(),
                BoardEntity.builder()
                        .title("게시글 2")
                        .content("내용 2")
                        .status(BoardStatus.PENDING)
                        .createdAt(Instant.now())
                        .updatedAt(Instant.now())
                        .build(),
                BoardEntity.builder()
                        .title("게시글 3")
                        .content("내용 3")
                        .status(BoardStatus.ACTIVE)
                        .createdAt(Instant.now())
                        .updatedAt(Instant.now())
                        .build()
            )
        )

        // When: 특정 상태 목록으로 게시글을 조회
        val statuses = listOf(BoardStatus.ACTIVE, BoardStatus.PENDING)
        val pageable = PageRequest.of(0, 10)
        val page = boardQueryAdapter.findByStatusesList(pageable, statuses)

        "[검증1] 필터링된 게시글 총 개수를 검증" {
            page.totalElements shouldBe 3
        }

        "[검증2] 필터링된 게시글의 상태를 검증" {
            page.content.forEach {
                it.status shouldBeIn statuses
            }
        }
    }

주요 작업 이력

  • 1차 작업

    • hexagonal 테스트 코드를 작성
    • queryDSL 이전의 방식으로 test code를 작성하였음)
  • 2차 작업

    • queryDSL 방식으로 테스트 코드를 수정함.
    • test code 구동을 위한 환경 설정(Spring Boot Test로 설정)
  • 3차 작업

    • kotest 방식으로 코드를 수정함(funSpec)
    • DataJpaTest로 환경설정을 하였으나 계속하여 구동이 되지 않았음
  • 4차 작업

    • queryDSL로 테스트를 위한 EntityManager 를 설정함
    • test code 형식을 변경함

회고

sweetykr7

테스트 코드 작성을 제대로 해본 것은 처음인 것 같습니다. 아직도 많이 부족하지만 첫 발을 떼게 되어서 기쁘게 생각합니다. 생각보다 환경 설정에 애를 많이 먹게 되었고, 아직도 잘 모르는 부분이 남아서 이 부분은 향후에 더 공부를 해나가야 할 것 같습니다. 쉽지는 않지만 제대로 된 테스트 코드가 있다면 향후에 에러를 줄일 수 있는 부분이 많아질 것이라고 기대하고 있습니다.

Clone this wiki locally