본문 바로가기
개발 지식/Kotlin

[Kotest] 클래스 프로퍼티 Value NULL 여부 한번에 검증하기

by 에르주 2023. 11. 25.
반응형

많은 개발자분들이 실무 및 개인 프로젝트 개발 시에 Test 파일을 작성한다.

내부 로직 검증 그리고 통합 테스트 검증등 여러가지 이유가 있겠지만 나 같은 경우 json 데이터를 내가 원하는 Object로 변환시에 그 값이 잘 매핑 되어 있는지 확인하고 싶을 때가 있다.

 

하지만, Kotest에서는 내부 필드값 검증과 shouldBeSameInstanceAs, shouldBeEqualToComparingFields와 같은 메소드들은 단순히 클래스 인스턴스의 동일 여부만 판단할 수 있었다.

 

 

 

이에 클래스 프로퍼티 Value NULL 여부 한번에 검증할 수 있는 메소드를 구현하고자 했다.

 

 

 

카프카 데이터 컨슈밍 -> 데이터 클래스 매핑 -> 추가 필드값에 대한 매핑 로직

place.json

// place.json

{
  "code" : "001",
  "name" : "플레이스",
  "location" : "2층",
  "isDelicious" : null,
  "isVisit" : false,
  "isEvent" : false,
  "isParking" : true,
  "price" : {
    "americano" : "5000",
    "cafeLatte" : "5500"
  }
}

Place.kt

@Serializable
data class Place(

    val code: String,
    val name: String,
    val location: String,
    var isDelicious: Boolean?,    // 맛집 여부
    var isVisit: Boolean?, // 방문 여부
    val isEvent: Boolean,  // 이벤트 여부
    val isParking: Boolean,    // 주차 여부
    val price: Price,
) {
    @Serializable
    data class Price(
        val americano: String,
        val cafeLatte: String,
//        val coldBrew: String?,
//        val chocoCake: String?
    )
}

 

 

1. 카프카 데이터 컨슈밍 -> 데이터 클래스 매핑

 

이 단계에서는 보통 ObjectMapper, Mapstruct, Serializable 등을 활용하여 객체 변환을 한다. 이 라이브러리를 활용하면 nullable 및 상태 체크를 알아서 해주기에 크게 휴먼 에러가 나지 않는다.

class PlaceServiceTest : DescribeSpec({

    describe("[Place] 데이터 클래스 변환 테스트") {
        val json = Json { ignoreUnknownKeys = true }
        val placeFile = File("src/test/kotlin/com/study/kotlin_spring/dumy/place.json")
        val place = json.decodeFromString<Place>(placeFile.readText())
        println (place)
    }
})

매핑 정상

혹시나 Price 데이터 클래스 안에 coldBrew를 명시해 준다면?

매핑 오류 발생

 

2. 추가 필드값에 대한 매핑 로직

이 단계에서는 다른 데이터 베이스를 조회하여 매핑 할 수도 있고 기타 비지니스 로직에서 버그가 발생할 수 있기 때문에 필드값 매핑 여부 확인이 필요했다.

하나의 시나리오 실행

플레이스라는 곳을 방문해보니 맛집이라고 해보자. 그래서 메소드를 하나 실행하여 값들을 아래와 같이 변경 하였다. (visitCheck)

isVisit : false -> true

isDelicious : null -> true

fun visitCheck(place: Place) {
    place.takeIf { it.isVisit == false }?.apply {
        isVisit = true
        isDelicious = true
    }
}

값이 변경


여기서 나는 isDelicious가 null값이 아닌 true, false 값인지 테스트를 진행해보려고 하였다.

가장 간단한 방법은 다음과 같다.

place.isDelicious shouldNotBe null // isDelicious는 null값이 아니다
place.isDelicious shouldBeIn listOf(true, false) // isDelicious가 true, false 중 무조건 하나에 해당된다.

하지만 해당 값이 Boolean이 아닌 문자열 또는 리스트일 경우와 place의 데이터 클래스 필드값이 늘어났을 경우

-> 테스트 파일 짜는데 모든 경우의 수를 생각해야 하고 중복된 코드 작업들이 많아진다. 또한 데이터 불안정한 테스트 파일이 된다.
(난 그저 값이 있는지 여부를 확인하고 싶은데..!!)

그래서를 통해

1) getNonNullProperties 메소드를 통해 NonNull인 데이터 클래스의 프로퍼티 추출

2) memberShouldBeIn 메소드를 통해 List값 중 code값이 포함 되어 있는지 체크

private fun getNonNullProperties(it: Any): List<String> {

    return it.javaClass.kotlin.memberProperties.filter { prop ->
        prop.get(it) != null
    }.map { prop -> prop.name }

}

fun List<*>.memberShouldBeIn(vararg code: String) {

    val codeList = code.map { it }
    this.forEach {
        it.shouldBeIn(codeList)
    }
}

다음과 같은 코드로 테스트 파일을 구현할 수 있다.

 

