NUnit的使用

NUnit是C#中最著名的测试框架。编写好测试用例之后,在Window/General/TestRunner中可以看见测试用例。测试用例应该放在Editor文件夹下(因为测试case与Unity Editor相关,所以应该放在Editor目录下面)。 如果不把测试用例放在Editor文件夹下,也可以随意一个目录,然后添加以下内容:xxx.asmdef

{
    "name": "Tests",
    "optionalUnityReferences": [
        "TestAssemblies"
    ]
}

写一段代码:

using UnityEngine;
using System.Collections;
using NUnit.Framework;

[TestFixture]
public class HealthComponentTests
{
  //测试伤害之后,血的值是否比0大
  [Test]
  public void TakeDamage_BeAttacked_BiggerZero()
    {
      //Arrange 
      UnMonoHealthClass health = new UnMonoHealthClass();
      health.healthAmount = 50f;

      //Act
      health.TakeDamage(60f);

      //Assert
        Assert.GreaterOrEqual(health.healthAmount, 0);
    }
}

常见的NUnit属性如下,对应的有UnitySetUp、UnityTearDown等为Unity进行特殊定制的属性。

[SetUp]
[TearDown]  
[TestFixture]
[Test]
[TestCase]
[Category]
[Ignore]

在游戏中,UI、交互方面的测试比较难以实现,只能在逻辑部分进行一些单元测试。

单元测试有两类:Edit模式和play模式 在Edit模式下,是本地测试。 在Play模式下,是运行在Android系统。 如果创建的是Edit模式的测试,则应该放在Editor目录下面。 如果创建的是Play模式的测试,则可以放在任意一个目录下面,并且添加xxxx.asdef文件。这里面的关键点是optionalUnityReferences设置为TestAssemblies。只有这样才能够引用到一些测试库。 如果要测试的是另一个assembly,则应该添加references才能引用到另外一个单元。

{
  "name": "Tests",
  "references": [
    "XXXX.Platform"
  ],
  "optionalUnityReferences": [
    "TestAssemblies"
  ]
}

asmdef.json

asmdef.json是unity特有的程序集描述文件。许多工程下面都有xxxx.asmdef文件。

{
    "schema": "http://json-schema.org/draft-06/schema#",
    "title": "Unity Assembly Definition",
    "description": "Defines an assembly in the Unity compilation pipeline",
    "type": "object",
    "properties": {
        "name": {
            "description": "The name of the assembly being defined",
            "type": "string",
            "minLength": 1
        },
        "rootNamespace": {
            "description": "The root namespace of the assembly. Requires Unity 2020.2",
            "type": "string",
            "minLength": 1
        },
        "references": {
            "description": "A list of assembly names or assembly definition asset GUIDs to reference",
            "type": "array",
            "items": {
                "type": "string",
                "minLength": 1
            },
            "uniqueItems": true
        },
        "includePlatforms": {
            "description": "Platforms to target",
            "$ref": "#/definitions/platformValues"
        },
        "excludePlatforms": {
            "description": "Platforms that are explicitly not targeted",
            "$ref": "#/definitions/platformValues"
        },
        "allowUnsafeCode": {
            "description": "Allow unsafe code",
            "type": "boolean",
            "default": false
        },
        "autoReferenced": {
            "description": "When true, this assembly definition is automatically referenced by predefined assemblies (Assembly-CSharp, Assembly-CSharp-firstpass, etc.)",
            "type": "boolean",
            "default": true
        },
        "noEngineReferences": {
            "description": "When true, no references to UnityEngine or UnityEditor will be added to this assembly project",
            "type": "boolean",
            "default": false
        },
        "overrideReferences": {
            "description": "When true, any references to precompiled assembly assets are manually chosen. When false, all precompiled assembly assets are automatically referenced",
            "type": "boolean",
            "default": "false"
        },
        "precompiledReferences": {
            "description": "A list of precompiled assembly assets that will be referenced. Only valid when overrideReferences is true",
            "type": "array",
            "uniqueItems": true
        },
        "defineConstraints": {
            "description": "A list of the C# compiler define symbols that must evaluate to true in order for the assembly to be compiled or referenced. Absence of a symbol can be checked with a bang symbol (!DEFINE)",
            "type": "array",
            "items": {
                "type": "string"
            },
            "uniqueItems": true
        },
        "optionalUnityReferences": {
            "description": "Additional optional Unity features to reference. Not supported since 2019.3",
            "type": "array",
            "items": {
                "enum": [
                    "TestAssemblies"
                ]
            },
            "uniqueItems": true
        },
        "versionDefines": {
            "description": "A set of expressions that will define a symbol in the C# project if a package or module version matches the given expression",
            "type": "array",
            "uniqueItems": true,
            "items": {
                "type": "object",
                "properties": {
                    "name": {
                        "description": "The package or module that will provide the version to be checked in the expression",
                        "type": "string",
                        "minLength": 1
                    },
                    "expression": {
                        "description": "The semantic version range of the chosen package or module",
                        "type": "string"
                    },
                    "define": {
                        "description": "The name of the define that is added to the project file when expression evaluates to true",
                        "type": "string"
                    }
                },
                "required": [ "name", "expression", "define" ],
                "minLength": 1
            }
        }
    },
    "definitions": {
        "platformValues": {
            "type": "array",
            "uniqueItems": true,
            "items": {
                "enum": [
                    "Android",
                    "CloudRendering",
                    "Editor",
                    "GameCoreXboxOne",
                    "iOS",
                    "LinuxStandalone64",
                    "Lumin",
                    "macOSStandalone",
                    "PS4",
                    "PS5",
                    "Stadia",
                    "Switch",
                    "tvOS",
                    "WSA",
                    "WebGL",
                    "WindowsStandalone32",
                    "WindowsStandalone64",
                    "XboxOne",

                    "GameCoreScarlett",
                    "LinuxStandalone32",
                    "LinuxStandaloneUniversal",
                    "Nintendo3DS",
                    "PSMobile",
                    "PSVita",
                    "Tizen",
                    "WiiU"
                ]
            }
        }
    },
    "required": ["name"],
    "anyOf": [
        {
            "properties": {
                "includePlatforms": {
                    "minItems": 1
                },
                "excludePlatforms": {
                    "maxItems": 0
                }
            }
        },
        {
            "properties": {
                "includePlatforms": {
                    "maxItems": 0
                },
                "excludePlatforms": {
                    "minItems": 1
                }
            }
        },
        {
            "properties": {
                "includePlatforms": {
                    "maxItems": 0
                },
                "excludePlatforms": {
                    "maxItems": 0
                }
            }
        }
    ]
}

