몇몇 테스트를 통해 JUnit 5 의 테스트가 어떻게 돌아가는지 근본적으로 파악해보자.

1. static 영역의 인스턴스.

먼저, 본인이 예전에 작성했다가 지웠던 코드를 간략하게 가져왔다.

@Repository
public class AccountRepository {
    private static final long START_ID = 0;
    private static AtomicLong ID_GENERATOR = new AtomicLong(START_ID);

    public long genId() {
        return ID_GENERATOR.incrementAndGet();
    }

    public void initGenerator() {
        ID_GENERATOR = new AtomicLong(START_ID);
    }
    ...
}

static으로 AtomicLong을 만들고 Auto increment 하면서 ID를 생산하고 있다.

다음은 테스트 코드이다.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j
class GroundTest {
    AccountRepository accountRepository = new AccountRepository();

    AtomicLong atomicLong = new AtomicLong(0);

    @Test
    @Order(1)
    void _1Test() {
        log.info("static-atomic_1: {}", accountRepository.genId());
        log.info("junit-atomic_1: {}", atomicLong.incrementAndGet());
    }
    @Test
    @Order(2)
    void _2Test() {
        log.info("static-atomic_2: {}", accountRepository.genId());
        log.info("junit-atomic_2: {}", atomicLong.incrementAndGet());
    }
    @Test
    @Order(3)
    void _3Test() {
        log.info("static-atomic_3: {}", accountRepository.genId());
        log.info("junit-atomic_3: {}", atomicLong.incrementAndGet());
    }
}

AccountRepository와 테스트 클래스 내에서 AtomicLong을 따로 만들어 값을 비교하고 있다.

본인은 두개의 경우 다같이 1, 1, 1 이 나오는 것을 기대하였지만 아니었다.

test-log1

클래스 스코프 내에 존재하는 atomicLong은 매번 새로 할당되며 값이 초기화되는 한편, static 영역의 인스턴스는 테스트가 끝날떄까지 다시 할당되거나 하지 않는다.


2. non-final static 필드에 대한 해결책

만약 위처럼 매 테스트가 static 필드에 의존하고 있고, 그 값이 계속 변하여 다음 테스트에 영향을 준다면 제대로 테스트가 진행되지 못할 것이다.

만약 위와 같은 코드를 사용하고 있고, 이를 쉽게 바꾸지 못하는 상황이라면 해결책은 있다.

Reflection으로 매 테스트마다 값을 주입하는 것이다.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j
class GroundTest {
    AccountRepository accountRepository = new AccountRepository();

    AtomicLong atomicLong = new AtomicLong(0);

    @BeforeEach
    void injectIdGenerator() throws Exception{
        // 기본적인 Java Reflection을 이용하는 방법
        Field generator = AccountRepository.class.getDeclaredField("ID_GENERATOR");
        generator.setAccessible(true);
        generator.set(null, new AtomicLong(0));

        // Spring framework에서 제공하는 ReflectionTestUtils를 사용하는 방법.
        ReflectionTestUtils.setField(
            AccountRepository.class, "ID_GENERATOR", new AtomicLong(0)
        );
    }

    @Test
    @Order(1)
    void _1Test() {
        log.info("static-atomic_1: {}", accountRepository.genId());
        log.info("junit-atomic_1: {}", atomicLong.incrementAndGet());
    }
    @Test
    @Order(2)
    void _2Test() {
        log.info("static-atomic_2: {}", accountRepository.genId());
        log.info("junit-atomic_2: {}", atomicLong.incrementAndGet());
    }
    @Test
    @Order(3)
    void _3Test() {
        log.info("static-atomic_3: {}", accountRepository.genId());
        log.info("junit-atomic_3: {}", atomicLong.incrementAndGet());
    }
}

결과에서 원하는 대로 테스트가 진행됨을 알 수 있다.

static-atomic_1: 1
junit-atomic_1: 1

static-atomic_2: 1
junit-atomic_2: 1

static-atomic_2: 1
junit-atomic_2: 1

3. final 필드의 경우에는?

final 필드를 리플렉션으로 변경하려 하는 경우 다음과 같은 예외를 마주할 수 있다.

java.lang.IllegalAccessException: Can not set static final java.util.concurrent.atomic.AtomicLong field com.flowerfulfort.testground.repository.AccountRepository.ID_GENERATOR to java.util.concurrent.atomic.AtomicLong

	at java.base/jdk.internal.reflect.FieldAccessorImpl.throwFinalFieldIllegalAccessException(FieldAccessorImpl.java:137)
	at java.base/jdk.internal.reflect.FieldAccessorImpl.throwFinalFieldIllegalAccessException(FieldAccessorImpl.java:141)
	at java.base/jdk.internal.reflect.MethodHandleObjectFieldAccessorImpl.set(MethodHandleObjectFieldAccessorImpl.java:103)
	at java.base/java.lang.reflect.Field.set(Field.java:836)
	at com.flowerfulfort.testground.test.GroundTest.injectIdGenerator(GroundTest.java:36)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

static final 필드는 리플렉션으로 변경할 수 없다. JDK 8 까지는 다음과 같은 방법으로 Field 참조에서 final modifier 자체를 지워 값을 주입할 수 있었다.

Field generator = AccountRepository.class.getDeclaredField("ID_GENERATOR");
generator.setAccessible(true);
generator.setInt(generator, generator.getModifiers() & ~Modifier.FINAL);
generator.set(null, new AtomicLong(0));

그러나 레거시 프로젝트가 아닌 이상 적어도 JDK 12 이상을 사용할 것이기에 static final 필드를 리플렉션으로 변경하려는 시도는 가능하지 않을 것이다.