65.9K
CodeProject 正在变化。 阅读更多。
Home

Excellence:BizTalk 单元测试框架 – 概述

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (3投票s)

2009 年 3 月 5 日

GPL3

12分钟阅读

viewsIcon

37251

downloadIcon

202

用于测试编排的单元测试框架,使用仪器。

引言

对于那些来自“红-绿-重构”——扎实的单元测试——背景的开发者来说,BizTalk 开发可能会带来令人失望的启示。

我自己从未成为纯粹的单元测试者,因为我相信单元测试的价值是通过常识和适度的方法而不是极端的方法获得的,但我震惊地发现 BizTalk 测试是多么麻烦。从编译到部署再到重启主机,一切都很慢,对外部服务的依赖很多,而且仪器仪表很难实现。你实际上看不到发生了什么。然而,随着时间和经验的积累,我们会找到克服一些问题的方法,但 BizTalk 测试通常会令人恼火且效率低下。

遵循正常的 C# 开发习惯,我开始寻找 BizTalk 单元测试框架,并且很高兴地发现了 Kevin Smith 的 BizUnit。在研究并使用了一段时间后,我意识到虽然它提高了开发质量,但仍有许多不足之处。首先,测试的粒度是系统输出的级别。对于复杂的编排,我们通常需要比仅仅是系统输出更多的控制,因为在任何消息从编排输出之前,可能会有许多步骤正在进行,如果出现问题,很难立即看到是什么出了问题。此外,我未能找到一种简单的方法来处理并行或多消息场景。我认为,基于配置的单元测试方法与纯代码方法一样好,虽然我个人并没有觉得它特别有用,并且更喜欢纯代码的灵活性——毕竟,我们正在编写通常会在开发人员机器上编译和运行的单元测试。

因此,我开始编写一个用于单元测试的迷你框架,该框架可以提供更多的控制,现在经过一年多在多个项目中使用它编写单元测试后,我认为它值得分享。Excellence 具有以下特点/优势:

  1. 测试的形状级别粒度
  2. 形状级别的错误报告,可节省调试编排的时间
  3. 处理并行或多消息场景的能力
  4. 访问编排中的中间值——如果您希望公开它们
  5. 强制执行一种仪器仪表模式,该模式在开发以及部署后都很有用
  6. 一套易于扩展的用于模拟各种系统和服务的工具
  7. 用于模拟生产环境中各种真实情况的随机化工具
  8. 测试系统输出的能力——类似于 BizUnit
  9. 可扩展——类似于 BizUnit

跟踪与 Excellence

在深入细节之前,对于那些纯粹主义者来说,了解 Excellence,就像 BizUnit 一样,并不是一个真正的单元测试框架,因为它不是为了测试代码单元本身而设计的,可能更有用。它可能是系统测试,或者有些人称之为集成测试,但我们这里处理的单元通常是编排——包括它调用的编排。在代码单元测试中通常很容易实现的隔离级别在 BizTalk 中是不可能实现的,因为如果可能单独调用一个形状,那将是纯粹的黑魔法。

Excellence 基于一种仪器仪表模式,在该模式下,在 BizTalk 编排中的每个形状之后,我们使用 System.Diagnostic.Trace 输出一个跟踪条目。 我相信大多数 BizTalk 开发者都非常熟悉跟踪输出,它类似于用于调试的 Console.WriteLine() 或老式的 MessageBox。跟踪成本很低,并且可以安全地保留在生产代码中,因为性能影响通常在于查看条目而不是编写它们。

每个跟踪条目将包含两个元素:形状 ID (步骤 ID) 和上下文 ID。我方便地使用了 <Project>_<Orchestration>_<ShapeNameOrAction> 命名约定作为我的形状 ID,但您也可以使用自己的。我通常将所有形状 ID 定义为枚举,并使用枚举的名称(而不是整数值)进行跟踪。上下文 ID 是“我们正在处理哪个消息”的熟悉概念。这取决于项目,但包含信息层次结构是有用的,例如 <CustomerId>_<OrderId>_<OrderItemId>,而不仅仅是 <OrderItemId>。这通常在开发调试以及在生产中查找和解决问题方面非常有价值。

现在,重要的是要理解 System.Diagnostic.Trace 实际上做了什么。这个对象公开用于输出跟踪条目的 static 方法。它的行为可以通过配置文件中的 <system.diagnostic> 部分进行控制。调用 Write()WriteLine() 将循环遍历为 Trace 定义的所有侦听器。默认情况下,只有一个侦听器:DefaultTraceListener。这个类是 Windows API OutputDebugString 的一个薄包装。

