异步测试在自身测试完成后执行其他测试的方法

3
当我逐个执行测试时,所有测试都可以正常工作,没有问题。但是当我一次性执行它们时,就会遇到问题,如NullReferenceExceptionSemaphoreFullException
经过大量的挖掘,我注意到我的测试线程似乎相互干扰:即使测试已经完成,(重复的?)后台调用似乎在另一个测试被执行时继续进行。
涉及的应用程序是从通用应用程序单元测试项目中执行的WinRT组件。下面所看到的所有调用都是异步执行的。为了给我的后台任务足够的时间来完成(如果测试在等待异步后台任务时继续运行,它将在达到测试结束时关闭),我使用主线程休眠适当的时间。
new ManualResetEvent(false).WaitOne(millisecondsTimeout: 60 * 1000);

此外,我还在拆卸方法中添加了一个睡眠时间,以确保所有事情都能完成。
[TestCleanup]
public void Cleanup()
{
   new ManualResetEvent(false).WaitOne(millisecondsTimeout: 60 * 1000);
}

为了诊断目前的情况,我查看了测试的执行计划,并且由于它们非常相似,我在代码传递的每个相关位置上都加入了一个 Debug.WriteLine(session.GetHashCode())。下面是剥离了mscorlib中的FileNotFoundExceptions、线程结束消息和符号加载消息的结果。
这个涉及到的Session对象是由用户创建并传递给Core,然后由MainApiStorageApi传递。前者将其封装成一个Dispatcher并将此调度程序传递给UserController,而StorageApi仅将会话对象传递给StorageController,随后传递给DatabaseController
在整个层次结构中,使用同一会话对象并通过传递引用来使用所有对象所在位置可用的引用。
当创建数据库时,信号量就发挥作用。在EventBus中有一个属性。
internal static SemaphoreSlim TablesCreatedSemaphore = new SemaphoreSlim(0, 1);

用于确保在表创建之前不会请求任何数据。通过将其放置在

中使用。
await EventBus.TablesCreatedSemaphore.WaitAsync();

在执行SuccesfulLogin事件和请求API数据以及放置数据之间

EventBus.TablesCreatedSemaphore.Release();

在创建最后一张表的地方。
Integration_DownloadAsset_WithInvalidId_ThrowsSomeException
*************************************************
                   NEW EXECUTION
*************************************************
Core (ctor): 53578018
Main Api (ctor): 53578018
Api dispatcher (ctor): 53578018
UserController (ctor): 53578018
Storage Api (ctor): 53578018
Storage controller (ctor): 53578018
Database controller (ctor): 53578018
User controller (GetApiKeyAsync:pre-call): 53578018
Session (IsLoggedIn): 53578018
User controller (GetApiKeyAsync:pre-successfullogin-event): 53578018
Storage controller (CreateFolderStructure): 53578018
Database Controller(CreateDatabase): 53578018
Session (IsLoggedIn): 53578018
Session (IsLoggedIn): 53578018
Database controller (ctor): 53578018



Integration_DownloadAsset_WhenFileAlreadyExists_IsLocalReturnsTrue
A first chance exception of type 'System.Exception' occurred in mscorlib.dll


Integration_Login_WithValidLogin_RemovesUnusedAssetsFromFolder
A first chance exception of type 'System.Exception' occurred in mscorlib.dll

Integration_DownloadAsset_WithValidId_DownloadsFileToLocalFolder
*************************************************
                   NEW EXECUTION
*************************************************
Core (ctor): 20039337
Main Api (ctor): 20039337
Api dispatcher (ctor): 20039337
UserController (ctor): 20039337
Storage Api (ctor): 20039337
Storage controller (ctor): 20039337
Database controller (ctor): 20039337
User controller (GetApiKeyAsync:pre-call): 20039337
Session (IsLoggedIn): 20039337
User controller (GetApiKeyAsync:pre-successfullogin-event): 20039337
Storage controller (CreateFolderStructure): 53578018
Storage controller (CreateFolderStructure): 20039337
Database Controller(CreateDatabase): 53578018
Database Controller(CreateDatabase): 20039337
Session (IsLoggedIn): 20039337
Session (IsLoggedIn): 20039337
Database controller (ctor): 20039337

*************************************************
                   NEW EXECUTION
