@DataJpaTest를 이용해서 단위 테스트 코드 작성하기

2024. 6. 21. 21:06TIL

✔오늘 배운 중요한 🔑 point

  • 테스트 코드 작성시 테스트용 applicaion.yml 파일을 하나 더 생성해줘야한다
  • @DataJpaTest 어노테이션을 사용하면 모든 테스트 메소드에 @Transactional 이 적용되기 때문에 save() 를 하더라도 쓰기지연 SQL 저장소에만 저장이된다.

🎯 오늘 배운 내용

 

테스트 코드 자동화

테스트를 수행함으로써 소프트웨어의 결함을 조기에 발견하고 완성도 높은 소프트웨어를 개발하기 위해서 테스트 코드의 작성은 필수적이다. 이러한 테스트를 테스트 코드를 통해서 자동화할 필요가 있다.

 

FIRST 원칙

  • F (Fast)
    • 테스트 코드는 빠르게 실행되어야 한다.
  • I (Isolated)
    • 테스트는 독립적이어야 한다. 즉, 다른 테스트나 외부 시스템을 의존하면 안된다.
  • R (Repeatable)
    • 테스트는 반복실행해도 동일한 결과여야한다.
  • S (Self-Validating)
    • 테스트는 스스로 검증되어야한다. 테스트 결과를 검증하기 위해 별도 리소스가 들어가서는 안된다.
  • T (Timely)
    • 테스트 코드는 가능한 빨리 작성되어야한다.

 

 

@DataJpaTest를 이용해서 테스트 코드를 작성하자!

 

@DataJpaTest 어노테이션은 JPA와 관련된 테스트를 작성할 때 유용하며 데이터베이스와의 상호작용을 테스트하는 데 필요한 최소한의 설정을 제공한다

 

기본 설정

build.gradle.kts

plugins {
    id("org.springframework.boot") version "3.3.1"
    id("io.spring.dependency-management") version "1.1.5"
    kotlin("plugin.jpa") version "1.9.24"
    kotlin("jvm") version "1.9.24"
    kotlin("plugin.spring") version "1.9.24"
    kotlin("kapt") version "1.9.22"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.MappedSuperclass")
    annotation("javax.persistence.Embeddable")
}


dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    // Query DSL 설정
    implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
    kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
    kapt("jakarta.annotation:jakarta.annotation-api")
    kapt("jakarta.persistence:jakarta.persistence-api")


    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")

    //Test Code
    runtimeOnly("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.mockk:mockk:1.13.9")
    testImplementation("io.kotest:kotest-assertions-core:5.7.2")
    testImplementation("io.kotest:kotest-runner-junit5:5.7.2")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

 

테스트 코드는 보통 테스트 패키지 하위에 작성한다

 

기존 application.yml과 별도로 테스트용 application.yml파일을 하나 더 생성한다

 

 

test용 application.yml

spring:
  config:
    activate:
      on-profile: test

  datasource:
    url: jdbc:h2:mem:test;MODE=MySQL;
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: create-drop

 

on-profile: test 설정은  test profile일 경우에만 해당 application.yml 파일을 사용하겠다는 의미이다

 

 

테스트용 MemberRepositoryTest

package com.example.sampletest.domain.member

import com.example.sampletest.domain.common.QueryDslConfig
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(value = [QueryDslConfig::class])
@ActiveProfiles("test")
class MemberRepositoryTest @Autowired constructor(
    private val memberRepository: MemberRepository
) {

    @Test
    fun `SearchType 이 NONE 일 경우 전체 데이터 조회되는지 확인`() {
        // GIVEN

        // WHEN

        // THEN
    }

    @Test
    fun `SearchType 이 NONE 이 아닌 경우 Keyword 에 의해 검색되는지 결과 확인`() {
        // GIVEN

        // WHEN

        // THEN
    }

    @Test
    fun `Keyword 에 의해 조회된 결과가 0건일 경우 결과 확인`() {
        // GIVEN

        // WHEN

        // THEN
    }

    @Test
    fun `조회된 결과가 10개, PageSize 6일 때 0Page 결과 확인`() {
        // GIVEN

        // WHEN

        // THEN
    }

    @Test
    fun `조회된 결과가 10개, PageSize 6일 때 1Page 결과 확인`() {
        // GIVEN

        // WHEN

        // THEN
    }

    companion object {
        private val DEFAULT_MEMBER_LIST = listOf(
            Member(email = "sample1@naver.com", password = "aaaa", nickname = "sample1"),
            Member(email = "sample2@gmail.com", password = "aaaa", nickname = "sample2"),
            Member(email = "sample3@daum.net", password = "aaaa", nickname = "sample3"),
            Member(email = "sample4@gmail.com", password = "aaaa", nickname = "sample4"),
            Member(email = "sample5@naver.com", password = "aaaa", nickname = "sample5"),
            Member(email = "sample6@daum.net", password = "aaaa", nickname = "sample6"),
            Member(email = "sample7@naver.com", password = "aaaa", nickname = "sample7"),
            Member(email = "sample8@gmail.com", password = "aaaa", nickname = "sample8"),
            Member(email = "sample9@naver.com", password = "aaaa", nickname = "sample9"),
            Member(email = "sample10@gmail.com", password = "aaaa", nickname = "sample10")
        )
    }
}

 

