본문으로 건너뛰기
버전: 1.20.x

게임 테스트

게임 테스트는 게임 속에서 단위 테스트를 수행할 수 있도록 해줍니다. 이 시스템은 확장하기 쉽고 한 번에 여러 테스트를 효율적으로 수행할 수 있습니다. 이 프레임워크는 여러 게임 속 객체의 상호 작용을 테스트할 때 유용합니다.

게임 테스트 작성하기

게임 테스트는 일반적으로 다음 세 단계로 이루어지는데:

  1. 게임 테스트를 수행할 장소를 불러옴. 이때 구조물 블록으로 생성한 .nbt 파일 등을 사용함.
  2. 해당 장소에서 테스트 로직 수행.
  3. 테스트 결과 확인. 만약 테스트에 실패했다면 해당 장소에 인접한 독서대에 결과를 기록함.

다시 말해서, 게임 테스트를 작성하기 위해선 테스트를 수행할 환경이 조성되어 있는 장소가 필요합니다. 구조물 블록을 활용하면 .nbt 파일을 생성하실 수 있습니다.

테스트 로직 작성하기

각 테스트 로직은 아래와 같이 Consumer<GameTestHelper>로 표현될 수 있는 메서드 입니다. 메서드를 게임 테스트 로직으로 사용하시려면 무조건 GameTest 어노테이션이 있어야 합니다:

public class ExampleGameTests {
@GameTest
public static void exampleTest(GameTestHelper helper) {
// 테스트 수행
}
}

@GameTest 어노테이션은 테스트 수행 방식을 설정할 수도 있습니다.

// In some class
@GameTest(
setupTicks = 20L, // 테스트를 준비하는 동안 20틱이 걸림
required = false // 테스트 실패 시 기록은 하나 전체 테스트 결과에 영향을 끼치지 않음
)
public static void exampleConfiguredTest(GameTestHelper helper) {
// 테스트 수행
}

상대 좌표

GameTestHelper는 주어진 상대 좌표를 구조체 블록의 위치를 활용해서 절대 좌표로 이동시키는 메서드 GameTestHelper#absolutePosGameTestHelper#relativePos를 제공합니다.

상대 좌표는 /test 명령어를 활용해 확인하실 수 있습니다, 먼저 상대 좌표를 확인하고 싶은 곳으로 이동한 다음 /test pos를 사용하시면 됩니다. 200블록 이내의 가장 가까운 구조체 블록을 기준 삼아 현재 플레이어의 상대 좌표를 찾아 Java 코드 형태로 출력해 줍니다.

만약 출력되는 Java 코드의 변수 이름까지 지정하고 싶으시다면 아래와 같이 변수 이름까지 적으시면 됩니다:

/test pos <var> # 출력: 'final BlockPos <var> = new BlockPos(...);'

테스트 통과 시