[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
public static extern void OutputDebugString(string message);

可以使用 Sysinternal 的 DebugView 查看此输出,这是一个非常有用的工具,并且正如我们将看到的,它是 Excellence 的必备工具。

您可以使用配置文件中的 <system.diagnostic> 部分添加更多侦听器,例如 TextFileListenerEventLogListener。或者,您可以使用 Trace 类的 static Listeners 属性来添加侦听器。

我怀疑您是否会想要禁用无害的默认跟踪输出,但如果有一天您需要这样做(例如,在资源稀缺的生产服务器上),您可以通过此配置轻松实现。

<system.diagnostics>
       <trace>
            <listeners>
                   <remove name="Default" />
            </listeners>
       </trace>
</system.diagnostics>

这意味着在生产代码中保留跟踪是绝对安全的,因为您可以禁用绝对任何跟踪输出,包括默认输出。

因此,Excellence 的想法很简单:设置测试,以便对于每个形状,我们等待正确的字符串输出(该字符串输出将形状 ID 和上下文 ID 合并为一个 string),然后当我们收到它时,我们等待下一个,依此类推。如果我们未能在超时时间内收到预期的跟踪输出,我们就知道该形状出了问题,测试就会失败。

捕获跟踪

现在,我们如何访问另一个进程的跟踪输出?嗯,如果 DebugView 可以做到,我们也可以。首先,我们介绍进程调试,这是以编程方式访问调试输出的自然方式,然后我们讨论最终使用的内容。因此,如果您对 Windows 调试输出的工作原理不感兴趣,请跳过本节。

那么,我们如何访问另一个进程的调试信息呢?这实际上是通过调试该进程来实现的。

  1. 找到运行编排的 BTSNTSvc.exe 进程的进程 ID。如果您只有一个,那么很容易通过 Process.GetProcesses() 循环并找到唯一一个 ProcesssName == “BTSNTSvc.exe” 的进程。如果您有多个主机(很可能),那会有点棘手,您可能需要将其作为参数提供给您的测试。
  2. 调用 DebugActiveProcess API 开始调试该进程。
  3. 调用 DebugSetProcessKillOnExit,以便在我们停止调试后,进程可以继续运行。
  4. 我们创建一个循环,在其中调用 WaitForDebugEvent 并设置超时时间以接收调试消息。其中一条消息是 OUTPUT_DEBUG_STRING_EVENT,我们已经使用 System.Diagnostic.Trace 发送了它,并通过创建缓冲区来读取 string 消息并复制 string。之后,我们调用 ContinueDebugEvent 继续。
  5. 在调试结束时,我们调用 DebugActiveProcessStop

容易吗?嗯,不完全是。首先,关于 Win32 Debug 的资源和示例非常有限且零散,而在托管代码中执行此操作实际上是黑魔法。主要头痛之一是在 C# 中定义结构。我找到的示例都不起作用,经过一些折腾,我让它在正常的托管代码中工作了。我用一个控制台应用程序测试了它,是的,它确实在工作。

然后,我尝试在 BizTalk 上进行测试,遇到了问题。首先,调试关键进程需要线程通常没有的权限。但是,这很容易实现

Process.EnterDebugMode();

而且,一旦您完成

Process.LeaveDebugMode();

然而,主要问题是,在调试 BTSNTSvc.exe 几秒钟后,它就会崩溃。我认为问题在于定义 DEBUG_EVENT 结构。它的主要问题之一是 .NET 是强类型的,而 WaitForDebugEvent 不是。它是一个捕获各种调试信息的通用处理,每个接收结构的大小都不同。因此,当我们调用 WaitForDebugEvent 时,我们不知道下一个调试事件的类型,也就无法使用适当的消息。我的解决方案(可能不是最好的)是定义一个通用的结构来覆盖所有场景。这在异常被抛出之前都可以正常工作。BizTalk 在第一次唤醒时总是会抛出两个异常(您可以在 DebugView 窗口中看到这些),这是导致崩溃的原因。无论如何,如果有人能指出问题所在以及如何解决,我将不胜感激。

如果您跳过了上一节,现在是时候加入我们了。因此,如果调试 BTSNTSvc.exe 进程不像我们希望的那样有趣,那么解决方案是什么?解决方案来自 DebugView 工具本身:它有一个将输出保存到文本文件的功能,我们可以简单地读取文件并查找跟踪条目。我们所要做的就是确保,在运行测试之前,DebugView 已打开并且我们正在将输出保存到预定义的文本文件中,这很容易做到。无论如何,在开发和测试时,您都会养成保留 DebugView 的习惯,唯一需要记住的事情(这已经让我吃了几次亏,并且很可能会让您也吃亏)是启用保存到日志文件的选项。当您这样做时,您会在磁盘图标上看到一个绿色的箭头;这是一个需要习惯的良好指示器。

DebugView2.PNG

因此,想法是有一个 ResetEvent——类似于 AutoResetEvent——它可以等待一个特定的跟踪条目并阻塞当前线程,直到您找到该条目或它超时。IInstrumentationResetEvent 接口定义了这种重置事件的通用行为,即一种调试并获取跟踪输出的事件,另一种是查看日志文件以查找测试指定的条目。另一种选择是类似的 ResetEvent,它查询事件日志以获取特定条目。

因此,基本上,您可以创建您的测试,并使用从工厂生成的 IInstrumentationResetEvent 的实现,并根据您的场景或偏好使用不同的源(调试、文件、事件日志等)。IInstrumentationResetEvent 公开熟悉的 Wait() 方法,该方法接受超时时间并返回一个布尔值——类似于 WaitOne()AutoResetEvent 接受超时时间并返回一个布尔值。区别在于 AutoResetEvent 会等待对其调用 Set(),而 IInstrumentationResetEvent 对象将在找到匹配的条目时执行 Set(),因此,Wait() 接受一个子字符串来匹配它查询的条目。WaitAll() 类似,唯一的区别是它接受一个 string 数组,并且直到找到所有子字符串(每个子字符串在一个或另一个条目中)才会返回。这用于单元测试并行场景,以及在我们测试非串行多消息时,很可能是在分解器管道中,该管道将一条消息分解为多条消息,并且每条消息都被单独处理,并且很可能并行处理。

一个基本的单元测试

正如您将在本文的第二部分看到的,一个基本的单元测试(针对编排的第一个形状)将是

// code to invoke the orchestartion
// ...

// test
using (LogFileResetEvent debug = 
       new LogFileResetEvent(TestConstants.LogFileName, 
       TestConstants.PollingInterval, 
       TestConstants.StepTimeOut))
{
    if (!debug.Wait(System.String.Format("{0}_{1}", 
        Connexita.StockPurchase.Helper.StockPurchaseSteps.
          Connexita_StockPurchase_ReceiveMessage, 
        _context.CustomerId)))
    Assert.Fail("Timed out on Connexita_StockPurchase_ReceiveMessage");
}

因此,在上面的代码中,在设置好调用编排的测试后,我们创建一个 LogFileResetEvent 类的实例,并等待一个条目,该条目由步骤 ID/形状 ID 和上下文 ID 组成。Wait() 方法使用默认超时时间调用,我们等待一个子字符串。如果 LogFileResetEvent 在超时之前找到子字符串,它将返回 true。否则,它将返回 false,测试将失败。

同样,您可以输出编排内的内部变量(对于整个消息不太有用,最好从原始类型使用),并使用 Wait() 方法来获取预期值。例如,如果我们对一个名为 count 的变量感兴趣,如果我们已经在编排内部编写了使用 string 格式“count:{0}”的跟踪输出,并且我们期望 count 值为 4,那么我们将等待“count:4”。这是针对不直接从编排输出的中间变量,否则,我们可以使用工具设置测试期望。

下面的代码片段展示了如何使用 WaitAll() 测试编排的并行部分。

// parallel 
if(!debug.WaitAll(new string[]
{
    System.String.Format("{0}_{1}", 
      Connexita.StockPurchase.Helper.StockPurchaseSteps.
          Connexita_StockPurchase_ParallelAction1, 
      _context.CustomerId),
    System.String.Format("{0}_{1}", 
      Connexita.StockPurchase.Helper.StockPurchaseSteps.
         Connexita_StockPurchase_ParallelAction2, 
      _context.CustomerId)
}, ParallelAction.MaxDelayInMS * 2))
    Assert.Fail("One of the parallel actions did not finish.");

在这里,我们设置期望,以便 WaitAll() 仅在找到 ParallelAction1ParallelAction2 条目后才返回,否则它会超时,在这种情况下测试会失败。这不会告诉您哪个并行操作没有完成,但可以很容易地从跟踪本身中找出。

因此,基本上,使用跟踪可以实现的没有限制,而且这些跟踪也可以用于排查生产问题。为了在服务器上保存跟踪输出,您只需添加另一个侦听器来为您存储跟踪输出即可。

<system.diagnostics>
        <trace autoflush="true" indentsize="4">
            <listeners>
                <add name="eventLog" 
                   type="System.Diagnostics.EventLogTraceListener" 
                   initializeData="TraceLog" />
            </listeners>
        </trace>
</system.diagnostics>

上面的配置片段会将您的跟踪输出到应用程序事件日志,以及可以使用 DebugView 查看的正常调试输出。使用这些跟踪,您可以确切地看到每条消息发生了什么。这也有助于您理解性能问题和瓶颈,特别是如果瓶颈在于从编排调用的代码中,在这种情况下,HAT 可能不会提供太多信息。

总之,我们讨论了 Excellence BizTalk 单元测试框架,该框架提供了有效单元测试编排的范例和工具。与现有框架相比,它提供了几个关键优势,并且易于设置和使用。

下一篇文章 中,我们将介绍一个实际使用此框架的示例。

© . All rights reserved.