You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于libgdx/ECS的JUnit4游戏测试:单测通过,批量运行失败

问题分析与解决方案

核心问题:测试间的状态污染

单独测试通过但批量失败,本质是前一个测试的状态残留影响了后续测试——你的清理逻辑只覆盖了实体和Body,但还有很多隐式状态没处理。


1. 静态单例的状态残留

你用的BodyFactory.getInstance、UnitFactory.getInstance都是单例,@BeforeClass只初始化一次,所有测试共享同一个实例。如果单例内部有缓存、计数器或状态变量,前一次测试的修改会留在单例里,直接干扰下一次测试。

解决方法:

  • 放弃静态单例,改为每次测试创建新的工厂实例:
    @Before
    public void setupPerTest() {
        world = new World(new Vector2(0, 0), true);
        engine = new PooledEngine();
        testUtility = new TestUtility(engine, world);
        // 每次测试创建全新的工厂实例,彻底隔离状态
        bodyFactory = new BodyFactory(world);
        unitFactory = new UnitFactory(bodyFactory, engine);
        battleMap = new BattleMap(sceneryFactory, engine, 123, 0);
        // 重新添加所有系统(确保系统也是全新实例)
        engine.addSystem(new FormationSystem());
        engine.addSystem(new PhysicsSystem(world));
        // ... 其他系统初始化
    }
    
  • 如果必须保留单例,给单例添加reset()方法,在@Before里调用重置内部状态:
    public class BodyFactory {
        private static BodyFactory instance;
        private SomeCache internalCache;
    
        public static void resetInstance() {
            instance = null; // 或者直接清空内部缓存/状态
        }
    
        // ... 其他单例逻辑
    }
    

2. ECS系统的内部状态未重置

你只删除了实体,但系统本身可能保存了前一次测试的状态:比如FightSystem可能记录了未完成的战斗对,TurnTowardsSystem可能残留了未完成的转向任务,这些状态不会随实体删除自动清空。

解决方法:

  • 给每个ECS系统添加reset()方法,手动清空内部状态:
    public class FightSystem extends IteratingSystem {
        private List<Entity> ongoingFights = new ArrayList<>();
    
        public void reset() {
            ongoingFights.clear();
            // 重置其他内部计时器、标志位等
        }
    
        // ... 系统逻辑
    }
    
  • 在@After里遍历所有系统,调用reset():
    @After
    public void cleanUp() throws Exception {
        // ... 现有清理逻辑
        // 重置所有系统的内部状态
        for (System system : engine.getSystems()) {
            if (system instanceof ResettableSystem) { // 定义统一的ResettableSystem接口
                ((ResettableSystem) system).reset();
            }
        }
    }
    

3. Box2D世界的隐式残留

除了Body和Fixture,Box2D世界还有容易被忽略的残留状态:

  • 关节(Joint):如果测试中创建了关节,你的清理逻辑可能没处理,导致world里残留关节实例。
  • 未清空的力:world中可能残留上一次模拟的力,影响下一次测试的物理表现。
  • 接触监听:如果系统注册了碰撞监听,可能残留前一次测试的碰撞数据。

解决方法:

  • 补充关节清理和力清空逻辑:
    @After
    public void cleanUp() throws Exception {
        // 先销毁所有关节
        Array<Joint> joints = new Array<>();
        world.getJoints(joints);
        for (Joint joint : joints) {
            world.destroyJoint(joint);
        }
        // ... 现有销毁Body的逻辑
        world.clearForces(); // 清空所有未处理的力
    }
    
  • 如果使用了接触监听,在@Before里重新创建监听实例,或在@After里重置监听状态。

4. PooledEngine的组件池未彻底清空

engine.clearPools()可能没覆盖所有自定义组件的池,或者组件的reset()方法没正确清空字段,导致复用组件时带有前一次测试的旧数据。

解决方法:

  • 确保所有自定义组件正确实现Poolable接口的reset()方法,清空所有字段:
    public class SteerableComponent implements Poolable {
        public Body body;
        public float angle;
    
        @Override
        public void reset() {
            body = null;
            angle = 0;
            // 清空所有其他字段
        }
    }
    
  • 在@After里直接删除所有实体(不限Family),避免遗漏:
    engine.removeAllEntities(); // 替代按Family删除的逻辑
    

5. 测试顺序依赖

JUnit默认按方法名字典序执行测试,批量执行的顺序可能和你单独测试的顺序不同,某个测试的残留状态刚好触发了另一个测试的错误。

临时排查方法:

  • 使用@FixMethodOrder固定测试顺序,快速定位是否是顺序问题:
    @RunWith(GdxTestRunner.class)
    @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    public class YourTestClass {
        // ... 测试方法
    }
    
  • 注意:这只是临时排查手段,核心还是要解决状态残留问题,不要让测试依赖执行顺序。

关于LibGDX/ECS的JUnit测试可行性

完全适合用JUnit测试,很多LibGDX游戏项目都用JUnit做单元测试,核心是确保每个测试都是独立的、无状态的——每个测试都从干净的环境开始,结束后完全清理所有状态。

内容的提问来源于stack exchange,提问作者Max

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 23:57:19