Search Unity

Support for async tests via an attribute

Discussion in 'Testing & Automation' started by HelpOrMe, Aug 9, 2021.

  1. HelpOrMe

    HelpOrMe

    Joined:
    Aug 9, 2021
    Posts:
    2
    A little trick on how to convert asynchronous tests into Unity IEnumerator tests using an attribute.


    Attribute

    Code (CSharp):
    1.    
    2. [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
    3. public class TestAsyncAttribute : NUnitAttribute, ISimpleTestBuilder, IImplyFixture
    4. {
    5.     private readonly NUnitTestCaseBuilder _builder = new NUnitTestCaseBuilder();
    6.  
    7.     public TestMethod BuildFrom(IMethodInfo method, Test suite)
    8.     {
    9.         var parms = new TestCaseParameters(new object[] {method, suite})
    10.         {
    11.             ExpectedResult = new object(),
    12.             HasExpectedResult = true
    13.         };
    14.  
    15.         Type type = GetType();
    16.         MethodInfo proxyMethod = type.GetMethod(nameof(AsyncMethodProxy), BindingFlags.Static | BindingFlags.Public);
    17.         var proxyMethodWrapper = new MethodWrapper(type, proxyMethod);
    18.         suite.Method = proxyMethodWrapper;
    19.  
    20.         TestMethod proxyTestMethod = _builder.BuildTestMethod(proxyMethodWrapper, suite, parms);
    21.         proxyTestMethod.Name = method.Name;
    22.         proxyTestMethod.parms.HasExpectedResult = false;
    23.        
    24.         return proxyTestMethod;
    25.     }
    26.  
    27.     public static IEnumerator AsyncMethodProxy(IMethodInfo method, Test suite)
    28.     {
    29.         return AsyncSupport.RunAsEnumerator(() => (Task) method.Invoke(suite.Fixture));
    30.     }
    31. }
    32.  

    Helper class

    Code (CSharp):
    1.    
    2. public static class AsyncSupport
    3. {
    4.     public static IEnumerator RunAsEnumerator(Func<Task> task)
    5.     {
    6.         SynchronizationContext oldContext = SynchronizationContext.Current;
    7.         var newContext = new EnumeratorSynchronizationContext();
    8.        
    9.         SynchronizationContext.SetSynchronizationContext(newContext);
    10.        
    11.         newContext.Post(async _ =>
    12.         {
    13.             try
    14.             {
    15.                 await task();
    16.             }
    17.             catch (Exception e)
    18.             {
    19.                 newContext.InnerException = e;
    20.                 throw;
    21.             }
    22.             finally
    23.             {
    24.                 newContext.EndMessageLoop();
    25.             }
    26.         }, null);
    27.        
    28.         SynchronizationContext.SetSynchronizationContext(oldContext);
    29.        
    30.         return newContext.BeginMessageLoop();
    31.     }
    32. }
    33.  

    Sync. context from here: https://social.msdn.microsoft.com/F...ing-an-async-method-synchronously?forum=async

    Code (CSharp):
    1.  
    2. public class EnumeratorSynchronizationContext : SynchronizationContext
    3. {
    4.     private bool _done;
    5.     public Exception InnerException { get; set; }
    6.     private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
    7.  
    8.     private readonly Queue<Tuple<SendOrPostCallback, object>> _items =
    9.         new Queue<Tuple<SendOrPostCallback, object>>();
    10.  
    11.     public override void Send(SendOrPostCallback d, object state)
    12.     {
    13.         throw new NotSupportedException("We cannot send to our same thread");
    14.     }
    15.  
    16.     public void EndMessageLoop()
    17.     {
    18.         Post(_ => _done = true, null);
    19.     }
    20.  
    21.     public override void Post(SendOrPostCallback d, object state)
    22.     {
    23.         lock (_items)
    24.         {
    25.             _items.Enqueue(Tuple.Create(d, state));
    26.         }
    27.         _workItemsWaiting.Set();
    28.     }
    29.  
    30.     public IEnumerator BeginMessageLoop()
    31.     {
    32.         while (!_done)
    33.         {
    34.             Tuple<SendOrPostCallback, object> task = null;
    35.             lock (_items)
    36.             {
    37.                 if (_items.Count > 0)
    38.                 {
    39.                     task = _items.Dequeue();
    40.                 }
    41.             }
    42.             if (task != null)
    43.             {
    44.                 task.Item1(task.Item2);
    45.                 if (InnerException != null)
    46.                 {
    47.                     throw InnerException;
    48.                 }
    49.             }
    50.  
    51.             yield return null;
    52.         }
    53.     }
    54.  
    55.     public override SynchronizationContext CreateCopy()
    56.     {
    57.         return this;
    58.     }
    59. }
    60.  

    Usage example

    Works exactly the way you think it does. Since TestAsyncAttribute converts the task method to the IEnumerator, Task.Delay (and any other task) does not block the current thread, so the player loop keeps running.

    Code (CSharp):
    1.  
    2. public class YourTestFixture
    3. {
    4.     [TestAsync]
    5.     public async Task YourAsyncTest()
    6.     {
    7.         await Task.Delay(1000);
    8.         Debug.Log("Hi!");
    9.     }
    10. }
    11.  
     
    cacolukia, M_R and JesseSTG like this.
  2. sbergen

    sbergen

    Joined:
    Jan 12, 2015
    Posts:
    53
    Inspired by this, I'm thinking of making an OpenUPM package that implements a similar attribute. I can add a credit line somewhere if you want :)

    My idea would be to probably extend it with things like AsyncTestCaseAttribute.

    Whipped up this GitHub repo quickly: https://github.com/sbergen/UniAsyncTest
     
    HelpOrMe likes this.