카테고리 없음

Spy와 Mock의 차이. 그리고 Spy 사용

voider 2022. 7. 31. 01:04

Mock과 Spy는 테스트 더블(대역)이다.
Test Double은 테스트를 목적으로 프로덕션 오브젝트를 대체하는 오브젝트를 뜻한다. ‘Test Double’이라는 말 때문에 처음에는 잘 이해가 되지 않았는데 영어권에서는 스턴트 맨을 스턴트 더블이라고 한단다. 그러니까 테스트 더블은 말하자면 테스트를 목적으로 진짜 오브젝트를 대신하는 테스트 계 스턴트맨이라고 볼 수 있다.

Mock

Mock은 가짜 오브젝트다. ‘가짜 오브젝트’란 뭘까? 간단한 Post 클래스를 작성해서 예시를 들어본다.

class Post(
    val id: Long? = null,
    val title: String,
    val content: String,
) {

    fun create(title: String, content: String) { 
        this.title = title
        this.content = content
    }

}

Post 에 대한 Mock 을 생성하면 아래와 같다. 이해하기 쉽게 Mock 클래스를 직접 작성했지만 Mock 라이브러리를 사용하면 간단하게 애노테이션 만으로도 생성할 수 있다.

class MockPost(
    val id: Long? = null,
    val title: String,
    val content: String,
) {

    fun create(title: String, content: String) { 
        //비어 있음
        //직접 create 동작을 지정해주어야 함
    }
}

Mock 클래스를 생성하면 create() 메소드 구현이 비어있다. 당연히 필드의 값도 비어있는 상태다. 이것을 일일이 지정해주어야 한다.

Spy

spy는 진짜 오브젝트를 생성한다. 따라서 Post 를 spy로 생성한다는 것은 그냥 Post 객체를 하나 만든다는 뜻이다. 스파이는 일반 오브젝트이기 때문에 (당연하게도)Mock과 달리 직접 행동을 지정해줄 필요가 없다. 그럼 왜 스파이를 쓸까? 스파이는 Mock과 달리 stub이 필요한 부분에만 stub을 할 수 있다는 특징이 있다.

stub이란, 해당 메소드(또는 필드)를 호출했을 때 반환해야 하는 값을 미리 지정하는 것을 뜻한다.
https://martinfowler.com/bliki/TestDouble.html

그동안 Mock과 Spy의 차이를 어렴풋이 알고 있었다. 개인적으로 Mock 오브젝트는 반드시 필요한 부분에만 사용하려고 한다. 테스트 코드를 보다보면 Request 객체까지 모두 Mock으로 선언해서 stub을 하는 경우도 보이는데 개인적으로 코드만 복잡해진다고 생각한다. 그냥 할 수 있는 것을 굳이 라이브러리에 의존하려고 한다는 느낌을 받는다.

그런데 오늘 엔티티 클래스를 Mock으로 생성해야 할 필요를 느꼈다. 아래와 같은 경우다. 애노테이션은 모두 생략한다.

Post

class BaseEntity(
    val id: Long? = null
)

class Post(
    var title: String,
    var content: String?,
): BaseEntity()

PostBaseEntity 를 상속받는다. BaseEntity 에는 id필드가 불변으로 선언되어 있다.

PostCommand


class PostCommand(
    private val postRepository: PostRepository,
) {
    @Transactional
    fun create(
        title: String,
        content: String?,
        status: String,
        dueDateTime: LocalDateTime?,
    ): Long {
        val post = Post(
            title = title,
            content = content,
            status = PostStatus.valueOf(status),
            dueDateTime = dueDateTime,
            author = null
        )

        val saved = postRepository.save(post)

        return saved.id!!
    }
}

Post를 생성한 다음 id 를 반환하는 간단한 메서드다.

create 에 대한 간단한 단위 테스트를 어떻게 작성할 수 있을까? 생성 시점에 (현재 코드로써는) 수퍼 클래스에 있는 id를 초기화할 방법이 없다. 따라서 create의 반환값은 계속 null 일 수밖에 없다.

이 문제를 해결하기 위해서 처음에 Post 를 Mock으로 생성하려고 했다. 앞서도 말했지만 Mock을 최소한으로 사용하려는 편이긴 하지만, valvar 로 바꿀 바에야, Mock을 사용하는 게 낫다고 판단했다.
그런데 이렇게 하면 id 뿐만 아니라 다른 모든 필드의 값을 stub 으로 지정해주어야 한다.

PostCommandTest


    @Test
    fun `post 등록하고 id 반환한다`() {
        //given
        val post = mockk<Post>()

          //post.content = "content" -> 동작 안 함

        every { post.id } returns 1
        every { post.title } returns "title"
        every { post.content } returns "content"
        every { postRepository.save(any()) } returns post

        //when
        val postId = sut.create(
            title = post.title,
            content = post.content,
        )

        assertThat(postId).isEqualTo(post.id)
    }

이렇게 하면 Post의 필드가 늘어날 때마다 테스트 코드도 함께 수정되어야 한다. 그거야 뭐 그럴 수 있다고 치더라도, create() 내부에서 Post 객체의 메소드를 호출하는 일이라도 생긴다면 그것 또한 함께 stub을 해줘야 한다는 번거로움이 있다.

이때 잊고 살던 spy가 문득 떠올랐다.
Spy 객체를 사용하면 다른 필드는 그대로 기존 것을 사용하되, 내가 필요한 부분(id)만 따로 stub을 해줄 수 있다.

PostCommandTest

@Test
fun `post 등록하고 id 반환한다`() {
    //given
    val post = spyk<Post>().also {
        it.title = "title"
        it.content = "content"
    }

    every { post.id } returns 1
    every { postRepository.save(any()) } returns post

    //when
    val postId = sut.create(
        title = post.title,
        content = post.content,
    )

      //then
    assertThat(postId).isEqualTo(post.id)
}

Spy 객체를 사용하면 내가 필요한 부분만 stub하고 나머지는 진짜 오브젝트의 구현을 그대로 사용할 수 있으니 불필요한 부분까지 stub을 해줄 필요가 없어진다.

이게 진짜 spy의 올바른 사용법인지는 확신하지 못하겠다. 다만 위와 같은 문제를 해결하는데 mock보다는 spy가 좋은 선택이라고 봤다.

끝.