Excellence:BizTalk 单元测试框架 – 示例






4.50/5 (2投票s)
关于使用 Excellence BizTalk 单元测试框架的演练。
引言
在第一篇文章中,我们回顾了 Excellence 的工作原理。在本文中,我们将创建一个使用 Excellence 的示例应用程序。
业务场景
这是一个示例业务场景(所有名称均为虚构)
Connexita 是一家股票经纪公司,通过 Web 服务接收客户的买入股票订单(为简单起见,我们不处理卖出股票)。每个订单都包含客户 ID 以及订单项列表。我们的编排将接收订单,并根据当前价格返回订单的总价值,以便最终报价可以发送给客户确认。
我们的编排需要从 Stock Services Limited 提供的第三方 Web 服务获取每只股票的当前价格。如果股票属于外国证券交易所,它还需要获取汇率(来自同一个 Web 服务)来计算订单项价格——Web 服务已通过将其价格转换为本地货币来简化此过程。最后,它计算所有订单项的总价,并返回结果以及订单 ID,以便客户对报价满意,就可以购买。为了让事情更有趣一些,我添加了两个并行操作,以便我们可以测试并行场景。

您可能会觉得这个场景有点模糊,但我选择它是因为它很简单,同时又提供了足够的测试挑战。
测试设置
我们像开发任何其他编排一样开发编排。然后,我们需要放置跟踪输出。正如我所说,即使您不使用任何单元测试,这也是有用的。在这里,我们定义了我们的步骤
public enum StockPurchaseSteps
{
Connexita_StockPurchase_ReceiveMessage,
Connexita_StockPurchase_GetRecordCount,
Connexita_StockPurchase_AssignMarket,
Connexita_StockPurchase_AssignGetStockPriceRequest,
Connexita_StockPurchase_SendStockPriceRequest,
Connexita_StockPurchase_ReceiveGetStockPriceResult,
Connexita_StockPurchase_ItIsForeignExchange,
Connexita_StockPurchase_ItIsLocalStock,
Connexita_StockPurchase_CalculateItemValue,
Connexita_StockPurchase_AssignExchangeRateRequest,
Connexita_StockPurchase_SendExchangeRateRequest,
Connexita_StockPurchase_ReceiveExchangeRateResult,
Connexita_StockPurchase_IncrementLoopCounter,
Connexita_StockPurchase_ParallelAction1,
Connexita_StockPurchase_ParallelAction2,
Connexita_StockPurchase_AssignOrderResult,
None
}
可以看出,使用了 <Project>_<Orchestration>_<Step> 命名约定,这保证了步骤名称是唯一的。
然后,在每个形状之后,我们放置一个表达式形状并输出跟踪。例如
System.Diagnostics.Trace.WriteLine(System.String.Format("{0}_{1}",
Connexita.StockPurchase.Helper.StockPurchaseSteps.
Connexita_StockPurchase_ReceiveMessage, msgOrder.customerId));
对于订单项级别的跟踪输出,我们需要比客户 ID 更多的上下文信息。在这里,我们还需要股票的符号
System.Diagnostics.Trace.WriteLine(System.String.Format("{0}_{1}_{2}",
Connexita.StockPurchase.Helper.StockPurchaseSteps.
Connexita_StockPurchase_AssignGetStockPriceRequest,
msgOrder.customerId, symbol));
现在,我们已经完成了所有需要的跟踪,可以开始单元测试了。我个人使用 NUnit,但您可以使用任何您喜欢的单元测试框架。您会看到我为测试上下文保留了一个成员变量,因为很多检查都需要在运行测试的线程之外的线程中进行——即,在我们订阅的测试事件中。私有类 TestContext
用于此目的并在此处初始化。所以,现在,让我们看看主要的测试。
首先,我们需要进行 Web 服务调用来调用我们的编排。为了做到这一点,我们首先需要构建一个订单。MessageHelper
类有助于我们创建随机订单项列表。此类使用随机化助手类来实现此目的。随机化类继承自泛型类 RandomGeneratorBase
。我包含了一些通用实现,它们是不言自明的,但其中一些值得简要解释。
ListRandomGenerator<T>
泛型类从其 T
对象列表中返回一个随机项。如果我们将其与“唯一”选项一起使用,它实际上会从其内部列表中删除它返回的每个项,这样就不会返回相同的项两次。PrioritisedListRandomGenerator<T>
类似,但我们可以用优先级或可能性来初始化列表的每个项,优先级/可能性越高,返回该项的可能性就越大。这对于负载测试(我最初写它的地方)尤其有用,因为选项的可能性不是相同的;例如,对于婚姻状况,单身、已婚和离婚的可能性不是相同的,所以我们可以用优先级/可能性因子来初始化它们。
回到我们的 Web 服务调用,我们需要调用编排并发送订单消息。由于我们对 LogFileResetEvent
的 Wait()
调用是阻塞的,因此我们需要异步调用 Web 服务——否则,Web 服务调用将是阻塞的,并且在 Wait()
的机会消失之前不会完成。SoapRequester
提供了 AsyncMakeRequest
以异步方式调用 Web 服务。我们在请求者的 ReceivedResponse
事件上注册,以便捕获响应。检查 Web 服务响应并确保总数匹配的代码在事件处理程序中
void requester_ReceivedResponse(object sender, ReceivedSoapResponseEventArgs e) {
Assert.AreEqual(_context.Total,
double.Parse(PseudoXPath.GetValueOfAttribute(e.State.Response, "total")));
}
由于这不会在运行测试的同一线程中,NUnit 会以不同的方式显示失败(如果值不匹配),但这很明显,不会被遗漏。正如您所看到的,我使用了 PseudoXPath
,它只是我小巧的正则表达式工具,比定义 XPath 容易得多,尤其是在您经常遇到恼人的命名空间问题时。
现在,我们需要考虑我们系统的外部依赖项。我们有两个需要模拟的 Web 服务调用。我在这里涵盖 Web 服务依赖项,因为它最复杂。我还开发了用于模拟 MQ Series 服务的工具,但没有将其包含在工具包中,因为我不想依赖 MQ Series——并非每个人都在使用它。但是,如果您有兴趣,我可以发送给您。我也没包含 MSMQ 工具,但开发它会与 MQ 非常相似。
那么,我们如何创建一个模拟的 Web 服务呢?有些人可能已经知道 SoapUI。对于任何处理 Web 服务和 WSDL 的人来说,这是一个绝佳的工具。您可以实际提供一个 WSDL,它会为您快速创建一个模拟的 Web 服务。在设置单元测试和开发过程中,我有时会使用 SoapUI 的模拟 Web 服务功能来确保一切就绪。但是,开箱即用的 SoapUI 在我们的单元测试中有两个问题
- 您无法在测试中设置响应。例如,您希望为一种股票返回价格 230,为另一种股票返回价格 340。虽然它允许使用 XPath 定义基于规则的响应,但这将在测试之外设置,因此不是理想的。
- 您无法在测试中检查发送到 SoapUI 的请求。
所以基本上,我们必须在代码中创建自己的 Web 服务模拟。WebServiceMock
类提供了此功能,它的开发方式是您可以将其与 SoapUI 结合使用,而无需更改发送端口的任何绑定或 URL。
因此,我们通过提供其需要服务的 URL 来创建一个模拟服务
WebServiceMock wm =
new WebServiceMock(new string[] { TestConstants.StockPriceServiceUrl });
URL 已按照 SoapUI 的方式设置
public const string StockPriceServiceUrl =
"http://127.0.0.1:8088/mockStockPriceServiceSoap/";
请注意,WebServiceMock
只服务末尾带有斜杠的 URL(HttpListener
的要求),但 SoapUI 设置的 URL 末尾没有斜杠。如果您想在不经常更改发送端口 URL 的情况下同时使用两者,您可以更改 SoapUI 的 URL,但在 SoapUI 中,这并不容易看到如何操作。首先,您需要停止模拟服务,在服务上按 Enter 打开服务窗口,然后单击开始和停止图标最右侧的工具图标以查看编辑屏幕
现在,您可以添加末尾的额外斜杠。
请注意,有时您在运行测试时无法保持 SoapUI 打开。原因是 SoapUI 一打开就会在端口 8088 上开始侦听,甚至不需要启动任何模拟服务。
那么,我们如何在 WebServiceMock
中设置响应呢?请记住,我们的 Web 服务不仅应该服务两个服务(股票价格和汇率),还必须为多个股票提供这些信息。为了实现这一点,我们设置了期望
WebServiceExpectation expPrice =
new WebServiceExpectation(TestConstants.StockPriceServiceUrl,
new string[] { item.Symbol, TestConstants.GetStockPriceKey },
MessageHelper.GetStockPriceResponse(item.CurrentPrice));
要创建 Web 服务期望,我们提供一个键列表以及它必须发送回的响应。键是 Web 服务模拟在请求中找到以发送回特定响应的所有子字符串。通常,我们会设置各种期望,模拟服务将循环遍历它们,并在找到与所有键匹配的期望时立即发送回。
对于股票价格,我们使用订单项符号和服务键设置期望,服务键只是每个服务的唯一元素名称。这样,我们将确保所有请求都有唯一的键集用于其期望。
现在,让我们看看如何设置测试来检查 BizTalk 发送的数据。在这里,我们使用一个类似于相关 ID 的概念;即,为每个请求分配一个 GUID,当模拟 Web 服务接收到请求并匹配期望时,就会传回此 GUID。我们所要做的就是将这些 GUID 与相关的订单项一起保留在我们的上下文中。WebServiceMock
有一个名为 MessageMatchedExpectation
的事件,当期望匹配时会引发该事件。同样,我们可以使用 PseudoXPath
在请求上进行操作,以确保 BizTalk 正确传递了值
Assert.AreEqual(_context.Guids[e.Expectation.Id].Symbol,
PseudoXPath.GetValueOfElement(e.Message, "symbol"));
另外,对于转换价格,我们可以编写
Assert.AreEqual(_context.ForeignGuids[e.Expectation.Id].CurrentPrice.ToString(),
PseudoXPath.GetValueOfElement(e.Message, "price"));
Assert.AreEqual(StockSymbolParser.GetMarket
(_context.ForeignGuids[e.Expectation.Id].Symbol),
PseudoXPath.GetValueOfElement(e.Message, "market"));
因此,如果所有这些测试都有效,那么我们可以确保我们的编排的所有输出都是正确的。
现在,让我们回到 Excellence 的核心,即仪器化跟踪。正如我们在本文第一部分中所述,对于我们编排中的每个步骤,我们都输出一个跟踪,并使用 LogFileResetEvent
来等待每个跟踪,并在未收到任何步骤的跟踪时使测试失败。