게임 테스트 로직은 테스트를 성공적으로 수행하면 무조건 해당 테스트를 통과했다고 표시해야 합니다, 만약 테스트가 (GameTest#timeoutTicks로 정의할 수 있는) 제한 시간이 끝나기 전에 통과했다고 표시되지 않는다면 자동으로 실패한 것으로 간주합니다.

GameTestHelper에선 이를 위해 여러 메서드들을 제공합니다, 이중 언제 무엇을 사용해야 하는지 숙지하도록 하세요.

메서드 이름설명
#succeed테스트 통과 표시.
#succeedIf주어진 Runnable을 실행하고, 만약 예외 GameTestAssertException이 발생하지 않았다면 성공한 것으로 간주. 또한 테스트는 첫 번째 틱에서 오직 한 번만 수행함.
#succeedWhen주어진 Runnable을 실행하고, 만약 예외 GameTestAssertException이 제한 시간이 끝나기 전까지 한 번도 발생하지 않았다면 성공한 것으로 간주.
#succeedOnTickWhen주어진 Runnable을 지정된 틱에 실행하고, 만약 예외 GameTestAssertException이 발생하지 않았다면 성공한 것으로 간주. 만약 주어진 Runnable이 지정된 틱 말고 다른 시간에 성공했다면 실패한 것으로 간주.
주의

게임 테스트는 성공할 때까지 매 틱마다 수행됩니다. 만약 일정 시간을 기다려야 하는 테스트가 있다면, 그전에는 무조건 테스트가 실패하여야만 합니다.

작업 스케줄

모든 작업이 테스트가 시작하자마자 수행되는 것은 아닙니다, 다음 메서드들을 활용해 작업을 언제 수행할지 설정하실 수 있습니다:

메서드 이름설명
#runAtTickTime주어진 작업을 지정된 틱에 수행함.
#runAfterDelay주어진 작업을 x 틱만큼 기다렸다가 수행함.
#onEachTick주어진 작업을 매 틱마다 수행함.

Assertions

게임 테스트를 수행하면서, 언제든지 특정 조건의 참/거짓 여부를 확인할 수 있습니다. GameTestHelper에는 이를 위해, 특정 조건이 만족하지 않을 경우 예외 GameTestAssertException을 발생시키는 여러 메서드가 있습니다.

테스트 로직 생성하기

만약 테스트 로직을 동적으로 생성하셔야 한다면, 다음과 같이 받는 인자는 없고 Collection<TestFunction>을 반환하는 테스트 로직 생성기를 정의하실 수 있습니다. 해당 생성기는 무조건 @GameTestGenerator가 있어야 합니다:

public class ExampleGameTests {
@GameTestGenerator
public static Collection<TestFunction> exampleTests() {
// Collection<TestFunction>만들어서 반환하기
}
}

TestFunction

TestFunction은 테스트의 수행 방법과 여러 설정, 제한 시간, 테스트 로직과 같이 테스트 수행에 있어 중요한 정보들을 종합적으로 담고 있는 클래스입니다.

@GameTest가 붙은 모든 메서드들은 GameTestRegistry#turnMethodIntoTestFunction를 통해 TestFunction으로 변환됩니다. 필요하시다면 이 메서드를 직접 호출하셔도 됩니다.

Batching

테스트를 등록 순서 여러 테스트를 하나의 배치로 묶어 실행할 수도 있습니다. 여러 테스트는 GameTest#batch에 동일한 문자열을 전달하는 것으로 하나의 배치로 묶을 수 있습니다.

배치 기능은 자체적으로 유용하진 않지만, 테스트 환경 조성 및 정리할 때 유용하게 쓰일 수 있습니다. @BeforeBatch를 테스트 환경을 조성할 때 사용할 수 있고, @AfterBatch로 정리할 수 있습니다.

위 두 어노테이션은 Consumer<ServerLevel>로 표현되는 메서드에 붙일 수 있습니다, 다시 말해서 이 어노테이션은 ServerLevel을 인자로 받고 반환값이 없는 함수에 사용해야 합니다:

public class ExampleGameTests {
@BeforeBatch(batch = "firstBatch")
public static void beforeTest(ServerLevel level) {
// 테스트 환경 조성
}

@GameTest(batch = "firstBatch")
public static void exampleTest2(GameTestHelper helper) {
// 테스트 수행
}
}

게임 테스트 등록하기

게임 테스트를 수행하기 위해선 먼저 등록되어야만 합니다, @GameTestHolder로 자동으로 등록할 수도 있고, RegisterGameTestsEvent로 직접 등록하실 수도 있습니다. 무엇을 사용하시든 간에, @GameTest, @GameTestGenerator, @BeforeBatch, 또는 @AfterBatch가 등록되는 메서드에 있어야 합니다.

GameTestHolder

@GameTestHolder는 특정 타입(클래스, 인터페이스, 열거형, 레코드) 안의 모든 테스트 메서드들을 자동으로 등록해 주는 어노테이션입니다. 해당 어노테이션은 #value라는 메서드가 있는데, 여러 사용 방법이 있지만 일단 지금은 모드 아이디를 사용하도록 하겠습니다.

@GameTestHolder(MODID)
public class ExampleGameTests {
// ...
}

RegisterGameTestsEvent

RegisterGameTestsEvent또한 클래스, 또는 메서드를 #register로 등록할 수 있습니다. 해당 이벤트는 모드 버스에 방송됩니다. 여기서 등록하는 모든 테스트 로직은 GameTest#templateNamespace로 무조건 모드 아이디를 지정해야 합니다.

// 이벤트 핸들러
public void registerTests(RegisterGameTestsEvent event) {
event.register(ExampleGameTests.class);
}

// 게임 테스트 클래스
@GameTest(templateNamespace = MODID)
public static void exampleTest3(GameTestHelper helper) {
// Perform setup
}
노트

GameTestHolder#valueGameTest#templateNamespace에 전달되는 문자열은 모드 아이디 말고 다른 것을 사용하셔도 됩니다만, 빌드스크립트의 기본 설정은 모드 아이디와 일치하는 네임 스페이스에 포함된 게임 테스트만 실행하기 때문에 이를 수정하셔야 합니다.

구조물 템플릿

이전에 각 게임 테스트는 특정한 장소에서 실행해야 한다고 했습니다. 이러한 장소는 구조물 블록으로 만들고 저장할 수 있는데, 테스트를 실행할 차원, 블록과 엔티티와 같은 초기 데이터 등을 지정할 수 있습니다. 각 구조물은 data/<namespace>/structures.nbt 확장자와 함께 저장되어야 합니다.

A structure template can be created and saved using a structure block.

게임 테스트를 실행할 구조물 파일의 위치는 다음 세 가지 기준을 바탕으로 정의됩니다:

  • 구조물의 네임 스페이스가 정의되어 있는가?
  • 테스트를 실행하는 클래스의 이름이 구조물 파일의 접두사로 와야 하는가?
  • 구조물 파일의 이름이 정의되어 있는가?

The namespace of the template is determined by GameTest#templateNamespace, then GameTestHolder#value if not specified, then minecraft if neither is specified.

The simple class name is not prepended to the name of the template if the @PrefixGameTestTemplate is applied to a class or method with the test annotations and set to false. Otherwise, the simple class name is made lowercase and prepended and followed by a dot before the template name.

The name of the template is determined by GameTest#template. If not specified, then the lowercase name of the method is used instead.

// 구조물 파일의 네임 스페이스는 MODID가 됨
@GameTestHolder(MODID)
public class ExampleGameTests {

// 구조물 파일 앞에 클래스 이름을 붙임, 구조물 파일 이름은 따로 지정하지 않음
// 구조물 파일 위치: 'modid:examplegametests.exampletest'
@GameTest
public static void exampleTest(GameTestHelper helper) { /*...*/ }

// 구조물 파일 앞에 클래스 이름을 붙이지 않음, 구조물 파일 이름은 따로 지정하지 않음
// 구조물 파일 위치: 'modid:exampletest2'
@PrefixGameTestTemplate(false)
@GameTest
public static void exampleTest2(GameTestHelper helper) { /*...*/ }

// 구조물 파일 앞에 클래스 이름을 붙임, 구조물 파일 이름 지정
// 구조물 파일 위치: 'modid:examplegametests.test_template'
@GameTest(template = "test_template")
public static void exampleTest3(GameTestHelper helper) { /*...*/ }

// 구조물 파일 앞에 클래스 이름을 붙이지 않음, 구조물 파일 이름 지정
// 구조물 파일 위치: 'modid:test_template2'
@PrefixGameTestTemplate(false)
@GameTest(template = "test_template2")
public static void exampleTest4(GameTestHelper helper) { /*...*/ }
}

게임 테스트 수행하기

게임 테스트는 /test 명령어로 실행할 수 있습니다. 이 명령어는 설정할 방법이 많지만, 대개 아래 5가지 부 명령어 중 하나를 사용합니다:

부 명령어설명
run지정된 테스트 실행: run <test_name>.
runall활성화된 모든 테스트 실행.
runclosest플레이어 기준 15칸 안에 있는 가장 가까운 테스트 실행.
runthese플레이어 기준 200칸 안에 있는 모든 테스트 실행.
runfailed방금 실패한 모든 테스트 다시 실행.
노트

위 부 명령어들은 테스트 명령 뒤에 따라붙습니다: /test <subcommand>.

빌드스크립트 설정

게임 세트는 빌드 스크립트(build.gradle 파일)에서도 설정하실 수 있습니다.

다른 네임 스페이스 활성화하기

If the buildscript was setup as recommended, then only Game Tests under the current mod id would be enabled. To enable other namespaces to load Game Tests from, a run configuration must set the property neoforge.enabledGameTestNamespaces to a string specifying each namespace separated by a comma. If the property is empty or not set, then all namespaces will be loaded.

// 빌드 스크립트 설정
property 'neoforge.enabledGameTestNamespaces', 'modid1,modid2,modid3'
주의

이때 띄어쓰기는 사용하지 마세요.

게임 테스트 실행 구성

게임 테스트 서버는 빌드 스크립트가 자동으로 서버를 실행하고 게임 테스트를 수행하도록 하는 실행 구성입니다. 이때 실행된 서버는 테스트 수행 이후, GameTest#requiredtrue인 모든 실패한 테스트의 개수를 프로세스 종료값으로 반환합니다. 모든 실패한 테스트는 출력 로그에 기록됩니다. 게임 테스트 서버는 gradlew runGameTestServer를 사용해 실행하실 수 있습니다.

NeoGradle 사용시 유의하실 점
주의

Gradle은 수행하던 작업이 시스템의 의해 종료되면, Gradle 데몬도 종료됩니다, 이로 인해 작업이 실패한 것으로 간주하게 됩니다. NeoGradle은 하위 프로젝트까지 실행되는 것을 막기 위해 강제로 데몬을 종료시키는데 이로 인해 테스트가 언제나 실패로 보고됩니다.

이는 #setForceExit로 강제 종료를 비활성화해 해결할 수 있습니다:

// 게임 테스트 서버 실행 구성
gameTestServer {
// ...
setForceExit false
}

다른 실행 구성에서 게임 테스트 사용하기

기본적으로, client, server, gameTestServer만 게임 테스트 기능이 활성화 되어있습니다. 만약 다른 실행 구성에서도 게임 테스트를 사용하시고 싶으시다면 neoforge.enableGameTest 속성값을 true로 설정하셔야만 합니다.

// 빌드 스크립트
property 'neoforge.enableGameTest', 'true'