GameTest 프레임워크 — 인게임 자동화 테스트
NeoForge 26의 GameTest 프레임워크로 실제 Minecraft 서버 인스턴스에서 블록·아이템 동작을 자동 검증하는 방법을 다룹니다.
GameTest란?
GameTest는 NeoForge가 제공하는 인게임 자동화 테스트 프레임워크입니다. 실제 Minecraft 서버 인스턴스를 띄운 뒤, 블록을 설치하고 동작을 확인하고 결과를 단언하는 과정을 코드로 기술합니다.
일반 JUnit 테스트와 근본적으로 다릅니다.
⚠️ JUnit과 GameTest 혼동 금지
JUnit은 Minecraft 환경(Level, BlockEntity 등) 없이 순수 Java 단위 테스트를 실행합니다. GameTest는 실제 서버 인스턴스를 사용하므로 World, Block, Entity 모두 접근 가능합니다. 모드 동작 검증은 항상 GameTest를 사용하세요.
GameTest가 필요한 상황은 명확합니다. 블록이 올바른 드롭을 내놓는지, 레드스톤 신호에 반응하는지, BlockEntity가 아이템을 제대로 처리하는지 같은 동작은 JUnit으로 검증할 수 없습니다. 실제 게임 환경이 있어야 합니다.
프로젝트 설정
examplemod-template-26.1.2에는 GameTest 의존성이 이미 포함되어 있습니다. build.gradle에서 확인할 수 있습니다.
// examplemod-template-26.1.2/build.gradle
dependencies {
// NeoForge가 GameTest API를 포함합니다 — 별도 의존성 불필요
implementation "net.neoforged:neoforge:${neo_version}"
}테스트 클래스는 src/test/java/ 아래에 두는 것이 관례지만, GameTest는 src/main/java/ 안에 두어도 동작합니다. 이 코스에서는 src/main/java/com/example/examplemod/tests/ 경로를 사용합니다.
@GameTestHolder + @GameTest 패턴
GameTest는 두 어노테이션의 조합으로 동작합니다.
// src/main/java/com/example/examplemod/tests/FirstBlockTests.java
package com.example.examplemod.tests;
import com.example.examplemod.ExampleMod;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.neoforged.neoforge.gametest.GameTestHolder;
@GameTestHolder("examplemod")
public class FirstBlockTests {
@GameTest(template = "examplemod:first_block_test", batch = "first_block")
public static void test_first_block_break(GameTestHelper helper) {
BlockPos pos = new BlockPos(1, 2, 1);
// 블록 설치
helper.setBlock(pos, ExampleMod.FIRST_BLOCK.get());
// 설치 확인
helper.assertBlockPresent(ExampleMod.FIRST_BLOCK.get(), pos);
// 블록 파괴
helper.destroyBlock(pos);
// 파괴 후 없어졌는지 확인
helper.succeedWhen(() ->
helper.assertBlockNotPresent(ExampleMod.FIRST_BLOCK.get(), pos)
);
}
}각 어노테이션의 역할을 정리합니다.
| 어노테이션 | 위치 | 역할 |
|---|---|---|
@GameTestHolder("examplemod") | 클래스 | 이 클래스의 테스트들이 속할 모드 ID 지정 |
@GameTest(template = "...", batch = "...") | 메서드 | 테스트 하나를 정의. template은 구조 파일 경로, batch는 병렬 실행 그룹 |
메서드 이름이 테스트 이름이 됩니다. test_first_block_break처럼 동작을 명확히 서술하는 이름을 쓰세요.
GameTestHelper API
GameTestHelper는 테스트 안에서 게임 세계를 조작하고 단언하는 모든 도구를 제공합니다.
블록 조작
// 블록 설치 (BlockState 직접 지정)
helper.setBlock(pos, ExampleMod.FIRST_BLOCK.get());
// 블록 파괴 (드롭 없이)
helper.destroyBlock(pos);
// 레드스톤 펄스 (pos에 2틱 동안 신호)
helper.pulseRedstone(pos, 2);단언 (Assertion)
// 해당 위치에 블록이 있어야 함
helper.assertBlockPresent(ExampleMod.FIRST_BLOCK.get(), pos);
// 해당 위치에 블록이 없어야 함
helper.assertBlockNotPresent(ExampleMod.FIRST_BLOCK.get(), pos);
// 특정 블록 상태 단언
helper.assertBlockState(pos, state ->
state.is(ExampleMod.FIRST_BLOCK.get()), () -> "블록 상태 불일치"
);테스트 종료
// 조건이 충족되면 통과 (비동기 — 조건이 될 때까지 매 틱 재시도)
helper.succeedWhen(() -> helper.assertBlockNotPresent(ExampleMod.FIRST_BLOCK.get(), pos));
// 즉시 통과
helper.succeed();
// 명시적 실패
helper.fail("예상한 블록이 없습니다");succeedWhen은 비동기입니다. 레드스톤 신호나 블록 업데이트처럼 다음 틱에 결과가 나오는 동작을 검증할 때 씁니다. 즉시 결과를 알 수 있는 경우라면 단언 후 succeed()를 호출해도 됩니다.
테스트 템플릿 구조 파일
@GameTest의 template 속성은 테스트가 실행될 공간을 정의하는 NBT 구조 파일을 가리킵니다.
data/
└── examplemod/
└── structures/
└── first_block_test.nbt
구조 파일은 Minecraft 내 /structure save 명령으로 만들거나, 빈 공간(공기 블록만 있는 구조)을 직접 생성해도 됩니다. 테스트 코드에서 setBlock으로 블록을 직접 배치하므로, 템플릿은 충분한 빈 공간만 있으면 됩니다.
최소 크기는 3×3×3입니다. BlockPos(1, 2, 1)처럼 인덱스 1부터 시작하는 이유가 여기 있습니다. 구조 경계 바깥에 블록을 두면 테스트가 실패합니다.
빈 구조 파일을 만드는 가장 빠른 방법은 게임 내에서 직접 만드는 것입니다.
# 게임 내 명령어 (크리에이티브 모드)
/structure save examplemod:first_block_test ~ ~ ~ ~2 ~2 ~2 false
빌드 및 실행
cd examplemod-template-26.1.2
.\gradlew.bat runGameTestServer실행하면 Minecraft 서버가 헤드리스(창 없이) 시작되고, 등록된 모든 GameTest를 순서대로 실행합니다. 콘솔에 결과가 출력됩니다.
[GameTest] Tests passed: 3, failed: 0
실패한 테스트가 있으면 어떤 단언이 실패했는지 스택 트레이스와 함께 출력됩니다.
[GameTest] FAILED: examplemod:test_first_block_break
java.lang.AssertionError: Expected block at (1, 2, 1) to be absent
실전 예제: 레드스톤 반응 테스트
블록이 레드스톤 신호에 반응하는지 검증하는 테스트입니다.
@GameTest(template = "examplemod:redstone_test", batch = "redstone")
public static void test_redstone_activation(GameTestHelper helper) {
BlockPos blockPos = new BlockPos(1, 2, 1);
BlockPos leverPos = new BlockPos(2, 2, 1);
// 블록과 레버 설치
helper.setBlock(blockPos, ExampleMod.FIRST_BLOCK.get());
helper.setBlock(leverPos, Blocks.LEVER);
// 레드스톤 펄스 (2틱)
helper.pulseRedstone(leverPos, 2);
// 다음 틱에 블록 상태 변화 확인
helper.succeedWhen(() ->
helper.assertBlockState(
blockPos,
state -> state.getValue(ExampleMod.POWERED),
() -> "레드스톤 신호 후 POWERED 상태여야 합니다"
)
);
}자주 하는 실수
템플릿 파일 누락: template 속성에 지정한 경로에 .nbt 파일이 없으면 테스트가 시작도 못 하고 실패합니다. 구조 파일을 먼저 만들고 테스트를 작성하세요.
경계 밖 좌표: BlockPos(0, 0, 0)은 구조 경계 바깥입니다. 항상 (1, 1, 1) 이상의 좌표를 사용하세요.
동기 단언 남용: 레드스톤이나 블록 업데이트 결과를 setBlock 직후 바로 단언하면 틱이 처리되기 전이라 실패합니다. 비동기 결과는 succeedWhen을 쓰세요.
모든 챕터에 테스트 추가: GameTest는 핵심 동작 검증용입니다. 모든 블록·아이템에 테스트를 붙일 필요는 없습니다. 복잡한 상태 전이나 레드스톤 연동처럼 회귀 위험이 있는 부분에 집중하세요.
정리
GameTest는 실제 Minecraft 환경에서 모드 동작을 자동으로 검증합니다. 핵심 흐름은 단순합니다.
@GameTestHolder로 클래스를 등록합니다.@GameTest로 각 테스트 메서드를 정의합니다.GameTestHelper로 블록을 배치하고 단언합니다.runGameTestServer로 실행해 결과를 확인합니다.
복잡한 블록 동작을 만들수록 GameTest의 가치가 커집니다. 리팩터링 후 회귀를 잡아내는 안전망이 됩니다.