Fixtures

TestFixtures 란 SUT(System Under Test) 으로 전달되는 인수를 의미한다. 각 테스트가 실행하기 전에 고유한 상태를 유지한다. 모든 TestFixtures 를 Lifecycle 을 가지고 있다. 보통 SetUp/TearDown 을 갖는다.

Meaning of #NUnit:

  • A test fixture is simply a class that is marked with the [TestFixture] attribute. This attribute tells NUnit that the class contains one or more test methods.
// TestFixture
public class SampleTestFixture {
    // [SetUp]
    public void SetUp() {
        // Code to run before each test method
    }

    // [TearDown]
    public void TearDown() {
        // Code to run after each test method
    }
    
    // Test methods will be defined here
}

Sharing Test Fixtures/Dependency

테스트를 작성하다 보면 가독성, Fixtures 의 재사용성 그리고 테스트 의존성에 대한 고려를 하게 된다. Object Mother 패턴 등을 통해 쉽게 test instance 를 생성할 수 있으며, Fixture Monkey 와 같은 라이브러리도 존재한다.

Readability

SUT 에 전달해야하는 매개변수가 너무 많은 경우 혹은, 특정 클래스에 대한 테스트 코드를 작성하는데 해당 클래스의 의존성이 수십개인 경우 Mockist 방식으로 의존성을 테스트 클래스의 상위에 정의하는 경우 테스트 메서드를 보기 위해서 스크롤을 해야할 것이다.

Bad Case:

class LegacyServiceTest: FunSpec({
    val clientA = mockk<ClientA>()
    val repositoryA mockk<RepositoryA>()
    more dependency ...
    val sut = LegacyService(clientA, repositoryA, ...)

    beforeTest {
        mockkStatic(::currentUTC) // extensions function
        val fixedAt = LocalDateTime.of(2024, 7, 25, 10, 0)
        every { currentUTC() } returns fixedAt
    }
    
    test("...") { }
})

어떤 Fixtures 가 있는지 보다, 어떤 테스트를 다루고 있는지가 더 관심사 일 것이다. 이 경우 가독성을 너무 저하시키기 때문에, TestFixtures 를 모아둔 클래스를 제공할 수 있다.

class LegacyServiceTest: FunSpec({
    val fixtures = LegacyTestFixtures()
    
    beforeTest {
        mockkStatic(::currentUTC) // extensions function
        val fixedAt = LocalDateTime.of(2024, 7, 25, 10, 0)
        every { currentUTC() } returns fixedAt
    }

    test("...") { 
        with(receiver = fixtures) {
            // ...
        }
    }
})

class LegacyTestFixtures {
    val clientA = mockk<ClientA>()
    val repositoryA mockk<RepositoryA>()
    more dependency ...
    val sut = LegacyService(clientA, repositoryA, ...)
}

많은 코드 라인 수를 차지하는 Fixture 를 테스트 코드에서 정의하게 되면 가독성이 심각하게 저하된다. 아래에서 소개할 Test fixture 디렉터리에 정의할 수 있지만, 어떤 기준, 이름 등으로 정의할지 명확해지기 전이라면 임시로 아래로 빼놓을 수 있다. 이렇게 아래에 정의해둔 픽스처들을 추후에 Refactoring 하는 것은, 테스트 코드에서 정의된 테스트 픽스처들을 찾아서 리팩토링 하는 것보다 상대적으로 비용이 저렴하다.

Reusability

java-test-fixtures 플러그인을 사용하면 테스트용으로 작성한 Builder, Helper 클래스 등을 다른 모듈과 공유할 수 있다. 또한 해당 모듈의 테스트 전용 의존성까지 전파시킬 수 있다. 이와 관련된 내용은 테스트 의존성 관리로 높은 품질의 테스트 코드 유지하기 - Toss Payments 에 잘 정리 되어있다.