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

Unity TestRunner批量运行测试失败,单独运行正常问题求助

问题:Unity测试用例单独运行正常,批量运行失败

使用Unity 2020.3.11f1版本,SpawnMultipleBlocks测试用例单独运行时可正常通过,但通过TestRunner点击“Run All”批量运行时始终失败。该测试负责在场景中按随机间隔生成方块。

报错信息

SpawnMultipleBlocks (0.020s)
---
Unhandled log message: '[Exception] NullReferenceException: Object reference not set to an instance of an object'. Use UnityEngine.TestTools.LogAssert.Expect
---
BlockSpawner+<SpawnedBlocks>d__14.MoveNext () (at Assets/Scripts/BlockSpawner.cs:69)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <127e81e1cb3441cc97d26b1910daae77>:0)
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
BlockSpawner:Start() (at Assets/Scripts/BlockSpawner.cs:24)
---
topPos is null
Generate at top : 0
NullReferenceException: Object reference not set to an instance of an object
topPos is null
Generate at top : 0
NullReferenceException: Object reference not set to an instance of an object
topPos is null
Generate at top : 0
NullReferenceException: Object reference not set to an instance of an object

测试类代码(BlockSpawnerTests.cs)

using System.Collections;
using NUnit.Framework;
using UnityEditor;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TestTools;

namespace Tests
{

    public class BlockSpawnerTests : MonoBehaviour
    {
        private static string resourceDirPath = "Tests/Block";

        [Test]
        public void SpawnBlock()
        {
            // Arrange
            var spawner = new GameObject().AddComponent<BlockSpawner>();
            spawner.setBlockReference(Resources.Load<GameObject>(resourceDirPath));

            // Act
            spawner.SpawnBlock(Vector3.zero);

            // Assert
            Assert.IsNotNull(spawner.getSpawnedBlock());
        }

        [Test]
        public void SpawnBlockAtPosition()
        {
            // Arrange
            var spawner = new GameObject().AddComponent<BlockSpawner>();
            spawner.setBlockReference(Resources.Load<GameObject>("Tests/Block"));

            // Act
            spawner.SpawnBlock(Vector3.one);

            // Assert
            Assert.AreEqual(spawner.getSpawnedBlock().transform.position, Vector3.one);
        }

        [Test]
        public void SpawnBlockSpeed()
        {
            // Arrange
            var spawner = new GameObject().AddComponent<BlockSpawner>();
            spawner.setBlockReference(Resources.Load<GameObject>("Tests/Block"));

            // Act
            spawner.SpawnBlock(Vector3.zero);

            // Assert
            Assert.IsTrue(spawner.getSpawnedBlock().GetComponent<Block>().GetSpeed() >= -10
                && spawner.getSpawnedBlock().GetComponent<Block>().GetSpeed() <= -4);
        }

        [Test]
        public void WhenBlockComponentIsNullShouldLog()
        {
            // Arrange
            var spawner = new GameObject().AddComponent<BlockSpawner>();
            spawner.setBlockReference(new GameObject("EmptyObject"));

            // Act
            spawner.SpawnBlock(Vector3.zero);

            // Assert
            LogAssert.Expect(LogType.Log, "Spawned block doesn't have a Block component");
            LogAssert.NoUnexpectedReceived();
            Destroy(spawner);
        }

        [UnityTest]
        public IEnumerator SpawnMultipleBlocks()
        {
            Debug.Log("block spawner create");
            var spawner = new GameObject();
            var blockSpawner = spawner.AddComponent<BlockSpawner>();
            Debug.Log("set top bottom middle");
            blockSpawner.SetTopPos(new GameObject().transform);
            blockSpawner.SetBottomPos(new GameObject().transform);
            blockSpawner.SetMiddlePos(new GameObject().transform);

            blockSpawner.SetWaitForSpawnLow(1);
            blockSpawner.SetWaitForSpawnHigh(2);
            blockSpawner.setBlockReference(Resources.Load<GameObject>("Tests/Block"));
            yield return new WaitForSeconds(1);
            blockSpawner.Start();

            // wait for at least 1 block to be spawned
            yield return new WaitForSeconds(1);
            Assert.GreaterOrEqual(blockSpawner.GetSpawnedBlocks().Count, 1);

            // wait for at least 2 block to be spawned
            yield return new WaitForSeconds(1);
            Assert.GreaterOrEqual(blockSpawner.GetSpawnedBlocks().Count, 2);
        }
    }
}
问题原因与解决方案