실제 테스트 코드 적용

package com.example.sampletest.domain.member

import com.example.sampletest.domain.common.QueryDslConfig
import com.example.sampletest.domain.member.type.MemberSearchType
import io.kotest.matchers.shouldBe

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.test.context.ActiveProfiles

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(value = [QueryDslConfig::class])
@ActiveProfiles("test")
class MemberRepositoryTest @Autowired constructor(
    private val memberRepository: MemberRepository
) {

    @Test
    fun `SearchType 이 NONE 일 경우 전체 데이터 조회되는지 확인`() {
        // GIVEN
        memberRepository.saveAllAndFlush(DEFAULT_MEMBER_LIST) // 총 10개

        // WHEN
        val result1: Page<Member> = memberRepository.search(MemberSearchType.NONE,"", Pageable.ofSize(10))
        val result2: Page<Member> = memberRepository.search(MemberSearchType.NONE,"", Pageable.ofSize(8))
        val result3: Page<Member> = memberRepository.search(MemberSearchType.NONE,"", Pageable.ofSize(15))

        // THEN
        result1.content.size shouldBe 10
        result2.content.size shouldBe 8
        result3.content.size shouldBe 15
    }

    @Test
    fun `SearchType 이 NONE 이 아닌 경우 Keyword 에 의해 검색되는지 결과 확인`() {
        // GIVEN
        memberRepository.saveAllAndFlush(DEFAULT_MEMBER_LIST)

        // WHEN
        val result: Page<Member> = memberRepository.search(MemberSearchType.EMAIL,"naver.com",Pageable.ofSize(10))

        // THEN
        result.content.size shouldBe 4
    }

    @Test
    fun `Keyword 에 의해 조회된 결과가 0건일 경우 결과 확인`() {
        // GIVEN
        memberRepository.saveAllAndFlush(DEFAULT_MEMBER_LIST)

        // WHEN
        val result: Page<Member> = memberRepository.search(MemberSearchType.EMAIL,"kakao.com",Pageable.ofSize(10))

        // THEN
        result.content.size shouldBe 0
    }

    @Test
    fun `조회된 결과가 10개, PageSize 6일 때 0Page 결과 확인`() {
        // GIVEN
        memberRepository.saveAllAndFlush(DEFAULT_MEMBER_LIST)
        // WHEN
        val result: Page<Member> = memberRepository.search(MemberSearchType.NICKNAME,"sample", PageRequest.of(0, 6))

        // THEN
        result.content.size shouldBe 6
        result.isLast shouldBe false
        result.totalPages shouldBe 2
        result.number shouldBe 0
        result.totalElements shouldBe 10

    }

    @Test
    fun `조회된 결과가 10개, PageSize 6일 때 1Page 결과 확인`() {
        // GIVEN
        memberRepository.saveAllAndFlush(DEFAULT_MEMBER_LIST)
        // WHEN
        val result: Page<Member> = memberRepository.search(MemberSearchType.NICKNAME,"sample", PageRequest.of(1, 6))
        // THEN
        result.content.size shouldBe 4
        result.isLast shouldBe true
        result.totalPages shouldBe 2
        result.number shouldBe 1
        result.totalElements shouldBe 10
    }

    companion object {
        private val DEFAULT_MEMBER_LIST = listOf(
            Member(email = "sample1@naver.com", password = "aaaa", nickname = "sample1"),
            Member(email = "sample2@gmail.com", password = "aaaa", nickname = "sample2"),
            Member(email = "sample3@daum.net", password = "aaaa", nickname = "sample3"),
            Member(email = "sample4@gmail.com", password = "aaaa", nickname = "sample4"),
            Member(email = "sample5@naver.com", password = "aaaa", nickname = "sample5"),
            Member(email = "sample6@daum.net", password = "aaaa", nickname = "sample6"),
            Member(email = "sample7@naver.com", password = "aaaa", nickname = "sample7"),
            Member(email = "sample8@gmail.com", password = "aaaa", nickname = "sample8"),
            Member(email = "sample9@naver.com", password = "aaaa", nickname = "sample9"),
            Member(email = "sample10@gmail.com", password = "aaaa", nickname = "sample10")
        )
    }
}

 

 

 

각각의 테스트 함수에 대해서 통과를 했는지 실패를 했는지를 확인할 수 있다

 

 

 

 

테스트에 실패할경우

 테스트에 실패할 경우에는 빨간색 X표시가 뜬것을 확인할 수 있다

 

 

🤔 어떻게 활용할까?

 

test 패키지를 우클릭하고 Run Test 를 검사하면 Test 하위에 있는 모든 test code를 실행시켜 테스트를 해볼수 있다

 

 

📓 오늘의 한줄

"The test of courage comes when we are in the minority. The test of tolerance comes when we are in the majority."

- Ralph W. Sockman -

 

 

'TIL' 카테고리의 다른 글

@Transactional과 롤백  (0) 2024.06.23
@DataJpaTest 를 이용한 통합 테스트 코드 작성하기  (0) 2024.06.22
Projection  (0) 2024.06.20
영속성 컨텍스트  (0) 2024.06.19
팀프로젝트 게임 채널 관리 앱 (회고)  (0) 2024.06.18