Unity的TestRunner生成的包名:com.UnityTestRunner.UnityTestRunner

WaitForSeconds

一切yield的东西都继承自:YieldInstruction。 UnityEngine.AsyncOperation (derived) UnityEngine.Coroutine (derived) UnityEngine.Coroutine.Coroutine() UnityEngine.WaitForEndOfFrame (derived) UnityEngine.WaitForFixedUpdate (derived) UnityEngine.WaitForFixedUpdate.WaitForFixedUpdate() UnityEngine.WaitForSeconds (derived) UnityEngine.WaitForSeconds.WaitForSeconds(float seconds)

Unity的自定义YieldInstruction

  public sealed class WaitUntil : CustomYieldInstruction
  {
    private Func<bool> m_Predicate;

    public override bool keepWaiting => !this.m_Predicate();

    public WaitUntil(Func<bool> predicate) => this.m_Predicate = predicate;
  }
  public sealed class WaitWhile : CustomYieldInstruction
  {
    private Func<bool> m_Predicate;

    public override bool keepWaiting => this.m_Predicate();

    public WaitWhile(Func<bool> predicate) => this.m_Predicate = predicate;
  }

一个小问题:下面两个语句等价吗?基本上是等价的,但是第一种写法在超时之后有可能陷入死循环,而第二种方法则不会陷入死循环。 如果改写为while (tasks > 0 && !timeout) yield return null;会更安全一些。

while (tasks > 0) yield return null;

yield return new WaitWhile(()=>tasks > 0);

理解UniyTest协程

NUnity默认的Test属性会在一帧渲染里面执行完毕,UnityTest则能够让测试在多帧里面执行。UnityTest函数中的每一次yield return都会立即退出当前帧,等待下一帧再继续执行。

自定义Wait

public class Wait
{
    static public IEnumerator Until(Func<bool> condition, float timeout = 30f)
    {
        float timePassed = 0f;
        while (!condition() && timePassed < timeout) {
            yield return new WaitForEndOfFrame();
            timePassed += Time.deltaTime;
        }
        if (timePassed >= timeout) {
            throw new TimeoutException("Condition was not fulfilled for " + timeout + " seconds.");
        }
    }
}

[UnityTest]
public IEnumerator TestSkeletonFollowsPlayer()
{
    Vector3 playerPos = new Vector3(2f, 1f, -5f);
    Quaternion playerDir = Quaternion.identity;
    Vector3 skeletonPos = new Vector3(2f, 0f, 5f);
    Quaternion skeletonDir = Quaternion.LookRotation(new Vector3(0f, 0f, -1f), Vector3.up);

    GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);
    GameObject skeleton = GameObject.Instantiate(skeletonPrefab, skeletonPos, skeletonDir);
    skeleton.GetComponent<Skeleton>().player = player.GetComponent();

    yield return Wait.Until(() => {
        float distance = Math.Abs((skeleton.transform.position - player.transform.position).magnitude);
        return distance <= 2f;
    }, timeout: 10f);
}

Unity测试用例的几个阶段

第一阶段:使用yield return new WaitForSeconds 缺点:时长不确定。

[UnityTest]
[UnityPlatform(RuntimePlatform.Android)]
[Timeout(10000)]
public IEnumerator UserGet()
{
    var hasCallback = false;
    UserService.GetLoggedInUser().OnComplete(u =>
    {
        Assert.IsFalse(u.IsError);
        ModelAssert.User(u.Data);
        UserService.Get(u.Data.ID).OnComplete(m =>
        {
            hasCallback = true;
            Assert.IsFalse(m.IsError);
            ModelAssert.User(m.Data);
        });
    });
    yield return new WaitForSeconds(5);
    Assert.IsTrue(hasCallback);
}

第二阶段:使用bool值+for循环 缺点:只能处理一个网络请求的情况,无法处理多个网络请求的情况。

[UnityTest]
[UnityPlatform(RuntimePlatform.Android)]
[Timeout(10000)]
public IEnumerator GetLoggedInUser()
{
    var hasCallback = false;
    UserService.GetLoggedInUser().OnComplete(m =>
    {
        hasCallback = true;
        Assert.IsFalse(m.IsError);
        ModelAssert.User(m.Data);
    });
    while (!hasCallback) yield return null;
}

第三阶段:使用int值 可以处理多个网络请求

Unity如何书写异步测试

Async unit test in Test Runner - Unity Answers

TestRunner会报错:Method has non-void return value, but no result is expectedNUnit实际上并没有报错,是Unity的TestRunner报了一个错,属于Unity TestRunner的bug。

        [Test]
        public async Task 测试异步expect()
        {
            Debug.Log("这是异步测试");
            return;
        }

解决方法就是绕过TestRunner,不要使用异步方法测试

    public static class UnityTestUtils {
      public static void RunAsyncMethodSync(t$$anonymous$$s Func < Task > asyncFunc) {
        Task.Run(async () => await asyncFunc()).GetAwaiter().GetResult();
      }
    }
     public class AsyncAwaitUnitTests {
      [Test]
      public void WithExt([Values(0, 500, 1000)] int delay) {
        UnityTestUtils.RunAsyncMethodSync(async () => {
          var sw = Stopwatch.StartNew();
 
          await Task.Delay(delay);
 
          Assert.AreEqual(delay, (int) sw.Elapsed.TotalMilliseconds, 300);
        });
      }
    }

有人提供了另一种工具方法封装:

         public static class UnityTestUtils {
      
             public static T RunAsyncMethodSync<T>(Func<Task<T>> asyncFunc) {
                 return Task.Run(async () => await asyncFunc()).GetAwaiter().GetResult();
             }
             public static void RunAsyncMethodSync(Func<Task> asyncFunc) {
                 Task.Run(async () => await asyncFunc()).GetAwaiter().GetResult();
             }
         }
         [Test]
         public void Test()
         {
             var result = RunAsyncMethodSync(() => GetTestTaskAsync(4));
             Assert.That(result, Is.EqualTo(4));
         }
     
         public async Task<int> GetTestTaskAsync(int a) {
             await Task.Delay(TimeSpan.FromMilliseconds(200));
             return a;
         }
     
         [Test]
         public void Testthrow() {
             Assert.Throws<InvalidOperationException>(
                            ()=> RunAsyncMethodSync(() => ThrowTaskAsync(4)));
         }
     
         public async Task<int> ThrowTaskAsync(int a) {
             await Task.Delay(TimeSpan.FromMilliseconds(200));
             throw new InvalidOperationException();
         }

参考资料

Unity Test Framework NUnit官网

学习NUIT学习什么

NUnit是C#语言中最重要的测试框架,对标Java中的JUnit。

  1. Assert系列函数,有两套Assert体系,一套是NUnit自己的,一套是Unity的。
  2. case上的注解,SetUp,TearDown,ExpectedException,Timeout等。