测试的asm

{
  "name": "Tests",
  "references": [
    "Pico.Platform"
  ],
  "optionalUnityReferences": [
    "TestAssemblies"
  ]
}

Mock和Stub有何区别?

Mock与Stub的区别:
Mock:关注行为验证。细粒度的测试,即代码的逻辑,多数情况下用于单元测试。
Stub:关注状态验证。粗粒度的测试,在某个依赖系统不存在或者还没实现或者难以测试的情况下使用,例如访问文件系统,数据库连接,远程协议等。

使用注解

断言抛出某种类型的异常

[ExpectedException(typeof(NegativeHealthException))]

设定运行时长

使用命令运行单元测试

Unity程序如果实现自动化测试,例如CI,在代码提交之后自动执行,那么就需要使用命令行运行测试,而不是使用TestRunner窗口。

Unity运行时支持以下参数:

runEditorTests
editorTestsResultFile
editorTestsFilter
editorTestsCategories
editorTestsVerboseLog

写法实例

OneTimeSetUp:全局执行一次 SetUp、TearDown:每个用例都要执行一次

using UnityEngine; //基于 Unity 引擎,必须引用
using NUnit.Framework; //引用NUnit测试框架

[TestFixture, Description("测试套")] //一个类对应一个测试套,通常一个测试特性对应一个测试套。
public class UnitTestDemoTest
{
    [OneTimeSetUp] //在执行该测试套时首先会执行该函数,在整个测试套中只执行一次。
    public void OneTimeSetUp()
    {
        Debug.Log("OneTimeSetUp"); 
    }

    [OneTimeTearDown] //在执行该测试套时最后会执行该函数,在整个测试套中只执行一次。
    public void OneTimeTearDown()
    {
        Debug.Log("OneTimeTearDown");
    }

    [SetUp]
    public void SetUp() //在执行每个用例之前都会执行一次该函数
    {
        Debug.Log("SetUp");
    }

    [TearDown] //在执行完每个用例之后都会执行一次该函数
    public void TearDown()
    {
        Debug.Log("TearDown");
    }

    [TestCase, Description("测试用例1")] //这个函数内部写测试用例
    public void TestCase1()
    {
        Debug.Log("TestCase1"); 
    }

    [TestCase, Description("测试用例2")] //这个函数内部写测试用例
    public void TestCase2()
    {
        Debug.Log("TestCase2");
    }
}

TestCase和Test的区别

TestCase可以接收参数。并且可以指定多组输入数据。

    // 多测试数据的GetTextLength测试
    [TestCase("", 0)]
    [TestCase("Hello World", 11)]
    public void GetTextLength_MultiTestData(string data, int exResult)
    {
        int result = GameUtils.GetTextLength(data);
        Assert.AreEqual(exResult, result);
    }