코프링 프로젝트의 테스트를 구성해보았다. 테스트를 하면서 사용한 패키지들은 아래와 같다.
fixture를 사용할 때 위 처럼 세가지 종류를 사용해봤는데 결론적으로 현재는 kotlinFixture를 사용하고 있다.
fixture들을 사용하면서 느낀 점들과 갈아탄 이유들을 정리하려고 한다.
예시를 위해서 다음과 같은 엔티티가 있다고 하자.
// User.kt
package example
import example.domain.base.BaseEntity
import example.domain.user.UserStatus
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Index
import javax.persistence.Table
@Table(name = "users")
@Entity
class User(
@Column(name = "name", length = 16, nullable = false)
val name: String,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
status: UserStatus,
) {
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: UserStatus = status
private set
@CreatedDate
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.MIN
private set
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.MIN
private set
}
faker는 예시이기는 하지만 아래와 같은 방식으로 사용하였다.
val faker: Faker = Faker.instance(Locale.US)
fun createUserFixture(
id: Long = faker.number().numberBetween(0L, 999999L),
name: String = faker.lorem().characters(),
status: UserStatus = faker.options().option(UserStatus::class.java),
) = User(
id = id,
name = name,
status = status,
)
이렇게 사용하다보니 몇 가지 불편한 점이 생겼는데 다음과 같다.
testImplementation
을 하려고 했으나 어느 시점부터인지 안됐다.@Profile(value = ["local", "test"])
이런 방식으로 사용하는데 여간 거슬리는게 아니었다.그래서 네이버에서 만든 fixtureMonkey라는 것을 사용해보려고 하였다. 사실 fixture monkey는 제대로 적용해보지 않고 아주 살짝 맛만보고 바로 버렸다.
그 이유는 다음과 같은데,
val sut = FixtureMonkey.create()
val userFixture = sut.giveMeBuilder(User::class.java)
.set("name", "FANA") // 여기서 공식 문서에 있는 모든 것을 시도해봐도 무조건 null로 나온다.
.sample()
첫 인상으로는 상당히 마음에 들었다. 무엇이 가장 마음에 들었냐면 mockk
와 함께 사용하면서 아래와 같은 코드를 작성할 때가 많은데
every { SomeService.doSomething(customizedUserFixture) } returns something
기본적인 사용법은 다음과 같다.
val fixture = kotlinFixture()
val userFixture = fixture<User>()
여기에, 기본적인 팩토리를 붙인다거나
val fixture = kotlinFixture {
factory<String> { "FANA", "SOMETHING", "CANDIDATES" }
factory<Int> { 1, 2, 3 }
}
특정 타입에도 쓸 수 있다.
// 참고
enum class UserStatus {
CREATED,
SOMETHING,
ELSE,
THIS,
COULD,
BE;
}
val fixture = kotlinFixture {
// fixture<User>()를 통해 만들어지는 유저 픽스쳐는 CREATED, SOMETHING 두가지로만 만들어진다.
factory<UserStatus> { UserStatus.CREATED, UserStatus.SOMETHING }
}
개별적으로 프로퍼티를 지정할 수도 있다.
val fixture = kotlinFixture()
val userFixture = fixture<User> {
property(User::name) { "FANA" }
property(User::status) { UserStatus.COMPLETED }
}
당연하게도 constructor에 없는 프로퍼티는 지정할 수 없다.
암튼, 그래서 결과적으로 만들어진 테스트 코드를 보면 다음과 같다.
package example
import com.appmattus.kotlinfixture.kotlinFixture
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
internal class SignUpServiceTest : BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf
val getUserService = mockk<GetUserService>()
val createUserService = mockk<CreateUserService>()
val cryptService = mockk<CryptService>()
val signUpService = SignUpService(
getUserService = getUserService,
createUserService = createUserService,
cryptService = cryptService,
)
val nickName = "FANA"
val plainPassword = "PLAIN_PASSWORD"
val hashedPassword = "HASHED_PASSWORD"
val fixture = kotlinFixture()
val loginMethodFixture = fixture<LoginMethod> {
property(LoginMethod::loginType) { LoginType.PASSWORD }
property(LoginMethod::version) { 1 }
property(LoginMethod::hashedValue) { hashedPassword }
}
val userFixture = fixture<User> {
property(User::loginMethods) { mutableListOf(loginMethodFixture) }
}
given("닉네임과 패스워드가 주어졌을 때") {
every { getUserService.findUserByNickName(any()) } returns null
every { createUserService.createUser(nickName) } returns userFixture
every { cryptService.encrypt(any(), any()) } returns hashedPassword
`when`("회원가입을 시도하면") {
then("회원가입이 된다") {
val user = signUpService.signUp(nickName, plainPassword)
val loginMethod = user.loginMethods.find { it.loginType === LoginType.PASSWORD && it.version == 1 }
user.nickName shouldBe userFixture.nickName
loginMethod!!.hashedValue shouldBe hashedPassword
}
}
// validations
`when`("닉네임이 이미 존재하면") {
every { getUserService.findUserByNickName(nickName) } returns userFixture
then("예외가 발생한다") {
shouldThrow<DuplicatedUserNickNameException> {
signUpService.signUp(nickName, plainPassword)
}
}
}
`when`("닉네임 길이가 유효하지 않으면") {
then("예외가 발생한다") {
shouldThrow<InvalidUserNickNameException> {
signUpService.signUp(
nickName = "thisIsTooooooooooooooooooooooooooooooooooooooooooooooooooooooooLongForNickName",
plainPassword = plainPassword
)
}
}
}
`when`("닉네임 패턴이 유효하지 않으면") {
then("예외가 발생한다") {
shouldThrow<InvalidUserNickNameException> {
signUpService.signUp(
nickName = "!@#%@$$%&$%&$%&$%&$&%$&$%&",
plainPassword = plainPassword
)
}
}
}
`when`("패스워드 길이가 유효하지 않으면") {
then("예외가 발생한다") {
shouldThrow<InvalidPasswordException> {
signUpService.signUp(
nickName = nickName,
plainPassword = "thisIsTooooooooooooooooooooooooooooooooooooooooooooooooooooooooLongForPassword"
)
}
}
}
}
})
// build.gradle.kts
// 생략
allprojects {
group = "example"
version = "0.0.1"
extra["kotlin-coroutines.version"] = Versions.coroutines // 이것 추가
// 생략
}
// 생략
안녕하세요 지나가다가 보고 남깁니다.
Kotlin 에서 FixtureMonkey 를 사용할 경우 KotlinPlugin 을 셋팅해주어야합니다.
아마 KotlinPlugin 을 주입하지 않아서 의도한 값이 셋팅되지 않고 null 이 들어간거로 보입니다.
https://naver.github.io/fixture-monkey/kr/docs/v0.4/third-party-modules/fixture-monkey-kotlin/
속성 변경시 Kotlin Exp 를 사용할 수 있는 것도 참고하시면 될거 같습니다.
https://naver.github.io/fixture-monkey/kr/docs/v0.4/examples/fieldset/#2-%EA%B0%92-%EC%84%A4%EC%A0%95
클래스에서 Validation 애노테이션을 붙이는건 필수사항이 아닌 필요한 경우 선택할 수 있는 옵션으로 보시면 될거 같습니다.