2017-01-08 54 views
2

我有包裹着MessageLoopWorker WebBrowser控件的幾個測試,這裏描述的創建形式,它掛起:WebBrowser Control in a new threadNUnit測試與應用迴路時前

但是,當另一個測試創建用戶控制或形式,測試凍結並無法完成:

[Test] 
    public async Task WorksFine() 
    { 
     await MessageLoopWorker.Run(async() => new {}); 
    } 

    [Test] 
    public async Task NeverCompletes() 
    { 
     using (new Form()) ; 
     await MessageLoopWorker.Run(async() => new {}); 
    } 

    // a helper class to start the message loop and execute an asynchronous task 
    public static class MessageLoopWorker 
    { 
     public static async Task<object> Run(Func<object[], Task<object>> worker, params object[] args) 
     { 
      var tcs = new TaskCompletionSource<object>(); 

      var thread = new Thread(() => 
      { 
       EventHandler idleHandler = null; 

       idleHandler = async (s, e) => 
       { 
        // handle Application.Idle just once 
        Application.Idle -= idleHandler; 

        // return to the message loop 
        await Task.Yield(); 

        // and continue asynchronously 
        // propogate the result or exception 
        try 
        { 
         var result = await worker(args); 
         tcs.SetResult(result); 
        } 
        catch (Exception ex) 
        { 
         tcs.SetException(ex); 
        } 

        // signal to exit the message loop 
        // Application.Run will exit at this point 
        Application.ExitThread(); 
       }; 

       // handle Application.Idle just once 
       // to make sure we're inside the message loop 
       // and SynchronizationContext has been correctly installed 
       Application.Idle += idleHandler; 
       Application.Run(); 
      }); 

      // set STA model for the new thread 
      thread.SetApartmentState(ApartmentState.STA); 

      // start the thread and await for the task 
      thread.Start(); 
      try 
      { 
       return await tcs.Task; 
      } 
      finally 
      { 
       thread.Join(); 
      } 
     } 
    } 

代碼的步驟,以及在除return await tcs.Task;永遠不會返回。

包裝new Form到MessageLoopWorker.Run(...)似乎使它更好,但它不適用於更復雜的代碼,不幸的是。我還有很多其他的測試,包括表單和用戶控件,我希望避免包裝到messageloopworker中。

也許MessageLoopWorker可以修復以避免干擾其他測試?

更新:繼@Noseratio的驚人答案我在MessageLoopWorker.Run調用之前重置了同步上下文,現在它運行良好。

更多有意義的代碼:

[Test] 
public async Task BasicControlTests() 
{ 
    var form = new CustomForm(); 
    form.Method1(); 
    Assert.... 
} 

[Test] 
public async Task BasicControlTests() 
{ 
    var form = new CustomForm(); 
    form.Method1(); 
    Assert.... 
} 

[Test] 
public async Task WebBrowserExtensionTest() 
{ 
    SynchronizationContext.SetSynchronizationContext(null); 

    await MessageLoopWorker.Run(async() => { 
     var browser = new WebBrowser(); 
     // subscribe on browser's events 
     // do something with browser 
     // assert the event order 
    }); 
} 

當運行測試,而歸零同步上下文WebBrowserExtensionTest塊時它遵循BasicControlTests。通過零位它傳遞良好。

可以保持這樣嗎?

回答

3

我在MSTest下重新評估了這個,但我相信下面的所有內容同樣適用於NUnit。

首先,我明白這段代碼可能已經脫離了上下文,但是現在看起來並不是很有用。爲什麼要在NeverCompletes內創建一個表單,該表單在隨機的MSTest/NUnit線程上運行,與MessageLoopWorker產生的線程不同?

無論如何,你遇到了死鎖,因爲using (new Form())在該原始單元測試線程上安裝了一個WindowsFormsSynchronizationContext的實例。在using聲明後檢查SynchronizationContext.Current。然後,你面對一個典型的僵局,並由Stephen Cleary在他的"Don't Block on Async Code"中做了很好的解釋。

對,您沒有阻止自己,但MSTest的/ NUnit的呢,因爲它是足夠聰明,認識到NeverCompletes方法async Task簽名,然後由它返回的Task執行類似Task.Wait。由於原始單元測試線程沒有消息循環並且不會收集消息(與WindowsFormsSynchronizationContext不同),NeverCompletes內的await延續永遠不會有機會執行,並且Task.Wait正在等待。

這就是說,MessageLoopWorker僅爲設計創建和運行async方法傳遞給MessageLoopWorker.Run的範圍內WinForms對象,然後來完成。例如。,下面不會阻礙:

[TestMethod] 
public async Task NeverCompletes() 
{ 
    await MessageLoopWorker.Run(async (args) => 
    { 
     using (new Form()) ; 
     return Type.Missing; 
    }); 
} 

設計有多個WinForms對象MessageLoopWorker.Run通話。如果這是你需要什麼,你可能想看看我的MessageLoopApartmenthere,如:

[TestMethod] 
public async Task NeverCompletes() 
{ 
    using (var apartment = new MessageLoopApartment()) 
    { 
     // create a form inside MessageLoopApartment 
     var form = apartment.Invoke(() => new Form { 
      Width = 400, Height = 300, Left = 10, Top = 10, Visible = true }); 

     try 
     { 
      // await outside MessageLoopApartment's thread 
      await Task.Delay(2000); 

      await apartment.Run(async() => 
      { 
       // this runs on MessageLoopApartment's STA thread 
       // which stays the same for the life time of 
       // this MessageLoopApartment instance 

       form.Show(); 
       await Task.Delay(1000); 
       form.BackColor = System.Drawing.Color.Green; 
       await Task.Delay(2000); 
       form.BackColor = System.Drawing.Color.Red; 
       await Task.Delay(3000); 

      }, CancellationToken.None); 
     } 
     finally 
     { 
      // dispose of WebBrowser inside MessageLoopApartment 
      apartment.Invoke(() => form.Dispose()); 
     } 
    } 
} 

或者,你甚至可以用它在多個單元測試方法,如果你不關心的測試潛在的耦合,例如(MSTest的):

[TestClass] 
public class MyTestClass 
{ 
    static MessageLoopApartment s_apartment; 

    [ClassInitialize] 
    public static void TestClassSetup() 
    { 
     s_apartment = new MessageLoopApartment(); 
    } 

    [ClassCleanup] 
    public void TestClassCleanup() 
    { 
     s_apartment.Dispose(); 
    } 

    // ... 
} 

最後,既不MessageLoopWorker也不MessageLoopApartment設計與不同的線程(這是幾乎從來沒有一個好主意,反正)創建WinForms對象的工作。只要您喜歡,您可以擁有儘可能多的實例,但是一旦在特定實例的線程上創建了一個對象,它應該只在同一個線程上被進一步訪問和正確銷燬。

+1

令人驚歎的是,它是超級有用的。謝謝 - 我現在更瞭解它。我已經添加了上下文,希望它會沒事的。 –

+0

@AlexAtNet,如果有幫助,很高興。我仍然不會在MessageLoopWorker的線程之外創建任何WinForms對象。在大多數情況下,它們需要消息循環。至少,如果你這樣做,一定要配置NUnit給你一個STA線程。 – Noseratio