1) visitCheck 실행 전 value 체크 : 성공 (기존 null이 아닌 값들 체크)

class PlaceServiceTest : DescribeSpec({

    describe("[Place] 데이터 클래스 변환 테스트") {
        val json = Json { ignoreUnknownKeys = true }
        val placeFile = File("src/test/kotlin/com/study/kotlin_spring/dumy/place.json")
        val place = json.decodeFromString<Place>(placeFile.readText())

        val nonNullProp = getNonNullProperties(place)
        nonNullProp.memberShouldBeIn(
            "code", "name", "location","isVisit", "isEvent", "isParking", "price"
        ) 
    }
})


private fun getNonNullProperties(it: Any): List<String> {

    return it.javaClass.kotlin.memberProperties.filter { prop ->
        prop.get(it) != null
    }.map { prop -> prop.name }

}


fun List<*>.memberShouldBeIn(vararg code: String) {

    val codeList = code.map { it }
    this.forEach {
        it.shouldBeIn(codeList)
    }
}

테스트 통과

2) visitCheck 실행 후 value 체크 (isDelicious nonNull) : 실패 -> memberShouldBeIn에 isDelicious 파라미터 추가 필요

class PlaceServiceTest : DescribeSpec({

    describe("[Place] 데이터 클래스 변환 테스트") {
        val json = Json { ignoreUnknownKeys = true }
        val placeFile = File("src/test/kotlin/com/study/kotlin_spring/dumy/place.json")
        val place = json.decodeFromString<Place>(placeFile.readText())

        visitCheck(place)

        val nonNullProp = getNonNullProperties(place)
        nonNullProp.memberShouldBeIn(
            "code", "name", "location", "isVisit", "isEvent", "isParking", "price"
        )

    }

})

fun visitCheck(place: Place) {
    place.takeIf { it.isVisit == false }?.apply {
        isVisit = true
        isDelicious = true
    }
}

private fun getNonNullProperties(it: Any): List<String> {

    return it.javaClass.kotlin.memberProperties.filter { prop ->
        prop.get(it) != null
    }.map { prop -> prop.name }

}


fun List<*>.memberShouldBeIn(vararg code: String) {

    val codeList = code.map { it }
    this.forEach {
        it.shouldBeIn(codeList)
    }
}

 

3) visitCheck 후 결과값 isDelicious 파라미터 여부 검증 : 성공

    describe("[Place] 데이터 클래스 변환 테스트") {
        val json = Json { ignoreUnknownKeys = true }
        val placeFile = File("src/test/kotlin/com/study/kotlin_spring/dumy/place.json")
        val place = json.decodeFromString<Place>(placeFile.readText())

        visitCheck(place)

        val nonNullProp = getNonNullProperties(place)
        nonNullProp.memberShouldBeIn(
            "code", "name", "location", "isDelicious", "isVisit", "isEvent", "isParking", "price"
        )
    }

테스트 성공

 

4) Place 데이터 클래스에 필드값 추가

Json 파일과 데이터 클래스에 제한 시간이라는  프로퍼티 및 메소드 내용 추가 된다라고 해보자.

{"isLimitTime" : null }

 

Place Data 클래스

var isLimitTime: String?,

 

 

기존 검증은 깨지게 된다.

class PlaceServiceTest : DescribeSpec({

    describe("[Place] 데이터 클래스 변환 테스트") {
        val json = Json { ignoreUnknownKeys = true }
        val placeFile = File("src/test/kotlin/com/study/kotlin_spring/dumy/place.json")
        val place = json.decodeFromString<Place>(placeFile.readText())

        visitCheck(place)

        val nonNullProp = getNonNullProperties(place)
        nonNullProp.memberShouldBeIn(
            "code", "name", "location", "isDelicious", "isVisit", "isEvent", "isParking", "price"
        )

    }

})

fun visitCheck(place: Place) {
    place.takeIf { it.isVisit == false }?.apply {
        isVisit = true
        isDelicious = true
        isLimitTime = "2"
    }
}

 

 

 

매핑 오류

이렇게 매핑 오류를 통해 해당 값이 있는지를 판단 할 수 있게 되었다.

val nonNullProp = getNonNullProperties(place)
nonNullProp.memberShouldBeIn(
    "code", "name", "location", "isDelicious", "isVisit", "isEvent", "isParking", "isLimitTime", "price"
)

검증 파라미터 값으로 isLimitTime을 넣게 되면

테스트 성공

 

 

즉 해당 과정으로 비지니스 로직 처리 후 DB에 저장하려는 해당 데이터 클래스의 프로퍼티 값이 Null인지 nonNull인지, 존재하는지에 대해 테스트 할 수 있었다.

 

물론 위의 코드에서 조금 더 다듬어 간단하게 체크할 수도 있겠지만 현재 위의 테스트 코드 구현으로 간단한 필드 추가 같은 경우 휴먼 에러를 최소화 할 수 있었다.

 

끝.

반응형

댓글