循环之前所有步骤的 Wait
子字符串将仅使用客户 ID 进行设置。但是,循环内的步骤将同时包含客户 ID 和股票符号。因此,我们将为编排的前两个步骤设置这两个 Wait()
语句
// receive message
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");
// record count
if(!debug.Wait(System.String.Format("{0}_{1}",
Connexita.StockPurchase.Helper.StockPurchaseSteps.
Connexita_StockPurchase_GetRecordCount,
_context.CustomerId)))
Assert.Fail("Timed out on Connexita_StockPurchase_GetRecordCount");
由于我们将以与编排相同的方式循环,因此我们可以将 Wait()
语句放在循环内,就像编排将执行它们一样。例如
if (!debug.Wait(System.String.Format("{0}_{1}_{2}",
Connexita.StockPurchase.Helper.StockPurchaseSteps.
Connexita_StockPurchase_AssignMarket,
_context.CustomerId, item.Symbol)))
Assert.Fail("Timed out on Connexita_StockPurchase_AssignMarket");
根据股票是本地还是外国,我们将在循环结束时进行不同的设置。此条件将匹配编排中的代码。
if (item.IsLocal())
{
// local
if (!debug.Wait(System.String.Format("{0}_{1}_{2}",
Connexita.StockPurchase.Helper.StockPurchaseSteps.
Connexita_StockPurchase_ItIsLocalStock,
_context.CustomerId, item.Symbol)))
Assert.Fail("Timed out on Connexita_StockPurchase_ItIsLocalStock");
….
最后,我们设置并行步骤检查
// 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.");
正如您所看到的,并行操作中并没有太多内容。它们所要做的就是调用 DoAction()
,这只是一个随机延迟,但这可能足以展示如何使用 Excellence 处理和单元测试并行形状。
好了,就到这里了!单元测试一个包含条件、循环、并行形状,并涉及公开 Web 服务和其他 Web 服务的编排,就是这么简单。
只有几点需要提及。首先,您可以使用 NUnit 的控制台窗口查看 LogFileResetEvent
输出的各种信息。这对于调试并行 WaitAll()
调用特别有用。此外,最好为每个并行分支只包含一个键;例如,如果您的并行形状有三个分支,请使用带有三个键的 WaitAll()
,每个分支一个,通常是最后一个。原因是,这将简化测试,而且我还遇到过由于同步导致的 WaitAll()
的怪异行为,虽然很少发生但仍然会出现。如果您碰巧发现这些小错误,我很感激您能告诉我。
历史
- 2009 年 3 月 9 日:初始发布
- 2009 年 3 月 24 日:已更新源代码