C和NETCore测试驱动开发全绝不原创的飞龙

您如何验证您的跨平台.NETCore应用程序在部署到任何地方时都能正常工作?随着业务、团队和技术环境的发展,您的代码能够随之发展吗?通过遵循测试驱动开发的原则,您可以简化代码库,使查找和修复错误变得微不足道,并确保您的代码能够按照您的想法运行。

本书指导开发人员通过建立专业的测试驱动开发流程来创建健壮、可投入生产的C#7和.NETCore应用程序。为此,您将首先学习TDD生命周期的各个阶段、一些最佳实践和一些反模式。

在第一章介绍了TDD的基础知识后,您将立即开始创建一个示例ASP.NETCoreMVC应用程序。您将学习如何使用SOLID原则编写可测试的代码,并设置依赖注入。

接下来,您将学习如何使用xUnit.net测试框架创建单元测试,以及如何使用其属性和断言。一旦掌握了基础知识,您将学习如何创建数据驱动的单元测试以及如何在代码中模拟依赖关系。

在本书的最后,您将通过使用GitHub、TeamCity、VSTS和Cake来创建一个健康的持续集成流程。最后,您将修改持续集成构建,以测试、版本化和打包一个示例应用程序。

本书适用于希望通过实施测试驱动开发原则构建质量、灵活、易于维护和高效企业应用程序的.NET开发人员。

第一章,“探索测试驱动开发”,向您介绍了如何通过学习和遵循测试驱动开发的成熟原则来改善编码习惯和代码。

第二章,“使用.NETCore入门”,向您介绍了.NETCore和C#7的超酷新跨平台功能。我们将通过实际操作来学习,在UbuntuLinux上使用测试驱动开发原则创建一个ASP.NETMVC应用程序。

第三章,“编写可测试的代码”,演示了为了获得测试驱动开发周期的好处,您必须编写可测试的代码。在本章中,我们将讨论创建可测试代码的SOLID原则,并学习如何为依赖注入设置我们的.NETCore应用程序。

第四章,“.NETCore单元测试”,介绍了.NETCore和C#可用的单元测试框架。我们将使用xUnit框架创建一个共享的测试上下文,包括设置和清除代码。您还将了解如何创建基本的单元测试,并使用xUnit断言来证明单元测试的结果。

第五章,“数据驱动的单元测试”,介绍了允许您通过一系列数据输入来测试代码的概念,可以是内联的,也可以来自数据源。在本章中,我们将创建xUnit中的数据驱动单元测试或理论。

第六章,“模拟依赖关系”,解释了模拟对象是模仿真实对象行为的模拟对象。在本章中,您将学习如何使用Moq框架,使用Moq创建的模拟对象来隔离您正在测试的类与其依赖关系。

第七章,持续集成和项目托管,侧重于测试驱动开发周期的目标,即快速提供有关代码质量的反馈。持续集成流程将这种反馈周期延伸到发现代码集成问题。在本章中,您将开始创建一个持续集成流程,该流程可以为开发团队提供有关代码质量和集成问题的快速反馈。

第八章,创建持续集成构建流程,解释了一个出色的持续集成流程将许多不同的步骤整合成一个易于重复的流程。在本章中,您将配置TeamCity和VSTS使用跨平台构建自动化系统Cake来清理、构建、恢复软件包依赖关系并测试您的解决方案。

第九章,测试和打包应用程序,教您修改Cake构建脚本以运行xUnit测试套件。您将通过为.NETCore支持的各种平台版本化和打包应用程序来完成该过程。

假定您具有C#编程和MicrosoftVisualStudio的工作知识。

您可以按照以下步骤下载代码文件:

文件下载后,请确保使用最新版本的解压缩或提取文件夹:

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter句柄。这是一个例子:“为了使测试通过,您必须迭代实现生产代码。当实现以下IsServerOnline方法时,预计Test_IsServerOnline_ShouldReturnTrue测试方法将通过。”

代码块设置如下:

[Fact]publicvoidTest_IsServerOnline_ShouldReturnTrue(){boolisOnline=IsServerOnline();Assert.True(isOnline);}任何命令行输入或输出都是按照以下格式编写的:

sudoapt-getupdatesudoapt-getinstalldotnet-sdk-2.0.0粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“VisualStudioCode将尝试下载Linux平台所需的依赖项,Linux的Omnisharp和.NETCore调试器。”

警告或重要说明看起来像这样。

技巧和窍门看起来像这样。

当开发人员使用开发方法、编码风格和实践来构建代码库时,这些方法会自动使源代码变得僵化且难以维护,软件项目的质量会迅速下降。本章指出了导致编写糟糕代码的习惯和实践,因此应该避免。解释了应该学习的编程习惯、开发风格和方法,以便编写清洁和可维护的代码。

在本章中,我们将涵盖以下主题:

有两种类型的代码——好的代码和糟糕的代码。这两种类型的代码在编译时语法可能是正确的,运行代码可以得到预期的结果。然而,由于编写方式的原因,糟糕的代码在扩展或甚至对代码进行小改动时会导致严重问题。

当程序员使用不专业的方法和风格编写代码时,通常会导致糟糕的代码。此外,使用难以阅读的编码风格或格式,以及未能正确有效地测试代码都是糟糕代码的先兆。当程序员为了满足即将到来的截止日期和项目里程碑而牺牲专业精神时,代码可能会写得很糟糕。

我曾遇到一些软件项目,它们迅速成为被遗弃的遗留软件项目,因为不断出现的生产错误和无法轻松地满足用户的变更请求。这是因为这些软件应用程序在投入生产时积累了严重的技术债务,这是由于软件开发人员编写了糟糕的代码,导致了糟糕的设计和开发决策,并使用了已知会导致未来维护问题的编程风格。

源代码元素——方法、类、注释和其他工件——应该易于阅读、理解、调试、重构和扩展,如果需要由原始开发人员以外的其他开发人员进行;否则,糟糕的代码已经被编写。

当你在扩展或添加新功能时,你会知道你的代码有问题,因为你会破坏现有的工作功能。当代码部分无法解码或对其进行任何更改会使系统停止时,也会发生这种情况。糟糕的代码通常是因为不遵守面向对象和“不要重复自己”(DRY)原则或错误使用这些原则。

DRY是编程中的一个重要原则,旨在将系统分解为小组件。这些组件可以轻松管理、维护和重复使用,以避免编写重复的代码并使代码的不同部分执行相同的功能。

糟糕的代码不仅仅出现在代码库中;程序员写了糟糕的代码。大多数情况下,糟糕的代码可能是由于以下任何原因之一而写成的:

以下代码片段显示了与第三方smpp库紧密耦合的示例:

publicvoidSendSMS(){SmppManagersmppManager=newSmppManager();smppManager.SendMessage("0802312345","Hello","John");}publicclassSmppManager{privatestringsourceAddress;privateSmppClientsmppClient;publicSmppManager(){smppClient=newSmppClient();smppClient.Start();}publicvoidSendMessage(stringrecipient,stringmessage,stringsenderName){//sendmessageusingreferencedlibrary}}代码异味代码异味是由KentBeck首次使用的一个术语,它指出了源代码中的更深层次的问题。代码库中的代码异味可能来自于源代码中的复制、使用不一致或模糊的命名约定和编码风格、创建具有长参数列表的方法以及具有庞大方法和类,即知道并做太多事情,从而违反了单一责任原则。列表还在继续。

在源代码中常见的代码异味是当开发人员创建两个或更多执行相同操作的方法,几乎没有变化或在应该在单个点中实现的程序细节或事实在多个方法或类中复制,导致代码库难以维护。

以下两个ASP.NETMVC动作方法有代码行,创建了一个强类型的字符串年份和月份列表。这些代码行本来可以被重构为第三个方法,并被这两个方法调用,但却在这两个方法中被复制:

[HttpGet]publicActionResultGetAllTransactions(){Listyears=newList();for(inti=DateTime.Now.Year;i>=2015;i--)years.Add(i.ToString());Listmonths=newList();for(intj=1;j<=12;j++)months.Add(j.ToString());ViewBag.Transactions=GetTransactions(years,months);returnView();}[HttpGet]publicActionResultSearchTransactions(){Listyears=newList();for(inti=DateTime.Now.Year;i>=2015;i--)years.Add(i.ToString());Listmonths=newList();for(intj=1;j<=12;j++)months.Add(j.ToString());ViewBag.Years=years;ViewBag.Months=months;returnView();}另一个常见的代码异味出现在开发人员创建具有长参数列表的方法时,就像以下方法中所示:

publicvoidProcessTransaction(stringusername,stringpassword,floattransactionAmount,stringtransactionType,DateTimetime,boolcanProcess,boolretryOnfailure){//Dosomething}坏或破损的设计在实施应用程序时,经常会出现结构或设计和模式导致糟糕的代码,尤其是在错误使用面向对象编程原则或设计模式时。一个常见的反模式是意大利面条式编码。这在对面向对象理解不深的开发人员中很常见,这涉及创建具有不清晰结构、几乎没有可重用性以及对象和组件之间没有关系的代码库。这导致应用程序难以维护和扩展。

在经验不足的开发人员中有一种常见的做法,即在解决应用程序复杂性时不必要或不适当地使用设计模式。当错误使用设计模式时,会给代码库带来糟糕的结构和设计。使用设计模式应该简化复杂性,并为软件问题创建可读和可维护的解决方案。当某个模式导致可读性问题并明显增加了程序的复杂性时,值得重新考虑是否使用该模式,因为该模式被误用了。

例如,单例模式用于创建对资源的单个实例。单例类的设计应该有一个私有构造函数,没有参数,一个静态变量引用资源的单个实例,以及一个管理的公共手段来引用静态变量。单例模式可以简化对单一共享资源的访问,但如果没有考虑线程安全性,也可能会导致很多问题。两个或更多线程可以同时访问if(smtpGateway==null)这一行,如果这行被评估为true,就会创建资源的多个实例,就像下面代码中所示的实现一样:

publicclassSMTPGateway{privatestaticSMTPGatewaysmtpGateway=null;privateSMTPGateway(){}publicstaticSMTPGatewaySMTPGatewayObject{get{if(smtpGateway==null){smtpGateway=newSMTPGateway();}returnsmtpGateway;}}}命名程序元素有意义和描述性的元素命名可以极大地提高源代码的可读性。它可以让程序的逻辑流程更容易理解。令人惊讶的是,软件开发人员仍然会给程序元素起太短或者不够描述性的名字,比如给变量起一个字母的名字,或者使用缩写来命名变量。

对元素使用通用或模糊的名称会导致歧义。例如,将一个方法命名为Extract()或Calculate(),乍一看会导致主观解释。对变量使用模糊的名称也是如此。例如:

intx2;stringxxya;虽然程序元素的命名本身就是一门艺术,但是名称应该被选择来定义目的,并简要描述元素,并确保所选名称符合所使用的编程语言的标准和规则。

以下代码片段将执行其预期的功能,尽管其中包含使用糟糕的命名约定编写的元素,这影响了代码的可读性:

publicvoidupdatetableloginentries(){com.Connection=conn;SqlParameterpar1=newSqlParameter();par1.ParameterName="@username";par1.Value=main.username;com.Parameters.Add(par1);SqlParameterpar2=newSqlParameter();par2.ParameterName="@date";par2.Value=main.date;com.Parameters.Add(par2);SqlParameterpar3=newSqlParameter();par3.ParameterName="@logintime";par3.Value=main.logintime;com.Parameters.Add(par3);SqlParameterpar4=newSqlParameter();par4.ParameterName="@logouttime";par4.Value=DateTime.Now.ToShortTimeString();;com.Parameters.Add(par4);com.CommandType=CommandType.Text;com.CommandText="updateloginentriessetlogouttime=@logouttimewhereusername=@usernameanddate=@dateandlogintime=@logintime";openconn();com.ExecuteNonQuery();closeconn();}糟糕的源代码文档当使用编程语言的编码风格和约定编写代码时,可以很容易地理解代码,同时避免之前讨论过的糟糕的代码陷阱。然而,源代码文档非常有价值,在软件项目中的重要性不可低估。对类和方法进行简要而有意义的文档编写可以让开发人员快速了解它们的内部结构和操作。

发布未经充分测试的应用程序可能会产生灾难性后果和维护问题。值得注意的是美国国家航空航天局于1998年12月11日发射的火星气候轨道飞行器在接近火星时失败,原因是由于转换错误导致的软件错误,其中轨道飞行器的程序代码在计算时使用的是磅而不是牛顿。对负责计算度量标准的特定模块进行简单的单元测试可能会检测到错误并可能防止失败。

此外,根据2016年测试优先方法的现状报告,由名为QASymphony的测试服务公司对来自15个不同国家的200多家软件组织的测试优先方法的采用进行了调查,结果显示近一半的受访者在他们开发的应用程序中没有实施测试优先方法。

编写干净的代码需要有意识地保持专业精神,并在软件开发过程的各个阶段遵循最佳行业标准。从软件项目开发的一开始就应该避免糟糕的代码,因为通过糟糕的代码积累的坏账可能会减慢软件项目的完成速度,并在软件部署到生产环境后造成未来问题。

要避免糟糕的代码,你必须懒惰,因为一般说来懒惰的程序员是最好的和最聪明的程序员,因为他们讨厌重复的任务,比如不得不回去修复本可以避免的问题。尽量使用避免编写糟糕代码的编程风格和方法,以避免不得不重写代码以修复可避免的问题、错误或支付技术债务。

//ThedependencyinjectionwouldbedoneusingNinjectpublicISmppManagersmppManager{get;privateset;}publicvoidSendSMS(){smppManager.SendMessage("0802312345","Hello","John");}publicclassSmppManager{privatestringsourceAddress;privateSmppClientsmppClient;publicSmppManager(){smppClient=newSmppClient();smppClient.Start();}publicvoidSendMessage(stringrecipient,stringmessage,stringsenderName){//sendmessageusingreferencedlibrary}}publicinterfaceISmppManager{voidSendMessage(stringrecipient,stringmessage,stringsenderName);}声音架构和设计通过使用良好的开发架构和设计策略可以避免糟糕的代码。这将确保开发团队和组织具有高级架构、策略、实践、准则和治理计划,团队成员必须遵循以防止走捷径和避免在整个开发过程中出现糟糕的代码。

[HttpGet]publicActionResultGetAllTransactions(){varyearsAndMonths=GetYearsAndMonths();ViewBag.Transactions=GetTransactions(yearsAndMonths.Item1,yearsAndMonths.Item2);returnView();}[HttpGet]publicActionResultSearchTransactions(){varyearsAndMonths=GetYearsAndMonths();ViewBag.Years=yearsAndMonths.Item1;ViewBag.Months=yearsAndMonths.Item2;returnView();}private(List,List)GetYearsAndMonths(){Listyears=newList();for(inti=DateTime.Now.Year;i>=2015;i--)years.Add(i.ToString());Listmonths=newList();for(intj=1;j<=12;j++)months.Add(j.ToString());return(years,months);}此外,在代码异味部分中具有长参数列表的方法可以重构为使用C#PlainOldCLRObject(POCO)以实现清晰和可重用性:

publicvoidProcessTransaction(Transactiontransaction){//Dosomething}publicclassTransaction{publicstringUsername{get;set;}publicstringPassword{get;set;}publicfloatTransactionAmount{get;set;}publicstringTransactionType{get;set;}publicDateTimeTime{get;set;}publicboolCanProcess{get;set;}publicboolRetryOnfailure{get;set;}}开发团队应该有由团队成员共同制定的准则、原则和编码约定和标准,并应不断更新和完善。有效使用这些将防止软件代码库中的代码异味,并允许团队成员轻松识别潜在的糟糕代码。

遵循C#编码约定指南有助于掌握编写清晰、可读、易于修改和易于维护的代码。使用描述性的变量名称,代表它们的用途,如下面的代码所示:

您应该始终尝试编写自解释的代码。这可以通过良好的编程风格实现。以这样一种方式编写代码,使得您的类、方法和其他对象都是自解释的。新的开发人员应该能够使用您的代码,而不必在理解代码及其内部结构之前感到紧张。

编码元素应该具有描述性和意义,以向读者提供洞察力。在必须记录方法或类以提供进一步澄清的情况下,采用“保持简单”的方法,简要说明某个决定的原因。检查以下代码片段;没有人希望为包含200行代码的类阅读两页文档:

//////ThisclassusesSHA1algorithmforencryptionwithrandomlygeneratedsaltforuniqueness///publicclassAESEncryptor{//Codegoeshere}KISS,也称为“保持简单,愚蠢”,是一种设计原则,它指出大多数系统在保持简单而不是使其不必要地复杂时运行得最好。该原则旨在帮助程序员尽可能简化代码,以确保未来可以轻松维护代码。

为了避免由于用户需求变化而对系统进行修改时可能导致的未来问题,以及由于代码库中固有的糟糕代码和累积的技术债务而暴露的错误,您需要具有以未来为考量并接受变化的思维方式。

使用灵活的模式,并且在编写代码时始终遵循良好的面向对象开发和设计原则。大多数软件项目的需求在其生命周期内都会发生变化。假设某个组件或部分不会发生变化是错误的,因此请尝试建立一个机制,使应用程序能够优雅地接受未来的变化。

测试驱动开发(TDD)是一种迭代的敏捷开发技术,强调先测试开发,这意味着在编写生产就绪的代码之前编写测试。TDD技术侧重于通过不断重构代码来确保代码通过先前编写的测试,从而编写干净和高质量的代码。

TDD作为一种先测试的开发方法,更加强调构建经过充分测试的软件应用程序。这使开发人员能够根据在经过深思熟虑后定义的测试任务来编写代码。在TDD中,常见的做法是在编写实际应用程序代码之前编写测试代码。

TDD引入了一个全新的开发范式,并改变了你的思维方式,开始在甚至开始编写代码之前考虑测试你的代码。这与传统的开发技术相反,传统技术将代码测试推迟到开发周期的后期阶段,这种方法被称为最后测试开发(TLD)。

你可能会想,就像每个新接触TDD的开发人员一样,为什么要先写测试,因为你相信自己的编码直觉可以编写始终有效的干净代码,并且通常在编码完成后会测试整个代码。你的编码直觉可能是正确的,也可能不是。在代码通过一组书面测试用例并通过验证之前,没有办法验证这个假设;信任是好的,但控制更好。

TDD中的测试用例是通过用户故事或正在开发的软件应用程序的用例来准备的。然后编写代码并进行迭代重构,直到测试通过。例如,编写用于验证信用卡长度的方法可能包含用例来验证正确长度、不正确长度,甚至当空或空信用卡作为参数传递给方法时。

自TDD最初被推广以来,已经提出了许多变体。其中一种是行为驱动开发(BDD)或验收测试驱动开发(ATDD),它遵循TDD的所有原则,而测试是基于预期的用户指定行为。

关于TDD实践是何时引入计算机编程或者是哪家公司首先使用的,实际上没有任何书面证据。然而,1957年D.D.McCracken的《数字计算机编程》中有一段摘录,表明TDD的概念并不新鲜,早期的人们已经使用过,尽管名称显然不同。

在编码开始之前,可能会对结账问题进行第一次攻击。为了充分确定答案的准确性,有必要准备一个手工计算的检查案例,以便将来与机器计算的答案进行比较。这意味着存储程序机永远不会用于真正的一次性问题。总是必须有迭代的元素来使其付出。

此外,在1960年代初,IBM的人们为NASA运行了一个项目(ProjectMecury),他们利用了类似TDD的技术,进行了半天的迭代,并且开发团队对所做的更改进行了审查。这是一个手动过程,无法与我们今天拥有的自动化测试相比。

TDD最初是由KentBeck推广的。他将其归因于他在一本古老书中读到的一段摘录,其中TDD被描述为简单的陈述,你拿输入磁带,手动输入你期望的输出磁带,然后编程直到实际输出磁带与期望输出相匹配。当他在Smalltalk开发了第一个xUnit测试框架时,KentBeck重新定义了TDD的概念。

可以肯定地说,Smalltalk社区在TDD变得普遍之前就已经使用了TDD,因为社区中使用了SUnit。直到KentBeck和其他爱好者将SUnit移植到JUnit之后,TDD才变得广为人知。从那时起,不同的测试框架已经被开发出来。一个流行的工具是xUnit,可以为大量编程语言提供端口。

此外,一些开发人员抱怨模拟可能会使TDD变得非常困难和令人沮丧,因为所需的依赖关系不应该在实现依赖代码的同时实现,而应该进行模拟。使用传统的测试最后的方法,可以实现依赖关系,然后可以测试代码的所有不同部分。

另一个常见的误解是,在真正意义上,直到确定设计依赖于代码实现之前,测试才不能被编写。这是不正确的,因为采用TDD将确保对代码实现的计划清晰明了,从而产生一个适当的设计,可以帮助编写高效可靠的测试。

有时候,一些人会将TDD和单元测试混为一谈,认为它们是一样的。TDD和单元测试并不相同。单元测试涉及在最小的编码单元或级别上实践TDD,这是一种方法或函数,而TDD是一种技术和设计方法,包括单元测试、集成测试以及验收测试。

刚接触TDD的开发人员经常认为在编写实际代码之前必须完全编写测试。事实恰恰相反,因为TDD是一种迭代技术。TDD倾向于探索性过程,你编写测试并编写足够的代码。如果失败,就重构代码直到通过,然后可以继续实现应用程序的下一个功能。

TDD并不是一个可以自动修复所有糟糕编码行为的灵丹妙药。你可以实践TDD,但仍然编写糟糕的代码甚至糟糕的测试。如果没有正确使用TDD原则和实践,或者试图在不适合使用TDD的地方使用TDD,这是可能的。

TDD,如果正确和适当地完成,可以带来良好的投资回报,因为它有助于开发自测代码,从而产生具有更少或没有错误的健壮软件应用程序。这是因为大部分可能出现在生产中的错误和问题在开发阶段已经被捕捉和修复了。

除了源代码文档,编写测试也是一种良好的编码实践,因为它们作为源代码的微型文档,可以快速理解代码的工作原理。测试将显示预期的输入以及预期的输出或结果。从测试中可以轻松理解应用程序的结构,因为所有对象都将有测试,以及对象方法的测试,显示它们的使用方式。

正确和持续地实践TDD有助于编写具有良好抽象、灵活设计和架构的优雅代码。这是因为,为了有效地测试应用程序的所有部分,各种依赖关系需要被分解成可以独立测试的组件,并在集成后进行测试。

代码的清晰性在于使用最佳行业标准编写代码,易于维护,可读性强,并且编写了用于验证其一致行为的测试。这表明没有测试的代码是糟糕的代码,因为没有直接验证其完整性的特定方式。

测试软件项目可以采用不同的形式,通常由开发人员和测试分析员或专家进行。测试是为了确定软件是否符合其指定的期望,如果可能的话,识别错误,并验证软件是否可用。大多数程序员通常认为测试和调试是一样的。调试是为了诊断软件中的错误和问题,并采取可能的纠正措施。

这是测试的一个级别,涉及测试构成软件应用程序组件的每个单元。这是测试的最低级别,它在方法或函数级别进行。它主要由程序员完成,特别是为了显示代码的正确性和要求是否已经正确实现。单元测试通常具有一个或多个输入和输出。

这是通常在软件开发中进行的第一级测试,旨在隔离软件系统的单元并独立或隔离地测试它们。通过单元测试,系统中固有的问题和错误可以在开发过程的早期轻松检测到。

集成测试是通过组合和测试不同的单元或组件来完成的,这些单元或组件必须在隔离状态下进行测试。这个测试是为了确保应用程序的不同单元可以共同工作以满足用户的需求。通过集成测试,您可以在不同组件交互和交换数据时发现系统中的错误。

这项测试可以由程序员、软件测试人员或质量保证分析员进行。可以使用不同的方法进行集成测试:

这个测试级别是您验证整个集成系统以确保其符合指定的用户需求。这个测试通常在集成测试之后立即进行,由专门的测试人员或质量保证分析员进行。

整个软件系统套件是从用户的角度进行测试,以识别隐藏的问题或错误和可用性问题。对实施的系统进行了严格的测试,使用系统应处理的真实输入,并验证输出是否符合预期数据。

用户验收测试通常用于指定软件应用程序的工作方式。这些测试是为业务用户和程序员编写的,用于确定系统是否符合期望和用户特定要求,以及系统是否根据规格完全和正确地开发。这项测试由最终用户与系统开发人员合作进行,以确定是否正式接受系统或进行调整或修改。

TDD的实践有助于设计清晰的代码,并作为大型代码库中回归的缓冲。它允许开发人员轻松确定新实施的功能是否通过运行测试时获得的即时反馈破坏了先前正常工作的其他功能。TDD的工作原理如下图所示:

这是技术的初始步骤,您必须编写描述要开发的组件或功能的测试。组件可以是用户界面、业务规则或逻辑、数据持久性例程,或实现特定用户需求的方法。测试需要简洁,并应包含组件测试所需的数据输入和期望的预期结果。

在编写测试时,从技术上讲,你已经解决了一半的开发任务,因为通过编写测试来构思代码的设计。在编写的测试之后,更容易处理困难的代码,这就是已经编写的测试。在这一点上,作为TDD新手,不要期望测试是100%完美或具有完整的代码覆盖率,但通过持续的练习和充分的重构,这是可以实现的。

在编写完测试之后,你应该编写足够的代码来实现之前编写的测试所需的功能。请记住,这里的目标是尽量采用良好的实践和标准来编写代码,以使测试通过。应避免所有导致编写糟糕或糟糕代码的方法。

尽量避免测试过度拟合,即为了使测试通过而编写代码的情况。相反,你应该编写代码来实现功能或用户需求,以确保覆盖功能的每种可能用例,避免代码在测试用例执行和生产环境中执行时具有不同的行为。

当你确信已经有足够的代码使测试通过时,你应该运行测试,使用你选择的测试套件。此时,测试可能会通过或失败。这取决于你如何编写代码。

TDD的一个基本规则是多次运行测试,直到测试通过。最初,在代码完全实现之前运行测试时,测试将失败,这是预期的行为。

为了实现完整的代码覆盖率,测试和源代码都必须进行重构和多次测试,以确保编写出健壮且干净的代码。重构应该是迭代的,直到实现完整的覆盖率。重构步骤应该删除代码中的重复部分,并尝试修复任何代码异味的迹象。

TDD的本质是编写干净的代码,从而构建可靠的应用程序,这取决于所编写的测试类型(单元测试、验收测试或集成测试)。重构可以局部地影响一个方法,也可以影响多个类。例如,在重构一个接口或一个类中的多个方法时,建议您逐渐进行更改,一次一个测试,直到所有测试及其实现代码都被重构。

部分采用该技术也可能减少TDD的全部好处。在团队中只有少数开发人员使用该技术而其他人不使用的情况下,这将导致代码片段化,其中一部分代码经过测试,另一部分没有经过测试,从而导致应用程序不可靠。

应避免为自然微不足道或不需要的代码编写测试;例如,为对象访问器编写测试。测试应该经常运行,特别是通过测试运行器、构建工具或持续集成工具。不经常运行测试可能导致情况,即即使已经进行了更改并且组件可能失败,代码基地的真实状态也不为人所知。

TDD技术遵循一个被称为红-绿-重构循环的原则,红色状态是初始状态,表示TDD循环的开始。在红色状态下,测试刚刚被编写,并且在运行时将失败。

下一个状态是绿色状态,它显示在实际应用代码编写后测试已通过。重构代码是确保代码完整性和健壮性的重要步骤。重构将反复进行,直到代码满足性能和需求期望为止。

在周期开始时,尚未编写用于运行测试的生产代码,因此预计测试将失败。例如,在以下代码片段中,IsServerOnline方法尚未实现,当运行Test_IsServerOnline_ShouldReturnTrue单元测试方法时,它应该失败。

publicboolIsServerOnline(){returnfalse;}[Fact]publicvoidTest_IsServerOnline_ShouldReturnTrue(){boolisOnline=IsServerOnline();Assert.True(isOnline);}为了使测试通过,您必须迭代实现生产代码。当实现以下IsServerOnline方法时,预期Test_IsServerOnline_ShouldReturnTrue测试方法将通过。

publicboolIsServerOnline(){stringaddress="localhost";intport=8034;SmppManagersmppManager=newSmppManager(address,port);boolisOnline=smppManager.TestConnection();returnisOnline;}[Fact]publicvoidTest_IsServerOnline_ShouldReturnTrue(){boolisOnline=IsServerOnline();Assert.True(isOnline);}当测试运行并通过时,根据您使用的测试运行器显示绿色,这会立即向您提供有关代码状态的反馈。这让您对代码的正确运行和预期行为感到自信和内心的喜悦。

重构是一个迭代的努力,您将不断修改先前编写的代码以通过测试,直到它达到了生产就绪状态,并且完全实现了需求,并且适用于所有可能的用例和场景。

通过本章讨论的原则和编码模式,可以避免大多数潜在的软件项目维护瓶颈。成为专业人士需要保持一致性,要有纪律性,并坚持良好的编码习惯、实践,并对TDD持有专业态度。

编写易于维护的清晰代码将在长期内得到回报,因为将需要更少的工作量来进行用户请求的更改,并且当应用程序始终可供使用且几乎没有错误时,用户将感到满意。

在下一章中,我们将探索.NETCore框架及其能力和局限性。此外,我们将在审查C#编程语言的第7版中介绍的新功能之前,先了解MicrosoftVisualStudioCode。

当微软发布第一个版本的.NETFramework时,这是一个创建、运行和部署服务和应用程序的平台,它改变了游戏规则,是微软开发社区的一场革命。使用初始版本的框架开发了几个尖端应用程序,然后发布了几个版本。

多年来,.NETFramework得到了蓬勃发展和成熟,支持多种编程语言,并包含了多个功能,使得在该平台上编程变得简单而有价值。但是,尽管框架非常强大和吸引人,但限制了开发和部署应用程序只能在微软操作系统变体上进行。

为了为开发人员解决.NETFramework的限制,创建一个面向云的、跨平台的.NETFramework实现,微软开始使用.NETFramework开发.NETCore平台。随着2016年版本1.0的推出,.NET平台的应用程序开发进入了一个新的维度,因为.NET开发人员现在可以轻松地构建在Windows、Linux、macOS和云、嵌入式和物联网设备上运行的应用程序。.NETCore与.NETFramework、Xamarin和Mono兼容,通过.NET标准。

本章将介绍.NETCore和C#7的超酷新跨平台功能。我们将在UbuntuLinux上使用TDD创建一个ASP.NETMVC应用程序来学习。在本章中,我们将涵盖以下主题:

.NETCore是一个跨平台的开源开发框架,可以在Windows、Linux和macOS上运行,并支持x86、x64和ARM架构。.NETCore是从.NETFramework分叉出来的,从技术上讲,它是后者的一个子集,尽管是简化的、模块化的。.NETCore是一个开发平台,可以让您在开发和部署应用程序时拥有很大的灵活性。新平台使您摆脱了通常在应用程序部署过程中遇到的麻烦。因此,您不必担心在部署服务器上管理应用程序运行时的版本。

目前,版本2.0.7中,.NETCore包括具有出色性能和许多功能的.NET运行时。微软声称这是最快的.NET平台版本。它有更多的API和更多的项目模板,比如用于在.NETCore上运行的ReactJS和AngularJS应用程序的模板。此外,版本2.0.7还有一组命令行工具,使您能够在不同平台上轻松构建和运行命令行应用程序,以及简化的打包和对Macintosh上的VisualStudio的支持。.NETCore的一个重要副产品是跨平台模块化Web框架ASP.NETCore,它是ASP.NET的全面重新设计,并在.NETCore上运行。

.NETFramework非常强大,并包含多个库用于应用程序开发。然而,一些框架的组件和库可能与Windows操作系统耦合。例如,System.Drawing库依赖于WindowsGDI,这就是为什么.NETFramework不能被认为是跨平台的,尽管它有不同的实现。

为了使.NETCore真正跨平台,像WindowsForms和WindowsPresentationFoundation(WPF)这样对Windows操作系统有很强依赖的组件已经从平台中移除。ASP.NETWebForms和WindowsCommunicationFoundation(WCF)也已被移除,并用ASP.NETCoreMVC和ASP.NETCoreWebAPI替代。此外,EntityFramework(EF)已经被简化,使其跨平台,并命名为EntityFrameworkCore。

此外,由于.NETFramework对Windows操作系统的依赖,微软无法开放源代码。然而,.NETCore是完全开源的,托管在GitHub上,并拥有一个不断努力开发新功能和扩展平台范围的蓬勃发展的开发者社区。

.NET标准是微软维护的一组规范和标准,所有.NET平台都必须遵循和实现。它正式规定了所有.NET平台变体都应该实现的API。目前.NET平台上有三个开发平台—.NETCore、.NETFramework和Xamarin。.NET平台需要提供统一性和一致性,使得在这三个.NET平台变体上更容易共享代码和重用库。

.NET平台提供了一组统一的基类库API的定义,所有.NET平台都必须实现,以便开发人员可以轻松地在.NET平台上开发应用程序和可重用库。目前的版本是2.0.7,.NET标准提供了新的API,这些API在.NETCore1.0中没有实现,但现在在2.0版本中已经实现。超过20,000个API已经添加到运行时组件中。

此外,.NET标准是一个目标框架,这意味着你可以开发你的应用程序以针对特定版本的.NET标准,使得应用程序可以在实现该标准的任何.NET平台上运行,并且你可以轻松地在不同的.NET平台之间共享代码、库和二进制文件。当构建应用程序以针对.NET标准时,你应该知道较高版本的.NET标准有更多可用的API,但并不是许多平台都实现了。建议你始终针对较低版本的标准,这将保证它被许多平台实现:

.NETCore作为通用应用程序开发平台,由CoreCLR、CoreFX、SDK和CLI工具、应用程序主机和dotnet应用程序启动器组成:

CoreCLR,也称为.NETCore运行时,是.NETCore的核心,是CLR的跨平台实现;原始的.NETFrameworkCLR已经重构为CoreCLR。CoreCLR,即公共语言运行时,管理对象的使用和引用,不同编程语言中的对象的通信和交互,并通过在对象不再使用时释放内存来执行垃圾收集。CoreCLR包括以下内容:

CoreFX是.NETCore的一组框架或基础库,它提供原始数据类型、文件系统、应用程序组合类型、控制台和基本实用工具。CoreFX包含了一系列精简的类库。

.NETCoreSDK包含一组工具,包括命令行界面(CLI)工具和编译器,用于构建应用程序和库在.NETCore上运行。SDK工具和语言编译器提供功能,通过CoreFX库支持的语言组件,使编码更加简单和快速。

为了启动一个.NETCore应用程序,dotnet应用程序主机是负责选择和托管应用程序所需运行时的组件。.NETCore有控制台应用程序作为主要应用程序模型,以及其他应用程序模型,如ASP.NETCore、Windows10通用Windows平台和XamarinForms。

.NETCore1.0仅支持C#和F#,但随着.NETCore2.0的发布,VB.NET现在也受到了平台的支持。支持的语言的编译器在.NETCore上运行,并提供对平台基础功能的访问。这是可能的,因为.NETCore实现了.NET标准规范,并公开了.NETFramework中可用的API。支持的语言和.NETSDK工具可以集成到不同的编辑器和IDE中,为您提供不同的编辑器选项,用于开发应用程序。

.NETCore和.NETFramework都非常适合用于开发健壮和可扩展的企业应用程序;这是因为这两个平台都建立在坚实的代码基础上,并提供了丰富的库和例程,简化了大多数开发任务。这两个平台共享许多相似的组件,因此可以在两个开发平台之间共享代码。然而,这两个平台是不同的,选择.NETCore作为首选的开发平台应受开发方法以及部署需求和要求的影响。

显然,当您开发的应用程序要在多个平台上运行时,应该使用.NETCore。由于.NETCore是跨平台的,因此适用于开发可以在Windows、Linux和macOS上运行的服务和Web应用程序。此外,微软推出了VisualStudioCode,这是一个具有对.NETCore的全面支持的编辑器,提供智能感知和调试功能,以及传统上仅在VisualStudioIDE中可用的其他IDE功能。

使用.NETCore,开发使用微服务架构的应用程序相对较容易。使用微服务架构,您可以开发使用不同技术混合的应用程序,例如使用PHP、Java或Rails开发的服务。您可以使用.NETCore开发微服务,以部署到云平台或容器中。使用.NETCore,您可以开发可扩展的应用程序,可以在高性能计算机或高端服务器上运行,从而使您的应用程序可以轻松为数十万用户提供服务。

虽然.NETCore是强大的、易于使用的,并在应用程序开发中提供了几个好处,但它目前并不适用于所有的开发问题和场景。微软从.NETFramework中删除了几项技术,以使.NETCore变得简化和跨平台。因此,这些技术在.NETCore中不可用。

当您的应用程序将使用.NETCore中不可用的技术时,例如在表示层使用WPF或WindowsForms,WCF服务器实现,甚至目前没有.NETCore版本的第三方库,建议您使用.NETFramework开发应用程序。

随着.NETCore2.0的发布,添加了新的模板,为可以在平台上运行的不同应用程序类型提供了更多选项。除了现有的项目模板之外,还添加了以下单页应用程序(SPA)模板:

.NETCore中的控制台应用程序与.NETFramework具有类似的结构,而ASP.NETCore具有一些新组件,包括以前版本的ASP.NET中没有的文件夹和文件。

多年来,ASP.NETWeb框架已经完全成熟,从Web表单过渡到MVC和WebAPI。ASP.NETCore是一个新的Web框架,用于开发可以在.NETCore上运行的Web应用程序和WebAPI。它是ASP.NET的精简和更简化版本,易于部署,并具有内置的依赖注入。ASP.NETCore可以与AngularJS、Bootstrap和ReactJS等框架集成。

ASP.NETCoreMVC,类似于ASP.NETMVC,是构建Web应用程序和API的框架,使用模型视图控制器模式。与ASP.NETMVC一样,它支持模型绑定和验证,标签助手,并使用Razor语法用于Razor页面和MVC视图。

ASP.NETCoreMVC应用程序的结构与ASP.NETMVC不同,添加了新的文件夹和文件。当您从VisualStudio2017,VisualStudioforMac或通过解决方案资源管理器中的CLI工具创建新的ASP.NETCore项目时,您可以看到添加到项目结构的新组件。

在ASP.NETCore中,新添加的wwwroot文件夹用于保存库和静态内容,例如图像,JavaScript文件和库,以及CSS和HTML,以便轻松访问并直接提供给Web客户端。wwwroot文件夹包含.css,图像,.js和.lib文件夹,用于组织站点的静态内容。

与ASP.NETMVC项目类似,ASP.NETMVC核心应用程序的根文件夹也包含模型,视图和控制器,遵循MVC模式的约定,以正确分离Web应用程序文件,代码和表示逻辑。

引入的一些其他文件包括appsettings.json,其中包含所有应用程序设置,bower.json,其中包含用于管理项目中使用的客户端包括CSS和JavaScript框架的条目,以及bundleconfig.json,其中包含用于配置项目的捆绑和最小化的条目。

与C#控制台应用程序类似,ASP.NETCore具有Program类,这是一个重要的类,包含应用程序的入口点。该文件具有用于运行应用程序的Main()方法,并用于创建WebHostBuilder的实例,用于创建应用程序的主机。在Main方法中指定要由应用程序使用的Startup类:

publicclassProgram{publicstaticvoidMain(string[]args){BuildWebHost(args).Run();}publicstaticIWebHostBuildWebHost(string[]args)=>WebHost.CreateDefaultBuilder(args).UseStartup().Build();}Startup.csASP.NETCore应用程序需要Startup类来管理应用程序的请求管道,配置服务和进行依赖注入。

不同的Startup类可以为不同的环境创建;例如,您可以在应用程序中创建两个Startup类,一个用于开发环境,另一个用于生产环境。您还可以指定一个Startup类用于所有环境。

Startup类有两个方法——Configure(),这是必须的,用于确定应用程序如何响应HTTP请求,以及ConfigureServices(),这是可选的,用于在调用Configure方法之前配置服务。这两种方法在应用程序启动时都会被调用:

publicclassStartup{publicStartup(IConfigurationconfiguration){Configuration=configuration;}publicIConfigurationConfiguration{get;}//Thismethodgetscalledbytheruntime.Usethismethodtoaddservicestothecontainer.publicvoidConfigureServices(IServiceCollectionservices){services.AddMvc();}//Thismethodgetscalledbytheruntime.UsethismethodtoconfiguretheHTTPrequestpipeline.publicvoidConfigure(IApplicationBuilderapp,IHostingEnvironmentenv){if(env.IsDevelopment()){app.UseDeveloperExceptionPage();}else{app.UseExceptionHandler("/Home/Error");}app.UseStaticFiles();app.UseMvc(routes=>{routes.MapRoute(name:"default",template:"{controller=Home}/{action=Index}/{id}");});}}微软的VisualStudioCode编辑器之旅开发.NETCore应用程序变得更加容易,不仅因为平台的流畅性和健壮性,还因为引入了VisualStudioCode,这是一个跨平台编辑器,可以在Windows、Linux和macOS上运行。在创建.NETCore应用程序之前,您不需要在系统上安装VisualStudioIDE。

VisualStudioCode虽然没有VisualStudioIDE那么强大和功能丰富,但确实具有内置的生产力工具和功能,使得使用它轻松创建.NETCore应用程序。您还可以在VisualStudioCode中安装用于多种编程语言的扩展,从VisualStudioMarketplace中获取,从而可以灵活地编辑其他编程语言编写的代码。

为了展示.NETCore的跨平台功能,让我们在Ubuntu17.04桌面版上设置.NETCore开发环境。在安装VisualStudioCode之前,让我们在UbuntuOS上安装.NETCore。首先,您需要通过在添加Microsoft产品feed之前注册Microsoft签名密钥来进行一次性注册:

cd/home/user/Documents/testappdotnetnewconsole这将产生以下输出:

由于VisualStudioCode是一个跨平台编辑器,可以安装在许多LinuxOS的变体上,逐渐添加其他Linux发行版的软件包。要在Ubuntu上安装VisualStudioCode,请执行以下步骤:

sudodpkg-i.debsudoapt-getinstall-f探索VisualStudioCode成功安装VisualStudioCode在您的Ubuntu实例上后,您需要在开始使用编辑器编写代码之前进行初始环境设置:

当您启动VisualStudioCode时,它会加载上次关闭时的状态,打开您上次访问的文件和文件夹。编辑器的布局易于导航和使用,并带有诸如:

多年来,C#编程语言已经成熟;随着每个版本的发布,越来越多的语言特性和构造被添加进来。这门语言最初只是由微软内部开发,并且只能在Windows操作系统上运行,现在已经成为开源和跨平台。这是通过.NETCore和语言的7版(7.0和7.1)实现的,它增加了语言的特色并改进了可用的功能。特别是语言的7.2版和8.0版的路线图承诺为语言增加更多功能。

元组在C#语言中的第4版中引入,并以简化形式使用,以提供具有两个或更多数据元素的结构,允许您创建可以返回两个或更多数据元素的方法。在C#7之前,引用元组的元素是通过使用Item1,Item2,...ItemN来完成的,其中N是元组结构中元素的数量。从C#7开始,元组现在支持包含字段的语义命名,引入了更清晰和更有效的创建和使用元组的方法。

您现在可以通过直接为每个成员分配一个值来创建元组。此赋值将创建一个包含元素Item1,Item2的元组:

varnames=("John","Doe");您还可以创建具有元组中包含的元素的语义名称的元组:

(stringfirstName,stringlastName)names=("John","Doe");元组的名称,而不是具有Item1,Item2等字段,将在编译时具有可以作为firstName和lastName引用的字段。

当使用POCO可能过于繁琐时,您可以创建自己的方法来返回具有两个或更多数据元素的元组:

classProgram{staticvoidMain(string[]args){GetNames(outstringfirstName,outstringlastName);}privatestaticvoidGetNames(outstringfirstName,outstringlastName){firstName="John";lastName="Doe";}}语言中已添加了对隐式类型输出变量的支持,允许编译器推断变量的类型:

publicrefstringGetFifthDayOfWeek(){string[]daysOfWeek=newstring[7]{"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};returnrefdaysOfWeek[4];}局部函数局部或嵌套函数允许您在另一个函数内定义一个函数。这个特性在一些编程语言中已经有很多年了,但是在C#7中才刚刚引入。当您需要一个小型且在container方法的上下文之外不可重用的函数时,这是一个理想的选择:

classProgram{staticvoidMain(string[]args){GetNames(outvarfirstName,outvarlastName);voidGetNames(outstringfirstName,outstringlastName){firstName="John";lastName="Doe";}}}模式匹配C#7包括模式,这是一种语言元素特性,允许您在除了对象类型之外的属性上执行方法分派。它扩展了已经在覆盖和虚拟方法中实现的语言构造,用于实现类型和数据元素的分派。在语言的7.0版本中,is和switch表达式已经更新以支持模式匹配,因此您现在可以使用这些表达式来确定感兴趣的对象是否具有特定模式。

引入的模式匹配可以采用三种形式:

publicvoidProcessLoan(Loanloan){if(loanisCarLoancarLoan){//dosomething}}publicvoidProcessLoan(Loanloan){if(loanisvarcarLoan){//dosomething}}publicvoidProcessLoan(Loanloan){if(loanisnull){//dosomething}}通过更新的switch表达式,您现在可以在case语句中使用模式和条件,并且可以在除了基本或原始类型之外的任何类型上进行switch,同时允许您使用when关键字来额外指定模式的规则:

publicvoidProcessLoan(Loanloan){switch(loan){caseCarLoancarLoan://dosomethingbreak;caseHouseLoanhouseLoanwhen(houseLoan.IsElligible==true)://dosomethingbreak;casenull://throwsomecustomexceptionbreak;default://dosomething}}数字分隔符和二进制字面量在C#7中添加了一种新的语法糖,即数字分隔符。这种构造极大地提高了代码的可读性,特别是在处理C#支持的不同数值类型的大量数字时。在C#7之前,操作大数值以添加分隔符有点混乱和难以阅读。引入数字分隔符后,您现在可以使用下划线(_)作为数字的分隔符:

varlongDigit=2_300_400_500_78;在这个版本中还新增了二进制字面量。现在可以通过简单地在二进制值前加上0b来创建二进制字面量:

varbinaryValue=0b11101011;创建一个ASP.NETMVCCore应用程序ASP.NETCore提供了一种优雅的方式来构建在Windows、Linux和macOS上运行的Web应用程序和API,这要归功于.NETCore平台的工具和SDK,这些工具和SDK简化了开发尖端应用程序并支持应用程序版本的并行。使用ASP.NETCore,您的应用程序的表面积更小,这可以提高性能,因为您只需要包含运行应用程序所需的NuGet包。ASP.NETCore还可以与客户端库和框架集成,允许您使用您已经熟悉的CSS和JS库来开发Web应用程序。

ASP.NETCore使用Kestrel运行,Kestrel是包含在ASP.NETCore项目模板中的Web服务器。Kestrel是一个基于libuv的进程内跨平台HTTP服务器实现,libuv是一个跨平台的异步I/O库,使构建和调试ASP.NETCore应用程序变得更加容易。它监听HTTP请求,然后将请求的详细信息和特性打包到一个HttpContext对象中。Kestrel可以作为独立的Web服务器使用,也可以与IIS或ApacheWeb服务器一起使用,其他Web服务器接收到的请求将被转发到Kestrel,这个概念被称为反向代理。

ASP.NETMVCCore为您提供了一个可测试的框架,用于使用ModelViewController模式进行现代Web应用程序开发,这使您可以充分实践测试驱动开发。在ASP.NET2.0中新增的是对Razor页面的支持,这现在是开发ASP.NETCoreWeb应用程序用户界面的推荐方法。

要创建一个新的ASP.NETMVCCore项目:

.NETCore平台虽然新,但正在迅速成熟,2.0.7版本引入了许多功能和增强功能,简化了构建不同类型的跨平台应用程序。在本章中,我们已经对平台进行了介绍,介绍了C#7的新功能,并在UbuntuLinux上设置了开发环境,同时创建了我们的第一个ASP.NETMVCCore应用程序。

在下一章中,我们将解释要注意避免编写不可测试代码,并且我们将带领您了解可以帮助您编写可测试和高质量代码的SOLID原则。

在第一章中,探索测试驱动开发,解释了编写代码以防止代码异味的陷阱。编写良好的代码本身就是一种艺术,而编写可以有效测试的代码的过程需要开发人员额外的努力和承诺,以编写可以反复测试而不费吹灰之力的干净代码。

编写测试和编写主要代码一样重要。为不可测试的代码编写测试非常累人且非常困难,这就是为什么首先应该避免不可测试的代码的原因。代码之所以不可测试,可能有不同的原因,比如代码做得太多(怪兽代码),违反了单一职责原则,架构使用错误,或者面向对象设计有缺陷。

有效和持续的TDD实践可以改善编写代码的过程,使测试变得更容易,从而提高代码质量和软件应用的健壮性。然而,当项目的代码库包含不可测试的代码部分时,编写单元测试或集成测试变得极其困难,甚至几乎不可能。

当软件项目的代码库中存在不可测试的代码时,软件开发团队无法明确验证应用程序功能和特性的一致行为。为了避免这种可预防的情况,编写可测试的代码不是一个选择,而是每个重视质量软件的严肃开发团队的必须。

不可测试的代码是由于违反了已被证明和测试可以提高代码质量的常见标准、实践和原则而产生的。虽然专业素养随着良好实践和经验的反复使用而来,但有一些常见的糟糕代码设计和编写方法即使对于初学者来说也是常识,比如在不需要时使用全局变量、代码的紧耦合、硬编码依赖关系或可能在代码中发生变化的值。

在本节中,我们将讨论一些常见的反模式和陷阱,当编写代码时应该注意,因为它们可能会使为生产代码编写测试变得困难。

具有紧密耦合依赖关系的单元测试代码将导致测试紧密耦合的不同对象。在单元测试期间,应该在构造函数中注入的依赖关系理想情况下应该很容易模拟,但这将是不可能的。这通常会减慢整体测试过程,因为所有依赖关系都必须在受测试的代码中构建。

在以下代码片段中,LoanProcessor与EligibilityChecker紧密耦合。这是因为EligibilityChecker在LoanProcessor构造函数中使用了new关键字进行实例化。对EligibilityChecker的更改将影响LoanProcessor,可能导致其出现故障。此外,对LoanProcessor中包含的任何方法进行单元测试都将导致EligibilityChecker被构造:

publicclassLoanProcessor{privateEligibilityCheckereligibilityChecker;publicLoanProcessor(){eligibilityChecker=newEligibilityChecker();}publicvoidProcessCustomerLoan(Loanloan){thrownewNotImplementedException();}}解决LoanProcessor中紧密耦合的一种方法是使用依赖注入(DI)。由于LoanProcessor无法在隔离环境中进行测试,因为EligibilityChecker对象将必须在构造函数中实例化,所以可以通过构造函数将EligibilityChecker注入到LoanProcessor中:

publicclassLoanProcessor{privateEligibilityCheckereligibilityChecker;publicLoanProcessor(EligibilityCheckereligibilityChecker){this.eligibilityChecker=eligibilityChecker;}publicvoidProcessCustomerLoan(Loanloan){boolisEligible=eligibilityChecker.CheckLoan(loan);thrownewNotImplementedException();}}通过注入EligibilityChecker,测试LoanProcessor变得更容易,因为这使您可以编写一个测试,其中模拟EligibilityChecker的实现,从而允许您在隔离环境中测试LoanProcessor。

另外,可以通过LoanProcessor类的属性或成员注入EligibilityChecker,而不是通过LoanProcessor构造函数传递依赖项:

publicclassLoanProcessor{privateEligibilityCheckereligibilityChecker;publicEligibilityCheckerEligibilityCheckerObject{set{eligibilityChecker=value;}}publicvoidProcessCustomerLoan(Loanloan){boolisEligible=eligibilityChecker.CheckLoan(eligibilityChecker);thrownewNotImplementedException();}}通过构造函数或属性注入依赖后,LoanProcessor和EligibilityChecker现在变得松散耦合,从而使得编写单元测试和模拟EligibilityChecker变得容易。

要使类松散耦合且可测试,必须确保该类不实例化其他类和对象。在类的构造函数或方法中实例化对象可能会导致无法注入模拟或虚拟对象,从而使代码无法进行测试。

要测试一个方法,您必须实例化或构造包含该方法的类。开发人员最常见的错误之一是创建我所谓的怪物构造函数,它只是一个做了太多工作或真正工作的构造函数,比如执行I/O操作、数据库调用、静态初始化、读取一些大文件或与外部服务建立通信。

当一个类设计有一个构造函数,用于初始化或实例化除值对象(列表、数组和字典)之外的对象时,该类在技术上具有非灵活的结构。这是糟糕的类设计,因为该类自动与其实例化的类紧密耦合,使得单元测试变得困难。具有这种设计的任何类也违反了单一责任原则,因为对象图的创建是可以委托给另一个类的责任。

在具有做大量工作的构造函数的类中测试方法会带来巨大的成本。实质上,要测试具有上述设计的类中的方法,您被迫要经历在构造函数中创建依赖对象的痛苦。如果依赖对象在构造时进行数据库调用,那么每次测试该类中的方法时,这个调用都会被重复,使得测试变得缓慢和痛苦:

publicclassLoanProcessor{privateEligibilityCheckereligibilityChecker;privateCurrencyConvertercurrencyConverter;publicLoanProcessor(){eligibilityChecker=newEligibilityChecker();currencyConverter=newCurrencyConverter();currencyConverter.DownloadCurrentRates();eligibilityChecker.CurrentRates=currencyConverter.Rates;}}在上述代码片段中,对象图的构建是在LoanProcessor构造函数中完成的,这肯定会使得该类难以测试。最好的做法是拥有一个精简的构造函数,它做很少的工作,并且对其他对象的了解很少,特别是它们能做什么,但不知道它们是如何做到的。

有时开发人员使用一种测试技巧,即为一个类创建多个构造函数。其中一个构造函数将被指定为仅用于测试的构造函数。虽然使用这种方法可以使类在隔离环境中进行测试,但也存在不好的一面。例如,使用多个构造函数创建的类可能会被其他类引用,并使用做大量工作的构造函数进行实例化。这可能会使得测试这些依赖类变得非常困难。

以下代码片段说明了为了测试类而创建单独构造函数的糟糕设计:

publicclassLoanProcessor{privateEligibilityCheckereligibilityChecker;privateCurrencyConvertercurrencyConverter;publicLoanProcessor(){eligibilityChecker=newEligibilityChecker();currencyConverter=newCurrencyConverter();currencyConverter.DownloadCurrentRates();eligibilityChecker.CurrentRates=currencyConverter.Rates;}//constructorfortestingpublicLoanProcessor(EligibilityCheckereligibilityChecker,CurrencyConvertercurrencyConverter){this.eligibilityChecker=eligibilityChecker;this.currencyConverter=currencyConverter;}}有一些重要的警告信号可以帮助您设计一个构造函数工作量较小的松散耦合类。避免在构造函数中使用new操作符,以允许注入依赖对象。您应该初始化并分配通过构造函数注入的所有对象到适当的字段中。轻量级值对象的实例化也应该在构造函数中完成。

此外,应避免静态方法调用,因为静态调用无法被注入或模拟。此外,应避免在构造函数中使用迭代或条件逻辑;每次测试类时,逻辑或循环都将被执行,导致过多的开销。

在设计类时要考虑测试,不要在构造函数中创建依赖对象或协作者。当您的类需要依赖其他类时,请注入依赖项。确保只创建值对象。在代码中创建对象图时,使用工厂方法来实现。工厂方法用于创建对象。

理想情况下,一个类应该只有一个责任。当您设计的类具有多个责任时,可能会在类之间产生交互,使得代码修改变得困难,并且几乎不可能对交互进行隔离测试。

有一些指标可以清楚地表明一个类做了太多事情并且具有多个责任。例如,当您在为一个类命名时感到困难,最终可能会在类名中使用and这个词,这表明该类做了太多事情。

一个具有多个责任的类的另一个标志是,类中的字段仅在某些方法中使用,或者类具有仅对参数而不是类字段进行操作的静态方法。此外,当一个类具有长列表的字段或方法以及许多依赖对象传递到类构造函数中时,表示该类做了太多事情。

在以下片段中,LoanProcessor类的依赖项已经整洁地注入到构造函数中,使其与依赖项松散耦合。然而,该类有多个改变的原因;该类既包含用于数据检索的代码,又包含业务规则处理的代码:

publicclassLoanProcessor{privateEligibilityCheckereligibilityChecker;privateDbContextdbContext;publicLoanProcessor(EligibilityCheckereligibilityChecker,DbContextdbContext){this.eligibilityChecker=eligibilityChecker;this.dbContext=dbContext;}publicdoubleCalculateCarLoanRate(Loanloan){doublerate=12.5F;boolisEligible=eligibilityChecker.IsApplicantEligible(loan);if(isEligible)rate=rate-loan.DiscountFactor;returnrate;}publicListGetCarLoans(){returndbContext.CarLoan;}}为了使类易于维护并且易于测试,GetCarLoans方法不应该在LoanProcessor中。应该将LoanProcessor与GetCarLoans一起重构到数据访问层类中。

具有本节描述的特征的类可能很难进行调试和测试。新团队成员可能很难快速理解类的内部工作原理。如果您的代码库中有具有这些属性的类,建议通过识别责任并将其分离到不同的类中,并根据其责任命名类来进行重构。

在代码中使用静态变量、方法和对象可能是有用的,因为这些允许对象在所有实例中具有相同的值,因为只创建了一个对象的副本并放入内存中。然而,测试包含静态内容的代码,特别是静态方法的代码,可能会产生测试问题,因为您无法在子类中覆盖静态方法,并且使用模拟框架来模拟静态方法是一项非常艰巨的任务:

publicstaticclassLoanProcessor{privatestaticEligibilityCheckereligibilityChecker=newEligibilityChecker();publicstaticdoubleCalculateCarLoanRate(Loanloan){doublerate=12.5F;boolisEligible=eligibilityChecker.IsApplicantEligible(loan);if(isEligible)rate=rate-loan.DiscountFactor;returnrate;}}当您创建维护状态的静态方法时,例如在前面片段中的LoanProcessor中的CalculateCarLoanRate方法,静态方法无法通过多态进行子类化或扩展。此外,静态方法无法使用接口进行定义,因此使得模拟变得不可能,因为大多数模拟框架都有效地使用接口。

在软件编程中,技术上没有严格遵循的硬性法律。然而,已经制定了各种原则和法律,作为指导方针,可以帮助软件开发人员和从业者,促进构建具有高内聚性和松耦合性的组件的软件应用程序,以充分封装数据,并确保产生易于理解和扩展的高质量源代码,从而降低软件的维护成本。其中之一就是迪米特法则(LoD)。

LoD,也称为最少知识原则,是开发面向对象软件应用程序的重要设计方法或规则。该规则于1987年由IanHolland在东北大学制定。通过正确理解这一原则,软件开发人员可以编写易于测试的代码,并构建具有更少或没有错误的软件应用程序。该法则的制定是:

LoD作为软件开发人员的启发式,以促进软件模块和组件中的信息隐藏。LoD有两种形式——对象或动态形式和类或静态形式。

LoD的类形式被公式化为:

类(C)的方法(M)只能向以下类的对象发送消息:

LoD的对象形式被公式化为:

在M中,消息只能发送到以下对象:

publicclassLoanProcessor{privateCurrencyConvertercurrencyConverter;publicLoanProcessor(LoanCalculatorloanCalculator){currencyConverter=loanCalculator.GetCurrencyConverter();}}前面的代码明显违反了LoD,这是因为LoanProcessor实际上并不关心LoanCalculator,因为它没有保留任何对它的引用。在代码中,LoanProcessor已经在与LoanCalculator进行交流,一个陌生人。这段代码实际上并不可重用,因为任何试图重用它们的类或代码都将需要CurrencyConverter和LoanProcessor,尽管从技术上讲,LoanCalculator在构造函数之外并未被使用。

为了对LoanProcessor编写单元测试,需要创建对象图。必须创建LoanCalculator以便CurrencyConverter可用。这会在系统中创建耦合,如果LoanCalculator被重构,这是可能的,那么可能会导致LoanProcessor出现故障,导致单元测试停止运行。

LoanCalculator类可以被模拟,以便单独测试LoanProcessor,但这有时会使测试变得难以阅读,最好避免耦合,这样可以编写灵活且易于测试的代码。

要重构前面的代码片段,并使其符合LoD并从类构造函数中获取其依赖项,从而消除对LoanCalculator的额外依赖,并减少代码的耦合:

loanCalculator.CalculateHouseLoan(loanDTO).GetPaymentRate().GetMaximumYearsToPay();你可能想知道这种现象如何违反了LoD。首先,代码缺乏可读性,不易维护。此外,代码行不可重用,因为一行代码中有三个方法调用。

这行代码可以通过减少交互和消除方法链来进行重构,以使其符合“不要和陌生人说话”的原则。这个原则解释了调用点或方法应该一次只与一个对象交互。通过消除方法链,生成的代码可以在其他地方重复使用,而不必费力理解代码的作用:

varhouseLoan=loanCalculator.CalculateHouseLoan(loanDTO);varpaymentRate=houseLoan.GetPaymentRate();varmaximumYears=paymentRate.GetMaximumYearsToPay();一个对象应该对其他对象的知识和信息有限。此外,对象中的方法应该对应用程序的对象图具有很少的认识。通过有意识的努力,使用LoD,你可以构建松散耦合且易于维护的软件应用程序。

软件应用程序开发的程序和方法,从第一步到最后一步,应该简单易懂,无论是新手还是专家都能理解。这些程序,当与正确的原则结合使用时,使开发和维护软件应用程序的过程变得简单和无缝。

开发人员不时采用和使用不同的开发原则和模式,以简化复杂性并使软件应用程序代码库易于维护。其中一个原则就是SOLID原则。这个原则已经被证明非常有用,是每个面向对象系统的严肃程序员必须了解的。

SOLID是开发面向对象系统的五个基本原则的首字母缩写。这五个原则是用于类设计的,表示为:

这些原则首次被整合成SOLID的首字母缩写,并在2000年代初由罗伯特·C·马丁(通常被称为鲍勃叔叔)推广。这五个原则是用于类设计的,遵守这些原则可以帮助管理依赖关系,避免创建混乱的、到处都是依赖的僵化代码库。

对SOLID原则的正确理解和运用可以使软件开发人员实现非常高的内聚度,并编写易于理解和维护的高质量代码。有了SOLID原则,你可以编写干净的代码,构建健壮且可扩展的软件应用程序。

事实上,鲍勃叔叔澄清了SOLID原则不是法律或规则,而是已经观察到在几种情况下起作用的启发式。要有效地使用这些原则,你必须搜索你的代码,检查违反原则的部分,然后进行重构。

单一职责原则(SRP)是五个SOLID原则中的第一个。该原则规定一个类在任何时候只能有一个改变的理由。这简单地意味着一个类一次只能执行一个职责或有一个责任。

软件项目的业务需求通常不是固定的。在软件项目发布之前,甚至在软件的整个生命周期中,需求会不时地发生变化,开发人员必须根据变化调整代码库。为了使软件应用程序满足其业务需求并适应变化,必须使用灵活的设计模式,并且类始终只有一个责任。

此外,重要的是要理解,当一个类有多个责任时,即使进行最微小的更改也会对整个代码库产生巨大影响。对类的更改可能会导致连锁反应,导致之前工作的功能或其他方法出现故障。例如,如果你有一个解析.csv文件的类,同时它还调用一个Web服务来检索与.csv文件解析无关的信息,那么这个类就有多个改变的原因。对Web服务调用的更改将影响该类,尽管这些更改与.csv文件解析无关。

以下代码片段中的LoanCalculator类的设计明显违反了SRP。LoanCalculator有两个责任——第一个是计算房屋和汽车贷款,第二个是从XML文件和XML字符串中解析贷款利率:

责任不应该混在一个类中。你应该避免在一个类中混淆责任,这会导致做太多事情的怪兽类。相反,如果你能想到一个改变类的理由或动机,那么它已经有了多个责任;将类分成每个只包含单一责任的类。

publicclassLoanService{privateIEligibilityCheckereligibilityChecker;publicLoanService(IEligibilityCheckereligibilityChecker){this.eligibilityChecker=eligibilityChecker;}publicdoubleCalculateCarLoanRate(CarLoancarLoan){doublerate=12.5F;boolisEligible=eligibilityChecker.IsApplicantEligible(carLoan);if(isEligible)rate=rate-carLoan.DiscountFactor;returnrate;}}通过将业务逻辑代码分离到LoanService类中,LoanRepository类现在只有一个依赖,即DbContext实体框架。未来,LoanRepository可以很容易地进行维护和测试。新的LoanService类也符合SRP:

publicclassLoanRepository{privateDbContextdbContext;publicLoanRepository(DbContextdbContext){this.dbContext=dbContext;}publicListGetCarLoans(){returndbContext.CarLoan;}publicListGetHouseLoans(){returndbContext.HouseLoan;}}当您的代码中的问题得到很好的管理时,代码库将具有高内聚性,并且将来会更加灵活、易于测试和维护。有了高内聚性,类将松散耦合,对类的更改将很少可能破坏整个系统。

设计和最终编写生产代码的方法应该是允许向项目的代码库添加新功能,而无需进行多次更改、更改代码库的几个部分或类,或破坏已经正常工作且状态良好的现有功能。

如果由于对类中的方法进行更改而导致必须对多个部分或模块进行更改,这表明代码设计存在问题。这就是开闭原则(OCP)所解决的问题,允许您的代码库设计灵活,以便您可以轻松进行修改和增强。

OCP规定软件实体(如类、方法和模块)应设计为对扩展开放,但对修改关闭。这个原则可以通过继承或设计模式(如工厂、观察者和策略模式)来实现。这是指类和方法可以被设计为允许添加新功能,以供现有代码使用,而无需实际修改或更改现有代码,而是通过扩展现有代码的行为。

在C#中,通过正确使用对象抽象,您可以拥有封闭的类,这些类对修改关闭,而类的行为可以通过派生类进行扩展。派生类是封闭类的子类。使用继承,您可以创建通过扩展其基类添加更多功能的类,而无需修改基类。

考虑以下代码片段中的LoanCalculator类,它具有一个CalculateLoan方法,必须能够计算传递给它的任何类型的贷款的详细信息。在不使用OCP的情况下,可以使用if..elseif语句来计算要求。

LoanCalculator类具有严格的结构,当需要支持新类型时需要进行大量工作。例如,如果您打算添加更多类型的客户贷款,您必须修改CalculateLoan方法并添加额外的elseif语句以适应新类型的贷款。LoanCalculator违反了OCP,因为该类不是封闭的以进行修改:

publicclassLoanCalculator{privateIRateParserrateParser;publicLoanCalculator(IRateParserrateParser){this.rateParser=rateParser;}publicLoanCalculateLoan(LoanDTOloanDTO){Loanloan=newLoan();if(loanDTO.LoanType==LoanType.CarLoan){loan.LoanType=LoanType.CarLoan;loan.InterestRate=rateParser.GetRateByLoanType(LoanType.CarLoan);//dootherprocessing}elseif(loanDTO.LoanType==LoanType.HouseLoan){loan.LoanType=LoanType.HouseLoan;loan.InterestRate=rateParser.GetRateByLoanType(LoanType.HouseLoan);//dootherprocessing}returnloan;}}为了使LoanCalculator类对扩展开放而对修改关闭,我们可以使用继承来简化重构。LoanCalculator将被重构以允许从中创建子类。将LoanCalculator作为基类将有助于创建两个派生类,HouseLoanCalculator和CarLoanCalulator。计算不同类型贷款的业务逻辑已从CalculateLoan方法中移除,并在两个派生类中实现,如下面的代码片段所示:

publicclassLoanCalculator{protectedIRateParserrateParser;publicLoanCalculator(IRateParserrateParser){this.rateParser=rateParser;}publicLoanCalculateLoan(LoanDTOloanDTO){Loanloan=newLoan();//dosomebaseprocessingreturnloan;}}LoanCalculator类中的If条件已从CalculateLoan方法中移除。现在,新的CarLoanCaculator类包含了获取汽车贷款计算的逻辑:

publicclassCarLoanCalculator:LoanCalculator{publicCarLoanCalculator(IRateParserrateParser):base(rateParser){base.rateParser=rateParser;}publicoverrideLoanCalculateLoan(LoanDTOloanDTO){Loanloan=newLoan();loan.LoanType=loanDTO.LoanType;loan.InterestRate=rateParser.GetRateByLoanType(loanDTO.LoanType);//dootherprocessingreturnloan}}HouseLoanCalculator类是从LoanCalculator创建的,具有覆盖基类LoanCalculator中的CalculateLoan方法的CalculateLoan方法。对HouseLoanCalculator进行的任何更改都不会影响其基类的CalculateLoan方法:

publicclassHouseLoanCalculator:LoanCalculator{publicHouseLoanCalculator(IRateParserrateParser):base(rateParser){base.rateParser=rateParser;}publicoverrideLoanCalculateLoan(LoanDTOloanDTO){Loanloan=newLoan();loan.LoanType=LoanType.HouseLoan;loan.InterestRate=rateParser.GetRateByLoanType(LoanType.HouseLoan);//dootherprocessingreturnloan;}}如果引入了新类型的贷款,比如研究生学习贷款,可以创建一个新类PostGraduateStudyLoan来扩展LoanCalculator并实现CalculateLoan方法,而无需对LoanCalculator类进行任何修改。

从技术上讲,观察OCP意味着您的代码中的类和方法应该对扩展开放,这意味着可以扩展类和方法以添加新的行为来支持新的或不断变化的应用程序需求。而且类和方法对于修改是封闭的,这意味着您不能对源代码进行更改。

为了使LoanCalculator对更改开放,我们将其作为其他类型的基类派生。或者,我们可以创建一个ILoanCalculator抽象,而不是使用经典的对象继承:

publicinterfaceILoanCalculator{LoanCalculateLoan(LoanDTOloanDTO);}CarLoanCalculator类现在可以被创建来实现ILoanCalculator接口。这将需要CarLoanCalculator类明确实现接口中定义的方法和属性。

publicclassCarLoanCalculator:ILoanCalculator{privateIRateParserrateParser;publicCarLoanCalculator(IRateParserrateParser){this.rateParser=rateParser;}publicLoanCalculateLoan(LoanDTOloanDTO){Loanloan=newLoan();loan.LoanType=loanDTO.LoanType;loan.InterestRate=rateParser.GetRateByLoanType(loanDTO.LoanType);//dootherprocessingreturnloan}}HouseLoanCalculator类也可以被创建来实现ILoanCalculator,通过构造函数将IRateParser对象注入其中,类似于CarLoanCalculator。CalculateLoan方法可以被实现为具有计算房屋贷款所需的特定代码。通过简单地创建类并使其实现ILoanCalculator接口,可以添加任何其他类型的贷款:

publicclassHouseLoanCalculator:ILoanCalculator{privateIRateParserrateParser;publicHouseLoanCalculator(IRateParserrateParser){this.rateParser=rateParser;}publicLoanCalculateLoan(LoanDTOloanDTO){Loanloan=newLoan();loan.LoanType=loanDTO.LoanType;loan.InterestRate=rateParser.GetRateByLoanType(loanDTO.LoanType);//dootherprocessingreturnloan}}使用OCP,您可以创建灵活的软件应用程序,其行为可以轻松扩展,从而避免代码基础僵化且缺乏可重用性。通过适当使用OCP,通过有效使用代码抽象和对象多态性,可以对代码基础进行更改,而无需更改许多部分,并且付出很少的努力。您真的不必重新编译代码基础来实现这一点。

Liskov替换原则(LSP),有时也称为按合同设计,是SOLID原则中的第三个原则,最初由BarbaraLiskov提出。LSP规定,派生类或子类应该可以替换基类或超类,而无需对基类进行修改或在系统中生成任何运行时错误。

LSP可以通过以下数学符号进一步解释——假设S是T的子集,T的对象可以替换S的对象,而不会破坏系统的现有工作功能或引起任何类型的错误。

为了说明LSP的概念,让我们考虑一个带有Drive方法的Car超类。如果Car有两个派生类,SalonCar和JeepCar,它们都有Drive方法的重写实现,那么无论何时需要Car,都应该可以使用SalonCar和JeepCar来替代Car类。派生类与Car有一个是一个的关系,因为SalonCar是Car,JeepCar是Car。

为了设计您的类并实现它们以符合LSP,您应该确保派生类的元素是按照合同设计的。派生类的方法定义应该与基类的相似,尽管实现可能会有所不同,因为不同的业务需求。

此外,重要的是派生类的实现不违反基类或接口中实现的任何约束。当您部分实现接口或基类时,通过具有未实现的方法,您正在违反LSP。

以下代码片段具有LoanCalculator基类,具有CalculateLoan方法和两个派生类,HouseLoanCalculator和CarLoanCalculator,它们具有CalculateLoan方法并且可以具有不同的实现:

publicclassLoanCalculator{publicLoanCalculateLoan(LoanDTOloanDTO){thrownewNotImplementedException();}}publicclassHouseLoanCalculator:LoanCalculator{publicoverrideLoanCalculateLoan(LoanDTOloanDTO){thrownewNotImplementedException();}}publicclassCarLoanCalculator:LoanCalculator{publicoverrideLoanCalculateLoan(LoanDTOloanDTO){thrownewNotImplementedException();}}如果在前面的代码片段中没有违反LSP,那么HouseLoanCalculator和CarLoanCalculator派生类可以在需要LoanCalculator引用的任何地方使用。这在以下代码片段中的Main方法中得到了证明:

接口隔离原则(ISP)规定接口应该是适度的,只包含所需的属性和方法的定义,客户端不应被强制实现他们不使用的接口,或依赖他们不需要的方法。

要有效地在代码库中实现ISP,您应该倾向于创建简单而薄的接口,这些接口具有逻辑上分组在一起以解决特定业务案例的方法。通过创建薄接口,类代码中包含的方法可以轻松实现,同时保持代码库的清晰和优雅。

另一方面,如果您的接口臃肿或臃肿,其中包含类不需要的功能的方法,您更有可能违反ISP并在代码中创建耦合,这将导致代码库无法轻松测试。

与其拥有臃肿或臃肿的接口,不如创建两个或更多个薄接口,将方法逻辑地分组,并让您的类实现多个接口,或让接口继承其他薄接口,这种现象被称为多重继承,在C#中得到支持。

以下片段中的IRateCalculator接口违反了ISP。它可以被视为一个污染的接口,因为唯一实现它的类不需要FindLender方法,因为RateCalculator类不需要它:

publicinterfaceIRateCalculator{RateGetYearlyCarLoanRate();RateGetYearlyHouseLoanRate();LenderFindLender(LoanTypeloanType);}RateCalculator类具有GetYearlyCarLoanRate和GetYearlyHouseLoanRate方法,这些方法是必需的以满足类的要求。通过实现IRateCalculator,RateCalculator被迫为FindLender方法实现,而这并不需要:

publicclassRateCalculator:IRateCalculator{publicRateGetYearlyCarLoanRate(){thrownewNotImplementedException();}publicRateGetYearlyHouseLoanRate(){thrownewNotImplementedException();}publicLenderFindLender(LoanTypeloanType){thrownewNotImplementedException();}}前述的IRateCalculator可以重构为两个具有可以逻辑分组在一起的方法的连贯接口。通过小接口,可以以极大的灵活性编写代码,并且易于对实现接口的类进行单元测试:

publicinterfaceIRateCalculator{RateGetYearlyCarLoanRate();RateGetYearlyHouseLonaRate();}publicinterfaceILenderManager{LenderFindLender(LoanTypeloanType);}通过将IRateCalculator重构为两个接口,RateCalculator可以被重构以删除不需要的FindLender方法:

publicclassRateCalculator:IRateCalculator{publicRateGetYearlyCarLoanRate(){thrownewNotImplementedException();}publicRateGetYearlyHouseLonaRate(){thrownewNotImplementedException();}}在实现符合ISP的接口时要注意的反模式是为每个方法创建一个接口,试图创建薄接口;这可能导致创建多个接口,从而导致难以维护的代码库。

刚性或糟糕的设计可能会使软件应用程序的组件或模块的更改变得非常困难,并创建维护问题。这些不灵活的设计通常会破坏先前正常工作的功能。这可能以原则和模式的错误使用、糟糕的代码和不同组件或层的耦合形式出现,从而使维护过程变得非常困难。

当应用程序代码库中存在严格的设计时,仔细检查代码将会发现模块之间紧密耦合,使得更改变得困难。对任何模块的更改可能会导致破坏先前正常工作的另一个模块的风险。观察SOLID原则中的最后一个——依赖反转原则(DIP)可以消除模块之间的任何耦合,使代码库灵活且易于维护。

DIP有两种形式,都旨在实现代码的灵活性和对象及其依赖项之间的松耦合:

当高级模块或实体直接耦合到低级模块时,对低级模块进行更改通常会直接影响高级模块,导致它们发生变化,产生连锁反应。在实际情况下,当对高级模块进行更改时,低级模块应该发生变化。

publicclassAuthenticationManager{privateDbContextdbContext;publicAuthenticationManager(DbContextdbContext){this.dbContext=dbContext;}}在上面的代码片段中,AuthenticationManager类代表了一个高级模块,而传递给类构造函数的DbContextEntityFramework是一个负责CRUD和数据层活动的低级模块。虽然非专业的开发人员可能不会在代码结构中看到任何问题,但它违反了DIP。这是因为AuthenticationManager类依赖于DbContext类,并且对DbContext内部代码进行更改的尝试将会传播到AuthenticationManager,导致它发生变化,从而违反OCP。

我们可以重构AuthenticationManager类,使其具有良好的设计并符合DIP。这将需要创建一个IDbContext接口,并使DbContext实现该接口。

publicinterfaceIDbContext{intSaveChanges();voidDispose();}publicclassDbContext:IDbContext{publicintSaveChanges(){thrownewNotImplementedException();}publicvoidDispose(){thrownewNotImplementedException();}}AuthenticationManager可以根据接口编码,从而打破与DbContext的耦合或直接依赖,并且依赖于抽象。对AuthenticationManager进行编码,使其针对IDbContext意味着接口将被注入到AuthenticationManager的构造函数中,或者使用属性注入:

publicclassAuthenticationManager{privateIDbContextdbContext;publicAuthenticationManager(IDbContextdbContext){this.dbContext=dbContext;}}重构完成后,AuthenticationManager现在使用依赖反转,并依赖于抽象—IDbContext。将来,如果对DbContext类进行更改,将不再影响AuthenticationManager类,并且不会违反OCP。

虽然通过构造函数将IDbContext注入到AutheticationManager中非常优雅,但IDbcontext也可以通过公共属性注入到AuthenticationManager中:

publicclassAuthenticationManager{privateIDbContextdbContext;privateIDbContextDbContext{set{dbContext=value;}}}此外,DI也可以通过接口注入来实现,其中对象引用是使用接口操作传递的。这简单地意味着使用接口来注入依赖项。以下代码片段解释了使用接口注入来实现依赖的概念。

IRateParser是使用ParseRate方法定义创建的。第二个接口IRepository包含InjectRateParser方法,该方法接受IRateParser作为参数,并将注入依赖项。

publicinterfaceIRateParser{RateParseRate();}publicinterfaceIRepository{voidInjectRateParser(IRateParserrateParser);}现在,让我们创建LoanRepository类来实现IRepository接口,并为InjectRateParser创建一个代码实现,以将IRateParser存储库作为依赖项注入到LoanRepository类中以供代码使用:

publicclassLoanRepository:IRepository{IRateParserrateParser;publicvoidInjectRateParser(IRateParserrateParser){this.rateParser=rateParser;}publicfloatGetCheapestRate(LoanTypeloanType){returnrateParser.GetRateByLoanType(loanType);}}接下来,我们可以创建IRateParser依赖的具体实现,XmlRateParser和RestServiceRateParser,它们都包含了从XML和REST源解析贷款利率的ParseRate方法的实现:

publicclassXmlRateParser:IRateParser{publicRateParseRate(){//ParserateavailablefromxmlfilethrownewNotImplementedException();}}publicclassRestServiceRateParser:IRateParser{publicRateParseRate(){//ParserateavailablefromRESTservicethrownewNotImplementedException();}}总之,我们可以使用在前面的代码片段中创建的接口和类来测试接口注入概念。创建了IRateParser的具体对象,它被注入到LoanRepository类中,通过IRepository接口,并且可以使用IRateParser接口的两种实现之一来构造它。

IRateParserrateParser=newXmlRateParser();LoanRepositoryloanRepository=newLoanRepository();((IRepository)loanRepository).InjectRateParser(rateParser);varrate=loanRepository.GetCheapestRate();rateParser=newRestServiceRateParser();((IRepository)loanRepository).InjectRateParser(rateParser);rate=loanRepository.GetCheapestRate();在本节中描述的任何三种技术都可以有效地用于在需要时将依赖项注入到代码中。适当有效地使用DIP可以促进创建易于维护的松散耦合的应用程序。

ASP.NETCore的核心是DI。该框架提供了内置的DI服务,允许开发人员创建松散耦合的应用程序,并防止依赖关系的实例化或构造。使用内置的DI服务,您的应用程序代码可以设置为使用DI,并且依赖项可以被注入到Startup类中的方法中。虽然默认的DI容器具有一些很酷的功能,但您仍然可以在ASP.NETCore应用程序中使用其他已知的成熟的DI容器。

您可以将代码配置为以两种模式使用DI:

DI容器,也称为控制反转(IoC)容器,通常是一个可以创建具有其关联依赖项的类的类或工厂。在成功构造具有注入依赖项的类之前,项目必须设计或设置为使用DI,并且DI容器必须已配置为具有依赖类型。实质上,DI将具有包含接口到其具体类的映射的配置,并将使用此配置来解析所需依赖项的类。

ASP.NETCore内置的IoC容器由IServiceProvider接口表示,您可以使用Startup类中的ConfigureService方法对其进行配置。容器默认支持构造函数注入。在ConfigureService方法中,可以定义服务和平台功能,例如EntityFrameworkCore和ASP.NETMVCCore:

publicvoidConfigureServices(IServiceCollectionservices){//Addframeworkservices.services.AddDbContext(options=>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));services.AddIdentity().AddEntityFrameworkStores().AddDefaultTokenProviders();services.AddMvc();//ConfiguredDIservices.AddTransient();services.AddTransient();}ASP.NETCore内置容器具有一些扩展方法,例如AddDbContext、AddIdentity和AddMvc,您可以使用这些方法添加其他服务。可以使用AddTransient方法配置应用程序依赖项,该方法接受两个泛型类型参数,第一个是接口,第二个是具体类。AddTransient方法将接口映射到具体类,因此每次请求时都会创建服务。容器使用此配置为在ASP.NETMVC项目中需要它的每个对象注入接口。

用于配置服务的其他扩展方法是AddScoped和AddSingleton方法。AddScoped每次请求只创建一次服务:

services.AddScoped();AddSingleton方法只在首次请求时创建服务,并将其保存在内存中,使其可供后续请求使用。您可以自行实例化单例,也可以让容器来处理:

//instantiatingsingletonservices.AddSingleton(newLenderManager());//alternativewayofconfiguringsingletonserviceservices.AddSingleton();ASP.NETCore的内置IoC容器轻量级且功能有限,但基本上您可以在应用程序中使用它进行DI配置。但是,您可以将其替换为.NET中可用的其他IoC容器,例如Ninject或Autofac。

使用DI将简化应用程序开发体验,并使您能够编写松散耦合且易于测试的代码。在典型的ASP.NETCoreMVC应用程序中,您应该使用DI来处理依赖项,例如存储库、控制器、适配器和服务,并避免对服务或HttpContext进行静态访问。

本章中使用的面向对象设计原则将帮助您掌握编写清晰、灵活、易于维护和易于测试代码所需的技能。本章中解释的LoD和SOLID原则可以作为创建松散耦合的面向对象软件应用程序的指导原则。

为了获得TDD周期的好处,您必须编写可测试的代码。所涵盖的SOLID原则描述了适当的实践,可以促进编写可轻松维护并在需要时进行增强的可测试代码。本章的最后一节着重介绍了为ASP.NETCoreMVC应用程序设置和使用依赖注入容器。

在下一章中,我们将讨论良好单元测试的属性,.NET生态系统中可用于创建测试的单元测试框架,以及在单元测试ASP.NETMVCCore项目时需要考虑的内容,我们还将深入探讨在.NETCore平台上使用xUnit库进行单元测试的属性。

通过单元测试,开发人员能够快速识别代码中的错误,从而增加开发团队对正在发布的软件产品质量的信心。单元测试主要由程序员和测试人员进行,这项活动涉及将应用程序的要求和功能分解为可以单独测试的单元。

单元测试旨在保持小型并经常运行,特别是在对代码进行更改时,以确保代码库中的工作功能不会出现故障。在进行TDD时,必须在编写要测试的代码之前编写单元测试。测试通常用作设计和编写代码的辅助工具,并且有效地是代码设计和规范的文档。

在本章中,我们将解释如何创建基本单元测试,并使用xUnit断言证明我们的单元测试结果。本章将涵盖以下主题:

单元测试是编写用于测试另一段代码的代码。有时它被称为最低级别的测试,因为它用于测试应用程序的最低级别的代码。单元测试调用要测试的方法或类来验证和断言有关被测试代码的逻辑、功能和行为的假设。

单元测试的主要目的是验证被测试代码单元,以确保代码片段执行其设计用途而不是其他用途。通过单元测试,可以证明代码单元的正确性,只有当单元测试编写得好时才能实现。虽然单元测试将证明正确性并有助于发现代码中的错误,但如果被测试的代码设计和编写不佳,代码质量可能不会得到改善。

当您正确编写单元测试时,您可以在一定程度上确信您的应用程序在发布时会正确运行。通过测试套件获得的测试覆盖率,您可以获得有关代码库中方法、类和其他对象的测试写入频率的指标,并且您将获得有关测试运行频率以及测试通过或失败次数的有意义信息。

单元测试的这一特性不容忽视。与被测试的代码类似,单元测试应该易于阅读和理解。编码标准和原则也适用于测试。应该避免使用魔术数字或常量等反模式,因为它们会使测试混乱并且难以阅读。在下面的测试中,整数10是一个魔术数字,因为它直接使用。这影响了测试的可读性和清晰度:

[Fact]publicvoidTest_CheckPasswordLength_ShouldReturnTrue(){stringpassword="civic";boolisValid=false;if(password.Length>=10)isValid=true;Assert.True(isValid);}有一个良好的测试结构模式可以采用,它被广泛称为三A模式或3A模式——安排,行动和断言——它将测试设置与验证分开。您需要确保测试所需的数据被安排好,然后是对被测试方法进行操作的代码行,最后断言被测试方法的结果是否符合预期:

单元测试基本上应该是一个单元,它应该被设计和编写成可以独立运行的形式。在这种情况下,被测试的单元,即一个方法,应该已经被编写成微妙地依赖于其他方法。如果可能的话,方法所需的数据应该通过方法参数传递,或者应该在单元内提供,它不应该需要外部请求或设置数据来进行功能。

单元测试不应该依赖于或受到任何其他测试的影响。当单元测试相互依赖时,如果其中一个测试在运行时失败,所有其他依赖测试也会失败。代码所需的所有数据应该由单元测试提供。

[Fact]publicvoidTest_DeleteLoan_ShouldReturnNull(){loanRepository.ArchiveLoan(12);loanRepository.DeleteLoan(12);varloan=loanRepository.GetById(12);Assert.Null(loan);}此片段中测试的问题在于同时发生了很多事情。如果测试失败,没有特定的方法来检查哪个方法调用导致了失败。为了清晰和易于维护,这个测试可以分解成不同的测试。

单元测试应该易于运行,而无需每次运行时都进行修改。实质上,测试应该准备好重复运行而无需修改。在下面的测试中,Test_DeleteLoan_ShouldReturnNull测试方法是不可重复的,因为每次运行测试都必须进行修改。为了避免这种情况,最好模拟loanRepository对象:

[Fact]publicvoidTest_DeleteLoan_ShouldReturnNull(){loanRepository.DeleteLoan(12);varloan=loanRepository.GetLoanById(12);Assert.Null(loan);}易维护且运行速度快单元测试应该以一种允许它们快速运行的方式编写。测试应该易于实现,任何开发团队的成员都应该能够运行它。因为软件应用是动态的,不断发展的,所以代码库的测试应该易于维护,因为被测试的底层代码发生变化。为了使测试运行更快,尽量减少依赖关系。

很多时候,大多数程序员在单元测试方面做错了,他们编写具有固有依赖关系的单元测试,这反过来使得测试运行变得更慢。一个快速的经验法则可以给你一个线索,表明你在单元测试中做错了什么,那就是测试运行得非常慢。此外,当你的单元测试调用后端服务器或执行一些繁琐的I/O操作时,这表明存在测试问题。

单元测试应该易于设置,并且与任何直接或外部依赖项解耦。应使用适当的模拟框架对外部依赖项进行模拟。适当的对象设置应在设置方法或测试类构造函数中完成。

最后,良好的单元测试应该具有良好的代码覆盖率。测试方法中的所有执行路径都应该被覆盖,所有测试都应该有定义的可测试标准。

.NETCore开发平台已经被设计为完全支持测试。这可以归因于采用的架构。这使得在.NETCore平台上进行TDD相对容易且值得。

在.NET和.NETCore中有几个可用的单元测试框架。这些框架基本上提供了从您喜欢的IDE、代码编辑器、专用测试运行器,或者有时通过命令行直接编写和执行单元测试的简单和灵活的方式。

.NET平台上存在着蓬勃发展的测试框架和套件生态系统。这些框架包含各种适配器,可用于创建单元测试项目以及用于持续集成和部署。

这个框架生态系统已经被.NETCore平台继承。这使得在.NETCore上实践TDD非常容易。VisualStudioIDE是开放且广泛的,可以更快、更容易地从NuGet安装测试插件和适配器,用于测试项目。

有许多免费和开源的测试框架,用于各种类型的测试。最流行的框架是MSTest、NUnit和xUnit.net。

MicrosoftMSTest是随VisualStudio一起提供的默认测试框架,由微软开发,最初是.NET框架的一部分,但也包含在.NETCore中。MSTest框架用于编写负载、功能、UI和单元测试。

MSTest可以作为统一的应用程序平台支持,也可以用于测试各种应用程序——桌面、商店、通用Windows平台(UWP)和ASP.NETCore。MSTest作为NuGet软件包提供。

基于MSTest的单元测试项目可以添加到包含要测试的项目的现有解决方案中,按照在VisualStudio2017中向解决方案添加新项目的步骤进行操作:

或者,在创建新项目或向现有解决方案添加新项目时,选择“类库(.NETCore)”选项,并从NuGet添加对MSTest的引用。从NuGet安装以下软件包到类库项目中,使用NuGet软件包管理器控制台或GUI选项。您可以从NuGet软件包管理器控制台运行以下命令:

Install-PackageMSTest.TestFrameworkInstall-Packagedotnet-test-mstest无论使用哪种方法创建MSTest测试项目,VisualStudio都会自动创建一个UnitTest1或Class1.cs文件。您可以重命名类或删除它以创建一个新的测试类,该类将使用MSTest的TestClass属性进行修饰,表示该类将包含测试方法。

实际的测试方法将使用TestMethod属性进行修饰,将它们标记为测试,这将使得MSTest测试运行器可以运行这些测试。MSTest有丰富的Assert辅助类集合,可用于验证单元测试的期望结果:

usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingLoanApplication.Core.Repository;namespaceMsTest{[TestClass]publicclassLoanRepositoryTest{privateLoanRepositoryloanRepository;publicLoanRepositoryTest(){loanRepository=newLoanRepository();}[TestMethod]publicvoidTest_GetLoanById_ShouldReturnLoan(){varloan=loanRepository.GetLoanById(12);Assert.IsNotNull(loan);}}}您可以从VisualStudio2017的测试资源管理器窗口中运行Test_GetLoanById_ShouldReturnLoan测试方法。可以从测试菜单中打开此窗口,选择窗口,然后选择测试资源管理器。右键单击测试并选择运行选定的测试:

您还可以从控制台运行测试。打开命令提示窗口并将目录更改为包含测试项目的文件夹,或者如果要运行解决方案中的所有测试项目,则更改为解决方案文件夹。运行dotnettest命令。项目将被构建,同时可用的测试将被发现和执行:

NUnit是一个最初从Java的JUnit移植的测试框架,可用于测试.NET平台上所有编程语言编写的项目。目前是第3版,其开源测试框架是在MIT许可下发布的。

NUnit测试框架包括引擎和控制台运行器。此外,它还有用于测试在移动设备上运行的应用程序的测试运行器—XamarinRunners。NUnit测试适配器和生成器基本上可以使使用VisualStudioIDE进行测试变得无缝和相对容易。

使用NUnit测试.NETCore或.NET标准应用程序需要使用VisualStudio测试适配器的NUnit3版本。需要安装NUnit测试项目模板,以便能够创建NUnit测试项目,通常只需要进行一次。

NUnit适配器可以通过以下步骤安装到VisualStudio2017中:

这将下载适配器并将其安装为VisualStudio2017的模板,您必须重新启动VisualStudio才能生效:

或者,您可以每次要创建测试项目时直接从NuGet安装NUnit3测试适配器。

要将NUnit测试项目添加到现有解决方案中,请按照以下步骤操作:

项目设置完成后,可以编写和运行单元测试。与MSTest类似,NUnit也有用于设置测试方法和测试类的属性。

TestFixture属性用于标记一个类作为测试方法的容器。Test属性用于修饰测试方法,并使这些方法可以从NUnit测试运行器中调用。

NUnit还有其他用于一些设置和测试目的的属性。OneTimeSetup属性用于修饰一个方法,该方法仅在运行所有子测试之前调用一次。类似的属性是SetUp,用于修饰在运行每个测试之前调用的方法:

usingLoanApplication.Core.Repository;usingNUnit;usingNUnit.Framework;namespaceMsTest{[TestFixture]publicclassLoanRepositoryTest{privateLoanRepositoryloanRepository;[OneTimeSetUp]publicvoidSetupTest(){loanRepository=newLoanRepository();}[Test]publicvoidTest_GetLoanById_ShouldReturnLoan(){varloan=loanRepository.GetLoanById(12);Assert.IsNotNull(loan);}}}测试可以从“测试资源管理器”窗口运行,类似于在MSTest测试项目中运行的方式。此外,可以使用dotnettest从命令行运行测试。但是,您必须将Microsoft.NET.Test.SdkVersion15.5.0添加为NUnit测试项目的引用:

xUnit.net是用于测试使用F#,VB.NET,C#和其他符合.NET的编程语言编写的项目的.NET平台的开源单元测试框架。xUnit.net是由NUnit的第2版的发明者编写的,并根据Apache2许可证获得许可。

xUnit.net可用于测试传统的.NET平台应用程序,包括控制台和ASP.NET应用程序,UWP应用程序,移动设备应用程序以及包括ASP.NETCore的.NETCore应用程序。

与NUnit或MSTest不同,测试类分别使用TestFixture和TestClass属性进行装饰,xUnit.net测试类不需要属性装饰。该框架会自动检测测试项目或程序集中所有公共类中的所有测试方法。

此外,在xUnit.net中不提供测试设置和拆卸属性,可以使用无参数构造函数来设置测试对象或模拟依赖项。测试类可以实现IDisposable接口,并在Dispose方法中清理对象或依赖项:

publicclassTestClass:IDisposable{publicTestClass(){//dotestclassdependenciesandobjectsetup}publicvoidDispose(){//docleanuphere}}xUnit.net支持两种主要类型的测试-事实和理论。事实是始终为真的测试;它们是没有参数的测试。理论是只有在传递特定数据集时才为真的测试;它们本质上是参数化测试。分别使用[Fact]和[Theory]属性来装饰事实和理论测试:

[Fact]publicvoidTestMethod1(){Assert.Equal(8,(4*2));}[Theory][InlineData("name")][InlineData("word")]publicvoidTestMethod2(stringvalue){Assert.Equal(4,value.Length);}[InlineData]属性用于在TestMethod2中装饰理论测试,以向测试方法提供测试数据,以在测试执行期间使用。

xUnit.net的配置有两种类型。xUnit.net允许配置文件为基于JSON或XML。必须为要测试的每个程序集进行xUnit.net配置。用于xUnit.net的配置文件取决于被测试应用程序的开发平台,尽管JSON配置文件可用于所有平台。

要使用JSON配置文件,在VisualStudio2017中创建测试项目后,应向测试项目的根文件夹添加一个新的JSON文件,并将其命名为xunit.runner.json:

将文件添加到项目后,必须指示VisualStudio将.json文件复制到项目的输出文件夹中,以便xUnit测试运行程序找到它。为此,应按照以下步骤操作:

这将确保在更改时配置文件始终被复制到输出文件夹。xUnit中支持的配置元素放置在配置文件中的顶级JSON对象中,如此处所见:

{"appDomain":"ifAvailable","methodDisplay":"classAndMethod","diagnosticMessages":false,"internalDiagnosticMessages":false,"maxParallelThreads":8}使用支持JSON的VisualStudio版本时,它将根据配置文件名称自动检测模式。此外,在编辑xunit.runner.json文件时,VisualStudioIntelliSense中将提供上下文帮助。此表中解释了各种配置元素及其可接受的值:

xUnit.net用于桌面和PCL测试项目的另一个配置文件选项是XML配置。如果测试项目尚未具有App.Config文件,则应将其添加到测试项目中。

在App.Config文件的appSettings部分下,您可以添加配置元素及其值。在使用XML配置文件时,必须在前面表中解释的配置元素后面添加xUnit。例如,JSON配置文件中的appDomain元素将写为xunit.appDomain:

xUnit.net测试运行器在xUnit.net中,有两个负责运行使用该框架编写的单元测试的角色——xUnit.net运行器和测试框架。测试运行器是一个程序,也可以是搜索程序集中的测试并激活发现的测试的第三方插件。xUnit.net测试运行器依赖于xunit.runner.utility库来发现和执行测试。

测试框架是实现测试发现和执行的代码。测试框架将发现的测试链接到xunit.core.dll和xunit.execution.dll库。这些库与单元测试一起存在。xunit.abstractions.dll是xUnit.net的另一个有用的库,其中包含测试运行器和测试框架在通信中使用的抽象。

测试并行化是在xUnit.net的2.0版本中引入的。这个功能允许开发人员并行运行多个测试。测试并行化是必要的,因为大型代码库通常有数千个测试运行,需要多次运行。

这些代码库有大量的测试,因为需要确保功能代码的工作正常且没有问题。它们还利用了现在可用的超快计算资源来运行并行测试,这要归功于计算机硬件技术的进步。

您可以编写使用并行化的测试,并利用计算机上可用的核心,从而使测试运行更快,或者让xUnit.net并行运行多个测试。通常情况下,后者更受欢迎,这可以确保测试以计算机运行它们的速度运行。在xUnit.net中,测试并行可以在框架级别进行,其中框架支持在同一程序集中并行运行多个测试,或者在测试运行器中进行并行化,其中运行器可以并行运行多个测试程序集。

测试是使用测试集合并行运行的。每个测试类都是一个测试集合,测试集合内的测试不会相互并行运行。例如,如果运行LoanCalculatorTest中的测试,测试运行器将按顺序运行类中的两个测试,因为它们属于同一个测试集合:

publicclassLoanCalculatorTest{[Fact]publicvoidTestCalculateLoan(){Assert.Equal(16,(4*4));}[Fact]publicvoidTestCalculateRate(){Assert.Equal(12,(4*3));}}不同的测试类中的测试可以并行运行,因为它们属于不同的测试集合。让我们修改LoanCalculatorTest,将TestCalculateRate测试方法放入一个单独的测试类RateCalculatorTest中:

不同的测试类中的测试可以配置为不并行运行。这可以通过使用相同名称的Collection属性对类进行装饰来实现。如果将Collection属性添加到LoanCalculatorTest和RateCalculatorTest中:

[Collection("Donotruninparallel")]publicclassLoanCalculatorTest{[Fact]publicvoidTestCalculateLoan(){Assert.Equal(16,(4*4));}}[Collection("Donotruninparallel")]publicclassRateCalculatorTest{[Fact]publicvoidTestCalculateRate(){Assert.Equal(12,(4*3));}}LoanCalculatorTest和RateCalculatorTest类中的测试不会并行运行,因为这些类基于属性装饰属于同一个测试集合。

MVC模式提供了清晰的演示逻辑和业务逻辑之间的分离,易于扩展和维护。它最初是为桌面应用程序设计的,但后来在Web应用程序中得到了广泛的使用和流行。

ASP.NETCoreMVC项目可以以与测试其他类型的.NETCore项目相同的方式进行测试。ASP.NETCore支持对控制器类、razor页面、页面模型、业务逻辑和应用程序数据访问层进行单元测试。为了构建健壮的MVC应用程序,各种应用程序组件必须在隔离环境中进行测试,并在集成后进行测试。

ASP.NETCoreMVC控制器类处理用户交互,这转化为浏览器上的请求。控制器获取适当的模型并选择要呈现的视图,以显示用户界面。控制器从视图中读取用户的输入数据、事件和交互,并将其传递给模型。控制器验证来自视图的输入,然后执行修改数据模型状态的业务操作。

Controller类应该轻量级,并包含渲染视图所需的最小逻辑,以便进行简单的测试和维护。控制器应该验证模型的状态并确定有效性,调用执行业务逻辑验证和管理数据持久性的适当代码,然后向用户显示适当的视图。

在对Controller类进行单元测试时,主要目的是在隔离环境中测试控制器动作方法的行为,这应该在不混淆测试与其他重要的MVC构造(如模型绑定、路由、过滤器和其他自定义控制器实用对象)的情况下进行。这些其他构造(如果是自定义编写的)应该以不同的方式进行单元测试,并在集成测试中与控制器一起进行整体测试。

审查LoanApplication项目的HomeController类,Controller类包含在VisualStudio中创建项目时添加的四个动作方法:

usingSystem;usingSystem.Collections.Generic;usingSystem.Diagnostics;usingSystem.Linq;usingSystem.Threading.Tasks;usingMicrosoft.AspNetCore.Mvc;usingLoanApplication.Models;namespaceLoanApplication.Controllers{publicclassHomeController:Controller{publicIActionResultIndex(){returnView();}publicIActionResultAbout(){ViewData["Message"]="Yourapplicationdescriptionpage.";returnView();}}}HomeController类当前包含具有返回视图的基本逻辑的动作方法。为了对MVC项目进行单元测试,应向解决方案添加一个新的xUnit.net测试项目,以便将测试与实际项目代码分开。将HomeControllerTest测试类添加到新创建的测试项目中。

将要编写的测试方法将验证HomeController类的Index和About动作方法返回的viewResult对象:

usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Threading.Tasks;usingMicrosoft.AspNetCore.Mvc;usingLoanApplication.Controllers;usingXunit;namespaceLoanApplication.Tests.Unit.Controller{publicclassHomeControllerTest{[Fact]publicvoidTestIndex(){varhomeController=newHomeController();varresult=homeController.Index();varviewResult=Assert.IsType(result);}[Fact]publicvoidTestAbout(){varhomeController=newHomeController();varresult=homeController.About();varviewResult=Assert.IsType(result);}}}在前面的控制器测试中编写的测试是基本的和非常简单的。为了进一步演示控制器单元测试,可以更新Controller类代码以支持依赖注入,这将允许通过对象模拟来测试方法。此外,通过使用AddModelError来添加错误,可以测试无效的模型状态:

publicclassHomeController:Controller{privateILoanRepositoryloanRepository;publicHomeController(ILoanRepositoryloanRepository){this.loanRepository=loanRepository;}publicIActionResultIndex(){varloanTypes=loanRepository.GetLoanTypes();ViewData["LoanTypes"]=loanTypes;returnView();}}ILoanRepository通过类构造函数注入到HomeController中,在测试类中,ILoanRepository将使用Moq框架进行模拟。在TestIndex测试方法中,使用LoanType列表设置了HomeController类中Index方法所需的模拟对象:

publicclassHomeControllerTest{privateMockloanRepository;privateHomeControllerhomeController;publicHomeControllerTest(){loanRepository=newMock();loanRepository.Setup(x=>x.GetLoanTypes()).Returns(GetLoanTypes());homeController=newHomeController(loanRepository.Object);}[Fact]publicvoidTestIndex(){varresult=homeController.Index();varviewResult=Assert.IsType(result);varloanTypes=Assert.IsAssignableFrom>(viewResult.ViewData["LoanTypes"]);Assert.Equal(2,loanTypes.Count());}privateListGetLoanTypes(){varloanTypes=newList();loanTypes.Add(newLoanType(){Id=1,Name="CarLoan"});loanTypes.Add(newLoanType(){Id=2,Name="HouseLoan"});returnloanTypes;}}razor页面单元测试在ASP.NETMVC中,视图是用于呈现Web应用程序用户界面的组件。视图以适当且易于理解的输出格式(如HTML、XML、XHTML或JSON)呈现模型中包含的信息。视图根据对模型执行的更新向用户生成输出。

Razor页面使得在页面上编写功能相对容易。Razor页面类似于Razor视图,但增加了@page指令。@page指令必须是页面中的第一个指令,它会自动将文件转换为MVC操作,处理请求而无需经过控制器。

在ASP.NETCore中,可以测试Razor页面,以确保它们在隔离和集成应用程序中正常工作。Razor页面测试可以涉及测试数据访问层代码、页面组件和页面模型。

以下代码片段显示了一个单元测试,用于验证页面模型是否正确重定向:

测试用例是包含测试方法的测试类。通常,每个被测试类都有一个测试类。开发人员在测试中构建测试的另一种常见做法是为每个被测试的方法创建一个嵌套类,或者为被测试的类创建一个基类测试类,为每个被测试的方法创建一个子类。此外,还有每个功能一个测试类的方法,其中所有共同验证应用程序功能的测试方法都分组在一个测试用例中。

这些测试结构方法促进了DRY原则,并在编写测试时实现了代码的可重用性。没有一种方法适用于所有目的,选择特定的方法应该基于应用程序开发周围的情况,并在与团队成员进行有效沟通后进行。

publicclassHomeControllerTest{privateMockloanRepository;privateHomeControllerhomeController;publicHomeControllerTest(){loanRepository=newMock();loanRepository.Setup(x=>x.GetLoanTypes()).Returns(GetLoanTypes());homeController=newHomeController(loanRepository.Object);}privateListGetLoanTypes(){varloanTypes=newList();loanTypes.Add(newLoanType(){Id=1,Name="CarLoan"});loanTypes.Add(newLoanType(){Id=2,Name="HouseLoan"});returnloanTypes;}}将创建两个测试类IndexMethod和AboutMethod。这两个类都将扩展HomeControllerTest类,并将分别拥有一个方法,遵循每个测试类一个方法的单元测试方法:

publicclassIndexMethod:HomeControllerTest{[Fact]publicvoidTestIndex(){varresult=homeController.Index();varviewResult=Assert.IsType(result);varloanTypes=Assert.IsAssignableFrom>(viewResult.ViewData["LoanTypes"]);Assert.Equal(3,loanTypes.Count());}}publicclassAboutMethod:HomeControllerTest{[Fact]publicvoidTestAbout(){varresult=homeController.About();varviewResult=Assert.IsType(result);}}重要的是要注意,给测试用例和测试方法赋予有意义和描述性的名称可以在使它们有意义和易于理解方面起到很大作用。测试方法的名称应包含被测试的方法或功能的名称。可选地,可以在测试方法的名称中进一步描述性地添加预期结果,以Should为前缀:

[Fact]publicvoidTestAbout_ShouldReturnViewResult(){varresult=homeController.About();varviewResult=Assert.IsType(result);}xUnit.net共享测试上下文测试上下文设置是在测试类构造函数中完成的,因为测试设置在xUnit中不适用。对于每个测试,xUnit会创建测试类的新实例,这意味着类构造函数中的代码将为每个测试运行。

往往,单元测试类希望共享测试上下文,因为创建和清理测试上下文可能很昂贵。xUnit提供了三种方法来实现这一点:

当您希望每个测试类中的每个测试都有一个新的测试上下文时,您应该使用构造函数和dispose。在下面的代码中,上下文对象将为LoanModuleTest类中的每个测试方法构造和处理:

publicclassLoanModuleTest:IDisposable{publicLoanAppContextContext{get;privateset;}publicLoanModuleTest(){Context=newLoanAppContext();}publicvoidDispose(){Context=null;}[Fact]publicvoidTestSaveLoan_ShouldReturnTrue(){Loanloan=newLoan{Description="CarLoan"};Context.Loan.Add(loan);varisSaved=Context.Save();Assert.True(isSaved);}}当您打算创建将在类中的所有测试之间共享的测试上下文,并在所有测试运行完成后进行清理时,可以使用类装置方法。要使用类装置,您必须创建一个具有包含要共享的对象代码的构造函数的装置类。测试类应该实现IClassFixture<>,并且您应该将装置类作为测试类的构造函数参数添加:

publicclassEFCoreFixture:IDisposable{publicLoanAppContextContext{get;privateset;}publicEFCoreFixture(){Context=newLoanAppContext();}publicvoidDispose(){Context=null;}}以下片段中的LoanModuleTest类实现了IClassFixture,并将EFCoreFixture作为参数传递。EFCoreFixture被注入到测试类构造函数中:

publicclassLoanModuleTest:IClassFixture{EFCoreFixtureefCoreFixture;publicLoanModuleTest(EFCoreFixtureefCoreFixture){this.efCoreFixture=efCoreFixture;}[Fact]publicvoidTestSaveLoan_ShouldReturnTrue(){//testtopersistusingEFCorecontext}}与类装置类似,集合装置用于创建在多个类中共享的测试上下文。测试上下文的创建将一次性完成所有测试类,并且如果实现了清理,则将在测试类中的所有测试运行完成后执行。

使用集合装置:

VisualStudio2017企业版目前支持NUnit、MSTest和xUnit的实时单元测试。可以从工具菜单配置实时单元测试——从顶级菜单选择选项,并在选项对话框的左窗格中选择实时单元测试。可以从选项对话框调整可用的实时单元测试配置选项:

可以通过选择实时单元测试并选择开始来从测试菜单启用实时单元测试:

启用实时单元测试后,实时单元测试菜单上的其他可用选项将显示。除了开始,还将有暂停、停止和重置清理。菜单功能在此处描述:

在下面的屏幕截图中,可以在启用实时单元测试时看到覆盖可视化。每行代码都会更新并用绿色、红色和蓝色装饰,以指示该行代码是由通过的测试、失败的测试覆盖还是未被任何测试覆盖的:

xUnit.net断言验证测试方法的行为。断言验证了预期结果应为真的条件。当断言失败时,当前测试的执行将终止,并抛出异常。以下表格解释了xUnit.net中可用的断言:

以下代码片段使用了前面表格中描述的一些xUnit.net断言方法。Assertions单元测试方法展示了在xUnit.net中进行单元测试时如何使用断言方法来验证方法的行为:

[Fact]publicvoidAssertions(){Assert.Equal(8,(4*2));Assert.NotEqual(6,(4*2));Listlist=newList{"Rick","John"};Assert.Contains("John",list);Assert.DoesNotContain("Dani",list);Assert.Empty(newList());Assert.NotEmpty(list);Assert.False(false);Assert.True(true);Assert.NotNull(list);Assert.Null(null);}在.NETCore和Windows上可用的测试运行器.NET平台有一个庞大的测试运行器生态系统,可以与流行的测试平台NUnit、MSTest和xUnit一起使用。测试框架都有随附的测试运行器,可以促进测试的顺利运行。此外,还有几个开源和商业测试运行器可以与可用的测试平台一起使用,其中之一就是ReSharper。

ReSharper是JetBrains开发的.NET开发人员的VisualStudio扩展。它的测试运行器是.NET平台上可用的测试运行器中最受欢迎的,ReSharper生产工具提供了增强程序员生产力的其他功能。它有一个单元测试运行器,可以帮助您基于xUnit.net、NUnit、MSTest和其他几个测试框架运行和调试单元测试。

ReShaper可以检测到.NET和.NETCore平台上使用的测试框架编写的测试。ReSharper在编辑器中添加图标,可以单击以调试或运行测试:

ReSharper使用UnitTestSessions窗口运行单元测试。ReSharper的单元测试会话窗口允许您并行运行任意数量的单元测试会话,彼此独立。但是在调试模式下只能运行一个会话。

您可以使用单元测试树来过滤测试,这样可以获得测试的结构。它显示了哪些测试失败、通过或尚未运行。此外,通过双击测试,您可以直接导航到源代码:

在本章中,讨论了良好单元测试的属性。我们还广泛讨论了使用xUnit.net框架中可用的测试功能的单元测试程序。解释了VisualStudio2017中的实时单元测试功能,并使用xUnit.net的Fact属性,使用断言来创建基本的单元测试。

在上一章中,我们讨论了良好单元测试的属性,以及xUnit.net支持的两种测试类型Fact和Theory。此外,我们还通过xUnit.net单元测试框架中可用的丰富测试断言集合创建了单元测试。

为软件项目编写的单元测试应该从开发阶段开始反复运行,在部署期间,维护期间,以及在项目的整个生命周期中都应该有效地运行。通常情况下,这些测试应该在不同的数据输入上运行相同的执行步骤,而测试和被测试的代码都应该在不同的数据输入下表现出一致的行为。

通过使用不同的数据集运行测试可以通过创建或复制具有相似步骤的现有测试来实现。这种方法的问题在于维护,因为必须在各种复制的测试中影响测试逻辑的更改。xUnit.net通过其数据驱动单元测试功能解决了这一挑战,称为theories,它允许在不同的测试数据集上运行测试。

在第四章中解释的示例数据驱动单元测试使用了内联方法。还有其他属性可以用于向测试提供数据,如MemberData和ClassData。

在本章中,我们将通过使用xUnit.net框架创建数据驱动单元测试,并涵盖以下主题:

数据驱动单元测试是一个概念,因为它能够使用不同的数据集执行测试,所以它能够对代码行为提供深入的见解。通过数据驱动单元测试获得的见解可以帮助我们对应用程序开发方法做出明智的决策,并且可以识别出需要改进的潜在领域。可以从数据单元测试的报告和代码覆盖率中制定策略,这些策略可以后来用于重构具有潜在性能问题和应用程序逻辑中的错误的代码。

数据驱动单元测试的一些好处在以下部分进行了解释。

通过数据驱动测试,可以更容易地减少冗余,同时保持全面的测试覆盖。这是因为可以避免测试代码的重复。传统上需要为不同数据集重复测试的测试现在可以用于不同的数据集。当存在具有相似结构但具有不同数据的测试时,这表明可以将这些测试重构为数据驱动测试。

让我们在以下片段中回顾CarLoanCalculator类和相应的LoanCalculatorTest测试类。与传统的编写测试方法相比,这将为我们提供宝贵的见解,说明为什么数据驱动测试可以简化测试,同时在编写代码时提供简洁性。

publicclassCarLoanCalculator:LoanCalculator{publicCarLoanCalculator(RateParserrateParser){base.rateParser=rateParser;}publicoverrideLoanCalculateLoan(LoanDTOloanDTO){Loanloan=newLoan();loan.LoanType=loanDTO.LoanType;loan.InterestRate=rateParser.GetRateByLoanType(loanDTO.LoanType,loanDTO.LocationType,loanDTO.JobType);//dootherprocessingreturnloan}}为了验证CarLoanCalculator类的一致行为,将使用以下测试场景验证CalculateLoan方法返回的Loan对象,当方法参数LoanDTO具有不同的LoanType、LocationType和JobType组合时。CarLoanCalculatorTest类中的Test_CalculateLoan_ShouldReturnLoan测试方法验证了描述的每个场景:

当业务人员和质量保证测试人员参与自动化测试过程时,可以改善软件应用程序的质量。他们可以使用数据文件作为数据源,无需太多的技术知识,就可以向数据源中填充执行测试所需的数据。可以使用不同的数据集多次运行测试,以彻底测试代码,以确保其健壮性。

使用数据驱动测试,您可以清晰地分离测试和数据。原本可能会与数据混在一起的测试现在将使用适当的逻辑进行分离。这确保了数据源可以在不更改使用它们的测试的情况下进行修改。

通过数据驱动单元测试,应用程序的整体质量得到改善,因为您可以使用各种数据集获得良好的覆盖率,并具有用于微调和优化正在开发的应用程序以获得改进性能的指标。

在xUnit.net中,数据驱动测试被称为理论。它们是使用Theory属性装饰的测试。当测试方法使用Theory属性装饰时,必须另外使用数据属性装饰,测试运行器将使用该属性确定要在执行测试时使用的数据源:

[Theory]publicvoidTest_CalculateRates_ShouldReturnRate(){//testnotimplementedyet}当测试标记为数据理论时,从数据源中提供的数据直接映射到测试方法的参数。与使用Fact属性装饰的常规测试不同,数据理论的执行次数基于从数据源获取的可用数据行数。

至少需要传递一个数据属性作为测试方法参数,以便xUnit.net将测试视为数据驱动并成功执行。要传递给测试的数据属性可以是InlineData、MemberData和ClassData中的任何一个。这些数据属性源自Xunit.sdk.DataAttribute。

内联数据驱动测试是使用xUnit.net框架编写数据驱动测试的最基本或最简单的方式。内联数据驱动测试使用InlineData属性编写,该属性用于装饰测试方法,除了Theory属性之外:

[Theory,InlineData("arguments")]当测试方法需要简单的参数并且不接受类实例化作为InlineData参数时,可以使用内联数据驱动测试。使用内联数据驱动测试的主要缺点是缺乏灵活性。不能将内联数据与另一个测试重复使用。

当在数据理论中使用InlineData属性时,数据行是硬编码的,并内联传递到测试方法中。要用于测试的所需数据可以是任何数据类型,并作为参数传递到InlineData属性中:

publicclassTheoryTest{[Theory,InlineData("name")]publicvoidTestCheckWordLength_ShouldReturnBoolean(stringword){Assert.Equal(4,word.Length);}}内联数据驱动测试可以有多个InlineData属性,指定测试方法的参数。多个InlineData数据理论的语法在以下代码中指定:

[Theory,InlineData("argument1"),InlineData("argument2"),InlineData("argumentn")]TestCheckWordLength_ShouldReturnBoolean方法可以更改为具有三个内联数据行,并且可以根据需要添加更多数据行到测试中。为了保持测试的清晰,建议每个测试不要超过必要或所需的内联数据:

publicclassTheoryTest{[Theory,InlineData("name"),InlineData("word"),InlineData("city")]publicvoidTestCheckWordLength_ShouldReturnBoolean(stringword){Assert.Equal(4,word.Length);}}在编写内联数据驱动单元测试时,必须确保测试方法中的参数数量与传递给InlineData属性的数据行中的参数数量匹配;否则,xUnit测试运行器将抛出System.InvalidOperationException。以下代码片段中TestCheckWordLength_ShouldReturnBoolean方法中的InlineData属性已被修改为接受两个参数:

publicclassTheoryTest{[Theory,InlineData("word","name")]publicvoidTestCheckWordLength_ShouldReturnBoolean(stringword){Assert.Equal(4,word.Length);}}当您在前面的代码片段中运行数据理论测试时,xUnit测试运行器会因为传递了两个参数"word"和"name"给InlineData属性,而不是预期的一个参数,导致测试失败并显示InvalidOperationException,如下面的屏幕截图所示:

当运行内联数据驱动测试时,xUnit.net将根据添加到测试方法的InlineData属性或数据行的数量创建测试的数量。在以下代码片段中,xUnit.net将创建两个测试,一个用于InlineData属性的参数"name",另一个用于参数"city":

[Theory,InlineData("name"),InlineData("city")]publicvoidTestCheckWordLength_ShouldReturnBoolean(stringword){Assert.Equal(4,word.Length);}如果您在VisualStudio中使用测试运行器运行TestCheckWordLength_ShouldReturnBoolean测试方法,测试应该成功运行并通过。基于属性创建的两个测试可以通过从InlineData属性传递给它们的参数来区分:

现在,让我们修改数据驱动单元测试的好处部分中的Test_CalculateLoan_ShouldReturnCorrectRate测试方法,使用InlineData来加载测试数据,而不是直接在测试方法的代码中硬编码测试数据:

[Theory,InlineData(newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location1})]publicvoidTest_CalculateLoan_ShouldReturnCorrectRate(LoanDTOloanDTO){Loanloan=carLoanCalculator.CalculateLoan(loanDTO);Assert.NotNull(loan);Assert.Equal(8,loan.InterestRate);}在VisualStudio中,上述代码片段将导致语法错误,IntelliSense上下文菜单显示错误——属性参数必须是常量表达式、表达式类型或属性参数类型的数组创建表达式:

在InlineData属性中使用属性或自定义类型作为参数类型是不允许的,这表明LoanDTO类的新实例不能作为InlineData属性的参数。这是InlineData属性的限制,因为它不能用于从属性、类、方法或自定义类型加载数据。

在编写内联数据驱动测试时遇到的灵活性不足可以通过使用属性数据驱动测试来克服。属性数据驱动单元测试是通过使用MemberData和ClassData属性在xUnit.net中编写的。使用这两个属性,可以创建从不同数据源(如文件或数据库)加载数据的数据理论。

当要创建并加载来自以下数据源的数据行的数据理论时,使用MemberData属性:

在使用MemberData时,数据源必须返回与IEnumerable兼容的独立对象集。这是因为在执行测试方法之前,return属性会被.ToList()方法枚举。

Test_CalculateLoan_ShouldReturnCorrectRate测试方法在数据驱动单元测试的好处部分中,可以重构以使用MemberData属性来加载测试的数据。创建一个静态的IEnumerable方法GetLoanDTOs,使用yield语句返回一个LoanDTO对象给测试方法:

publicstaticIEnumerableGetLoanDTOs(){yieldreturnnewobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location1}};yieldreturnnewobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location2}};}MemberData属性要求将数据源的名称作为参数传递给它,以便在后续调用中加载测试执行所需的数据行。静态方法、属性或字段的名称可以作为字符串传递到MemberData属性中,形式为MemberData("methodName"):

[Theory,MemberData("GetLoanDTOs")]publicvoidTest_CalculateLoan_ShouldReturnCorrectRate(LoanDTOloanDTO){Loanloan=carLoanCalculator.CalculateLoan(loanDTO);Assert.NotNull(loan);Assert.InRange(loan.InterestRate,8,12);}另外,数据源名称可以通过nameof表达式传递给MemeberData属性,nameof是C#关键字,用于获取变量、类型或成员的字符串名称。语法是MemberData(nameof(methodName)):

[Theory,MemberData(nameof(GetLoanDTOs))]publicvoidTest_CalculateLoan_ShouldReturnCorrectRate(LoanDTOloanDTO){Loanloan=carLoanCalculator.CalculateLoan(loanDTO);Assert.NotNull(loan);Assert.InRange(loan.InterestRate,8,12);}与MemberData属性一起使用静态方法类似,静态字段和属性可以用于提供数据理论的数据集。

Test_CalculateLoan_ShouldReturnCorrectRate可以重构以使用静态属性代替方法:

[Theory,MemberData("LoanDTOs")]publicvoidTest_CalculateLoan_ShouldReturnCorrectRate(LoanDTOloanDTO){Loanloan=carLoanCalculator.CalculateLoan(loanDTO);Assert.NotNull(loan);Assert.InRange(loan.InterestRate,8,12);}创建一个静态属性LoanDTOs,返回IEnumerable,这是作为MemberData属性参数的资格要求。LoanDTOs随后用作属性的参数:

publicstaticIEnumerableLoanDTOs{get{yieldreturnnewobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location1}};yieldreturnnewobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location2}};}每当运行Test_CalculateLoan_ShouldReturnCorrectRate时,将创建两个测试,对应于作为数据源返回的两个数据集。

遵循上述方法要求静态方法、字段或属性用于加载测试数据的位置与数据理论相同。为了使测试组织良好,有时需要将测试方法与用于加载数据的静态方法或属性分开放在不同的类中:

publicclassDataClass{publicstaticIEnumerableLoanDTOs{get{yieldreturnnewobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location1}};yieldreturnnewobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location2}};}}}当测试方法写在与静态方法不同的单独类中时,必须在MemberData属性中指定包含方法的类,使用MemberType,并分配包含类,使用类名,如下面的代码片段所示:

[Theory,MemberData(nameof(LoanDTOs),MemberType=typeof(DataClass))]publicvoidTest_CalculateLoan_ShouldReturnCorrectRate(LoanDTOloanDTO){Loanloan=carLoanCalculator.CalculateLoan(loanDTO);Assert.NotNull(loan);Assert.InRange(loan.InterestRate,8,12);}在使用静态方法时,该方法也可以有一个参数,当处理数据时可能需要使用该参数。例如,可以将整数值传递给方法,以指定要返回的记录数。该参数可以直接从MemberData属性传递给静态方法:

[Theory,MemberData(nameof(GetLoanDTOs),parameters:1,MemberType=typeof(DataClass))]publicvoidTest_CalculateLoan_ShouldReturnCorrectRate3(LoanDTOloanDTO){Loanloan=carLoanCalculator.CalculateLoan(loanDTO);Assert.NotNull(loan);Assert.InRange(loan.InterestRate,8,12);}DataClass中的GetLoanDTOs方法可以重构为接受一个整数参数,用于限制要返回的记录数,以填充执行Test_CalculateLoan_ShouldReturnCorrectRate所需的数据行:

publicclassDataClass{publicstaticIEnumerableGetLoanDTOs(intrecords){varloanDTOs=newList{newobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location1}},newobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location2}}};returnloanDTOs.TakeLast(records);}}ClassData属性ClassData是另一个属性,可以使用它来通过来自类的数据创建数据驱动测试。ClassData属性接受一个可以实例化以获取将用于执行数据理论的数据的类。具有数据的类必须实现IEnumerable,每个数据项都作为object数组返回。还必须实现GetEnumerator方法。

让我们创建一个LoanDTOData类,用于提供数据以测试Test_CalculateLoan_ShouldReturnCorrectRate方法。LoanDTOData将返回LoanDTO的IEnumerable对象:

publicclassLoanDTOData:IEnumerable{privateIEnumerabledata=>new[]{newobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location1}},newobject[]{newLoanDTO{LoanType=LoanType.CarLoan,JobType=JobType.Professional,LocationType=LocationType.Location2}}};IEnumeratorIEnumerable.GetEnumerator(){returnGetEnumerator();}publicIEnumeratorGetEnumerator(){returndata.GetEnumerator();}}实现了LoanDTOData类之后,可以使用ClassData属性装饰Test_CalculateLoan_ShouldReturnCorrectRate,并将LoanDTOData作为属性参数传递,以指定LoanDTOData将被实例化以返回测试方法执行所需的数据:

[Theory,ClassData(typeof(LoanDTOData))]publicvoidTest_CalculateLoan_ShouldReturnCorrectRate(LoanDTOloanDTO){Loanloan=carLoanCalculator.CalculateLoan(loanDTO);Assert.NotNull(loan);Assert.InRange(loan.InterestRate,8,12);}使用任何合适的方法,都可以灵活地实现枚举器,无论是使用类属性还是方法。在运行测试之前,xUnit.net框架将在类上调用.ToList()。在使用ClassData属性将数据传递给您的测试时,您总是需要创建一个专用类来包含您的数据。

在项目的SqlDataExample文件夹中,有一些文件可以复制到您的项目中,以便为您提供直接连接到SQLServer数据库或可以使用OLEDB访问的任何数据源的功能。该文件夹中的四个类是DataAdapterDataAttribute,DataAdapterDataAttributeDiscoverer,OleDbDataAttribute和SqlServerDataAttribute。

需要注意的是,由于.NETCore不支持OLEDB,因此无法在.NETCore项目中使用前面的扩展。这是因为OLEDB技术是基于COM的,依赖于仅在Windows上可用的组件。但是您可以在常规.NET项目中使用此扩展。

GitHub上的xUnit.net存储库中提供了SqlServerData属性的代码清单,该属性可用于装饰数据理论,以直接从MicrosoftSQLServer数据库表中获取测试执行所需的数据。

为了测试SqlServerData属性,您应该在您的SQLServer实例中创建一个名为TheoryDb的数据库。创建一个名为Palindrome的表;它应该有一个名为varchar的列。用样本数据填充表,以便用于测试:

CREATETABLE[dbo].PalindromeNOTNULL);INSERTINTO[dbo].[Palindrome]([word])VALUES('civic')GOINSERTINTO[dbo].[Palindrome]([word])VALUES('dad')GOINSERTINTO[dbo].[Palindrome]([word])VALUES('omo')GOPalindronmeChecker类运行一个IsWordPalindrome方法来验证一个单词是否是回文,如下面的代码片段所示。回文是一个可以在两个方向上阅读的单词,例如dad或civic。在不使用算法实现的情况下,快速检查这一点的方法是反转单词并使用字符串SequenceEqual方法来检查这两个单词是否相等:

publicclassPalindromeChecker{publicboolIsWordPalindrome(stringword){returnword.SequenceEqual(word.Reverse());}}为了测试IsWordPalindrome方法,将实现一个测试方法Test_IsWordPalindrome_ShouldReturnTrue,并用SqlServerData属性进行装饰。此属性需要三个参数——数据库服务器地址、数据库名称和用于从包含要加载到测试中的数据的表或视图中检索数据的选择语句:

publicclassPalindromeCheckerTest{[Theory,SqlServerData(@".\sqlexpress","TheoryDb","selectwordfromPalindrome")]publicvoidTest_IsWordPalindrome_ShouldReturnTrue(stringword){PalindromeCheckerpalindromeChecker=newPalindromeChecker();Assert.True(palindromeChecker.IsWordPalindrome(word));}}当运行Test_IsWordPalindrome_ShouldReturnTrue时,将执行SqlServerData属性,以从数据库表中获取记录,用于执行测试方法。要创建的测试数量取决于表中可用的记录。在这种情况下,将创建并执行三个测试:

与xUnit.netGitHub存储库中可用的SqlServerData属性类似,您可以创建一个自定义属性来从任何源加载数据。自定义属性类必须实现DataAttribute,这是一个表示理论要使用的数据源的抽象类。自定义属性类必须重写并实现GetData方法。该方法返回IEnumerable,用于包装要返回的数据集的内容。

让我们创建一个CsvData自定义属性,可以用于从.csv文件中加载数据,用于数据驱动的单元测试。该类将具有一个构造函数,它接受两个参数。第一个是包含.csv文件的完整路径的字符串参数。第二个参数是一个布尔值,当为true时,指定是否应使用包含在.csv文件中的数据的第一行作为列标题,当为false时,指定忽略文件中的列标题,这意味着CSV数据从第一行开始。

自定义属性类是CsvDataAttribute,它实现了DataAttribute类。该类用AttributeUsage属性修饰,该属性具有以下参数—AttributeTargets用于指定应用属性的有效应用元素,AllowMultiple用于指定是否可以在单个应用元素上指定属性的多个实例,Inherited用于指定属性是否可以被派生类或覆盖成员继承:

[AttributeUsage(AttributeTargets.Method,AllowMultiple=false,Inherited=false)]publicclassCsvDataAttribute:DataAttribute{privatereadonlystringfilePath;privatereadonlyboolhasHeaders;publicCsvDataAttribute(stringfilePath,boolhasHeaders){this.filePath=filePath;this.hasHeaders=hasHeaders;}//TobefollowedbyGetDataimplementation}下一步是实现GetData方法,该方法将覆盖DataAttribute类中可用的实现。此方法使用System.IO命名空间中的StreamReader类逐行读取.csv文件的内容。实现了第二个实用方法ConverCsv,用于将CSV数据转换为整数值:

publicoverrideIEnumerableGetData(MethodInfomethodInfo){varmethodParameters=methodInfo.GetParameters();varparameterTypes=methodParameters.Select(x=>x.ParameterType).ToArray();using(varstreamReader=newStreamReader(filePath)){if(hasHeaders)streamReader.ReadLine();stringcsvLine=string.Empty;while((csvLine=streamReader.ReadLine())!=null){varcsvRow=csvLine.Split(',');yieldreturnConvertCsv((object[])csvRow,parameterTypes);}}}privatestaticobject[]ConvertCsv(IReadOnlyListcsvRow,IReadOnlyListparameterTypes){varconvertedObject=newobject[parameterTypes.Count];//convertobjectifintegerfor(inti=0;i

Test_IsWordPalindrome_ShouldReturnTrue测试方法将被修改以使用新创建的CsvData属性,以从.csv文件中获取测试执行的数据:

publicclassPalindromeCheckerTest{[Theory,CsvData(@"C:\data.csv",false)]publicvoidTest_IsWordPalindrome_ShouldReturnTrue(stringword){PalindromeCheckerpalindromeChecker=newPalindromeChecker();Assert.True(palindromeChecker.IsWordPalindrome(word));}}当您在VisualStudio中运行前面片段中的Test_IsWordPalindrome_ShouldReturnTrue测试方法时,测试运行器将创建三个测试。这应该对应于从.csv文件中检索到的记录或数据行数。测试信息可以从测试资源管理器中查看:

CsvData自定义属性可以从任何.csv文件中检索数据,无论单行上存在多少列。记录将被提取并传递给测试方法中的Theory属性。

让我们创建一个具有两个整数参数firstNumber和secondNumber的方法。该方法将计算整数值firstNumber和secondNumber的最大公约数。这两个整数的最大公约数是能够整除这两个整数的最大值:

publicintGetGcd(intfirstNumber,intsecondNumber){if(secondNumber==0)returnfirstNumber;elsereturnGetGcd(secondNumber,firstNumber%secondNumber);}现在,让我们编写一个测试方法来验证GetGcd方法。Test_GetGcd_ShouldRetunTrue将是一个数据理论,并具有三个整数参数—firstNumber、secondNumber和gcdValue。该方法将检查在调用时gdcValue参数中提供的值是否与调用时GetGcd方法返回的值匹配。测试的数据将从.csv文件中加载:

[Theory,CsvData(@"C:\gcd.csv",false)]publicvoidTest_GetGcd_ShouldRetunTrue(intfirstNumber,intsecondNumber,intgcd){intgcdValue=GetGcd(firstNumber,secondNumber);Assert.Equal(gcd,gcdValue);}根据.csv文件中提供的值,将创建测试。以下屏幕截图显示了运行时Test_GetGcdShouldReturnTrue的结果。创建了三个测试;一个通过,两个失败:

数据驱动的单元测试是TDD的重要概念,它带来了许多好处,可以让您使用来自多个数据源的真实数据广泛测试代码库,为您提供调整和重构代码以获得更好性能和健壮性所需的洞察力。

在本章中,我们介绍了数据驱动测试的好处,以及如何使用xUnit.net的内联和属性属性编写有效的数据驱动测试。此外,我们还探讨了在xUnit.net中使用的Theory属性进行数据驱动的单元测试。这使您能够针对来自不同数据源的广泛输入对代码进行适当的验证和验证。

虽然xUnit.net提供的默认数据源属性非常有用,但您可以进一步扩展DataAttribute类,并创建一个自定义属性来从另一个源加载数据。我们演示了CsvData自定义属性的实现,以从.csv文件加载测试数据。

在下一章中,我们将深入探讨另一个重要且有用的TDD概念,即依赖项模拟。模拟允许您在不必直接构造或执行依赖项代码的情况下,有效地对方法和类进行单元测试。

在软件项目的代码库中通常存在对象依赖,无论是简单项目还是复杂项目。这是因为各种对象需要相互交互并在边界之间共享信息。然而,为了有效地对对象进行单元测试并隔离它们的行为,每个对象必须在隔离的环境中进行测试,而不考虑它们对其他对象的依赖。

为了实现这一点,类中的依赖对象被替换为模拟对象,以便在测试时能够有效地进行隔离测试,而无需经历构造依赖对象的痛苦,有时这些依赖对象可能并未完全实现,或者在编写被测试对象时构造它们可能是不切实际的。

模拟对象用于模拟真实对象以进行代码测试。模拟对象用于替换真实对象;它们是从真实接口或类创建的,并用于验证交互。模拟对象是另一个类中引用的必要实例,用于模拟这些类的行为。由于软件系统的组件需要相互交互和协作,模拟对象用于替换协作者。使用模拟对象时,可以验证使用是否正确且符合预期。模拟对象可以使用模拟框架或库创建,或者通过手工编写模拟对象的代码生成。

本章将详细探讨Moq框架,并将用它来创建模拟对象。Moq是一个功能齐全的模拟框架,可以轻松设置。它可用于创建用于单元测试的模拟对象。Moq具有模拟框架应具备的几个基本和高级特性,以创建有用的模拟对象,并基本上编写良好的单元测试。

本章将涵盖以下主题:

在良好架构的软件系统中,通常有相互交互和协调以实现基于业务或自动化需求的设定目标的对象。这些对象往往复杂,并依赖于其他外部组件或系统,如数据库、SOAP或REST服务,用于数据和内部状态更新。

单元测试的主要特征是它应该运行非常快,并且即使使用相同的数据集多次执行,也应该给出一致的结果。然而,为了有效地运行单元测试并保持具有高效和快速运行的单元测试的属性,重要的是在被测试的代码中存在依赖关系时设置模拟对象。

例如,在以下代码片段中,LoanRepository类依赖于EntityFramework的DbContext类,后者创建与数据库服务器的连接以进行数据库操作。要为LoanRepository类中的GetCarLoans方法编写单元测试,将需要构造DbContext对象。可以对DbContext对象进行模拟,以避免每次对该类运行单元测试时打开和关闭数据库连接的昂贵操作:

使用依赖项模拟,您在代码中实际上创建了依赖项的替代方案,可以进行实验。当您在适当位置有依赖项的模拟实现时,您可以进行更改并测试更改的效果,因为测试将针对模拟对象而不是真实对象运行。

当您将依赖项隔离时,您可以专注于正在运行的测试,从而将测试的范围限制在对测试真正重要的代码上。实质上,通过减少范围,您可以轻松重构被测试的代码以及测试本身,从而清晰地了解代码可以改进的地方。

为了在以下代码片段中隔离地测试LoanRepository类,可以对该类依赖的DbContext对象进行模拟。这将限制单元测试的范围仅限于LoanRepository类:

publicclassLoanRepository{privateDbContextdbContext;publicLoanRepository(DbContextdbContext){this.dbContext=dbContext;}}此外,通过隔离依赖项来保持测试范围较小,使得测试易于理解并促进了易于维护。通过不模拟依赖项来增加测试范围,最终会使测试维护变得困难,并减少测试的高级详细覆盖。由于必须对依赖项进行测试,这可能导致由于范围增加而导致测试的细节减少。

遗留源代码是由您或其他人编写的代码,通常没有测试或使用旧的框架、架构或技术。这样的代码库可能很难重写或维护。它有时可能是难以阅读和理解的混乱代码,因此很难更改。

通过模拟,您可以确保进行广泛的测试覆盖,因为您可以轻松使用模拟对象来模拟可能的异常、执行场景和条件,否则这些情况将很难实现。例如,如果您有一个清除或删除数据库表的方法,使用模拟对象测试这个方法比每次运行单元测试时在实时数据库上运行更安全。

大多数嘲弄框架的架构要求必须创建接口来模拟对象。实质上,你不能直接模拟一个类;必须通过类实现的接口来进行。为了在单元测试期间模拟依赖关系,为每个要模拟的对象或依赖关系创建一个接口,即使在生产代码中使用该依赖关系时并不需要该接口。这导致创建了太多的接口,这种情况被称为接口爆炸。

大多数模拟框架使用反射或创建代理来调用方法并创建单元测试中所需的模拟。这个过程很慢,并给单元测试过程增加了额外的开销。特别是当希望使用模拟来模拟所有类和依赖关系之间的交互时,这一点尤其明显,这可能导致模拟返回其他模拟的情况。

使用模拟框架可以促进流畅的单元测试体验,特别是在单元测试具有依赖关系的代码部分时,模拟对象被创建并替代依赖关系。虽然使用模拟框架更容易,但有时你可能更喜欢手动编写模拟对象进行单元测试,而不向项目或代码库添加额外的复杂性或附加库。

手动编写的模拟是为了测试而创建的类,用于替换生产对象。这些创建的类将具有与生产类相同的方法和定义,以及返回值,以有效模拟生产类并用作单元测试中依赖关系的替代品。

创建模拟的第一步应该是识别依赖关系。单元测试的目标应该是编写清晰的代码,并尽可能快地运行具有良好覆盖率的测试。你应该识别可能减慢测试速度的依赖关系。例如,Web服务或数据库调用就是模拟的候选对象。

创建模拟对象的方法可以根据被模拟的依赖关系的类型而变化。然而,模拟的概念可以遵循模拟对象在调用方法时应返回特定预定义值的基本概念。应该有适当的验证机制来确保模拟的方法被调用,并且如果根据测试要求进行配置,模拟对象可以抛出异常。

了解模拟对象的类型对于有效地手动编写模拟对象非常重要。可以创建两种类型的模拟对象——动态和静态模拟对象。动态对象可以通过反射或代理类创建。这类似于模拟框架的工作方式。静态模拟对象可以通过实现接口的类以及有时作为要模拟的依赖关系的实际具体类来创建。当你手动编写模拟对象时,实质上你正在创建静态模拟对象。

反射可以用来创建模拟对象。C#中的反射是一个有用的构造,允许你创建一个类型的实例对象,以及获取或绑定类型到现有对象,并调用类型中可用的字段和方法。此外,你可以使用反射来创建描述模块和程序集的对象。

手动编写您的模拟有时可能是一种有效的方法,当您打算完全控制测试设置并指定测试设置的行为时。此外,当测试相对简单时,使用模拟框架不是一个选择;最好手动编写模拟并保持一切简单。

使用模拟框架时,对被模拟的真实对象进行更改将需要更改在其使用的任何地方的模拟对象。这是因为对依赖项进行的更改将破坏测试。例如,如果依赖对象上的方法名称发生更改,您必须在动态模拟中进行更改。因此,必须在代码库的几个部分进行更改。使用手动编写的模拟,您只需要在一个地方进行更改,因为您可以控制向测试呈现的方法。

模拟和存根都很相似,因为它们用于替换类依赖项或协作者,并且大多数模拟框架都提供创建两者的功能。存根可以以与手动编写模拟相同的方式手动编写。

那么模拟和存根真正的区别是什么?模拟用于测试协作。这包括验证实际协作者的期望。模拟被编程为具有包含要接收的方法调用详细信息的期望,而存根用于模拟协作者。让我们通过一个例子进一步解释这一点。

以下片段中的LoanService类具有一个GetBadCarLoans方法,该方法接受要从数据库中检索的Loan对象列表:

publicclassLoanService{publicListGetBadCarLoans(ListcarLoans){ListbadLoans=newList();//dobusinesslogiccomputationsontheloansreturnbadLoans;}}以下片段中Test_GetBadCarLoans_ShouldReturnLoans的GetBadCarLoans方法的测试使用了存根,这是一个Loan对象列表,作为参数传递给GetBadCarLoans方法,而不是调用数据库以获取用于Test类的Loan对象列表:

[Fact]publicvoidTest_GetBadCarLoans_ShouldReturnLoans(){Listloans=newList();loans.Add(newLoan{Amount=120000,Rate=12.5,ServiceYear=5,HasDefaulted=false});loans.Add(newLoan{Amount=150000,Rate=12.5,ServiceYear=4,HasDefaulted=true});loans.Add(newLoan{Amount=200000,Rate=12.5,ServiceYear=5,HasDefaulted=false});LoanServiceloanService=newLoanService();ListbadLoans=loanService.GetBadCarLoans(loanDTO);Assert.NotNull(badLoans);}以下片段中的LoanService类具有连接到数据库以获取记录的LoanRepositoryDI。该类具有一个构造函数,在该构造函数中注入了ILoanRepository对象。LoanService类具有一个GetBadCarLoans方法,该方法调用依赖项上的GetCarLoan方法,后者又调用数据库获取Loan对象列表:

publicclassLoanService{privateILoanRepositoryloanRepository;publicLoanService(ILoanRepositoryloanRepository){this.loanRepository=loanRepository;}publicListGetBadCarLoans(){ListbadLoans=newList();varcarLoans=loanRepository.GetCarLoans();//dobusinesslogiccomputationsontheloansreturnbadLoans;}}与使用存根时不同,模拟将验证调用依赖项中的方法。这意味着模拟对象将设置依赖项中要调用的方法。在以下片段中的LoanServiceTest类中,从ILoanRepository创建了一个模拟对象:

publicclassLoanServiceTest{privateMockloanRepository;privateLoanServiceloanService;publicLoanServiceTest(){loanRepository=newMock();Listloans=newList{newLoan{Amount=120000,Rate=12.5,ServiceYear=5,HasDefaulted=false},newLoan{Amount=150000,Rate=12.5,ServiceYear=4,HasDefaulted=true},newLoan{Amount=200000,Rate=12.5,ServiceYear=5,HasDefaulted=false}};loanRepository.Setup(x=>x.GetCarLoans()).Returns(loans);loanService=newLoanService(loanRepository.Object);}[Fact]publicvoidTest_GetBadCarLoans_ShouldReturnLoans(){ListbadLoans=loanService.GetBadCarLoans();Assert.NotNull(badLoans);}}在LoanServiceTest类的构造函数中,首先创建了模拟对象要返回的数据,然后设置了依赖项中的方法,如loanRepository.Setup(x=>x.GetCarLoans()).Returns(loans);。然后将模拟对象传递给LoanService构造函数,loanService=newloanService(loanRepository.Object);。

我们可以手动编写一个模拟对象来测试LoanService类。要创建的模拟对象将实现ILoanRepository接口,并且仅用于单元测试,因为在生产代码中不需要它。模拟对象将返回一个Loan对象列表,这将模拟对数据库的实际调用。

publicclassLoanRepositoryMock:ILoanRepository{publicListGetCarLoans(){Listloans=newList{newLoan{Amount=120000,Rate=12.5,ServiceYear=5,HasDefaulted=false},newLoan{Amount=150000,Rate=12.5,ServiceYear=4,HasDefaulted=true},newLoan{Amount=200000,Rate=12.5,ServiceYear=5,HasDefaulted=false}};returnloans;}}现在可以在LoanService类中使用创建的LoanRepositoryMock类来模拟ILoanRepository,而不是使用从模拟框架创建的模拟对象。在LoanServiceTest类的构造函数中,将实例化LoanRepositoryMock类并将其注入到LoanService类中,该类在Test类中使用:

publicclassLoanServiceTest{privateILoanRepositoryloanRepository;privateLoanServiceloanService;publicLoanServiceTest(){loanRepository=newLoanRepositoryMock();loanService=newLoanService(loanRepository);}[Fact]publicvoidTest_GetBadCarLoans_ShouldReturnLoans(){ListbadLoans=loanService.GetBadCarLoans();Assert.NotNull(badLoans);}}因为LoanRepositoryMock被用作ILoanRepository接口的具体类,是LoanService类的依赖项,所以每当在ILoanRepository接口上调用GetCarLoans方法时,LoanRepositoryMock的GetCarLoans方法将被调用以返回测试运行所需的数据。

选择用于模拟对象的模拟框架对于顺利进行单元测试是很重要的。然而,并没有必须遵循的书面规则。在选择用于测试的模拟框架时,您可以考虑一些因素和功能。

在选择模拟框架时,性能和可用功能应该是首要考虑因素。您应该检查模拟框架创建模拟的方式;使用继承、虚拟和静态方法的框架无法被模拟。要注意的其他功能可能包括方法、属性、事件,甚至是框架是否支持LINQ。

此外,没有什么比库的简单性和易用性更好。您应该选择一个易于使用的框架,并且具有良好的可用功能文档。在本章的后续部分中,将使用Moq框架来解释模拟的其他概念,这是一个易于使用的强类型库。

使用Moq时,模拟对象是一个实际的虚拟类,它是使用反射为您创建的,其中包含了被模拟的接口中包含的方法的实现。在Moq设置中,您将指定要模拟的接口以及测试类需要有效运行测试的方法。

要使用Moq,您需要通过NuGet包管理器或NuGet控制台安装该库:

Install-PackageMoq为了解释使用Moq进行模拟,让我们创建一个ILoanRepository接口,其中包含两种方法,GetCarLoan用于从数据库中检索汽车贷款列表,以及GetLoanTypes方法,用于返回LoanType对象的列表:

publicinterfaceILoanRepository{ListGetLoanTypes();ListGetCarLoans();}LoanRepository类使用EntityFramework作为数据访问和检索的ORM,并实现了ILoanRepository。GetLoanTypes和GetCarLoans两种方法已经被LoanRepository类实现:

publicclassLoanRepository:ILoanRepository{publicListGetLoanTypes(){ListloanTypes=newList();using(LoanContextcontext=newLoanContext()){loanTypes=context.LoanType.ToList();}returnloanTypes;}publicListGetCarLoans(){Listloans=newList();using(LoanContextcontext=newLoanContext()){loans=context.Loan.ToList();}returnloans;}}让我们为ILoanRepository创建一个模拟对象,以便在不依赖任何具体类实现的情况下测试这两种方法。

使用Moq很容易创建一个模拟对象:

MockloanRepository=newMock();在上一行代码中,已经创建了一个实现ILoanRepository接口的模拟对象。该对象可以被用作ILoanRepository的常规实现,并注入到任何具有ILoanRepository依赖的类中。

在测试中使用模拟对象的方法之前,它们需要被设置。这个设置最好是在测试类的构造函数中完成,模拟对象创建后,但在将对象注入到需要依赖的类之前。

首先,需要创建要由设置的方法返回的数据;这是测试中要使用的虚拟数据:

Listloans=newList{newLoan{Amount=120000,Rate=12.5,ServiceYear=5,HasDefaulted=false},newLoan{Amount=150000,Rate=12.5,ServiceYear=4,HasDefaulted=true},newLoan{Amount=200000,Rate=12.5,ServiceYear=5,HasDefaulted=false}};在设置方法的时候,返回数据将被传递给它,以及任何方法参数(如果适用)。在下一行代码中,GetCarLoans方法被设置为以Loan对象的列表作为返回数据。这意味着每当在单元测试中使用模拟对象调用GetCarLoans方法时,之前创建的列表将作为方法的返回值返回:

MockloanRepository=newMock();loanRepository.Setup(x=>x.GetCarLoans()).Returns(loans);您可以对方法返回值进行延迟评估。这是使用LINQ提供的语法糖:

loanRepository.Setup(x=>x.GetCarLoans()).Returns(()=>loans);Moq有一个It对象,它可以用来指定方法中参数的匹配条件。It指的是被匹配的参数。假设GetCarLoans方法有一个字符串参数loanType,那么方法设置的语法可以改变以包括参数和返回值:

loanRepository.Setup(x=>x.GetCarLoans(It.IsAny())).Returns(loans);可以设置一个方法,每次调用时返回不同的返回值。例如,可以设置GetCarLoans方法的设置,以便在每次调用该方法时返回不同大小的列表:

Randomrandom=newRandom();loanRepository.Setup(x=>x.GetCarLoans()).Returns(loans).Callback(()=>loans.GetRange(0,random.Next(1,3));在上面的片段中,生成了1和3之间的随机数,以设置。这将确保由GetCarLoans方法返回的列表的大小随每次调用而变化。第一次调用GetCarLoans方法时,将调用Returns方法,而在随后的调用中,将执行Callback中的代码。

Moq的一个特性是提供异常测试的功能。您可以设置方法以测试异常。在以下方法设置中,当调用时,GetCarLoans方法会抛出InvalidOperationException:

loanRepository.Setup(x=>x.GetCarLoans()).Throws();属性如果您有一个具有要在方法调用中使用的属性的依赖项,可以使用Moq的SetupProperty方法为这些属性设置虚拟值。让我们向ILoanRepository接口添加两个属性,LoanType和Rate:

publicinterfaceILoanRepository{LoanTypeLoanType{get;set;}floatRate{get;set;}ListGetLoanTypes();ListGetCarLoans();}使用Moq的SetupProperty方法,您可以指定属性应具有的行为,这实质上意味着每当请求属性时,将返回在SetupProperty方法中设置的值:

MockloanRepository=newMock();loanRepository.Setup(x=>x.LoanType,LoanType.CarLoan);loanRepository.Setup(x=>x.Rate,12.5);在上面的片段中的代码将LoanType属性设置为枚举值CarLoan,并将Rate设置为12.5。在测试中请求属性时,将返回设置的值到调用点。

使用SetupProperty方法设置属性会自动将属性设置为存根,并允许跟踪属性的值并为属性提供默认值。

此外,在设置属性时,还可以使用SetupSet方法,该方法接受lambda表达式来指定对属性设置器的调用类型,并允许您将值传递到表达式中:

loanRepository.SetupSet(x=>x.Rate=12.5F);SetupSet类似于SetupGet,用于为属性的调用指定类型的设置:

loanRepository.SetupGet(x=>x.Rate);递归模拟允许您模拟复杂的对象类型,特别是嵌套的复杂类型。例如,您可能希望模拟Loan类型中Person复杂类型的Age属性。Moq框架可以以优雅的方式遍历此图以模拟属性:

loanRepository.SetupSet(x=>x.CarLoan.Person.Age=40);您可以使用SetupAllProperties方法存根模拟对象上的所有属性。此方法将指定模拟上的所有属性都具有属性行为设置。通过在模拟中为每个属性生成默认值,使用Moq框架的Mock.DefaultProperty属性生成默认属性:

loanRepository.SetupAllProperties();匹配参数在使用Moq创建模拟对象时,您可以匹配参数以确保在测试期间传递了预期的参数。使用此功能,您可以确定在测试期间调用方法时传递的参数的有效性。这仅适用于具有参数的方法,并且匹配将在方法设置期间进行。

使用Moq的It关键字,您可以在设置期间为方法参数指定不同的表达式和验证。让我们向ILoanRepository接口添加一个GetCarLoanDefaulters方法定义。LoanRepository类中的实现接受一个整数参数,该参数是贷款的服务年限,并返回汽车贷款拖欠者的列表。以下片段显示了GetCarLoanDefaulters方法的代码:

publicListGetCarLoanDefaulters(intyear){Listdefaulters=newList();using(LoanContextcontext=newLoanContext()){defaulters=context.Loan.Where(c=>c.HasDefaulted&&c.ServiceYear==year).Select(c=>c.Person).ToList();}returndefaulters;}现在,让我们在LoanServiceTest构造函数中设置GetCarLoanDefaulters方法,以使用Moq的It关键字接受不同的year参数值:

Listpeople=newList{newPerson{FirstName="Donald",LastName="Duke",Age=30},newPerson{FirstName="Ayobami",LastName="Adewole",Age=20}};MockloanRepository=newMock();loanRepository.Setup(x=>x.GetCarLoanDefaulters(It.IsInRange(1,5,Range.Inclusive))).Returns(people);已创建了一个Person对象列表,将传递给模拟设置的Returns方法。GetCarLoanDefaulters方法现在将接受指定范围内的值,因为It.IsInRange方法已经使用了上限和下限值。

It类有其他有用的方法,用于在设置期间指定方法的匹配条件,而不必指定特定的值:

您可以创建一个自定义匹配器,并在方法设置中使用它。例如,让我们为GetCarLoanDefaulters方法创建一个自定义匹配器IsOutOfRange,以确保不会提供大于12的值作为参数。通过使用Match.Create来创建自定义匹配器:

publicintIsOutOfRange(){returnMatch.Create(x=>x>12);}现在可以在模拟对象的方法设置中使用创建的IsOutOfRange匹配器:

loanRepository.Setup(x=>x.GetCarLoanDefaulters(IsOutOfRange())).Throws();事件Moq有一个功能,允许您在模拟对象上引发事件。要引发事件,您使用Raise方法。该方法有两个参数。第一个是Lambda表达式,用于订阅事件以在模拟上引发事件。第二个参数提供将包含在事件中的参数。要在loanRepository模拟对象上引发LoanDefaulterNotification事件,并使用空参数,您可以使用以下代码行:

MockloanRepository=newMock();loanRepository.Raise(x=>x.LoanDefaulterNotification+=null,EventArgs.Empty);真实用例是当您希望模拟对象响应动作引发事件或响应方法调用引发事件时。在模拟对象上设置方法以允许引发事件时,模拟上的Returns方法将被替换为Raises方法,该方法指示在测试中调用方法时,应该引发事件:

loanRepository.Setup(x=>x.GetCarLoans()).Raises(x=>x.LoanDefaulterNotification+=null,newLoanDefualterEventArgs{OK=true});回调使用Moq的Callback方法,您可以指定在调用方法之前和之后要调用的回调。有一些测试场景可能无法使用简单的模拟期望轻松测试。在这种复杂的情况下,您可以使用回调来执行特定的操作,当调用模拟对象时。Callback方法接受一个动作参数,根据回调是在方法调用之前还是之后设置,将执行该动作。该动作可以是要评估的表达式或要调用的另一个方法。

例如,您可以设置一个回调,在调用特定方法之后更改数据。此功能允许您创建提供更大灵活性的测试,同时简化测试复杂性。让我们向loanRepository模拟对象添加一个回调。

回调可以是一个将被调用的方法,或者是您需要设置值的属性:

Listpeople=newList{newPerson{FirstName="Donald",LastName="Duke",Age=30},newPerson{FirstName="Ayobami",LastName="Adewole",Age=20}};MockloanRepository=newMock();loanRepository.Setup(x=>x.GetCarLoanDefaulters()).Callback(()=>CarLoanDefaultersCallbackAfter()).Returns(()=>people).Callback(()=>CarLoanDefaultersCallbackAfter());上面的片段为方法设置设置了两个回调。CarLoanDefaultersCallback方法在实际调用GetCarLoanDefaulters方法之前被调用,CarLoanDefaultersCallbackAfter在在模拟对象上调用GetCarLoanDefaulters方法之后被调用。CarLoanDefaultersCallback向List添加一个新的Person对象,CarLoanDefaultersCallback删除列表中的第一个元素:

publicvoidCarLoanDefaultersCallback(){people.Add(newPerson{FirstName="John",LastName="Doe",Age=40});}publicvoidCarLoanDefaultersCallbackAfter(){people.RemoveAt(0);}模拟定制在使用Moq框架时,您可以进一步定制模拟对象,以增强有效的单元测试体验。可以将MockBehavior枚举传递到Moq的Mock对象构造函数中,以指定模拟的行为。枚举成员有Default、Strict和Loose:

loanRepository=newMock(MockBehavior.Loose);当选择Loose成员时,模拟将不会抛出任何异常。默认值将始终返回。这意味着对于引用类型,将返回null,对于值类型,将返回零或空数组和可枚举类型:

loanRepository=newMock(MockBehavior.Strict);选择Strict成员将使模拟对于每次在模拟上没有适当设置的调用都抛出异常。最后,Default成员是模拟的默认行为,从技术上讲等同于Loose枚举成员。

在模拟构造期间初始化CallBase时,用于指定是否在没有匹配的设置时调用基类虚拟实现。默认值为false。这在模拟System.Web命名空间的HTML/web控件时非常有用:

loanRepository=newMock{CallBase=true};模拟存储库通过使用Moq中的MockRepository,可以避免在测试中分散创建模拟对象的代码,从而避免重复的代码。MockRepository可用于在单个位置创建和验证模拟,从而确保您可以通过设置CallBase、DefaultValue和MockBehavior进行模拟配置,并在一个地方验证模拟:

varmockRepository=newMockRepository(MockBehavior.Strict){DefaultValue=DefaultValue.Mock};varloanRepository=repository.Create(MockBehavior.Loose);varuserRepository=repository.Create();mockRepository.Verify();在上述代码片段中,使用MockBehaviour.Strict创建了一个模拟存储库,并创建了两个模拟对象,每个对象都使用loanRepository模拟,覆盖了存储库中指定的默认MockBehaviour。最后一条语句是对Verify方法的调用,以验证存储库中创建的所有模拟对象的所有期望。

此外,您可以在单个模拟中实现多个接口。例如,我们可以创建一个模拟,实现ILoanRepository,然后使用As<>方法实现IDisposable接口,该方法用于向模拟添加接口实现并为其指定设置:

varloanRepository=newMock();loanRepository.Setup(x=>x.GetCarLoanDefaulters(It.IsInRange(1,5,Range.Inclusive))).Returns(people);loanRepository.As().Setup(disposable=>disposable.Dispose());使用Moq进行验证的方法和属性调用模拟行为在设置期间指定。这是对象和协作者的预期行为。在单元测试时,模拟不完整,直到验证了所有模拟依赖项的调用。了解方法执行的次数或属性访问的次数可能会有所帮助。

Moq框架具有有用的验证方法,可用于验证模拟的方法和属性。此外,Times结构包含有用的成员,显示可以在方法上允许的调用次数。

Verify方法可用于验证在模拟上执行的方法调用及提供的参数是否与先前在模拟设置期间配置的内容匹配,并且使用了默认的MockBehaviour,即Loose。为了解释Moq中的验证概念,让我们创建一个依赖于ILoanRepository的LoanService类,并向其添加一个名为GetOlderCarLoanDefaulters的方法,以返回年龄大于20岁的贷款拖欠人的列表。ILoanRepository通过构造函数注入到LoanService中:

publicclassLoanService{privateILoanRepositoryloanRepository;publicLoanService(ILoanRepositoryloanRepository){this.loanRepository=loanRepository;}publicListGetOlderCarLoanDefaulters(intyear){Listdefaulters=loanRepository.GetCarLoanDefaulters(year);varfilteredDefaulters=defaulters.Where(x=>x.Age>20).ToList();returnfilteredDefaulters;}}为了测试LoanService类,我们将创建一个LoanServiceTest测试类,该类使用依赖模拟来隔离LoanService进行单元测试。LoanServiceTest将包含一个构造函数,用于设置LoanService类所需的ILoanRepository的模拟:

publicclassLoanServiceTest{privateMockloanRepository;privateLoanServiceloanService;publicLoanServiceTest(){loanRepository=newMock();Listpeople=newList{newPerson{FirstName="Donald",LastName="Duke",Age=30},newPerson{FirstName="Ayobami",LastName="Adewole",Age=20}};loanRepository.Setup(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive))).Returns(()=>people);loanService=newLoanService(loanRepository.Object);}}LoanServiceTest构造函数包含对ILoanRepository接口的GetCarLoanDefaulters方法的模拟设置,包括参数期望和返回值。让我们创建一个名为Test_GetOlderCarLoanDefaulters_ShouldReturnList的测试方法,以测试GetCarLoanDefaulters。在断言语句之后,有Verify方法来检查GetCarLoanDefaulters是否被调用了一次:

[Fact]publicvoidTest_GetOlderCarLoanDefaulters_ShouldReturnList(){Listdefaulters=loanService.GetOlderCarLoanDefaulters(12);Assert.NotNull(defaulters);Assert.All(defaulters,x=>Assert.Contains("Donald",x.FirstName));loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.Once());}Verify方法接受两个参数:要验证的方法和Time结构。使用了Time.Once,指定模拟方法只能被调用一次。

Times.AtLeast(intcallCount)用于指定模拟方法应该被调用的最小次数,该次数由callCount参数的值指定。这可用于验证方法被调用的次数:

[Fact]publicvoidTest_GetOlderCarLoanDefaulters_ShouldReturnList(){Listdefaulters=loanService.GetOlderCarLoanDefaulters(12);Assert.NotNull(defaulters);Assert.All(defaulters,x=>Assert.Contains("Donald",x.FirstName));loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.AtLeast(2));}在上述测试片段中,将Times.AtLeast(2)传递给Verify方法。当运行测试时,由于被测试的代码中的GetCarLoanDefaulters方法只被调用了一次,测试将失败,并显示Moq.MoqException。

Times.AtLeastOnce可用于指定模拟方法应至少调用一次,这意味着该方法可以在被测试的代码中被多次调用。我们可以修改Test_GetOlderCarLoanDefaulters_ShouldReturnList中的Verify方法,以将第二个参数设置为Time.AtLeastOnce,以验证测试运行后GetCarLoanDefaulters至少在被测试的代码中被调用一次:

[Fact]publicvoidTest_GetOlderCarLoanDefaulters_ShouldReturnList(){Listdefaulters=loanService.GetOlderCarLoanDefaulters(12);Assert.NotNull(defaulters);Assert.All(defaulters,x=>Assert.Contains("Donald",x.FirstName));loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.AtLeastOnce);}Times.AtMost(intcallCount)可用于指定在被测试的代码中应调用模拟方法的最大次数。callCount参数用于传递方法的最大调用次数的值。这可用于限制允许对模拟方法的调用。如果调用方法的次数超过指定的callCount值,则会抛出Moq异常:

loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.AtMost(1));Times.AtMostOnce类似于Time.Once或Time.AtLeastOnce,但不同之处在于模拟方法最多只能调用一次。如果方法被调用多次,则会抛出Moq异常,但如果在运行代码时未调用该方法,则不会抛出异常:

loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.AtMostOnce);Times.Between(callCountFrom,callCountTo,Range)可用于在Verify方法中指定模拟方法应在callCountFrom和callCountTo之间调用,并且Range枚举用于指定是否包括或排除指定的范围:

loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.Between(1,2,Range.Inclusive));Times.Exactly(callCount)在您希望指定模拟方法应在指定的callCount处调用时非常有用。如果模拟方法的调用次数少于指定的callCount或多次,将生成Moq异常,并提供期望和失败的详细描述:

[Fact]publicvoidTest_GetOlderCarLoanDefaulters_ShouldReturnList(){Listdefaulters=loanService.GetOlderCarLoanDefaulters(12);Assert.NotNull(defaulters);Assert.All(defaulters,x=>Assert.Contains("Donald",x.FirstName));loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.Exactly(2));}现在让我们检查代码:

还有一个重要的是Times.Never。当使用时,它可以验证模拟方法从未被使用。当您不希望调用模拟方法时,可以使用此选项:

loanRepository.Verify(x=>x.GetCarLoanDefaulters(It.IsInRange(1,12,Range.Inclusive)),Times.Never);模拟属性验证与使用VerifySet和VerifyGet方法的模拟方法类似进行。VerifySet方法用于验证在模拟对象上设置了属性。此外,VerifyGet方法用于验证在模拟对象上读取了属性,而不管属性中包含的值是什么:

loanRepository.VerifyGet(x=>x.Rate);要验证在模拟对象上设置了属性,而不管设置了什么值,可以使用VerifySet方法,语法如下:

loanRepository.VerifySet(x=>x.Rate);有时,您可能希望验证在模拟对象上分配了特定值给属性。您可以通过将值分配给VerifySet方法中的属性来执行此操作:

loanRepository.VerifySet(x=>x.Rate=12.5);Moq4.8中引入的VerifyNoOtherCalls()方法可用于确定除了已经验证的调用之外没有进行其他调用。VerifyAll()方法用于验证所有期望,无论它们是否已被标记为可验证。

在本章中,我们使用Lambda表达式语法创建了模拟对象。Moq框架中提供的另一个令人兴奋的功能是LINQ到模拟,它允许您使用类似LINQ的语法设置模拟。

LINQ到模拟非常适用于简单的模拟,并且在您真的不关心验证依赖关系时。使用Of<>方法,您可以创建指定类型的模拟对象。

您可以使用LINQ到模拟来在单个模拟和递归模拟上进行多个设置,使用类似LINQ的语法:

varloanRepository=Mock.Of(x=>x.Rate==12.5F&&x.LoanType.Name=="CarLoan"&&LoanType.Id==3);在前面的模拟初始化中,Rate和LoanType属性被设置为存根,在测试调用期间访问这些属性时,它们将使用属性的默认值。

有时,Moq提供的默认值可能不适用于某些测试场景,您需要创建自定义的默认值生成方法来补充Moq当前提供的DefaultValue.Empty和DefaultValue.Mock。这可以通过扩展Moq4.8及更高版本中提供的DefaultValueProvider或LookupOrFallbackDefaultValueProvider来实现:

publicclassTestDefaultValueProvider:LookupOrFallbackDefaultValueProvider{publicTestDefaultValueProvider(){base.Register(typeof(string),(type,mock)=>string.empty);base.Register(typeof(List<>),(type,mock)=>Activator.CreateInstance(type));}}TestDefaultValueProvider类创建了子类LookupOrFallbackDefaultValueProvider,并为string和List的默认值进行了实现。对于任何类型的string,都将返回string.empty,并创建一个空列表,其中包含任何类型的List。TestDefaultValueProvider现在可以在Mock构造函数中用于模拟创建:

varloanRepository=newMock{DefaultValueProvider=newTestDefaultValueProvider()};varobjectName=loanRepository.Object.Name;在前面的代码片段中,objectName变量将包含一个零长度的字符串,因为TestDefaultValueProvider中的实现表明应该为string类型分配一个空字符串。

如果包含内部类型的程序集尚未具有AssemblyInfo.cs文件,您可以添加它。此外,当程序集没有强名称时,您可以添加InternalsVisibleTo属性,其中排除了公钥。您必须指定要与之共享可见性的项目名称,在这种情况下应该是测试项目。

如果将LoanService的访问修饰符更改为internal,您将收到错误消息,LoanService由于其保护级别而无法访问。为了能够测试LoanService,而不更改访问修饰符,我们需要将AssemblyInfo.cs文件添加到项目中,并添加所需的属性,指定测试项目名称,以便与包含LoanService的程序集共享:

AssemblyInfo.cs文件中添加的属性如下所示:

[assembly:InternalsVisibleTo("LoanApplication.Tests.Unit")总结Moq框架与xUnit.net框架一起使用时,可以提供流畅的单元测试体验,并使整个TDD过程变得有价值。Moq提供了强大的功能,有效使用时,可以简化单元测试的依赖项模拟的创建。

使用Moq创建的模拟对象可以让您在单元测试中替换具体的依赖项,以便通过您创建的模拟对象来隔离代码中的不同单元进行测试和后续重构,这有助于编写优雅的生产就绪代码。此外,您可以使用模拟对象来实验和测试依赖项中可用的功能,否则可能无法轻松地使用实际依赖项来完成。

在本章中,我们探讨了模拟的基础知识,并在单元测试中广泛使用了模拟。此外,我们配置了模拟以设置方法和属性,并返回异常。还解释了Moq库提供的一些其他功能,并介绍了模拟验证。

项目托管和持续集成将在下一章中介绍。这将包括测试和企业方法来自动运行测试,以确保能够提供有关代码覆盖率的质量反馈。

在第四章中,我们探讨了.NETCore和C#可用的各种单元测试框架,然后详细探讨了xUnit.net框架。然后我们转向第五章中的数据驱动单元测试,这有助于创建可以使用来自不同数据源加载的数据执行的单元测试。在第六章中,我们详细解释了依赖项模拟,其中我们通过Moq框架创建了模拟对象。

有效的TDD实践可以帮助提供有用和深刻的反馈,评估软件项目的代码库质量。通过持续集成,构建自动化和代码自动化测试的过程被提升到了一个新的水平,允许开发团队充分利用现代源代码版本控制系统中提供的基本和高级功能。

正确的持续集成设置和实践会产生有益的持续交付,使软件项目的开发过程能够在项目的生命周期中被交付或部署到生产环境。

在本章中,我们将探讨持续集成和持续交付的概念。本章将涵盖以下主题:

持续集成(CI)是软件开发实践,软件项目的源代码每天由软件开发团队的成员集成到存储库中。最好在开发过程的早期阶段开始。代码集成通常由CI工具执行,该工具使用自动构建脚本对代码进行验证。

在开发团队中,通常有多个开发人员在项目的不同部分上工作,项目的源代码托管在存储库中。每个开发人员可以在他们的计算机上拥有主分支或主线的本地版本或工作副本。

负责某个功能的开发人员会对本地副本进行更改,并使用一组准备好的自动化测试来测试代码,以确保代码能够正常工作并不会破坏任何现有的工作功能。一旦可以验证,本地副本将更新为存储库中的最新版本。如果更新导致任何冲突,这些冲突需要在最终提交或集成工作之前解决。

CI要求建立适当的工作流程。CI的第一个重要组成部分是建立一个可工作的源代码存储库。这是为了跟踪项目贡献者所做的所有更改,并协调不同的活动。

为了实现一个稳健和有效的CI设置,需要涵盖并正确设置以下领域。

为了有效地使用源代码存储库,所有成功构建项目的所需文件都应该放在一个单一的源代码存储库中。这些文件应该包括源文件、属性文件、数据库脚本和架构,以及第三方库和使用的资产。

其他配置文件也可以放在存储库中,特别是开发环境配置。这将确保项目上的开发人员拥有一致的环境设置。开发团队的新成员可以轻松地使用存储库中可用的配置来设置他们的环境。

CI工作流程的构建自动化步骤是为了确保项目代码库中的更改被检测并自动进行测试和构建。构建自动化通常是通过构建脚本完成的,这些脚本分析需要进行的更改和编译。源代码应该经常构建,最好是每天或每晚。提交的成功与否是根据代码库是否成功构建来衡量的。

构建自动化脚本应该能够在有或没有测试的情况下构建系统。这应该在构建中进行配置。无论开发人员的集成开发环境是否具有内置的构建管理,都应该在服务器上配置一个中央构建脚本,以确保项目可以构建并在开发服务器上轻松运行。

通过适当的自动化测试,源代码中的错误可以在自动化构建脚本运行时轻松被检测到。将自动化测试整合到构建过程中将确保良好的测试覆盖率,并提供失败或通过测试的报告,以便便于重构代码。

为了确保顺利的CI体验,重要的是要确保测试和生产环境是相同的。两个环境应该具有类似的硬件和操作系统配置,以及环境设置。

此外,对于使用数据库的应用程序,测试和生产环境应该具有相同的版本。运行时和库也应该是相似的。然而,有时可能无法在每个生产环境实例中进行测试,比如桌面应用程序,但必须确保在测试中使用生产环境的副本。

代码库的整体健康状况取决于成功运行的构建过程。项目的主干应该经常更新,以便开发人员提交。提交代码的开发人员有责任确保在推送到存储库之前对代码进行测试。

在开发人员的提交导致构建失败的情况下,不应该拖延。可以回滚以在提交更改之前独立修复问题。项目的主干或主分支应该始终保持良好状态。通常更喜欢每日提交更改。

将CI纳入开发流程中对开发团队非常有价值。CI流程提供了许多好处,下面将解释其中一些。

通过CI流程,自动化测试经常运行,可以及时发现并修复错误,从而产生高质量的健壮系统。CI不会自动消除系统中的错误;开发人员必须努力编写经过充分测试的清洁代码。然而,CI可以促进及时发现本来可能会进入生产环境的错误。

通过CI,开发团队的整体生产力可以得到提高,因为开发人员可以摆脱单调或手动的任务,这些任务已经作为CI过程的一部分自动化了。开发人员可以专注于开发系统的功能。

对于使用CI的开发团队,持续或频繁的部署变得相对容易。这是因为新功能或需求可以快速交付和部署。这将允许用户对产品提供充分和有用的反馈,这可以用来进一步完善软件并提高质量。

有许多可用的CI工具,每个工具都具有不同的功能,可以促进简单的CI并为部署流水线提供良好的结构。选择CI工具取决于几个因素,包括:

接下来将解释一些流行和最常用的CI工具。这些CI工具在有效使用时可以帮助开发团队在软件项目中达到质量标准。

微软TeamFoundationServer(TFS)是一个集成的服务器套件,包含一组协作工具,以提高软件开发团队的生产力。TFS提供可以与IDE(如VisualStudio、Eclipse等)集成的工具和代码编辑器。

TFS提供了一套工具和扩展,可以促进流畅的CI过程。使用TFS,可以自动化构建、测试和部署应用程序。TFS通过支持各种编程语言和源代码存储库,提供了很大的灵活性。

TeamCity是JetBrains的企业级CI工具。它支持捆绑的.NETCLI,并且与TFS类似,它提供了自动化部署和组合构建的支持。TeamCity可以通过IDE的可用插件在服务器上验证和运行自动化测试,然后再提交代码。

持续交付是CI的续篇或延伸。它是一组软件开发实践,确保项目的代码可以部署到与生产环境相同的测试环境。持续交付确保所有更改都是最新的,并且一旦更改通过自动化测试,就可以立即发货和部署到生产环境。

众所周知,实践CI将促进团队成员之间的良好沟通,并消除潜在风险。开发团队需要进一步实践持续交付,以确保他们的开发活动对客户有益。这可以通过确保应用程序在开发周期的任何阶段都可以部署和准备好生产来实现。

通过开发团队成员的有效沟通和协作,可以实现持续交付。这要求应用程序交付过程的主要部分通过开发和完善的部署管道进行自动化。在任何时候,正在开发的应用程序都应该可以部署。产品所有者或客户将确定应用程序何时部署。

由于测试、构建和部署过程的自动化,软件产品可以很快地提供给最终用户。用户将能够提供有用和宝贵的反馈意见,这些意见可以用来进一步完善和提高应用程序的质量。

GitHub是一个源代码托管平台,用于版本控制,允许开发团队成员协作和开发软件项目,无论他们的地理位置在哪里。GitHub目前托管了多个不同编程语言的开源和专有项目。

GitHub提供了基本和高级功能,使协作变得更加容易。它本质上是一个基于Web的源代码存储库或托管服务,使用Git作为版本控制系统,基于Git的分布式版本控制行为。

有趣的是,像Microsoft、Google、Facebook和Twitter这样的顶级公司在GitHub上托管他们的开源项目。基本上,任何CI工具都可以与GitHub一起使用。这使得开发团队可以根据预算选择CI工具。

GitHub支持公共和私人项目存储库托管。任何人都可以查看公共存储库的文件和提交历史,而私人存储库的访问仅限于添加的成员。GitHub上的私人存储库托管是需要付费的。

GitHub存储库用于组织项目文件夹、文件和资产。文件可以是图像、视频和源文件。在GitHub中,存储库通常会有一个包含项目简要描述的README文件。还可以向项目添加软件许可文件。

以下步骤描述了如何在GitHub中创建一个新存储库:

GitHub有一个基于分支的工作流程,称为GitHubFlow,为开发团队提供了很好的支持和工具,以便频繁地协作和部署项目。

GitHubFlow便于以下操作:

从项目创建分支是Git的核心,并且是GitHub流程的扩展,这是GitHubFlow的核心概念。分支用于尝试新概念和想法,或用于修复功能。分支是存储库的不同版本。

GitHub上的以下图表进一步解释了项目分支的GitHub流程,其中对分支进行的提交更改通过拉取请求合并到主分支:

主分支必须始终可以随时部署。创建的分支上的更改应该只在拉取请求打开后合并到主分支。更改将在通过必要的验证和自动化测试后进行仔细审查和接受。

要从之前创建的LoanApplication存储库创建新分支,请执行以下步骤:

目前,新创建的分支和主分支完全相同。您可以开始对创建的分支进行更改,添加和修改源文件。更改直接提交到分支而不是主分支。

在存储库中,每个提交都是一个独立的更改单元。如果由于提交而导致工作代码库中断,或者提交引入错误,可以回滚提交。

无论您对代码库所做的更改是小还是大,您都可以在项目开发过程中的任何时候发起拉取请求。拉取请求对于GitHub中的协作至关重要,因为它们促进了提交的讨论和审查。

当您发起拉取请求时,项目的所有者或维护者将收到有关待定更改和您意图进行合并的通知。在对分支所做的更改进行适当审查后,可以提供必要的反馈以进一步完善代码。拉取请求显示了文件的差异以及您的分支和主分支的内容。如果所做的贡献被认为是可以接受的,它们将被接受并合并到主分支中:

一旦拉取请求经过审查并被接受,它们将被合并到主分支中。可以按以下步骤在GitHub中合并请求。单击“合并拉取请求”按钮将更改合并到主分支中。然后单击“确认合并”,这将将分支上的提交合并到主分支中:

Git是一种分布式版本控制系统(DVCS)。Git的分支系统非常强大,使其在其他版本控制系统中脱颖而出。使用Git,可以创建项目的多个独立分支。分支的创建、合并和删除过程是无缝且非常快速的。

Git极大地支持无摩擦的上下文切换概念,您可以轻松地创建一个分支来探索您的想法,创建和应用补丁,进行提交,合并分支,然后稍后切换回您正在工作的早期分支。使用的分支工作流程将决定是否为每个功能或一组功能创建一个分支,同时在分支之间轻松切换以测试功能。

通过为生产、测试和开发设置不同的分支,您的开发可以得到组织并且高效,从而控制进入每个分支的文件和提交的流程。通过拥有良好的存储库结构,您可以轻松快速地尝试新的想法,并在完成后删除分支。

Git具有丰富的有用命令集,掌握后可以完全访问其内部,并允许基本和高级源代码版本控制操作。Git为Windows、Macintosh和Linux操作系统提供命令行界面和图形用户界面客户端。命令可以从Mac和Linux上的终端运行,而在Windows上有GitBash,用于从命令行运行Git的仿真器。

有一组命令可用于配置用户信息,这些命令跨越安装了Git的计算机上的所有本地存储库。gitconfig命令用于获取和设置全局存储库选项。它接受--global选项,后跟要从全局.gitconfig文件中获取或设置的特定配置。

要设置将附加到所有提交事务的全局用户名,请运行以下命令:

gitconfig--globaluser.name"[name]"也可以设置全局用户电子邮件地址。这将将设置的电子邮件地址附加到所有提交事务。运行以下命令来实现这一点:

gitconfig--globaluser.email"[emailaddress]"为了美观,可以使用以下命令启用命令行输出的颜色:

gitconfig--globalcolor.uiauto初始化存储库命令gitinit命令用于创建一个空的Git存储库,以及重新初始化现有存储库。运行gitinit命令时,会创建一个.git目录,以及用于保存对象、refs/heads、refs/tags、模板文件和初始HEAD文件的子目录,该文件引用主分支的HEAD。在其最简单的形式中,gitinit命令传递存储库名称,这将创建一个具有指定名称的存储库:

gitinit[repository-name]要更新并选择新添加的模板或将存储库重新定位到另一个位置,可以在现有存储库中重新运行gitinit。该命令不会覆盖存储库中已有的配置。完整的gitinit命令概要如下:

gitinit[-q|--quiet][--bare][--template=][--separate-git-dir][--shared[=]][directory]让我们详细讨论前面的命令:

使用gitclone命令,可以将现有存储库克隆到新目录中。该命令为克隆存储库中的所有分支创建远程跟踪分支。它将下载项目及其整个版本历史。gitclone命令可以通过传递存储库的URL作为选项来简单使用:

gitclone[url]传递给命令的URL将包含传输协议的信息、远程服务器的地址和存储库路径。Git支持的协议有SSH、Git、HTTP和HTTPS。该命令还有其他选项可以传递给它,以配置要克隆的存储库。

Git有一组有用的命令,用于检查存储库中文件的状态,审查对文件所做的更新,并提交对项目文件所做的更改。

gitstatus命令用于显示存储库的工作状态。该命令基本上提供了已更改并准备提交的文件的摘要。它显示了当前HEAD提交和索引文件之间存在差异的文件路径。它还显示了索引文件和工作树之间存在差异的文件路径,以及当前未被Git跟踪但未在.gitignore文件中添加的文件路径:

gitstatusgitadd命令使用工作树中找到的内容来更新索引。它基本上是将文件内容添加到索引中。它用于添加现有路径的当前内容。它可以用于删除树中不再存在的路径,或者添加工作树中所做更改的部分内容。

通常的做法是在执行提交之前多次运行该命令。它会添加文件的内容,就像在运行命令时的那样。它接受用于调整其行为的选项:

gitadd[file]gitcommit命令用于将索引的内容与用户提供的提交消息一起记录或存储到提交中,以描述对项目文件所做的更改。在运行该命令之前,必须使用gitadd添加更改。

该命令灵活,使用允许不同的选项来记录更改。一种方法是将具有更改的文件列为提交命令的参数,这会告诉Git忽略在索引中暂存的更改,并存储列出的文件的当前内容。

此外,可以使用-a开关与该命令一起使用,以添加索引中列出但不在工作树中的所有文件的更改。开关-m用于指定提交消息:

gitcommit-m"[commitmessage]"有时,希望显示索引和工作树之间的差异或更改,两个文件或blob对象之间可用的更改。gitdiff命令用于此目的。当传递--staged选项给命令时,Git显示暂存和最后一个文件版本之间的差异:

gitdiffgitrm命令从工作树和索引中删除文件。要删除的文件作为命令的选项传递。作为参数传递给命令的文件将从工作目录中删除并标记为删除。当传递--cached选项给命令时,Git不会从工作目录中删除文件,而是从版本控制中删除它:

gitrm[files]gitreset命令可用于取消暂存并保留已在存储库中暂存的文件的内容。该命令用于将当前HEAD重置为指定状态。此外,它还可以根据指定的选项修改索引和工作树。

该命令有三种形式。第一和第二种形式用于从树复制条目到索引,而最后一种形式用于将当前分支HEAD设置为特定提交:

gitreset[-q][][--]…gitreset(--patch|-p)[][--][…]gitreset[--soft|--mixed[-N]|--hard|--merge|--keep][-q][]分支和合并命令gitbranch命令是Git版本控制系统的核心。它用于在存储库中创建、移动、重命名、删除和列出可用的分支。该命令有几种形式,并接受用于设置和配置存储库分支的不同选项。在Bash上运行gitbranch命令,不指定选项时,将列出存储库中可用的分支。这类似于使用--list选项。

要创建一个新分支,使用gitbranch命令并将分支名称作为参数运行:

gitbranch[branchname]--delete选项用于删除指定的分支,--copy选项用于创建指定分支的副本以及其reflog。

要将工作树或分支中的文件更新为另一个工作树中可用的内容,使用gitcheckout命令。该命令用于切换分支或恢复工作树文件。与gitbranch类似,它有几种形式并接受不同的选项。

当使用分支名称作为参数运行该命令时,Git切换到指定的分支,更新工作目录,并将HEAD指向该分支:

gitcheckout[branchname]如前一节所述,分支概念允许开发团队尝试新想法,并从现有项目创建新版本。分支的美妙之处在于能够将一个分支的更改合并到另一个分支中,实质上是将分支或开发线连接或合并在一起。

在Git中,gitmerge命令用于将从一个分支创建的开发分支集成到单个分支中。例如,如果有一个从主分支创建的开发分支来测试某个功能,当运行gitmerge[分支名称]命令时,Git将追溯对该分支所做的更改。这是因为它是从主分支分出的,直到最新的分支,并将这些更改存储在主分支上的新提交中:

gitmerge[branchname]gitmerge--abortgitmerge--continue经常,合并过程可能会导致不同分支的文件之间发生冲突。运行gitmerge--abort命令将中止合并过程并将分支恢复到合并前的状态。解决了遇到的冲突后,可以运行gitmerge--continue重新运行合并过程。

WebHook是通过HTTPPOST传递的事件通知。WebHook通常被称为Web回调或HTTP推送API。WebHook提供了一种机制,应用程序可以实时将数据传递给其他应用程序。

WebHook与常规API不同之处在于,它没有通过轮询数据来获取最新数据的持续资源利用。当数据可用时,订阅者或消费应用程序将通过已在WebHook提供程序注册的URL接收数据。WebHook对数据提供程序和消费者都是有效且高效的。

要从WebHook接收通知或数据,消费应用程序需要向提供程序注册一个URL。提供程序将通过POST将数据传递到URL。URL必须从网络公开访问并可达。

WebHook提供程序通常通过HTTPPOST以JSON、XML或作为多部分或URL编码的表单数据的形式传递数据。订阅者URL上的API的实现将受到WebHook提供程序使用的数据传递模式的影响。

经常会出现需要调试WebHooks的情况。这可能是为了解决错误。由于WebHooks的异步性质,有时可能会有挑战。首先,必须理解来自WebHook的数据。可以使用能够获取和解析WebHook请求的工具来实现这一点。根据对WebHook数据结构和内容的了解,可以模拟请求以测试URLAPI代码以解决问题。

在从WebHook消费数据时,重要的是要注意安全性,并将其纳入消费应用程序的设计中。因为WebHook提供程序将POST数据到的回调URL是公开可用的,所以可能会受到恶意攻击。

一种常见且简单的方法是在URL中附加一个强制身份验证令牌,每次请求都将对其进行验证。还可以围绕URL构建基本身份验证,以在接受和处理数据之前验证发起POST的一方。或者,如果请求签名已经在提供程序端实现,提供程序可以对每个WebHook请求进行签名。每个发布的请求的签名将由消费者进行验证。

根据订阅者生成事件的频率,WebHooks可能会引发大量请求。如果订阅者未能正确设计以处理这样大量的请求,这可能会导致资源利用率高,无论是带宽还是服务器资源。当资源被充分利用并用完时,消费者可能无法处理更多请求,导致消费应用程序的拒绝服务。

WebHooks可以在任何存储库或组织级别进行配置。成功配置后,每当触发订阅的事件或操作时,WebHook都将被触发。GitHub允许为存储库或组织的每个事件创建多达20个WebHooks。安装后,WebHooks可以在存储库或组织上触发。

在GitHub的WebHook配置点,您可以指定要从GitHub接收请求的事件。GitHub中的WebHook请求数据称为有效负载。最好只订阅所需数据的事件,以限制从GitHub发送到应用程序服务器的HTTP请求。默认情况下,即使在GitHub上创建的WebHook也订阅了push事件。事件订阅可以通过GitHubWeb或API进行修改。

以下表格中解释了GitHub上可订阅的一些可用事件:

push事件具有包含更详细信息的有效负载。GitHub中的每个事件都具有特定的有效负载格式,用于描述该事件所需的信息。除了特定于事件的特定字段外,每个事件在有效负载中都包括触发事件的用户或发送者。

要配置WebHook,我们将使用之前创建的LoanApplication存储库。单击存储库的设置页面,单击Webhooks,然后单击添加Webhook:

GitHub将要求您对操作进行身份验证。提供您的GitHub帐户密码以继续。将加载WebHook配置页面,在那里您可以配置WebHook的选项:

运行以下命令告诉Ngrok将端口54113暴露到互联网上:

单击“重新交付”按钮。这将显示一个对话框,询问您是否要重新交付有效负载。单击“是,重新交付此有效负载”按钮。这将尝试将JSON有效负载POST到有效负载URL字段中指定的新端点。这次,有效负载交付将成功,HTTP响应代码为200,表示端点已成功联系:

您可以编写消费者Web应用程序以按照您的意愿处理有效负载数据。成功配置后,GitHub将在WebHook订阅的任何事件引发时将有效负载POST到端点。

TeamCity是JetBrains推出的一个独立于平台的CI工具。它是一个用户友好的CI工具,专门为软件开发人员和工程师设计。TeamCity是一个强大而功能强大的CI工具,因为它能够充分优化集成周期。

在本节中,将解释TeamCity中经常使用的一些基本术语。这是为了理解成功配置构建步骤以及质量连续过程所需的一些概念。让我们来看看一些基本术语:

TeamCity可以在开发团队的服务器基础设施上本地托管,也可以通过与云解决方案集成来托管TeamCity。这允许虚拟机被配置以运行TeamCity。TeamCity安装将包括服务器安装和默认的构建代理。

要安装TeamCity服务器,请转到JetBrains下载站点,获取TeamCity服务器的免费专业版,该版本附带免费许可密钥,可解锁3个构建代理和100个构建配置。如果您使用Windows操作系统,请运行捆绑了TomcatJavaJRE1.8的下载.exe。按照对话框提示提取和安装TeamCity核心文件。

在安装过程中,您可以设置TeamCity将监听的端口,也可以将其保留为默认的8080。如果安装成功,TeamCity将在浏览器中打开,并提示您通过在服务器上指定数据目录位置来完成安装过程。指定路径并单击“继续”:

在数据目录位置路径初始化后,您将进入数据库选择页面,在该页面上,您将有选择任何受支持的数据库的选项。选择内部(HSQLDB)并单击“继续”按钮:

数据库配置将需要几秒钟,然后您将看到许可协议页面。接受许可协议并单击“继续”按钮。下一页是管理员帐户创建页面。使用所需的凭据创建帐户以完成安装。安装完成后,您将被引导到概述页面:

TeamCity构建生命周期描述了服务器和代理之间的数据流。这基本上是传递给代理的信息以及TeamCity检索结果的过程。工作流描述了为项目配置的构建步骤是如何端到端执行的:

基本上,项目应包含运行成功构建所需的配置和项目属性。使用TeamCityCI服务器,可以自动化运行测试、执行环境检查、编译、构建,并提供可部署版本的项目。

您将看到创建项目的几个选项,可以从存储库、手动创建,或连接到GitHub、Bitbucket或VisualStudioTeamServices中的任何一个。点击“来自GitHub.com”按钮,将TeamCity连接到我们之前在GitHub上创建的LoanApplication存储库:

添加连接对话框显示了TeamCity将连接到GitHub。需要创建一个新的GitHubOAuth应用程序才能成功将TeamCity连接到GitHub。要在GitHub中创建新的OAuth应用程序,请执行以下步骤:

在本章中,我们广泛探讨了CI的概念,这是一种软件开发实践,可以帮助开发团队频繁地集成其代码。开发人员预计每天多次检查代码,然后由CI工具通过自动化构建过程进行验证。

还讨论了CI的常见术语,用于持续交付。我们解释了如何在GitHub和在线托管平台上托管软件项目的步骤,然后讨论了基本的Git命令。

探讨了创建GitHubWebHooks以配置与构建管理系统集成的过程。最后,给出了安装和配置TeamCityCI平台的逐步说明。

在下一章中,我们将探讨CakeBootstrapper并配置TeamCity以使用名为Cake的跨平台构建自动化系统来清理、构建和恢复软件包依赖项并测试我们的LoanApplication项目。

在第七章中,持续集成和项目托管,我们设置了TeamCity,一个强大的持续集成工具,简化和自动化了管理源代码检入和更改、测试、构建和部署软件项目的过程。我们演示了在TeamCity中创建构建步骤,并将其连接到我们在GitHub上的LoanApplication项目。TeamCity具有内置功能,可以连接到托管在GitHub或Bitbucket上的软件项目。

CI流程将许多不同的步骤整合成一个易于重复的过程。这些步骤根据软件项目类型而有所不同,但有一些步骤是常见的,并适用于大多数项目。可以使用构建自动化系统自动化这些步骤。

在本章中,我们将配置TeamCity使用名为Cake的跨平台构建自动化系统,来清理、构建、恢复软件包依赖,并测试LoanApplication解决方案。本章后面,我们将探讨在VisualStudioTeamServices中使用Cake任务创建构建步骤。我们将涵盖以下主题:

Cake真正实现了跨平台;其NuGet包Cake.CoreCLR允许它在Windows、Linux和Mac上使用.NETCore运行。它有一个NuGet包,可以在Windows上依赖.NETFramework4.6.1运行。此外,它可以使用Mono框架在Linux和Max上运行,建议使用Mono版本4.4.2。

无论使用哪种CI工具,Cake在所有支持的工具中都具有一致的行为。它广泛支持大多数构建过程中使用的工具,包括MSBuild、ILMerge、Wix和Signtool。

在示例存储库中,有一些感兴趣的文件——build.ps1和build.sh。它们是引导程序脚本,确保Cake所需的依赖项与Cake和必要的文件一起安装。这些脚本使调用Cake变得更容易。build.cake文件是构建脚本;构建脚本可以重命名,但引导程序将默认定位build.cake文件。tools.config/packages.config文件是包配置,指示引导程序脚本在tools文件夹中安装哪些NuGet包。

解压下载的示例存储库存档文件。在Windows上,打开PowerShell提示符并通过运行.\build.ps1执行引导程序脚本。在Linux和Mac上,打开终端并运行.\build.sh。引导程序脚本将检测到计算机上未安装Cake,并自动从NuGet下载它。

根据引导程序脚本的执行,在Cake下载完成后,将运行下载的示例build.cake脚本,该脚本将清理输出目录,并在构建项目之前恢复引用的NuGet包。运行build.cake文件时,它应该清理测试项目,恢复NuGet包,并运行项目中的单元测试。运行设置和测试运行摘要将如下截图所示呈现:

通常,PowerShell可能会阻止运行build.ps1文件。您可能会在PowerShell屏幕上收到错误消息,指出由于系统上禁用了运行脚本,无法加载build.ps1。由于PowerShell中默认的安全设置,对文件的运行限制。

打开PowerShell窗口,将目录更改为之前下载的Cake构建示例存储库的文件夹,并运行.\build.ps1命令。如果系统上的执行策略未从默认值更改,这应该会给您以下错误:

要查看系统上当前的执行策略配置,请在PowerShell屏幕上运行Get-ExecutionPolicy-List命令;此命令将呈现一个包含可用范围和执行策略的表格,就像以下屏幕上显示的那样。根据您运行PowerShell的方式,您的实例可能具有不同的设置:

要更改执行策略以允许随后运行脚本,运行Set-ExecutionPolicyRemoteSigned-ScopeProcess命令,该命令旨在将进程范围从未定义更改为RemoteSigned。运行该命令将在PowerShell屏幕上显示一个警告并提示您的PC可能会面临安全风险。输入Y以确认并按Enter。运行命令时,PowerShell屏幕上显示的内容如下截图所示:

这将更改PC的执行策略并允许运行PowerShell脚本。

安装Cake引导程序的步骤对于不同平台是相似的,只有一些小的差异。执行以下步骤设置引导程序。

导航到Cake资源存储库以下载引导程序。对于Windows,下载PowerShellbuild.ps1文件,对于Mac和Linux,下载build.shbash文件。

在Windows上,打开一个新的PowerShell窗口并运行以下命令:

vartarget=Argument("target","Default");Task("Default").Does(()=>{Information("InstallationSuccessful");});RunTarget(target);步骤3现在可以通过调用Cake引导程序来运行步骤2中创建的Cake脚本。

在Windows上,您需要指示PowerShell允许运行脚本,方法是更改WindowsPowerShell脚本执行策略。由于执行策略,PowerShell脚本执行可能会失败。

要执行Cake脚本,请运行以下命令:

./build.ps1在Linux或Mac上,您应该运行以下命令,以授予当前所有者执行脚本的权限:

chmod+xbuild.sh运行命令后,可以调用引导程序来运行步骤2中创建的Cake脚本:

使用Cake资源库上可用的示例build.cake文件可以作为编写项目的构建脚本的起点。但是,为了实现更多功能,我们将介绍一些基本的Cake概念,以便编写用于自动化构建和部署任务的健壮脚本。

可以使用Task方法来定义任务,将任务名称或标题作为参数传递给它:

Task("Action").Does(()=>{//Taskcodegoeshere});例如,以下代码片段中的build任务会清理debugFolder文件夹以删除其中的内容。运行任务时,将调用CleanDirectory方法:

vardebugFolder=Directory("./bin/Debug");Task("CleanFolder").Does(()=>{CleanDirectory(debugFolder);});Cake允许您使用C#在任务中使用异步和等待功能来创建异步任务。实质上,任务本身将以单个线程同步运行,但任务中包含的代码可以受益于异步编程功能并利用异步API。

Cake具有DoesForEach方法,可用于将一系列项目或产生一系列项目的委托作为任务的操作添加。当将委托添加到任务时,委托将在任务执行后执行:

Task("LongRunningTask").Does(async()=>{//useawaitkeywordtomultithreadcode});通过将DoesForEach链接到Task方法来定义DoesForEach,如以下代码片段所示:

Task("ProcessCsv").Does(()=>{}).DoesForEach(GetFiles("**/*.csv"),(file)=>{//Processeachcsvfile.});TaskSetup和TaskTeardownTaskSetup和TaskTeardown用于包装要在执行每个任务之前和之后执行的构建操作。当执行诸如配置初始化和自定义日志记录等操作时,这些方法尤其有用:

TaskSetup(setupContext=>{vartaskName=setupContext.Task.Name;//performaction});TaskTeardown(teardownContext=>{vartaskName=teardownContext.Task.Name;//performaction});与任务的TaskSetup和TaskTeardown类似,Cake具有Setup和Teardown方法,可用于在第一个任务之前和最后一个任务之后执行操作。这些方法在构建自动化中非常有用,例如,当您打算在运行任务之前启动一些服务器和服务以及在运行任务后进行清理活动时。应在RunTarget之前调用Setup或Teardown方法以确保它们正常工作:

Setup(context=>{//ThiswillbeexecutedBEFOREthefirsttask.});Teardown(context=>{//ThiswillbeexecutedAFTERthelasttask.});配置和预处理指令Cake操作可以通过使用环境变量、配置文件和将参数传递到Cake可执行文件来进行控制。这是基于指定的优先级,配置文件会覆盖环境变量和传递给Cake的参数,然后覆盖在环境变量和配置文件中定义的条目。

例如,如果您打算指定工具路径,即cake在恢复工具时检查的目录,您可以创建CAKE_PATHS_TOOLS环境变量名称,并将值设置为Cake工具文件夹路径。

在使用配置文件时,文件应放置在与build.cake文件相同的目录中。可以在配置文件中指定Cake工具路径,就像在以下代码片段中一样,它会覆盖环境变量中设置的任何内容:

[Paths]Tools=./toolsCake工具路径可以直接传递给Cake,这将覆盖环境变量和配置文件中设置的内容:

cake.exe--paths_tools=./toolsCake具有默认用于配置条目的值,如果它们没有使用任何配置Cake的方法进行覆盖。这里显示了可用的配置条目及其默认值,以及如何使用配置方法进行配置:

预处理器指令用于在Cake中引用程序集、命名空间和脚本。预处理器行指令在脚本执行之前运行。

通常,您将创建依赖于其他任务完成的任务;为了实现这一点,您可以使用IsDependentOn和IsDependeeOf方法。要创建依赖于另一个任务的任务,请使用IsDependentOn方法。在以下构建脚本中,Cake将在执行Task2之前执行Task1:

Task("Task1").Does(()=>{});Task("Task2").IsDependentOn("Task1").Does(()=>{});RunTarget("Task2");使用IsDependeeOf方法,您可以定义具有相反关系的任务依赖关系。这意味着依赖于任务的任务在该任务中定义。前面的构建脚本可以重构为使用反向关系:

Task("Task1").IsDependeeOf("Task2").Does(()=>{});Task("Task2").Does(()=>{});RunTarget("Task2");标准在Cake脚本中使用标准允许您控制构建脚本的执行流程。标准是必须满足才能执行任务的谓词。标准不会影响后续任务的执行。标准用于根据指定的配置、环境状态、存储库分支和任何其他所需选项来控制任务执行。

最简单的形式是使用WithCriteria方法来指定特定任务的执行标准。例如,如果您只想在下午清理debugFolder文件夹,可以在以下脚本中指定标准:

vardebugFolder=Directory("./bin/Debug");Task("CleanFolder").WithCriteria(DateTime.Now.Hour>=12).Does(()=>{CleanDirectory(debugFolder);});RunTarget("CleanFolder");您可以有一个任务的执行取决于另一个任务;在以下脚本中,CleanFolder任务的标准将在创建任务时设置,而ProcessCsv任务评估的标准将在任务执行期间进行:

vardebugFolder=Directory("./bin/Debug");Task("CleanFolder").WithCriteria(DateTime.Now.Hour>=12).Does(()=>{CleanDirectory(debugFolder);});Task("ProcessCsv").WithCriteria(DateTime.Now.Hour>=12).IsDependentOn("CleanFolder").Does(()=>{}).DoesForEach(GetFiles("**/*.csv"),(file)=>{//Processeachcsvfile.});RunTarget("ProcessCsv");一个更有用的用例是编写一个带有标准的Cake脚本,检查本地构建并执行一些操作,以清理、构建和部署项目。将定义四个任务,每个任务执行一个要执行的操作,第四个任务将链接这些操作在一起:

varisLocalBuild=BuildSystem.IsLocalBuildTask("Clean").WithCriteria(isLocalBuild).Does(()=>{//cleanallprojectsinthesoution});Task("Build").WithCriteria(isLocalBuild).Does(()=>{//buildallprojectsinthesoution});Task("Deploy").WithCriteria(isLocalBuild).Does(()=>{//Deploytotestserver});Task("Main").IsDependentOn("Clean").IsDependentOn("Build").IsDependentOn("Deploy").Does(()=>{});RunTarget("Main");Cake的错误处理和最终块Cake具有错误处理技术,您可以使用这些技术从错误中恢复,或者在构建过程中发生异常时优雅地处理异常。有时,构建步骤调用外部服务或进程;调用这些外部依赖项可能会导致错误,从而导致整个构建失败。强大的构建应该在不停止整个构建过程的情况下处理这些异常。

OnError方法是一个任务扩展,用于在构建中生成异常时执行操作。您可以在OnError方法中编写代码来处理错误,而不是强制终止脚本:

Task("Task1").Does(()=>{}).OnError(exception=>{//Codetohandleexception.});有时,您可能希望忽略抛出的错误并继续执行生成异常的任务;您可以使用ContinueOnError任务扩展来实现这一点。使用ContinueOnError方法时,您不能与之一起使用OnError方法:

Task("Task1").ContinueOnError().Does(()=>{});如果您希望报告任务中生成的异常,并仍然允许异常传播并采取其课程,请使用ReportError方法。如果由于任何原因,在ReportError方法内引发异常,则会被吞噬:

Task("Task1").Does(()=>{}).ReportError(exception=>{//Reportgeneratedexception.});此外,您可以使用DeferOnError方法将任何抛出的异常推迟到执行的任务完成。这将确保任务在抛出异常并使脚本失败之前执行其指定的所有操作:

Task("Task1").Does(()=>{}).DeferOnError();最后,您可以使用Finally方法执行任何操作,而不管任务执行的结果如何:

Task("Task1").Does(()=>{}).Finally(()=>{//Performaction.});LoanApplication构建脚本为了展示Cake的强大功能,让我们编写一个Cake脚本来构建LoanApplication项目。Cake脚本将清理项目文件夹,还原所有包引用,构建整个解决方案,并运行解决方案中的单元测试项目。

以下脚本设置要在整个脚本中使用的参数,定义目录和任务以清理LoanApplication.Core项目的bin文件夹,并使用DotNetCoreRestore方法恢复包。可以使用DotNetCoreRestore方法来还原NuGet包,该方法又使用dotnetrestore命令:

//Argumentsvartarget=Argument("target","Default");varconfiguration=Argument("configuration","Release");varsolution="./LoanApplication.sln";//Definedirectories.varbuildDir=Directory("./LoanApplication.Core/bin")+Directory(configuration);//TasksTask("Clean").Does(()=>{CleanDirectory(buildDir);});Task("Restore-NuGet-Packages").IsDependentOn("Clean").Does(()=>{Information("RestoringNuGetPackages");DotNetCoreRestore();});脚本的后部分包含使用DotNetCoreBuild方法构建整个解决方案的任务,该方法使用DotNetCoreBuildSettings对象中提供的设置使用dotnetbuild命令构建解决方案。使用DotNetCoreTest方法执行测试项目,该方法使用DotNetCoreTestSettings对象中提供的设置在解决方案中的所有测试项目中运行测试使用dotnettest:

Task("Build").IsDependentOn("Restore-NuGet-Packages").Does(()=>{Information("BuildSolution");DotNetCoreBuild(solution,newDotNetCoreBuildSettings(){Configuration=configuration});});Task("Run-Tests").IsDependentOn("Build").Does(()=>{vartestProjects=GetFiles("./LoanApplication.Tests.Units/*.csproj");foreach(varprojectintestProjects){DotNetCoreTool(projectPath:project.FullPath,command:"xunit",arguments:$"-configuration{configuration}-diagnostics-stoponfail");}});Task("Default").IsDependentOn("Run-Tests");RunTarget(target);CakeBootstrapper可用于通过从PowerShell窗口调用引导程序来运行Cakebuild文件。当调用引导程序时,Cake将使用build文件中可用的任务定义来开始执行定义的构建任务。执行开始时,执行的进度和状态将显示在PowerShell窗口中:

从市场下载的.vsix文件本质上是一个.zip文件。该文件包含要安装在VisualStudio中的Cake扩展的内容。运行下载的.vsix文件时,它将为VisualStudio2015和2017安装Cake支持:

安装扩展后,在创建新项目时,VisualStudio的可用选项中将添加一个Cake模板。该扩展将添加四种不同的Cake项目模板类型:

以下图片显示了不同的Cake项目模板:

在使用Cake脚本进行构建自动化的VisualStudio解决方案中,当发现build.cake文件时,Cake任务运行器将被触发。Cake扩展激活了任务运行器资源管理器集成,允许您在VisualStudio中直接运行包含的绑定的Cake任务。

要打开任务运行器资源管理器,请右键单击Cake脚本(build.cake文件)并从显示的上下文菜单中选择任务运行器资源管理器;它应该打开任务运行器资源管理器,并在窗口中列出Cake脚本中的所有任务:

有时,当右键单击Cake脚本时,任务运行器资源管理器可能不会显示在上下文菜单中。如果是这样,请单击“查看”菜单,选择“其他窗口”,然后选择“任务运行器资源管理器”以打开它:

通过安装Cake扩展,VisualStudio的构建菜单现在将包含一个Cake构建的条目,可以用来安装Cake配置文件、PowerShell引导程序和Bash引导程序,如果它们在解决方案中尚未配置的话:

现在,您可以通过双击或右键单击并选择运行,直接从任务运行器资源管理器中执行每个任务。任务执行的进度将显示在任务运行器资源管理器上:

Cake扩展为VisualStudio添加了语法高亮显示功能。这是IDE的常见功能,其中源代码以不同的格式、颜色和字体呈现。源代码高亮显示是基于定义的组、类别和部分进行的。

安装扩展后,任何带有.cake扩展名的文件都可以在VisualStudio中打开,并具有完整的任务和语法高亮显示。目前,VisualStudio中的.cake脚本文件没有IntelliSense支持;预计这个功能将在以后推出。

以下截图显示了在VisualStudio中对build.cake文件进行的语法高亮显示:

使用任务运行器资源管理器来运行用Cake脚本编写的构建任务更加简单和方便。这通常是通过VisualStudio的Cake扩展或直接调用Cake引导文件来完成的。然而,还有一种更有效的替代方法,那就是使用TeamCityCI工具来运行Cake构建脚本。

TeamCity构建步骤可用于执行Cake脚本作为构建步骤执行过程的一部分。让我们按照以下步骤为LoanApplication项目创建执行Cake脚本的构建步骤:

新的构建步骤应该出现在TeamCity项目中配置的可用构建步骤列表中。在参数描述选项卡中,将显示有关构建步骤的信息,显示运行器类型和要执行的PowerShell文件,如下截图所示:

MicrosoftVisualStudioTeamServices(VSTS)是TeamFoundationServer(TFS)的云版本。它提供了让开发人员协作进行软件项目开发的出色功能。与TFS类似,它提供了易于理解的服务器管理体验,并增强了与远程站点的连接。

VSTS为实践CI和持续交付(CD)的开发团队提供了出色的体验。它支持Git存储库进行源代码控制,易于理解的报告以及可定制的仪表板,用于监视软件项目的整体进展。

此外,它还具有内置的构建和发布管理功能,规划和跟踪项目,使用Kanban和Scrum方法管理代码缺陷和问题。它同样还有一个内置的维基用于与开发团队进行信息传播。

您可以通过互联网连接到VSTS,使用开发人员需要已创建的Microsoft帐户。但是,组织中的开发团队可以配置VSTS身份验证以与AzureActiveDirectory(AzureAD)一起使用,或者设置AzureAD以具有IP地址限制和多因素身份验证等安全功能。

创建帐户后,单击“项目”菜单导航到项目页面,然后单击“新建项目”创建新项目。这将加载项目创建屏幕,在那里您将指定项目名称,描述,要使用的版本控制以及工作项过程。单击“创建”按钮完成项目创建:

项目创建完成后,您将看到“入门”屏幕。该屏幕提供了克隆现有项目或将现有项目推送到其中的选项。让我们导入我们之前在GitHub上创建的LoanApplication项目。单击“导入”按钮开始导入过程:

单击“单击此处导航到代码视图”以查看VSTS导入的文件和文件夹。文件屏幕将显示项目中可用的文件和文件夹以及提交和日期详细信息:

Cake在VSTS中有一个扩展,允许您相对容易地直接从VSTS构建任务运行Cake脚本。安装了扩展后,Cake脚本就不必像在TeamCity中运行Cake脚本时那样使用PowerShell来运行。

单击“获取免费”按钮将重定向到VSTSVisualStudio|Marketplace集成页面。在此页面上,选择要安装Cake的帐户,然后单击“安装”按钮:

安装成功后,将显示一条消息,说明一切都已设置,类似于以下截图中的内容。点击“转到帐户”按钮,将您重定向到VSTS帐户页面:

成功将Cake安装到VSTS后,您可以继续配置代码的构建方式以及软件的部署方式。VSTS提供了简单的方法来构建您的源代码并发布您的软件。

要创建由Cake提供支持的VSTS构建,请单击“生成和发布”,然后选择“生成”子菜单。这将加载构建定义页面;单击此页面上的+新建按钮,开始构建创建过程。

向下滚动到模板列表的底部,或者在搜索框中简单地输入Empty以选择空模板。将鼠标悬停在模板上以激活“应用”按钮,然后单击该按钮,以继续到任务创建页面:

当任务屏幕加载时,单击+按钮以向构建添加任务。滚动浏览显示的任务模板列表,选择Cake,或使用搜索框过滤到Cake。单击“添加”按钮,将Cake任务添加到构建阶段可用任务列表中:

添加Cake任务后,单击任务以加载属性屏幕。单击“浏览”按钮以选择包含LoanApplication项目的构建脚本的build.cake文件,以与构建任务关联。您可以修改显示名称并更改目标和详细程度属性。此外,如果有要传递给Cake脚本的参数,可以在提供的字段中提供它们:

单击“保存并排队”菜单,然后选择“保存并排队”,以确保创建的构建将在托管代理上排队。这将加载构建定义和排队屏幕,您可以在其中指定注释和代理队列:

托管代理是运行构建作业的软件。使用托管代理是执行构建的最简单和最简单的方法。托管代理由VSTS团队管理。

如果构建成功排队,您应该会收到屏幕上显示构建编号的通知,指出构建已排队:

单击构建编号以导航到构建执行页面。托管代理将处理队列并执行队列中构建的配置任务。构建代理将显示构建执行的进度。执行完成后,将报告构建的成功或失败。

VSTS提供了巨大的好处,并简化了CI和CD过程。它提供了工具和功能,允许不同的IDE轻松集成,并使端到端的开发和项目测试相对容易。

在本章中,我们详细探讨了Cake构建自动化。我们介绍了安装Cake和CakeBootstrapper的步骤。之后,我们探讨了编写Cake构建脚本和任务创建的过程,并提供了可用于各种构建活动的示例任务。

此外,我们为LoanApplication项目创建了一个构建脚本,其中包含了清理、恢复和构建解决方案中所有项目以及构建解决方案中包含的单元测试项目的任务。

后来,我们在TeamCity中创建了一个构建步骤,通过使用PowerShell作为运行器类型来执行Cake脚本。在本章的后面,我们介绍了如何设置MicrosoftVisualStudioTeamServices,安装Cake到VSTS,并配置了一个包含Cake任务的构建步骤。

在最后一章中,我们将探讨如何使用Cake脚本执行xUnit.net测试。在本章的后面,我们将探讨.NETCore版本控制、.NETCore打包和元包。我们将为NuGet分发打包LoanApplication项目。

在第八章中,创建持续集成构建流程,我们介绍了Cake自动化构建工具的安装和设置过程。此外,我们广泛演示了使用Cake编写构建脚本的过程,以及其丰富的C#领域特定语言。我们还介绍了在VisualStudio中安装Cake扩展,并使用任务资源管理器窗口运行Cake脚本。

CI流程为软件开发带来的好处不言而喻;它通过早期和快速检测,促进了项目代码库中错误的轻松修复。使用CI,可以自动化运行和报告单元测试项目的测试覆盖率,以及项目构建和部署。

为了有效地利用CI流程的功能,代码库中的单元测试项目应该运行,并且应该由CI工具生成测试覆盖报告。在本章中,我们将修改Cake构建脚本,以运行我们的一系列xUnit.net测试。

在本章后面,我们将探讨.NETCore版本控制以及它对应用程序开发的影响。最后,我们将为在.NETCore支持的各种平台上分发的LoanApplication项目进行打包。之后,我们将探讨如何将.NETCore应用程序打包以在NuGet上共享。

在第八章中,创建持续集成构建流程,在LoanApplication构建脚本部分,我们介绍了使用Cake自动化构建脚本创建和运行构建步骤的过程。使用xUnit控制台运行程序和xUnit适配器,可以更轻松地从VisualStudioIDE、VisualStudioCode或任何其他适合构建.NET和.NETCore应用程序的IDE中获取单元测试的测试结果和覆盖率。然而,为了使CI流程和构建流程完整和有效,单元测试项目应该作为构建步骤的一部分进行编译和执行。

Cake对运行xUnit.net测试有很好的支持。Cake有两个别名,用于运行不同版本的xUnit.net测试——xUnit用于运行早期版本的xUnit.net,xUnit2用于xUnit.net的版本2。要使用别名的命令,必须在XUnit2Settings类中指定到xUnit.net的ToolPath,或者在build.cake文件中包含工具指令,以指示Cake从NuGet获取运行xUnit.net测试所需的二进制文件。

以下是包含xUnit.net工具指令的语法:

#tool"nuget:package=xunit.runner.console"Cake的XUnit2Alias有不同形式的重载,用于运行指定程序集中的xUnit.net版本测试。该别名位于Cake的Cake.Common.Tools.XUnit命名空间中。第一种形式是XUnit2(ICakeContext,IEnumerable),用于在IEnumerable参数中运行指定程序集中的所有xUnit.net测试。以下脚本显示了如何使用GetFiles方法将要执行的测试程序集获取到IEnumerable对象,并将其传递给XUnit2方法:

#tool"nuget:package=xunit.runner.console"Task("Execute-Test").Does(()=>{varassemblies=GetFiles("./LoanApplication.Tests.Unit/bin/Release/LoanApplication.Tests.Unit.dll");XUnit2(assemblies);});XUnit2(ICakeContext,IEnumerable,XUnit2Settings)别名类似于第一种形式,还增加了XUnit2Settings类,用于指定Cake应该如何执行xUnit.net测试的选项。以下代码片段描述了用法:

#tool"nuget:package=xunit.runner.console"Task("Execute-Test").Does(()=>{varassemblies=GetFiles("./LoanApplication.Tests.Unit/bin/Release/LoanApplication.Tests.Unit.dll");XUnit2(assemblies,newXUnit2Settings{Parallelism=ParallelismOption.All,HtmlReport=true,NoAppDomain=true,OutputDirectory="./build"});});另外,XUnit2别名允许传递字符串的IEnumerable,该字符串应包含要执行的xUnit.net版本2测试项目的程序集路径。形式为XUnit2(ICakeContext,IEnumerable),以下代码片段描述了用法:

#tool"nuget:package=xunit.runner.console"Task("Execute-Test").Does(()=>{XUnit2(new[]{"./LoanApplication.Tests.Unit/bin/Release/LoanApplication.Tests.Unit.dll","./LoanApplication.Tests/bin/Release/LoanApplication.Tests.dll"});});在.NETCore项目中执行xUnit.net测试为了成功完成构建过程,重要的是在解决方案中运行测试项目,以验证代码是否正常工作。通过使用DotNetCoreTest别名,相对容易地在.NETCore项目中运行xUnit.net测试,使用dotnettest命令。为了访问dotnet-xunit工具的其他功能,最好使用DotNetCoreTool运行测试。

在.NETCore项目中,通过运行dotnettest命令来执行单元测试。该命令支持编写.NETCore测试的所有主要单元测试框架,前提是该框架具有测试适配器,dotnettest命令可以集成以公开可用的单元测试功能。

使用dotnet-xunit框架工具运行.NETCore测试可以访问xUnit.net中的功能和设置,并且是执行.NETCore测试的首选方式。要开始,应该通过编辑.csproj文件并在ItemGroup部分包含DotNetCliToolReference条目,将dotnet-xunit工具安装到.NETCore测试项目中。还应该添加xunit.runner.visualstudio和Microsoft.NET.Test.Sdk包,以便能够使用dotnettest或dotnetxunit命令执行测试:

PackageReferenceInclude="xunit.runner.visualstudio"Version="2.3.1"/>此外,还有其他参数可用于在使用dotnetxunit命令执行.NETCore单元测试时自定义xUnit.net框架的行为。可以通过在终端上运行dotnetxunit--help命令来显示这些参数及其用法。

Cake具有别名,可用于调用dotnetSDK命令来执行xUnit.net测试。DotNetCoreRestore别名使用dotnetrestore命令还原解决方案中使用的NuGet包。此外,DotNetCoreBuild别名负责使用dotnetbuild命令构建.NETCore解决方案。使用DotNetCoreTest别名执行测试项目中的单元测试,该别名使用dotnettest命令。请参见以下Cake片段,了解别名的用法。

varconfiguration=Argument("Configuration","Release");Task("Execute-Restore").Does(()=>{DotNetCoreRestore();});Task("Execute-Build").IsDependentOn("Execute-Restore").Does(()=>{DotNetCoreBuild("./LoanApplication.sln"newDotNetCoreBuildSettings(){Configuration=configuration});});Task("Execute-Test").IsDependentOn("Execute-Build").Does(()=>{vartestProjects=GetFiles("./LoanApplication.Tests.Unit/*.csproj");foreach(varprojectintestProjects){DotNetCoreTest(project.FullPath,newDotNetCoreTestSettings(){Configuration=configuration,NoBuild=true});}});另外,可以使用DotNetCoreTool别名来执行.NETCore项目的xUnit.net测试。DotNetCoreTool是Cake中的通用别名,可用于执行任何dotnet工具。这是通过提供工具名称和必要的参数(如果有)来完成的。DotNetCoreTool公开了dotnetxunit命令中可用的其他功能,从而灵活地调整单元测试的执行方式。使用DotNetCoreTool别名时,需要手动将命令行参数传递给别名。请参见以下片段中别名的用法:

varconfiguration=Argument("Configuration","Release");Task("Execute-Test").Does(()=>{vartestProjects=GetFiles("./LoanApplication.Tests.Unit/*.csproj");foreach(vartestProjectintestProjects){DotNetCoreTool(projectPath:testProject.FullPath,command:"xunit",arguments:$"-configuration{configuration}-diagnostics-stoponfail");}});.NETCore版本对.NETCoreSDK和运行时进行版本控制使得平台易于理解,并且具有更好的灵活性。.NETCore平台本质上是作为一个单元分发的,其中包括不同发行版的框架、工具、安装程序和NuGet包。此外,对.NETCore平台进行版本控制可以在不同的.NETCore平台上实现并行应用程序开发,具有很大的灵活性。

从.NETCore2.0开始,使用了易于理解的顶级版本号来对.NETCore进行版本控制。一些.NETCore版本组件一起进行版本控制,而另一些则不是。然而,从2.0版本开始,对.NETCore发行版和组件采用了一致的版本控制策略,其中包括网页、安装程序和NuGet包。

.NETCore使用的版本模型基于框架的运行时组件[major].[minor]版本号。与运行时版本号类似,SDK版本使用带有额外独立[patch]的[major].[minor]版本号,该版本号结合了SDK的功能和补丁语义。

截至.NETCore2.0版本,采用了以下原则:

每日构建和发布的下载符合新的命名方案。从.NETCore2.0开始,下载中提供的安装程序UI也已修改,以显示正在安装的组件的名称和版本。命名方案格式如下:

[product]-[component]-[major].[minor].[patch]-[previewN]-[optionalbuild#]-[rid].[fileext]此外,格式详细显示了正在下载的内容,其版本,可以在哪种操作系统上使用,以及它是否可读。请参见下面显示的格式示例:

dotnet-runtime-2.0.7-osx-x64.pkg#macOSruntimeinstallerdotnet-runtime-2.0.7-win-x64.exe#WindowsSDKinstaller安装程序中包含的网站和UI字符串的描述保持简单、准确和一致。有时,SDK版本可能包含多个运行时版本。在这种情况下,当安装过程完成时,安装程序UX仅在摘要页面上显示SDK版本和已安装的运行时版本。这适用于Windows和macOS的安装程序。

此外,可能需要更新.NETCore工具,而不一定需要更新运行时。在这种情况下,SDK版本会增加,例如到2.1.2。下次更新时,运行时版本将增加,例如,下次更新时,运行时和SDK都将作为2.1.3进行发布。

.NETCore平台的灵活性使得分发不仅仅由微软完成;其他实体也可以分发该平台。该平台的灵活性使得为Linux发行版所有者分发安装程序和软件包变得容易。同时,也使得软件包维护者可以轻松地将.NETCore软件包添加到其软件包管理器中。

最小软件包集的详细信息包括dotnet-runtime-[major].[minor],这是具有特定major+minor版本组合的.NET运行时,并且在软件包管理器中可用。dotnet-sdk包括前向major、minor、patch版本以及更新卷。软件包集中还包括dotnet-sdk-[major].[minor],这是具有最高指定版本的共享框架和最新主机的SDK,即dotnet-host。

与安装程序和软件包管理器类似,docker标签采用命名约定,其中版本号放在组件名称之前。可用的docker标签包括以下运行时版本:

当包含在SDK中的.NETCoreCLI工具被修复并重新发布时,SDK版本会增加,例如,当版本从2.1.1增加到版本2.1.2。此外,重要的是要注意,SDK标签已更新以表示SDK版本而不是运行时。基于此,运行时将在下次发布时赶上SDK版本编号,例如,下次发布时,SDK和运行时将都采用版本号2.1.3。

.NETCore使用语义版本控制来描述.NETCore版本中发生的更改的类型和程度。语义版本控制(SemVer)使用MAJOR.MINOR.PATCH版本模式:

MAJOR.MINOR.PATCH[-PRERELEASE-BUILDNUMBER]SemVer的PRERELEASE和BUILDNUMBER部分是可选的,不是受支持的版本的一部分。它们专门用于夜间构建、从源目标进行本地构建和不受支持的预览版本。

当旧版本不再受支持时,采用现有依赖项的较新MAJOR版本,或者切换兼容性怪癖的设置时,将递增版本的MAJOR部分。每当现有依赖项有较新的MINOR版本,或者有新的依赖项、公共API表面积或新行为添加时,将递增MINOR。每当现有依赖项有较新的PATCH版本、对较新平台的支持或有错误修复时,将递增PATCH。

当MAJOR被递增时,MINOR和PATCH被重置为零。同样,当MINOR被递增时,PATCH被重置为零,而MAJOR不受影响。这意味着每当有多个更改时,受影响的最高元素会被递增,而其他部分会被重置为零。

通常,预览版本的版本会附加-preview[number]-([build]|"final"),例如,2.1.1-preview1-final。开发人员可以根据.NETCore的两种可用发布类型长期支持(LTS)和当前,选择所需的功能和稳定级别。

.NETCore平台是作为一组通常称为metapackages的软件包进行发布的。该平台基本上由NuGet软件包组成,这有助于使其轻量级且易于分发。.NETCore中的软件包提供了平台上可用的原语和更高级别的数据类型和常用实用程序。此外,每个软件包直接映射到一个具有相同名称的程序集;System.IO.FileSystem.dll程序集是System.IO.FileSystem软件包。

.NETCore中的软件包被定义为细粒度。这带来了巨大的好处,因为在该平台上开发的应用程序的结果是印刷小,只包含在项目中引用和使用的软件包。未引用的软件包不会作为应用程序分发的一部分进行发布。此外,细粒度软件包可以提供不同的操作系统和CPU支持,以及仅适用于一个库的特定依赖关系。.NETCore软件包通常与平台支持一起发布。这允许修复作为轻量级软件包更新进行分发和安装。

以下是.NETCore可用的一些NuGet软件包:

在您的.NetCore项目中引用软件包相对容易。例如,如果您在项目中包含System.Reflection,则可以在项目中引用它,如下所示:

netstandard2.0Metapackage元包是除了项目中已引用的目标框架之外,添加到.NETCore项目中的引用或依赖关系。例如,您可以将Microsoft.NETCore.App或NetStandard.Library添加到.NETCore项目中。

有时,需要在项目中使用一组包。这是通过使用元包来完成的。元包是经常一起使用的一组包。此外,元包是描述一组或一套包的NuGet包。当指定框架时,元包可以为这些包创建一个框架。

当您引用一个元包时,实质上是引用了元包中包含的所有包。实质上,这使得这些包中的库在使用VisualStudio进行项目开发时可以进行智能感知。此外,这些库在项目发布时也将可用。

在.NETCore项目中,元包是由项目中的目标框架引用的,这意味着元包与特定框架强烈关联或绑定在一起。元包可以访问已经确认和测试过可以一起工作的一组包。

.NETStandard元包是NETStandard.Library,它构成了.NET标准中的一组库。这适用于.NET平台的不同变体:.NETCore、.NETFramework和Mono框架。

Microsoft.NETCore.App和Microsoft.NETCore.Portable.Compatibility是主要的.NETCore元包。Microsoft.NETCore.App描述了构成.NETCore分发的库集,并依赖于NETStandard.Library。

Microsoft.NETCore.Portable.Compatibility描述了一组facade,使得基于mscorlib的可移植类库(PCLs)可以在.NETCore上工作。

Microsoft.AspNetCore.All是ASP.NETCore的元包。该元包包括由ASP.NETCore团队支持和维护的包,EntityFrameworkCore支持的包,以及ASP.NETCore和EntityFrameworkCore都使用的内部和第三方依赖项。

针对ASP.NETCore2.0的可用默认项目模板使用Microsoft.AspNetCore.All包。ASP.NETCore版本号和EntityFrameworkCore版本号与Microsoft.AspNetCore.All元包的版本号相似。ASP.NETCore2.x和EntityFrameworkCore2.x中的所有可用功能都包含在Microsoft.AspNetCore.All包中。

当您创建一个引用Microsoft.AspNetCore.All元包的ASP.NETCore应用程序时,.NETCoreRuntimeStore将可供您使用。.NETCoreRuntimeStore公开了运行ASP.NETCore2.x应用程序所需的运行时资源。

要使用Microsoft.AspNetCore.All包,应将其添加为.NETCore的.csproj项目文件的引用,就像以下XML配置中所示:

netcoreapp2.0NuGet分发的打包.NETCore的灵活性不仅限于应用程序的开发,还延伸到部署过程。部署.NETCore应用程序可以采用两种形式——基于框架的部署(FDD)和独立部署(SCD)。

使用FDD方法需要在开发应用程序的计算机上安装系统范围的.NETCore。安装的.NETCore运行时将被应用程序和在该计算机上部署的其他应用程序共享。

这使得应用程序可以在不同版本或安装的.NETCore框架之间轻松移植。此外,使用此方法时,部署将是轻量级的,只包含应用程序的代码和使用的第三方库。使用此方法时,为应用程序创建了.dll文件,以便可以从命令行启动。

SCD允许您将应用程序与运行所需的.NETCore库和.NETCore运行时一起打包。实质上,您的应用程序不依赖于部署计算机上已安装的.NETCore的存在。

使用此方法时,可执行文件(本质上是平台特定的.NETCore主机的重命名版本)将作为应用程序的一部分打包。在Windows上,此可执行文件为app.exe,在Linux和macOS上为app。与使用依赖于框架的方法部署应用程序时一样,为应用程序创建了.dll文件,以便启动应用程序。

dotnetpublish命令用于编译应用程序,并在将应用程序和依赖项复制到准备部署的文件夹之前检查应用程序的依赖项。执行该命令是准备.NETCore应用程序进行部署的唯一官方支持的方式。概要在此处:

dotnetpublish[][-c|--configuration][-f|--framework][--force][--manifest][--no-dependencies][--no-restore][-o|--output][-r|--runtime][--self-contained][-v|--verbosity][--version-suffix]dotnetpublish[-h|--help]运行命令时,输出将包含.dll程序集中包含的中间语言(IL)代码,包含项目依赖项的.deps.json文件,指定预期共享运行时的.runtime.config.json文件,以及从NuGet缓存中复制到输出文件夹中的应用程序依赖项。

命令的参数和选项在此处解释:

命令的使用示例是在命令行上运行dotnetpublish。这将发布当前文件夹中的项目。要发布本书中使用的LoanApplication项目,可以运行dotnetpublish命令。这将使用项目中指定的框架发布应用程序。ASP.NETCore应用程序依赖的解决方案中的项目将与之一起构建。请参阅以下屏幕截图:

在netcoreapp2.0文件夹中创建了一个publish文件夹,其中将复制所有编译文件和依赖项:

NuGet是.NET的软件包管理器,它是一个开源的软件包管理器,为构建在.NETFramework和.NETCore平台上的应用程序提供了更简单的版本控制和分发库的方式。NuGet库是.NET的中央软件包存储库,用于托管包作者和消费者使用的所有软件包。

使用.NETCore的dotnetpack命令可以轻松创建NuGet软件包。运行此命令时,它会构建.NETCore项目,并从中创建一个NuGet软件包。打包的.NETCore项目的NuGet依赖项将被添加到.nuspec文件中,以确保在安装软件包时它们被解析。显示以下命令概要:

dotnetpack[][-c|--configuration][--force][--include-source][--include-symbols][--no-build][--no-dependencies][--no-restore][-o|--output][--runtime][-s|--serviceable][-v|--verbosity][--version-suffix]dotnetpack[-h|--help]这里解释了命令的参数和选项:

运行dotnetpack命令将打包当前目录中的项目。要打包LoanApplication.Core项目,可以运行以下命令:

dotnetpackC:\LoanApplication\LoanApplication.Core\LoanApplication.Core.csproj--outputnupkgs运行该命令时,LoanApplication.Core项目将被构建并打包到项目文件夹中的nupkgs文件中。将创建LoanApplication.Core.1.0.0.nupkg文件,其中包含打包项目的库:

应用程序打包后,可以使用dotnetnugetpush命令将其发布到NuGet库。为了能够将软件包推送到NuGet,您需要注册NuGetAPI密钥。在上传软件包到NuGet时,这些密钥需要作为dotnetnugetpush命令的选项进行指定。

当您将软件包上传到NuGet库时,其他程序员可以直接从VisualStudio使用NuGet软件包管理器搜索您的软件包,并在其项目中添加对库的引用。

在本章中,我们首先使用Cake执行了xUnit.net测试。此外,我们广泛讨论了.NETCore的版本控制、概念以及它对.NETCore平台应用开发的影响。之后,我们为NuGet分发打包了本书中使用的LoanApplication项目。

在本书中,您已经经历了一次激动人心的TDD之旅。使用xUnit.net单元测试框架,TDD的概念被介绍并进行了广泛讨论。还涵盖了数据驱动的单元测试,这使您能够使用不同数据源的数据来测试您的代码。

Moq框架被用来介绍和解释如何对具有依赖关系的代码进行单元测试。TeamCityCI服务器被用来解释CI的概念。Cake,一个跨平台构建系统被探讨并用于创建在TeamCity中执行的构建步骤。此外,另一个CI工具MicrosoftVSTS被用来执行Cake脚本。

最后,有效地使用TDD在代码质量和最终应用方面是非常有益的。通过持续的实践,本书中解释的所有概念都可以成为您日常编程例行的一部分。

THE END
1.跨境电商平台存在的问题和不足有哪些跨境电商平台在发展过程中面临着诸多问题和不足。要解决这些问题,平台需要从多个方面入手进行改进和优化。加强物流体系建设,提高配送效率和服务质量;优化支付系统,降低消费者的风险和成本;深入了解各国文化和法律法规,确保合规经营;此外,完善售后服务体系,提高消费者的满意度;最后,加强技术创新和数据安全保护,提升平台的竞https://www.51969.com/post/19880309.html
2.知识服务平台总是用不好的原因有哪些?1、对平台和工具的理解不足 知识服务平台通常提供各种工具和资源,可帮助用户提高工作效率或获得专业知识。但很多用户在初次接触这些平台时,并没有充分了解其功能、结构和使用方法。例如,一些用户可能只会使用平台的基础功能,而忽视了平台上其他高级工具,比如没有利用其讨论区、在线问答、专家辅导等功能。 https://www.xiaoe-tech.com/extendRead/4350.html
3.深度解析,虚拟主机功能与常见缺点一览,全面了解其优劣对比1、虚拟主机的稳定性不足,且速度可能较快,但缺乏远程服务操作权限,它更适用于流量较低的网站,如中小型网站和博客等。 2、作为一种流行的网络托管服务,虚拟主机虽然广受欢迎,但也存在一些明显缺点,其中最显著的是资源共享问题:虚拟主机意味着多个用户共同使用一台物理服务器的计算能力、带宽和存储空间。 http://www.cloud12.cn/E79CE71ea934.html
4.独立站与电商平台在客户服务上有什么不同?完全控制:企业拥有完全的客户服务控制权,可以根据自身品牌定位和客户需求定制服务流程和策略。 个性化服务:可以根据客户的购买历史和偏好提供个性化服务,增强客户体验和忠诚度。 电商平台: 平台规则限制:客户服务需要遵循平台的规则和政策,企业在服务方式和流程上的自由度较低。 https://blog.csdn.net/2401_89446003/article/details/144293558
5.论线上服务的优缺点现在,“云端笔记”、“云端服务”、“云端存储”都非常的流行,他的优点非常的明显,但是缺点也不小,下面,我们就来总结一下云端的优缺点。 1,不占用本地空间 这是吸引用户的一个很大的理由,谁不想让自己的文件不占用自己计算机的内存呢? 2,可以在多设备之间同步 https://www.jianshu.com/p/7d5f93e37f1b
6.96333电梯应急救援管理平台,电梯96333应急救援处置服务平台解决方案3、电梯编码定位+专项呼叫中心平台服务、电梯厂家系统监测等主要几种管理方式, 缺点 1、作为电梯紧急呼叫求助对讲的电梯五方对讲系统运行状况均无法做到设备在线状态管理监测,只有出现紧急情况,或者是质监局检查的时候,才知道电梯五方通话系统不通。无法避免因为电梯困人无法向外界救援事故的发生。 http://www.changshengtai.net/jiejuefangan/225.html
7.第三方短信发送平台的优点和缺点是什么?思锐在线发短信平台支持发送验证、通知及营销类短信,适用于交易提醒、汽车新品发布、账号登录等内容的短信发送。接下来,由思锐短信平台小编为大家介绍“第三方短信发送平台的优点和缺点是什么?”相关内容: 一、第三方短信发送平台的优点和缺点是什么? 第三方短信发送平台的优点是可以提供高效、稳定的短信发送服务。这些平https://www.surlink.com.cn/news/article/2240.html
8.十大主流跨境电商平台入驻要求与优劣势,看这篇就够了4.在线客服精英团队 技术专业高效率的客户服务精英团队协助商家立即处理订单信息或顾客难题。 Coupang的劣势: 人工成本高,韩国而且是一个重税的国家,他们国家的货品的优势很明显就是快递时效性高。当然缺点很明显就是贵。语言不通,操作便利性差一点,跨境贸易有风险。 https://www.shangyexinzhi.com/article/20776146.html
9.一篇文章搞懂国外流行的7大跨境电商建站平台费用及优缺点!目前Ecwid服务了20多万个网站,支持170多个国家,50几种语言,算得上是后起之秀中发展得最为迅速的。Ecwid和其他在线建站平台比起来有个最大的区别就是,它本身相当于一个在线电商工具,可以作为插件整合进其他cms系统中,比如你可以在Wordpress安装Ecwid插件,甚至可以在另一个在线建站平台——Wix中安装Ecwid插件。 https://www.cifnews.com/article/146683
10.推荐5大知名线上课程平台,让你马上开课当老师(详细比较)缺点分享 需要掌握网站搭建的技术,日后的网站维护也需自行负责。 需要熟悉 WordPress 多项插件功能,才能完善整体网站的功能。 WordPress 的功能多样,需要熟悉接口操作。 谁适合 WordPress 在线教程平台搭建? 任何人都适合使用 WordPress 搭建自己的教程网站,特别是 想要扩展个人品牌的创作者,不用受到任何平台的权限限制,可https://www.itaoda.cn/blog/9804.html
11.单点登录再升级:一种新型扩展方案算法子系统认证中心在现代信息系统中,业务系统的统一认证和单点登录(Single Sign-On, SSO)功能已成为多个业务系统整合、跨组织协作平台、大型在线服务平台以及政府和公共服务平台必不可少的组成部分。统一认证和SSO的主要优势在于实现用户管理的集中化,提高系统安全性,并为用户提供一致且简化的访问体验。 https://www.163.com/dy/article/J8DV3CD20553MTRD.html
12.总工程师履职报告(精选18篇)为了提高集团客户服务的服务质量和管理效率,在集团领导指示下,信息化工程部去年启动了呼叫中心和crm系统建设,我全程负责了项目建设的系统选型,方案编写,设备采购洽谈、推广实施的工作,目前系统已在各置业公司营销职能部门、物业公司及商管公司全面应用,为领导及时掌握第一手的客服信息提供了基础的数据平台。 https://www.ruiwen.com/baogao/6414703.html
13.工程师职称评定工作报告(通用13篇)公司发展逐步扩大,对于公司所有的业务支撑平台,服务器,为重中之重。本年度我司服务器相应出现几次重大故障,分别如下: 1、网络故障x次,重大x次,因服务器遭xx攻击,导致我司服务器无法正常工作。事后通过紧急处理后得以恢复正常。其它几次分别为机房断电、网络升级、电信与联通xx解析故障影响到我司服务器平台网络连接不https://mip.wenshubang.com/gongzuobaogao/114770.html
14.交强险的副本应该在哪里获取?汽车频道为了更直观地展示不同获取途径的优缺点,以下是一个简单的表格: 总之,获取交强险副本的途径多样,车主可以根据自己的实际情况选择最合适的方式。无论是通过保险公司、在线服务平台还是车管所,确保您的交强险副本随时可用,是每位车主应尽的责任。https://m.hexun.com/auto/2024-08-06/213872972.html
15.热门关于电子政务的论文12篇一方面在数据交换设计中,通过采用可以定义适配器的中间件产品,实现数据的灵活定义和转换,能够提供数据的转换、过滤、压缩和加密、例外处理等功能;另一方面在传输设计中,可以通过数据交换平台配置消息的传输路径,即消息路由,以实现消息传输的灵活配置。 (三)网上在线服务系统 这个系统主要包含两个子系统,一个是窗口业务https://www.yjbys.com/biyelunwen/fanwen/dianzixinxigongcheng/734076.html
16.线上教育的优点和缺点云课堂使用方法及录制百科→MAIGOO知识云课堂有哪些平台 中国大学MOOC 学堂在线 学银在线 智慧树网 智慧职教icve 网易云课堂 哔哩哔哩bilibili 华文慕课 优学院 国图公开课 更多推荐 推荐阅读 01 线上教育的优点和缺点 云课堂使用方法及录制百科 云课堂是一类基于云计算技术的一种高效、便捷、实时互动的远程教学课堂形式,和网课、直播教https://www.maigoo.com/goomai/283906.html
17.在线打印兰湾智能入选2021国家中小企业公共服务示范平台 兰湾在线 · 2021-12-23 兰湾智能入选2021国家中小企业公共服务示范平台 兰湾在线 · 2021-12-23 兰湾智能入选国家中小企业公共服务示范平台 兰湾在线 · 2021-12-23 兰湾智能为健康产业——助力第二届国际健康产业大会 https://www.gdlwzn.com/