单元测试工作流和活动

来源:互联网 发布:程序员35岁 以后的路 编辑:程序博客网 时间:2024/05/14 22:53
http://msdn.microsoft.com/zh-cn/magazine/dd179724.aspx
作为 Windows Workflow Foundation (Windows WF) 组件的作者和讲师,经常有人问我如何对这些组件进行测试。在学习如何使用新框架的过程中,我们需要确定如何将工具和组件合并到您的开发过程中。本月我将介绍与测试活动、工作流和相关组件有关的一些挑战和技术。这绝对不能算做单元测试或测试驱动开发的最佳实践指南。而是为了向您提供一些可用于在您的首选开发方法中测试工作流的技术。

测试注意事项
编写单元测试时,需要通过代码测试常规途径,但事情绝不那样简单。除“适宜的途径”或预期的执行途径外,还必须测试特殊途径。另外,许多要进行测试的项目都具有依赖关系,对所有这些项目进行测试是不切实际的。例如,业务对象可能依赖于数据访问层。但在测试中,通常使用模拟对象(此类对象与数据访问层具有相同的接口,但不会在运行时访问数据库),而不依赖于数据库。
工作流也存在同样的现象,只是在某些情况下方法略有不同。有时,仅由于 Windows WF 是一项新技术,可能会导致一些开发人员很难决定如何测试其代码。
除了典型的测试问题以外,使用 Windows WF 之类的框架还会在其他方面加大复杂性。由于 Windows WF 以管理工作流和活动执行的运行时为基础,因此,测试基本上必须要使用运行时。运行时执行中涉及的许多类都是密封或受保护的,因此,模拟这些对象几乎是不可能实现的。另外,很难确保创建的所有模拟对象能提供运行时中的全部必需功能。因此,测试工作流或活动通常需要运行时。
使用工作流运行时会带来许多有趣的挑战。首先,由于运行时可以使用多种计划程序,每一个计划程序都拥有其自己的线程模型,因此必须使用特定的线程模型编写单元测试。最适合用于测试的计划程序可能不是最适合您工作流最终宿主的计划程序。
运行时将为开发人员提供对运行时有用的服务,但也会使测试更具挑战性,异常处理就是一个例子。运行时会捕捉所有异常并在工作流中对其进行管理。这意味着预期异常测试的工作方式将与在标准 Microsoft .NET Framework 代码中的略有不同。
当测试使用 Windows WF 构建的解决方案时,通常需要进行测试的组件是:活动、工作流、规则和自定义运行时服务。在本文的其余部分,我将介绍开发人员在测试这些组件时会遇到的常见问题,以及一些解决这些问题的技巧。