核心原因

  1. 临时对象被提前回收:批量运行时,Unity测试的场景清理机制会自动回收未被持有引用的临时GameObject,SpawnMultipleBlocks中创建的top/bottom/middle定位对象没有被保存引用,导致topPos等变量变为null,触发空引用异常。单独运行时测试环境的生命周期管理更宽松,临时对象能保持到测试结束。
  2. 测试类不必要的继承:测试类继承MonoBehaviour是多余的,NUnit测试类不需要该继承,反而可能干扰测试的生命周期管理逻辑。
  3. 测试间状态污染:没有统一的测试清理逻辑,前序测试生成的对象可能影响后续测试的运行环境。

修复步骤

  1. 移除测试类的MonoBehaviour继承
    修改测试类定义,去掉不必要的继承:

    public class BlockSpawnerTests // 移除: MonoBehaviour
    
  2. 保留临时定位对象的引用
    在SpawnMultipleBlocks中保存定位对象的引用,防止被自动回收,并在测试结束后手动清理:

    [UnityTest]
    public IEnumerator SpawnMultipleBlocks()
    {
        Debug.Log("block spawner create");
        var spawner = new GameObject();
        var blockSpawner = spawner.AddComponent<BlockSpawner>();
        Debug.Log("set top bottom middle");
        
        // 保存临时对象引用,避免被自动回收
        var topObj = new GameObject();
        var bottomObj = new GameObject();
        var middleObj = new GameObject();
        
        blockSpawner.SetTopPos(topObj.transform);
        blockSpawner.SetBottomPos(bottomObj.transform);
        blockSpawner.SetMiddlePos(middleObj.transform);
    
        blockSpawner.SetWaitForSpawnLow(1);
        blockSpawner.SetWaitForSpawnHigh(2);
        blockSpawner.setBlockReference(Resources.Load<GameObject>("Tests/Block"));
        yield return new WaitForSeconds(1);
        blockSpawner.Start();
    
        // 等待至少1个方块生成
        yield return new WaitForSeconds(1);
        Assert.GreaterOrEqual(blockSpawner.GetSpawnedBlocks().Count, 1);
    
        // 等待至少2个方块生成
        yield return new WaitForSeconds(1);
        Assert.GreaterOrEqual(blockSpawner.GetSpawnedBlocks().Count, 2);
    
        // 测试结束后手动清理所有对象
        Destroy(spawner);
        Destroy(topObj);
        Destroy(bottomObj);
        Destroy(middleObj);
    }
    
  3. 添加统一测试清理逻辑
    新增TearDown方法,确保每个测试结束后清理场景中的测试对象,避免状态污染:

    [TearDown]
    public void TearDown()
    {
        // 销毁所有测试生成的BlockSpawner
        foreach (var spawner in Object.FindObjectsOfType<BlockSpawner>())
        {
            Destroy(spawner.gameObject);
        }
        // 销毁所有生成的方块
        foreach (var block in Object.FindObjectsOfType<Block>())
        {
            Destroy(block.gameObject);
        }
    }
    
  4. 优化BlockSpawner的启动逻辑
    若BlockSpawner的Start方法是默认MonoBehaviour生命周期方法,建议将生成逻辑移到单独的公共方法(如StartSpawning),避免测试初始化完成前自动执行导致空引用:

    // 在BlockSpawner中新增方法
    public void StartSpawning()
    {
        StartCoroutine(SpawnedBlocks());
    }
    // 测试中调用该方法代替Start()
    blockSpawner.StartSpawning();
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 00:42:40