2016-06-16 103 views
1

我有一个设置,其中更改类的变量引发异步运行的事件,我尝试单元测试。单元测试异步事件

class A { 

    Action<int> OnIntChange; 

    private int _a; 
    public int a { 
     set { 
      OnIntChange(value); 
     } 
    } 
} 

class B { 

    RaiseAsyncEvent(int value) { 
     Task.Factory.StartNew(() => {c = value;}); 
    } 

    int c; 
} 

以前的一切是同步的,所以很容易地我能单元测试:

B b = new B(); 
A.OnIntChange += b.RaiseAsyncEvent;  
A.a = 10; 
Assert.AreEqual(10, A.b.c); 

但是现在这个失败。我不能使用ManualResetEvents,因为我没有直接提出这个事件,并且不能没有代码重组。看来我唯一的选择是在调用A.a = 10之后添加一个System.Threading.Thread.Sleep()调用,但我正在寻找其他选项。建议?

~~~~~~~~~~编辑~~~~~~~~

我在寻找一个潜在的解决方案是让RaiseAsyncEvent返回它创建的任务和字段设置到任务。喜欢的东西:

class B { 

    Task task; 

    RaiseAsyncEvent(int value) { 
     task = Task.Factory.StartNew(() => {c = value;}); 
    } 

    int c; 
} 


B b = new B(); 
A.OnIntChange += b.RaiseAsyncEvent; 
b.task.Wait(); 
A.a = 10; 
Assert.AreEqual(10, A.b.c); 

~~~~~~~~~~进一步编辑~~~~~~~~~~

我最终什么事做这是改变B到解决以下:

class B { 

    bool RunAsync = true; 

    RaiseAsyncEvent(int value) { 
     Task task = Task.Factory.StartNew(() => {c = value;}); 
     if(!RunAsync) { task.Wait();} 
    } 

    int c; 
} 

,并改变了测试:

B b = new B(); 
b.RunAsync = false; 
A.OnIntChange += b.RaiseAsyncEvent;  
A.a = 10; 
Assert.AreEqual(10, A.b.c); 

回答

2

您可以在B的构造函数的定制版本通过一个 TaskScheduler可以等待单元测试中所有任务的完成。

class B { 
    public B(TaskScheduler taskScheduler) 
    { 
     _taskScheduler = taskScheduler; 
    } 


    public B(): this(TaskScheduler.Default) 
    { 
    } 


    public void RaiseAsyncEvent(int value) 
    { 
     Task.Factory.StartNew(() => {c = value;}, CancellationToken.None, 
     TaskCreationOptions.DenyChildAttach, _taskScheduler); 
    } 

    TaskScheduler _taskScheduler; 

} 

ConcurrentExclusiveSchedulerPair有能力等待我们寻找的完成。

// Unit test 

var schedulerPair = new ConcurrentExclusiveSchedulerPair(); 

B b = new B(schedulerPair.ConcurrentScheduler); 
A.OnIntChange += b.RaiseAsyncEvent;  
A.a = 10; 

schedulerPair.Complete(); 
schedulerPair.Completion.Wait(); 

Assert.AreEqual(10, A.b.c); 

当你需要留意一个以上的异步操作它扩展到更高级的情景:

B b1 = new B(schedulerPair.ConcurrentScheduler); 
B b2 = new B(schedulerPair.ConcurrentScheduler); 
B b3 = new B(schedulerPair.ConcurrentScheduler); 

A.OnIntChange += b1.RaiseAsyncEvent;  
A.OnIntChange += b2.RaiseAsyncEvent;  
A.OnIntChange += b3.RaiseAsyncEvent;  
A.a = 10; 

schedulerPair.Complete(); 
schedulerPair.Completion.Wait(); 

Assert.AreEqual(10, b1.c); 
Assert.AreEqual(10, b2.c); 
Assert.AreEqual(10, b3.c); 

编辑:TaskScheduler情况下也可以使用一个静态属性共享,在这种情况下, B构造函数签名不会更改。

static class EventScheduler 
{ 

    public static TaskScheduler TaskScheduler 
    { 
     get {return _taskScheduler; } 
     set {_taskScheduler = value; } 
    } 

    static TaskScheduler _taskScheduler = TaskScheduler.Default; 

    public static Task RunAsync(Action<T> action) 
    { 
     return Task.Factory.StartNew(() => {c = value;}, 
     CancellationToken.None, TaskCreationOptions.DenyChildAttach, 
     _taskScheduler); 
    } 
} 

class B 
{ 
    public void RaiseAsyncEvent(int value) 
    { 
     EventScheduler.RunAsync(()=>{c = value;}); 
    } 
} 

// Unit test 

var schedulerPair = new ConcurrentExclusiveSchedulerPair(); 
EventScheduler.TaskScheduler = schedulerPair.ConcurrentScheduler; 

B b = new B(); 
A.OnIntChange += b.RaiseAsyncEvent;  
A.a = 10; 

schedulerPair.Complete(); 
schedulerPair.Completion.Wait(); 

Assert.AreEqual(10, A.b.c); 
+0

我喜欢这个答案,我将它标记为已接受的答案 - 如果我绝对不得不以我提问的方式照顾我的问题,那就是对的。但是我最终所做的是通过在B上添加一个“RunAsync”布尔字段来强制任务同步运行,如果(!RunAsync){task.wait();)加上 。 } 里面的RaiseAsyncEvent,并设置b.RunAsync = false;在TestInitialize中。不是最漂亮的,但它是一个简单的修复,可能是一个非常复杂的问题。 –

+0

如果您想避免更改构造函数签名,请考虑由所有事件共享的TaskScheduler(单例)的静态实例。然后,不要在所有对象上设置RunAsync属性,而只需将ConcurrentScheduler分配给静态属性。 – alexm

+0

我正在使用的代码比我在这里展示的要复杂得多 - 我实际上实现了我自己的静态CallbackManager,它将事件传入并生成一个新的任务,这个任务根据几个因素运行异步或同步,保存句柄,并处理返回逻辑。我添加的布尔允许我绕过异步逻辑并强制它从一开始就同步,这样我的所有单元测试仍然可以通过。由于管理器是静态的,因此在测试开始时,不需要对RunAsync bool进行一次更改,而且它可以正常运行。 –