测试活动
测试活动时,能够分别执行这些测试并提供输入和测试输出是很重要的。应将活动作为单个组件进行测试,而不是在工作流上下文中测试活动。幸运的是,在 Windows WF 中,所有工作流只是活动。这意味着运行时可以将任何活动视为工作流执行,即使是像电子邮件活动一样简单的活动。
在执行活动之前,必须设置运行时并向其添加相应的计划程序和其他运行时服务。通常,测试活动时最容易使用的是 ManualWorkflowSchedulerService,因为它允许活动在单元测试所在的同一线程上执行,并能最大程度地控制执行。在宿主(在本例中为测试)提供完成工作的线程之前,计划程序在工作流或活动中并不执行任何操作。
测试工作流时,我通常使用测试设置和清除方法来初始化运行时并将其正常关闭。在测试方法中,由于知道我执行测试的运行时是干净的,我可以重点执行特定的测试活动。图 1 显示了用于简单活动测试的代码,该测试使用 Windows WF 中的功能将参数传递给工作流,从而对工作流进行初始化。
Dictionary<string, object> results = null;Exception ex = null;runtime.WorkflowCompleted += delegate(  object sender, WorkflowCompletedEventArgs wce) {  results = wce.OutputParameters; };Dictionary<string, object> wfParams = new Dictionary<string, object>();wfParams.Add("To", "mmnet30@microsoft.com");wfParams.Add("From", "donotreply@example.org");wfParams.Add("Subject", "Unit testing");WorkflowInstance instance = runtime.CreateWorkflow(  typeof(WFComponents.SendMailActivity), wfParams); instance.Start();ManualWorkflowSchedulerService man =   runtime.GetService<ManualWorkflowSchedulerService>();man.RunWorkflow(instance.InstanceId);//the workflow is done, now we can test the outputsAssert.IsNotNull(results, "No results found");Assert.AreEqual<string>(wfParams["To"].ToString(),   results["To"].ToString(), "To address was changed");
此代码中有几个需要注意的地方。首先,SendMailActivity 被用作工作流的定义并独立于任何包含的工作流执行。参数通过标准 Windows WF 技术传递给活动,在这些技术中由值组成的字典被传递给运行时并映射为活动的属性。采用手动计划程序来执行工作流,因此,当活动已做好执行的准备时会使用 RunWorkflow 方法。RunWorkflow 方法完成后,简单的活动便完成,可以提取输出参数并做出声明。
请注意,输出参数是在 WorkflowCompleted 事件期间收集的,引用被映射为局部变量,然后可以检查此变量。由于使用的是手动计划程序,因此 WorkflowCompleted 事件与该测试中的所有其他工作都是在同一线程上发生的。
测试期间可以使用其他计划程序,但这需要使用更加复杂的测试代码。例如,如果使用默认工作流计划程序,则需要某种等待句柄来阻止执行,直到工作流完成为止。您在 Visual Studio 中可以看到此类型等待的一个示例,即创建新顺序工作流控制台应用程序并检查 program.cs 文件。
第一个测试负责正常执行,但出现异常情况会怎么样?大部分单元测试框架可以声明预期异常来简化测试开发。例如,此代码展现的是:如何通过 Visual Studio 2008 中的单元测试在测试方法中声明预期异常:
[TestMethod][ExpectedException(typeof(ArgumentNullException))]public void MailWithInvalidFrom(){...}
如在 Windows WF 中进行测试时使用此方法,运行时会捕获异常并在其终止工作流时引发事件(而不是在宿主中抛出异常),从而在事件参数中传递异常。这为处理异常提供了两个选项:不使用测试框架的内置行为,或者处理终止事件并重新抛出异常。
您可能认为从事件处理程序抛出异常会起作用,但遗憾的是并不是这样,因为运行时会捕获重新抛出的异常。图 2 显示了 SendMailActivity 的另一个测试,这次是无法传递 From 地址,这会导致代码中出现 ArgumentNullException。
[TestMethod][ExpectedException(typeof(ArgumentNullException))]public void MailWithInvalidFrom() {  Exception ex = null;  runtime.WorkflowTerminated += delegate(    object sender, WorkflowTerminatedEventArgs wte) {    ex = wte.Exception; };  Dictionary<string, object> wfParams =     new Dictionary<string, object>();  wfParams.Add("To", "mmnet30@microsoft.com");  //wfParams.Add("From", "donotreply@example.org");  wfParams.Add("Subject", "Unit testing exception");  WorkflowInstance instance = runtime.CreateWorkflow(  typeof(WFComponents.SendMailActivity), wfParams);  instance.Start();  ManualWorkflowSchedulerService man =     runtime.GetService<ManualWorkflowSchedulerService>();  man.RunWorkflow(instance.InstanceId);  //exception should be thrown by now  if (ex != null)  throw (ex);  else  Assert.Fail("Exception should have been thrown");}
正如您所看到的,可以从 WorkflowTerminated 事件的参数提取异常。但这样做之后,必须抛出异常才能使用 ExpectedException 行为。此时,您可能会觉得最好是使用 Assert 并确保异常属于正确的类型,而不在抛出异常之后使用 ExpectedException 属性。
至此,您可能已经注意到,测试代码还没有测试发送电子邮件的实际输出。对于电子邮件而言,很容易在应用程序配置文件中设置 system.net/mailSettings 配置,来确保消息写入文件系统。但之后必须打开并读取这些消息以验证输出。测试此功能一个更好的方式是使用模拟对象提取出电子邮件进程。
在 Windows WF 中,活动可以直接进行工作,也可以使用加载到运行时的对象(称为运行时服务)。使用运行时服务来处理工作的一个好处在于由宿主进程负责将服务添加到运行时中。如果活动要在 ASP.NET 应用程序中使用,则代码可能与该活动在 Windows 服务中使用时有所不同。活动只向运行时查询特定类型的对象,然后对其调用方法。
使用此模型来测试非常有效,因为可在测试期间使用模拟对象替换运行时服务来帮助测试。您的活动中似乎有许多额外的工作,而且似乎还有一项硬性要求:即所有宿主必须添加运行时服务来支持您的活动。但是,您的活动可以被编写为如果运行时服务存在,则此活动使用该服务,否则,此活动提供其自己的实现。
以下是 SendMailActivity 的执行方法,显示了运行时服务的存在状况产生的不同执行。如果发现了服务,则通过该服务发送消息;否则,直接使用 System.Net.Mail 命名空间发送消息:
IEmailService mailSvc =  executionContext.GetService<IEmailService>();MailMessage msg = new MailMessage(From, To);msg.Subject = Subject;//use the mail service if it is presentif (mailSvc != null) {  mailSvc.SendMessage(msg);}else {  SmtpClient client = new SmtpClient();  client.Send(msg);} 
请注意,此活动是使用接口而不是具体类,这提供了关键抽象点。此活动并不依赖于具体实现,而是接受运行时添加的服务实现。然后,测试可以使用服务的模拟实现来辅助测试,如图 3 所示。
[TestMethod]public void TestEmailWithMock() {  MockEmailService mock = new MockEmailService();  runtime.AddService(mock);  ...  //the workflow is done, now we can test the outputs  Assert.IsNotNull(results, "No results found");  MockEmailService email =     runtime.GetService<MockEmailService>();  Assert.IsNotNull(email,     "No email service found in the runtime");  Assert.AreEqual<string>(wfParams["To"].ToString(),    email.Message.To.ToString(),     "To address was changed");}
现在实际上并不一定要在测试中发送电子邮件,而且使用已添加的实现更易于验证测试的结果。这种抽象方法非常适合测试和灵活实现常规托管。

测试规则
作为许多工作流解决方案的重要组成部分,业务规则也为测试带来了挑战。要在工作流上下文外部完成执行,Windows WF 中的规则集并不是所面临的唯一挑战,但规则集是基于特定类型定义的,该类型一般是工作流类。通常,规则需要以某一个对象或一组对象作为输入,以可对其进行评估并采取相应的操作。对于 Windows WF,工作流本身通常是根对象,其他任何与规则进行交互的对象只是工作流的属性或活动。
执行工作流时,最常使用 Policy 活动来执行规则。在单元测试方案中,在没有活动的情况下直接测试规则更适合。要执行规则集,可以使用来自 System.Workflow.Activities.Rules 命名空间的多个类。另外,RuleSet 类可以通过 WorkflowMarkupSerializer 类从流进行反序列化,这样,您就可以在测试环境中使用在运行时所用的规则表示。
图 4 显示用于在单元测试中执行规则集的基本代码。规则集执行完成后,可在声明中使用在规则执行期间使用的对象验证预期行为。在本例中,首先要注意的是规则是针对客户对象(而不是针对工作流类型)执行的。这种刻意安排有几个原因。
//initialize test objectCustomer cust = new Customer();cust.Level = CustomerLevel.Gold;cust.CurrentAnnualPurchases = 1578;//create rule execution objectsRuleValidation val =   new RuleValidation(typeof(Customer), null);RuleExecution exec = new RuleExecution(val, cust);RuleSet ruleset = null;//get rules from file / deserializeWorkflowMarkupSerializer serializer =   new WorkflowMarkupSerializer();string rulePath = Path.Combine(  testContextInstance.TestDeploymentDir, "Customer.rules");RuleDefinitions defs =   (RuleDefinitions)serializer.Deserialize(  XmlReader.Create(rulePath));ruleset = defs.RuleSets[0];//execute rulesetruleset.Execute(exec);//test the outcomeAssert.AreEqual<BLL.CustomerLevel>(cust.Level,   CustomerLevel.Platinum);
使用 Policy 活动创建规则集的默认模式意味着根据工作流类型创建策略,并将其作为一种资源嵌入到程序集中。这不仅会引发测试问题,还可能使运行时的灵活性成为一大难题。它为测试带来的挑战是:为了测试规则集,必须创建并初始化工作流的一个实例。针对其他类型(如业务对象)创建规则不但使测试变得更加容易,而且让从工作流内使用规则也更容易了。您可以在 Windows WF MSDN 站点上找到使用规则的几个示例(包括创建自定义活动)。
测试规则包括创建和初始化充当规则输入的对象,以及初始化规则集和执行对象(包括规则验证程序和执行程序)所示的代码大部分是样板代码,可用于为在自定义对象(而不是工作流)上执行规则而构建的自定义活动。
有时,这种将规则的输入隔离成单独类型的方法是不可行的,因为规则依赖于工作流中各种活动的属性。在这些情况下,必须使用运行时的 CreateWorkflow 方法和初始化活动的属性创建工作流的实例。创建工作流时传递参数便可轻松地初始化工作流的属性,但初始化活动可能稍微有些棘手。如果活动的属性是依赖属性,那么它们只需绑定到工作流的属性,并在创建时进行初始化。
但是,如果活动的属性是标准 .NET 属性,则需要使用其他方法。简单的方法是将代码放入工作流的 Initialize 方法中,找出层次结构中的所有活动实例并设置其值。请注意,您不能从单元测试本身执行此项操作,因为工作流创建完成后,宿主代码无权直接访问正在运行的实例状态。
如果活动本身是动态创建的(如在 While 活动或状态机中),则使用活动作为您规则的输入时,情况会变得复杂。有关迭代活动相关问题的详细信息,请参阅 2007 年 6 月的基础内容专栏文章“工作流中的 ActivityExecutionContext”。在这些情形中,针对测试正确初始化活动的唯一方式是执行工作流并让复合活动创建活动克隆。我不打算花时间详细讨论这一方法,因为在此情况下大多数开发人员不愿意编写规则,因为规则本身将变得非常复杂。

测试运行时服务
运行时服务是指被添加到工作流运行时的任何 .NET 对象。Framework 附带了一些服务,它们提供了持久性、跟踪、计划以及运行时熟悉的其他广为人知的服务。这些服务是 Framework 的一部分,不应作为测试的重点。但是,开发自定义解决方案时,有两种运行时服务可能需要测试:为运行时提供已知功能的自定义服务以及与活动或运行时交互的自定义服务。
对于提供已知功能的服务(如自定义持久性服务),唯一可行的测试方法是在运行时执行这些服务。要管理执行并控制测试,最佳方法是使用工作流作为驱动因子。使用工作流允许控制可能导致您的服务被调用的条件(如工作流空闲、挂起和终止)。
就其他待测试服务而言,测试要求取决于服务的实现。例如,一些服务通过其引发的事件与运行时交互。其他服务更加直接地与工作流实例的相关信息进行交互,例如,要求实例标识符或当某个活动调用服务时使用当前 WorkBatch。在这些情况下,服务的最佳处理方式是在驱动程序工作流的上下文中进行。本文开头所示的方法可用来帮助进行测试。
一些对象仅存在于运行时中为活动提供服务,并不依赖于其他任何服务。如果属于这一情况,这些服务通常可以作为标准 .NET 组件进行测试。虽然对工作流运行时并没有特定的要求,但对于其他服务或依赖对象,测试可能仍使用模拟对象。或者,将单一活动用作驱动因子(如本专栏前面所示),此时该活动完全执行服务,确保在运行时执行时测试功能起作用。最后这个选择可能看似需要一些额外工作,但它进一步确保了您的代码将在运行时正确执行。

测试工作流
开发人员尝试对整个工作流进行单元测试时会遇到一个最大的难题。由于工作流通常对长时间运行的进程建模,这些进程要与多种服务和应用程序进行交互,因此测试的复杂性迅速加大。例如,给定工作流可以使用 Windows Communication Foundation (WCF) 调用多项服务,并将结果汇总为其逻辑的一部分。
可以通过为测试注入模拟对象来处理这些交互。实际上,通过 Windows WF 中的 WCF 活动,能更轻松地注入可测试服务端点。Send 活动会尝试使用 ChannelManagerService 类来解析端点。测试类可以轻松地添加与工作流中的预期端点匹配的命名端点,从而将绑定和地址更改为指向本地服务实现。使用此中间环节,调用多项服务的工作流在许多情况下都可以进行测试。有关使用 ChannelManagerService 类的详细信息,请参阅 2008 年 8 月期的《MSDN 杂志》
总之,真正的问题并不在于您是否需要测试您的工作流,而在于您将如何测试它们。在许多开发团队中,工作流的测试并不视为单元测试任务,而是被视为一项集成测试,在单元测试完成而且各种组件已处于良好的运转状态之后加以完成。如果您选择对您的工作流使用单元测试或对其他自动化测试使用驱动因子,您可以使用本文中提供的信息来帮助执行测试。

请将您想询问的问题和提出的意见发送至 mmnet30@microsoft.com。

Matt Milner 是一名独立软件顾问,擅长 Microsoft 技术,包括 .NET、Web 服务、Windows Workflow Foundation、Windows Communication Foundation 和 BizTalk Server。作为 Pluralsight 的一名讲师,Matt 讲授工作流、BizTalk Server 和 Windows Communication Foundation 方面的课程。Matt 与妻子 Kristen 和两个儿子住在明尼苏达州。您可通过 Matt 在 pluralsight.com/community/blogs/matt 开设的博客与他联系。
0 0
原创粉丝点击