*************************************************
Core (ctor): 20995649
Main Api (ctor): 20995649
Api dispatcher (ctor): 20995649
UserController (ctor): 20995649
Storage Api (ctor): 20995649
Storage controller (ctor): 20995649
Database controller (ctor): 20995649
User controller (GetApiKeyAsync:pre-call): 20995649
Session (IsLoggedIn): 20995649
User controller (GetApiKeyAsync:pre-successfullogin-event): 20995649
Storage controller (CreateFolderStructure): 53578018
Storage controller (CreateFolderStructure): 20039337
Database Controller(CreateDatabase): 53578018
Database Controller(CreateDatabase): 20039337
Storage controller (CreateFolderStructure): 20995649
Database Controller(CreateDatabase): 20995649
Session (IsLoggedIn): 20995649
A first chance exception of type 'System.Threading.SemaphoreFullException' occurred in mscorlib.dll
A first chance exception of type 'System.Threading.SemaphoreFullException' occurred in mscorlib.dll

笔记

  • 每个测试通过将其名称打印到Debug.WriteLine,但显然最后一个没有这样做。

  • 具有Exception的两个测试是失败的测试,并且很可能是正确的异常。

  • 第一个测试仅具有它创建的对象的哈希码,第二个测试具有使用具有先前测试的哈希码的对象调用其方法的内容,而最后一个测试具有对其方法的调用 具有来自前两个测试的哈希码。

  • 这些发现和我的假设与我注意到的其他奇怪行为相一致(有时候Session对象在某个地方不会具有身份验证信息,但在其他地方会具有,或者数据会插入数据库两次,尽管主键冲突 - 尽管我不知道为什么这甚至在第一次就是可能的)。

  • 我考虑过这可能只是延迟将数据刷新到输出窗口的问题,但它似乎时间太好了。

  • 更改测试的执行顺序仍会导致前两个测试成功,第三个测试失败。每次运行行为都相同:每个测试都使用前一个测试的会话对象执行额外的调用。

示例测试

[TestMethod]
public async Task Integration_DownloadAsset_WithValidId_DownloadsFileToLocalFolder()
{
  Debug.WriteLine("Integration_DownloadAsset_WithValidId_DownloadsFileToLocalFolder");
  var core = new Core(new Session());
  await core.Api.GetApiKeyAsync(new GetApiKeyRequestParameters
  {
      // Authentication parameters
  });

  //TODO: why is this necessary?
  new ManualResetEvent(false).WaitOne(millisecondsTimeout: 60 * 1000);

  await core.Api.DownloadAssetAsync("1891");

  // Allow the program to download the file
  new ManualResetEvent(false).WaitOne(millisecondsTimeout: 60 * 1000);

  var localFolder = await (await (await ApplicationData.Current.LocalFolder.GetFolderAsync("Data")).GetFolderAsync("AccountData")).GetFolderAsync("Files");
  var fileNames = (await localFolder.GetFilesAsync()).Select(x => x.Name).ToList();
  CollectionAssert.Contains(fileNames, "1891");
}

问题

我认为原因是即使测试已经完成,方法调用仍然存在。这是如何可能的?我该如何防止测试相互交织?

如果这不是原因,那是什么?


1
这些实际上是单元测试吗?还是它们真的是集成测试?因为如果它们是单元测试,你的API将被存根,它将同步执行,你就不会有这个问题。在一个合适的单元测试中,几乎从不涉及多个线程。 - Tim Rogers
啊,标记错了。它们是集成测试(这是一个API包装器,集成就是全部)。 - Jeroen Vannevel
2个回答

1
这有点令人失望,因为事实证明我的思维是错误的。我错误地假设可以使 EventBus(传输OnSuccesfulLogin)内部事件 static,因为它们不是动态注册侦听器。
由于 static 元素在 AppDomain 级别上保持,这意味着每次执行我的测试都会向已经存在的静态侦听器添加自己的连接,从而导致调用的递增积累。
通过将 EventBus 设为非静态类型,并使用实例字段,所有测试现在都能按照预期运行。
感谢 @NETscape 将我引入正确的轨道。

0

1
我可能已经有一个修复方案了(正在测试中),所以这可能不是解决方案,但其中关于异步测试的注释与我等待线程相关。你能否在你的答案中加入这些内容,这样它就不仅仅是一些链接了吗? - Jeroen Vannevel
@JeroenVannevel 希望它能够正常工作。更新了答案,包括博客文章中的重要部分。 - Oliver
AsyncUnitTests包仅适用于VS2010;该操作正在使用VS2013。 - Stephen Cleary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接