原文:
zh.annas-archive.org/md5/e94fc480b4a73285f00b144d1072c83f译者:飞龙
第四部分:应对不可避免未来演化的编程解决方案
本节将重点关注交付良好代码的必要性。目标是展示一些技术,这些技术将帮助您交付可以持续维护和演化的良好软件。
在第十四章,C# 8 编码的最佳实践中,我们将介绍一些编码最佳实践,以帮助开发者编写安全、简单且易于维护的软件。本章还包括 C#编码的技巧和窍门。
在第十五章,使用单元测试和 TDD 测试您的代码中,我们将介绍代码测试技术和原则,观点是软件开发的一个必要元素是确保应用程序无错误并且满足所有规范。此外,它还将介绍测试驱动开发,这是一种软件开发方法,将单元测试置于核心地位。
然后,在第十六章,使用工具编写更好的代码中,将介绍您在项目中实现良好代码所需的一系列技术和工具。这里的理念是,尽管编码可以被视为一种艺术,但编写可理解的代码无疑是一种哲学,而且有一些工具可以帮助您实现这一点。
本节包括以下章节:
-
第十四章,C# 8 编码的最佳实践
-
第十五章,使用单元测试和 TDD 测试您的代码
-
第十六章,使用工具编写更好的代码
第十四章:C# 8 编码的最佳实践
当你在项目中担任软件架构师时,你有责任定义和/或维护一个编码标准,这将指导团队根据公司的期望进行编程。本章涵盖了帮助你编写安全、简单和可维护软件的一些最佳编码实践。它还包括 C#编码的技巧和窍门。
本章将涵盖以下主题:
-
你的代码复杂度如何影响性能
-
使用版本控制系统的必要性
-
在 C#中编写安全代码
-
.NET core 编码技巧和窍门
-
书籍使用案例 - 编写代码时的注意事项与禁忌
技术要求
本章需要 Visual Studio 2019 免费社区版或更高版本,并安装所有数据库工具。
你可以在这里找到本章的示例代码:github.com/PacktPublishing/Hands-On-Software-Architecture-with-CSharp-8/tree/master/ch14.
你的代码越复杂,你的编程能力就越差
对于许多人来说,一个好的程序员是编写复杂代码的人。然而,软件开发成熟度的演变意味着对此有不同的思考方式。复杂性并不意味着工作做得好,它意味着代码质量差。一些令人难以置信的科学家和研究人员证实了这一理论,并强调专业代码需要关注时间、高质量,并在预算内完成。
因此,如果你想编写好的代码,你需要关注如何编写它,考虑到你不是唯一一个将来会阅读它的人。这是一个改变你编写代码方式的良好建议。这就是我们将如何讨论本章的每个要点。
如果你对编写良好代码重要性的理解与编写时的简洁性和清晰性理念相一致,你应该看看 Visual Studio 工具代码度量:
代码度量工具将提供度量值,这些度量值将为你提供关于你交付的软件质量的洞察。该工具提供的度量值列在这里,并可在以下链接中找到 docs.microsoft.com/en-us/visualstudio/code-quality/code-metrics-values?view=vs-2019:
-
可维护性指数
-
圈复杂度
-
继承深度
-
类耦合
-
代码行数
下一个子节将专注于描述它们在现实生活场景中的有用性。
可维护性指数
这个指数表明了维护代码的难易程度——代码越容易维护,指数就越高(限于 100)。易于维护是保持软件健康的关键点之一。显然,任何软件在未来都需要进行更改,因为变化是不可避免的。因此,如果你有低维护性,考虑重构你的代码。编写专门负责单一职责的类和方法,避免重复代码,并限制每个方法的代码行数,这些都是你可以提高维护性指数的例子。
循环复杂度
《循环复杂度度量》一书的作者是托马斯·J·麦卡贝。他根据可用的代码路径数(图节点)来定义软件函数的复杂度。路径越多,函数越复杂。麦卡贝认为每个函数的复杂度得分必须小于 10。这意味着,如果代码中有更复杂的方法,你必须重构它,将这些代码的部分转换为独立的方法。有一些真实场景中,这种行为很容易被检测到:
-
循环嵌套循环
-
连续的多个
if-else -
在同一方法内部对每个
case进行代码处理的switch
例如,看看这个方法处理信用卡交易不同响应的第一版本。正如你可以检查的,循环复杂度大于麦卡贝作为基础的数字。这种情况发生的原因是每个主switch案例中的if-else的数量:
static void Main()
{
var billingMode = GetBillingMode();
var messageResponse = ProcessCreditCardMethod();
switch (messageResponse)
{
case "A":
if (billingMode == "M1")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
case "B":
if (billingMode == "M2")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
case "C":
if (billingMode == "M3")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
case "D":
if (billingMode == "M4")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
case "E":
if (billingMode == "M5")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
case "F":
if (billingMode == "M6")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
case "G":
if (billingMode == "M7")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
case "S":
if (billingMode == "M8")
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
else
Console.WriteLine($"Billing Mode {billingMode} for Message
Response {messageResponse}");
break;
default:
Console.WriteLine("The result of processing is unknown");
break;
}
}
如果你计算这段代码的代码度量,你会发现当涉及到循环复杂度时,结果非常糟糕,就像你在下面的截图中所看到的那样:
代码本身没有意义,但这里的关键是展示你可以做出多少改进来编写更好的代码:
-
switch-case选项可以用Enum来编写。 -
每个情况的处理都可以在一个特定的方法中完成。
-
switch-case可以用Dictionary<Enum, Method>来替换。
通过使用上述技术重构这段代码,结果是一段更容易理解的代码,就像你在下面的代码片段中看到的主方法一样:
static void Main()
{
var billingMode = GetBillingMode();
var messageResponse = ProcessCreditCardMethod();
Dictionary<CreditCardProcessingResult, CheckResultMethod>
methodsForCheckingResult =
GetMethodsForCheckingResult();
if (methodsForCheckingResult.ContainsKey(messageResponse))
methodsForCheckingResultmessageResponse;
else
Console.WriteLine("The result of processing is unknown");
}
完整的代码可以在本章的 GitHub 上找到,展示了如何实现低复杂度的代码。下面的截图显示了根据代码度量得出的这些结果:
正如你在前面的截图中所看到的,重构后复杂度有相当大的降低。这里的关键点是,应用了这些技术后,代码的理解性提高了,复杂度降低了,这证明了循环复杂度的重要性。
继承深度
此指标表示连接到正在分析的那个类的类的数量。你继承的类越多,你的代码就越糟糕。这与类耦合类似,表明改变你的代码有多困难。
例如,以下截图有四个继承的类:
你可以在下面的截图看到,深度更大的类由于有三个其他类可以改变其行为,其指标更差:
继承是面向对象分析的基本原则之一。然而,有时它可能对你的代码不利,因为它可能引起依赖。因此,如果你这样做有意义,考虑使用聚合而不是继承。
类耦合
当你在单个类中连接太多的类时,显然你会得到耦合,这可能导致你的代码维护不良。例如,参见下面的截图。它显示了一个进行了大量聚合的设计。代码本身没有意义:
一旦你计算了前面设计的代码指标,你就会看到调用ExecuteTypeA()、ExecuteTypeB()和ExecuteTypeC()的ProcessData()方法的类耦合实例数等于三个(3):
一些论文指出,类耦合实例的最大数量应该是九(9)。由于聚合比继承是一个更好的实践,使用接口将解决类耦合问题。例如,以下设计相同的代码将给出更好的结果:
注意,在设计中使用界面将允许你在不增加解决方案的类耦合的情况下增加执行类型:
作为一名软件架构师,你必须考虑设计你的解决方案,使其具有比耦合更高的内聚性。文献指出,好的软件具有低耦合和高内聚。这是一个基本的原则,可以指导你到一个更好的架构模型。
代码行数
此指标有助于你理解你正在处理的代码的大小。由于行数并不能表明复杂性,因此无法将代码行数与复杂性联系起来。另一方面,代码行数显示了软件的大小和软件设计。例如,如果你在一个类中有太多的代码行(超过 1,000 行代码—1 KLOC),这表明这是一个糟糕的设计。
使用版本控制系统
你可能会觉得这本书中关于这个话题的内容有点明显,但许多人和公司仍然没有将版本控制系统视为软件开发的基本工具!写这个话题的目的是强迫你理解它。如果你不使用版本控制系统,没有任何架构模型或最佳实践可以拯救软件开发。
在过去几年里,我们一直在享受在线版本控制系统(如 GitHub、BitBucket 和 Azure DevOps)带来的优势。事实上,在你的软件开发生命周期中必须有一个这样的工具,而且没有理由不再使用它,因为大多数提供商都为小型团队提供免费版本。即使是你自己开发,这些工具也有助于跟踪你的更改、管理你的软件版本,并保证你的代码的一致性和完整性。
在团队中处理版本控制系统
当你一个人使用版本控制系统工具时,这很显然。你希望保持你的代码安全。但这类系统肯定是为了解决编写代码时的团队问题而开发的。因此,引入了一些功能,如分支和合并,以保持代码完整性,即使在开发者数量相当大的情况下也是如此。
作为一名软件架构师,你将不得不决定在你的团队中采用哪种分支策略。Azure DevOps 和 GitHub 建议不同的方法来实现这一点,并且在某些场景下它们都是很有用的。
关于 Azure DevOps 团队如何处理这个问题的信息可以在这里找到:devblogs.microsoft.com/devops/release-flow-how-we-do-branching-on-the-vsts-team/。GitHub 在这里描述了其流程:guides.github.com/introduction/flow/。我们不知道哪一个最适合你的需求,但我们确实想让你明白,你需要有一个控制代码的策略。
在 C#中编写安全代码
C#可以被认为是一种设计上安全的编程语言。除非你强制使用,否则不需要指针,并且大多数情况下,内存释放由垃圾回收器管理。即便如此,你也应该注意一些事项,以便从你的代码中获得更好的安全结果。让我们来看看它们。
try-catch
编程中的异常如此频繁,以至于你可能会找到一种方法来管理它们,无论何时发生。try-catch语句就是为了管理这些异常而构建的,它们对于保持代码安全至关重要。有很多情况下应用程序崩溃,原因就是没有使用try-catch。以下代码展示了缺少使用try-catch语句的例子:
private static int CodeWithNoTryCatch(string textToConvert)
{
return Convert.ToInt32(textToConvert);
}
另一方面,错误的try-catch使用也可能对你的代码造成损害,特别是因为你将看不到该代码的正确行为,并可能误解提供的结果。以下代码展示了空try-catch语句的示例:
private static int CodeWithEmptyTryCatch(string textToConvert)
{
try
{
return Convert.ToInt32(textToConvert);
}
catch
{
return 0;
}
}
try-catch语句必须始终与日志解决方案相关联,这样你就可以得到系统的响应,指示正确的行为,同时不会导致应用程序崩溃。以下代码展示了带有日志管理的理想try-catch语句:
private static int CodeWithCorrectTryCatch(string textToConvert)
{
try
{
return Convert.ToInt32(textToConvert);
}
catch (Exception err)
{
Logger.GenerateLog(err);
return 0;
}
}
作为一名软件架构师,你应该进行代码审查以修复代码中发现的这类行为。系统的不稳定通常与代码中缺少 try-catch 语句有关。
try-finally 和 using
内存泄漏可以被认为是软件最糟糕的行为之一。它们导致系统不稳定,计算机资源使用不当,以及不希望的应用程序崩溃。C#试图通过垃圾回收器解决这个问题,垃圾回收器会在意识到对象可以被释放时自动释放对象。
与 I/O 交互的对象通常不是由垃圾回收器管理的:文件系统、套接字等。以下代码是FileStream对象使用错误的示例,因为它认为垃圾回收器会释放使用的内存,但实际上不会:
private static void CodeWithIncorrectFileStreamManagement()
{
FileStream file = new FileStream("C:\\file.txt", FileMode.CreateNew);
byte[] data = GetFileData();
file.Write(data, 0, data.Length);
}
此外,垃圾回收器与需要释放的对象交互需要一段时间,有时你可能想自己来做这件事。在这两种情况下,使用try-finally或using语句是最佳实践:
private static void CodeWithCorrectFileStreamManagementFirstOption()
{
using (FileStream file = new FileStream("C:\\file.txt",
FileMode.CreateNew))
{
byte[] data = GetFileData();
file.Write(data, 0, data.Length);
}
}
private static void CodeWithCorrectFileStreamManagementSecondOption()
{
FileStream file = new FileStream("C:\\file.txt", FileMode.CreateNew);
try
{
byte[] data = GetFileData();
file.Write(data, 0, data.Length);
}
finally
{
file.Dispose();
}
}
上述代码展示了如何处理不由垃圾回收器管理的对象。你既有try-finally也有using的实现。作为一名软件架构师,你需要注意这类代码。缺少try-finally或using语句可能导致软件在运行时出现严重问题。
IDisposable接口
同样,如果你不使用 try-finally/using 语句管理在方法内部创建的对象,那么在未正确实现IDisposable接口的类中创建的对象可能会导致你的应用程序出现内存泄漏。因此,当你有一个处理和创建对象的类时,你应该实现可处置模式,以确保释放它创建的所有资源:
好消息是,Visual Studio 通过在代码中指示并右键单击“快速操作和重构”选项来实现此接口的代码片段,就像你在前面的截图中所看到的那样。一旦你插入了代码,你需要遵循 TODO 指令,以确保正确实现了模式。
.NET Core 编码技巧和窍门
.NET Core 实现了一些有助于我们编写更好代码的良好功能。其中最有用的是依赖注入(DI),这在第九章,设计模式和.NET Core 实现中已经讨论过。考虑这一点有几个很好的理由。第一个是因为你不需要担心注入对象的释放,因为你不是它们的创建者。
此外,DI 使你能够注入ILogger,这是一个非常有用的调试异常的工具,这些异常将需要在你的代码中通过 try-catch 语句来管理。此外,使用.NET Core 进行 C#编程必须遵循任何编程语言的通用良好实践。以下列表显示了其中的一些:
-
类、方法和变量应该有可理解的名称:名称应该解释读者需要知道的一切。不应该需要解释性注释。
-
方法不能有高复杂度:应该检查方法的圈复杂度,以确保方法不要有太多的代码行。
-
应避免重复代码:在像 C#这样的高级编程语言中,没有必要存在重复代码。
-
在使用对象之前应进行检查:由于可能存在 null 对象,代码必须进行 null 类型检查。
-
应使用常量和枚举器:避免代码中的魔法数字和文本的好方法是将这些信息转换为常量和枚举器,这通常更容易理解。
-
应避免不安全代码:除非没有其他实现代码的方法,否则应避免使用不安全代码。
-
try-catch 语句不能为空:在没有处理
catch区域的try-catch语句中没有理由。 -
try-finally/using 语句应该始终使用:即使对于垃圾回收器将负责处理的对象,也应考虑处理你负责创建的对象。
-
至少公共方法应该有注释:考虑到公共方法是用于你库外部的那些方法,它们必须被解释以正确使用。
-
switch-case 语句必须有默认处理:由于
switch-case语句可能接收到在某些情况下未知的人口变量,默认处理将保证在这种情况下代码不会中断。
作为软件架构师,一个好的做法是为你的开发者提供一个所有程序员都会使用的代码模式,以此来保持代码风格的一致性。你可以将代码模式用作编码检查的清单,这将丰富软件代码的质量。
WWTravelClub – 编写代码的注意事项和禁忌
作为一名软件架构师,你必须定义一个符合你所服务公司需求的代码标准。
在本书的示例项目中(关于 WWTravelClub 项目的更多信息,请参阅第一章,理解软件架构的重要性),情况并无不同。我们决定展示该标准的做法是描述我们在编写示例时遵循的一系列“做”和“不做”的清单。值得一提的是,这个清单是一个很好的开始标准,作为软件架构师,你应该与团队中的开发者讨论这个清单,以便以实际和良好的方式对其进行改进:
-
务必用英语编写你的代码。
-
务必遵循 C#编码标准,使用驼峰命名法。
-
务必使用易于理解的名称编写类、方法和变量。
-
务必对公共类、方法和属性进行注释。
-
务必尽可能使用
using语句。 -
务必尽可能使用
async实现。 -
务必在实现非托管代码之前请求授权。
-
务必在实现线程之前请求授权。
-
不要编写空的
try-catch语句。 -
不要编写超过 10 个循环复杂度的方法。
-
不要在
for/while/do-while/foreach语句中使用break和continue。 -
不要使用
goto语句。
这些“做”和“不做”的规则很容易遵循,而且,更重要的是,它们将为你的团队产生的代码带来巨大的成果。在第十六章,使用工具编写更好的代码中,我们将讨论帮助你实施这些规则的工具。
摘要
在本章中,我们讨论了一些编写安全代码的重要提示。本章介绍了一个分析代码指标的工具,这样你可以管理你正在开发的软件的复杂性和可维护性。最后,我们提出了一些保证你的软件不会因为内存泄漏和异常而崩溃的好建议。在现实生活中,软件架构师总会被要求解决这类问题。
在下一章中,我们将学习一些单元测试技术、单元测试的原则以及一个专注于 C#测试项目的软件过程模型。
问题
-
为什么我们需要关注可维护性?
-
循环复杂度是什么?
-
列出使用版本控制系统的优点。
-
try-catch、try-finally和try-catch-finally之间的区别是什么? -
垃圾收集器是什么?
-
实现
IDisposable接口的重要性是什么? -
当涉及到编码时,我们从.NET Core 中获得了哪些优势?
进一步阅读
这些是一些书籍和网站,你可以在其中找到更多关于本章主题的信息:
-
《嵌入式系统设计艺术》by Jack G. Ganssle. Elsevier, 1999。
-
《重构:改善既有代码的设计》by Martin Fowler. Addison Wesley, 1999。
-
托马斯·J·麦卡贝的复杂度度量。IEEE 软件工程杂志,第 2 卷第 4 期,1976 年,第 308-320 页 (
dblp.uni-trier.de/db/journals/tse/tse2.html). -
blogs.msdn.microsoft.com/zainnab/2011/05/25/code-metrics-class-coupling/ -
docs.microsoft.com/en-us/visualstudio/code-quality/code-metrics-values?view=vs-2019 -
blogs.msdn.microsoft.com/devops/2018/04/19/release-flow-how-we-do-branching-on-the-vsts-team/
第十五章:使用单元测试用例和 TDD 测试您的代码
在开发软件时,确保应用程序无错误并满足所有规范至关重要。这可以通过在开发过程中测试所有模块或在整体应用程序完全或部分实现后进行测试来实现。
由于大多数测试必须在每次修改应用程序时执行,手动执行所有测试不是一个可行的选项。正如本书中解释的那样,现代软件正在不断修改,以适应快速变化的市场需求。本章讨论了交付可靠软件所需的所有测试类型,以及如何组织和自动化它们。
更具体地说,本章涵盖了以下主题:
-
理解自动化测试及其用法
-
理解测试驱动开发(TDD)的基本原理
-
使用 TDD 优化软件投资
-
在 Visual Studio 中定义 C#测试项目
在本章中,我们将了解哪些类型的测试值得实施,以及单元测试是什么。我们将了解可用的不同类型的项目以及如何在其中编写单元测试。到本章结束时,书籍用例将帮助我们自动在 Azure DevOps 中执行应用程序的持续集成/持续交付(CI/CD)周期中的测试。
技术要求
本章需要安装所有数据库工具的 2019 免费社区版。它还需要一个免费的 Azure 账户;如果您尚未创建,请参阅第一章,理解软件架构的重要性中的创建 Azure 账户部分。
本章中的所有概念都通过基于 WWTravelClub 书籍用例的实用示例进行了阐明。本章的代码可在以下网址找到:github.com/PacktPublishing/Hands-On-Software-Architecture-with-CSharp-8。
理解自动化测试
必须避免在大多数功能完全实现后立即进行应用程序测试,原因如下:
-
如果一个类或模块设计或实现不当,它可能已经影响了其他模块的实现方式。因此,在此阶段,修复问题可能代价高昂。
-
需要测试所有可能执行路径的输入组合的数量随着一起测试的模块或类的数量呈指数增长。例如,如果一个类方法
A的执行可以采取三条不同的路径,而另一个方法B的执行可以采取四条路径,那么测试A和B一起就需要 3 x 4 种不同的输入。一般来说,如果我们一起测试几个模块,需要测试的总路径数是每个模块测试路径数的乘积。相反,如果单独测试模块,所需的输入数量只是测试每个模块所需路径的总和。 -
如果由 N 个模块组成的聚合测试失败,那么在 N 个模块中定位错误源通常是一个非常耗时的活动。
-
当测试 N 个模块时,我们必须重新定义涉及 N 个模块的所有测试,即使只是在应用程序的 CI/CD 周期中有一个 N 个模块发生变化。
前面的考虑表明,单独测试每个模块的方法更为方便。不幸的是,一组独立于其上下文验证所有方法的测试是不完整的,因为一些错误可能是由模块之间不正确的交互引起的。
因此,测试被组织成两个阶段:
-
单元测试:这些测试验证每个模块的所有执行路径都表现正常。它们相当完整,通常覆盖所有可能的路径。这是可行的,因为与整个应用程序的可能执行路径相比,每个方法或模块的可能执行路径并不多。
-
集成测试:这些测试在软件通过所有单元测试后执行。集成测试验证所有模块是否正确交互以获得预期结果。由于单元测试已经验证了每个模块的所有执行路径都正常工作,因此集成测试不需要完全覆盖。它们需要验证所有交互模式,即各种模块可能合作的所有可能方式。
通常,每个交互模式都与多个测试相关联:一个典型的模式激活,以及一些激活的极端情况。例如,如果一个完整的交互模式接收一个数组作为输入,我们将为典型的数组大小编写一个测试,一个null数组的测试,一个空数组的测试,以及一个非常大的数组的测试。这样我们就可以验证单个模块的设计方式是否与整个交互模式的需求兼容。
在实施上述策略的情况下,如果我们修改单个模块而不改变其公共接口,我们需要更改该模块的单元测试。
如果,相反,更改涉及某些模块的交互方式,那么我们也必须添加新的集成测试或修改现有的测试。然而,通常这并不是一个大问题,因为大多数测试都是单元测试,所以重写大部分集成测试不需要太大的努力。此外,如果应用程序是根据单一职责、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则(SOLID)设计的,那么在单次代码修改后必须更改的集成测试数量应该很小,因为修改应该只影响与修改的方法或类直接交互的几个类。
到目前为止,应该很清楚,单元测试和集成测试在整个软件生命周期中都必须被重用。这就是为什么自动化它们是值得的。单元和集成测试的自动化避免了手动测试执行中可能出现的错误,并节省了时间。几千个自动化测试的整个系列可以在几分钟内验证软件的完整性,在每次对软件进行的小修改之后,从而使得现代软件的 CI/CD 周期中所需的频繁更改成为可能。
随着新错误的发现,会添加新的测试来发现它们,以确保它们不会在未来版本的软件中再次出现。这样,自动化测试总是变得更加可靠,并更好地保护软件免受新更改引入的错误的影响。因此,添加新错误(不是立即被发现)的概率大大降低。
下一节将为我们提供组织和设计自动化单元和集成测试的基础,以及如何在“C# 测试项目”部分中用 C#编写测试的实用细节。
编写自动化(单元和集成)测试
测试不是从头开始编写的;所有软件开发平台都有工具帮助我们编写和运行测试(或其中一些)。一旦选定的测试被执行,所有工具都会显示报告,并提供调试所有失败测试代码的可能性。
更具体地说,所有单元和集成测试框架都由三个重要部分组成:
-
定义所有测试的设施:它们验证实际结果是否与预期结果相符。通常,一个测试被组织成测试类,其中每个测试调用一个单独的应用程序类或一个单独的类方法。每个测试分为三个阶段:
-
测试准备:测试所需的一般环境已经准备就绪。这一阶段并不准备为每种方法单独输入的单个输入,而是仅准备全局环境,例如在类构造函数中注入的对象或数据库表的模拟。通常,相同的准备程序会在多个测试中使用,因此测试准备被提取到专门的模块中。
-
测试执行:使用适当的输入调用要测试的方法,并将它们的执行结果与预期结果进行比较,使用如
Assert.Equal(x, y)、Assert.NotNull(x)等构造。 -
清理:整个环境被清理,以避免测试的执行影响其他测试。这一步骤是步骤 1 的逆操作。
-
-
模拟设施:虽然集成测试使用所有(或几乎所有)参与对象合作模式的类,但在单元测试中禁止使用其他应用程序类。因此,如果一个正在测试的类,比如
A,在其方法M中使用了另一个应用程序类B的方法,该方法是将其构造函数中注入的,那么为了测试M,我们必须注入B的模拟实现。值得注意的是,只有执行某些处理的类在单元测试期间不允许使用另一个类,而纯数据类可以。模拟框架包含定义接口和接口方法的模拟实现的功能,这些方法可以在测试中定义数据。通常,这些模拟实现也能报告所有模拟方法调用的信息。这种模拟实现不需要实际类文件的定义,而是在测试代码中通过调用如new Mock<IMyInterface>()这样的方法在线完成。 -
执行和报告工具:这是一个基于可视配置的工具,开发者可以使用它来决定要启动哪些测试以及何时启动它们。此外,它还以包含所有成功测试、所有失败测试、每个测试的执行时间以及其他依赖于特定工具及其配置的信息的报告形式显示测试的最终结果。通常,在开发 IDE(如 Visual Studio)中执行的执行和报告工具还提供了在每个失败的测试上启动调试会话的可能性。
由于模拟框架只能创建接口的模拟实现,而不能创建类的模拟实现,因此我们应该在类构造函数和方法中注入接口或纯数据类(不需要模拟),否则类无法进行单元测试。因此,对于每个我们想要注入到另一个类中的合作类,我们必须定义一个相应的接口。
此外,类应该使用在其构造函数或方法中注入的实例,而不是其他类公共静态字段中可用的类实例;否则,在编写测试时可能会忘记隐藏的交互,这可能会使测试的准备步骤复杂化。
下一个部分将描述在软件开发中使用的其他类型的测试。
编写验收和性能测试
接受测试定义了项目利益相关者与开发团队之间的合同。它们被用来验证开发的软件是否确实按照与他们的约定行事。接受测试不仅验证功能规范,还验证软件可用性和用户界面的约束。由于它们的目的还包括展示软件在实际计算机显示器和显示设备上的外观和行为,因此它们永远不会完全自动化,主要由必须由操作员遵循的食谱和验证列表组成。
有时,自动测试的开发是为了验证仅功能规范,但此类测试通常绕过用户界面,直接将测试输入注入到用户界面后面的逻辑中。例如,在 ASP.NET Core MVC 应用程序的情况下,整个网站在一个完整的环境中运行,包括所有需要的存储空间,并填充了测试数据;输入不提供给 HTML 页面,而是直接注入到 ASP.NET Core 控制器中。绕过用户界面的测试被称为皮下测试。ASP.NET Core 提供了各种工具来执行皮下测试,以及自动化与 HTML 页面交互的工具。
在自动测试的情况下,通常更倾向于使用皮下测试,而完整测试则手动执行,以下是一些原因:
-
没有自动测试可以验证用户界面的外观和可用性。
-
自动化与用户界面的实际交互是一个非常耗时的工作。
-
用户界面经常更改以提高其可用性并添加新功能,而且单个应用程序屏幕的微小更改也可能迫使对该屏幕上运行的全部测试进行完全重写。
简而言之,用户界面测试非常昂贵且可重用性低,因此很少值得自动化它们。然而,ASP.NET Core 提供了Microsoft.AspNetCore.Mvc.Testing NuGet 包,用于在测试环境中运行整个网站。使用它与AngleSharp NuGet 包一起,该包将 HTML 页面解析成 DOM 树,您可以用可接受的编程工作量编写自动化全面测试。将在第二十章中详细描述 ASP.NET Core 接受测试,软件测试自动化。
性能测试向应用程序施加模拟负载,以查看其是否能够处理典型的生产负载,以发现其负载限制,并定位瓶颈。应用程序部署在一个预演环境中,该环境在硬件资源方面是实际生产环境的副本。然后,创建并应用模拟请求到系统中,并收集响应时间和其他指标。模拟请求批次应与实际生产批次具有相同的组成。如果可用,它们可以从实际生产请求日志中生成。
如果响应时间不满意,将收集其他指标以发现可能的瓶颈(低内存、慢速存储或慢速软件模块)。一旦定位到负责问题的软件组件,就可以在调试器中分析它,以测量典型请求中涉及的各个方法调用的执行时间。
性能测试中的失败可能导致对应用程序所需硬件的重定义,或者对某些软件模块、类或方法的优化。
Azure 和 Visual Studio 都提供了创建模拟负载和报告执行指标的工具。然而,它们已被宣布过时,并将很快停止使用(从写这本书起大约一年),因此我们不会描述它们。作为替代方案,有开源和第三方工具可以使用。其中一些在进一步阅读部分列出。
下一个部分将描述一种将测试置于核心位置的软件开发方法。
理解测试驱动开发(TDD)
测试驱动开发(TDD)是一种软件开发方法,它将单元测试置于核心和中心位置。根据这种方法,单元测试是每个类规范的正式化,因此必须在类的代码之前编写。实际上,一个覆盖所有代码路径的完整测试明确定义了代码行为,因此它可以被视为代码的规范。这不是通过某种正式语言定义代码行为的正式规范,而是一种基于行为示例的规范。
测试软件的理想方式是编写整个软件行为的正式规范,并使用一些完全自动化的工具来验证实际产生的软件是否符合这些规范。在过去,一些研究工作被用于定义用于描述代码规范的正式语言,但使用类似语言表达开发者心中的行为是一个非常困难且容易出错的任务。因此,这些尝试很快就被放弃了,转而采用基于示例的方法。当时,主要目的是自动生成代码。如今,自动代码生成已被大量放弃,仅在小应用领域幸存,如设备驱动程序的开发。在这些领域,将行为在正式语言中形式化的努力值得在尝试测试难以重现的并行线程行为时节省的时间。
单元测试最初被构想为以完全独立的方式编码基于示例的规范,作为名为极限编程(Extreme Programming)的特定敏捷开发方法的一部分。然而,如今,TDD 可以独立于极限编程使用,并被纳入其他敏捷方法中的强制规定。
虽然毫无疑问,在发现数百个错误后经过优化的单元测试可以作为可靠的代码规范,但开发者是否能够轻松设计出可以直接用作编写代码的可靠规范的单元测试并不明显。实际上,通常情况下,如果随机选择示例,你需要无限或至少是大量的示例来唯一地定义代码的行为。
只有在你理解了所有可能的执行路径之后,才能用可接受数量的示例定义行为。实际上,在这个时候,选择每个执行路径的典型示例就足够了。因此,在完全编写完方法之后编写该方法的单元测试是很容易的:它只需要为现有代码的每个执行路径选择一个典型实例。然而,以这种方式编写单元测试并不能防止执行路径设计本身的错误。例如,它不能防止忘记在调用成员之前测试变量的null值的典型错误。这就是为什么 TDD 建议在编写应用程序代码之前编写单元测试。
我们可以得出结论,在编写单元测试时,开发者必须通过寻找极端情况以及可能添加比严格需要的更多示例来预测所有可能的执行路径。然而,开发者可能在编写应用程序代码时犯错误,他们也可能在设计单元测试时预测所有可能的执行路径时犯错误。
我们发现了 TDD 的主要缺点:单元测试本身可能是错误的。也就是说,不仅应用程序代码,与其相关的 TDD 单元测试也可能与开发者心中的行为不一致。因此,在开始时,单元测试不能被视为软件规范,而更可能是一个可能的错误和不完整的软件行为描述。因此,我们有两个关于我们心中所想行为的描述,即应用程序代码及其在应用程序代码之前编写的 TDD 单元测试。
TDD 之所以有效,是因为在编写测试和编写代码时犯完全相同错误的概率非常低。因此,每当测试失败时,要么是测试中存在错误,要么是应用程序代码中存在错误,反之亦然,如果应用程序代码或测试中存在错误,那么有很高的概率测试会失败。也就是说,TDD 的使用确保了大多数错误都能立即被发现!
使用 TDD 编写类方法或代码块是一个由三个阶段组成的循环:
-
红色阶段:在这个阶段,开发者设计新的单元测试,这些测试必然会失败,因为此时还没有代码实现他们所描述的行为。
-
绿色阶段:在这个阶段,开发者编写最少的代码或对现有代码进行必要的最小修改,以通过所有单元测试。
-
重构阶段:一旦测试通过,代码将被重构以确保良好的代码质量并应用最佳实践和模式。特别是,在这个阶段,某些代码可以被提取到其他方法或其他类中。在这个阶段,我们可能还会发现需要其他单元测试,因为发现了新的执行路径或新的极端情况。
循环会在所有测试通过且没有编写新代码或修改现有代码的情况下立即停止。
有时候,设计初始单元测试非常困难,因为很难想象代码可能的工作方式和它可能采取的执行路径。在这种情况下,你可以通过编写应用程序代码的初始草图来更好地理解要使用的特定算法。在这个初始阶段,我们只需要关注主要执行路径,完全忽略极端情况和输入验证。一旦我们清楚地了解了应该工作的算法背后的主要思想,我们就可以进入标准的三个阶段的 TDD 循环。
在下一节中,我们将列出 Visual Studio 中所有可用的测试项目,并详细描述 xUnit。
定义 C# 测试项目
Visual Studio 包含三种单元测试框架的项目模板,分别是 MSTest、xUnit 和 NUnit。一旦你启动新项目向导,为了可视化所有这些框架的版本,适合 .NET Core C# 应用程序,请将项目类型设置为测试,语言设置为 C#,平台设置为 Linux,因为 .NET Core 项目是唯一可以在 Linux 上部署的项目。
以下截图显示了应出现的选项:
所有的前述项目都自动包含在 Visual Studio 测试用户界面(Visual Studio 测试运行器)中运行所有测试的 NuGet 包。然而,它们不包含任何用于模拟接口的功能,因此你需要添加包含流行模拟框架的 Moq NuGet 包。
所有测试项目都必须包含对要测试的项目引用。
在下一节中,我们将描述 xUnit,因为它可能是三个框架中最受欢迎的一个。然而,所有三个框架都非常相似,主要区别在于方法名称和用于装饰各种测试内容的属性名称。
使用 xUnit 测试框架
在 xUnit 中,测试是通过 [Fact] 或 [Theory] 属性装饰的方法。测试由测试运行器自动发现,并在用户界面中列出所有测试,以便用户可以运行所有测试或仅运行部分测试。
在运行每个测试之前,都会创建测试类的新的实例,因此类构造函数中包含的测试准备代码会在每个测试之前执行。如果您还需要清理代码,则测试类必须实现IDisposable接口,以便将清理代码包含在IDisposable.Dispose方法中。
测试代码调用要测试的方法,然后使用Assert静态类的方法测试结果,例如Assert.NotNull(x)、Assert.Equal(x, y)和Assert.NotEmpty(IEnumerable x)。还有验证是否调用抛出特定类型异常的方法,例如:
Assert.Throws<MyException>(() => {/* test code */ ...}).
当一个断言失败时,会抛出一个异常。如果测试代码或断言抛出了未被捕获的异常,则测试失败。
以下是一个定义单个测试的方法的示例:
[Fact]
public void Test1()
{
var myInstanceToTest = new ClassToTest();
Assert.Equal(5, myInstanceToTest.MethodToTest(1));
}
当一个方法仅定义一个测试时,使用[Fact]属性,而当同一个方法定义多个测试,每个测试在不同的数据元组上时,使用[Theory]属性。数据元组可以以多种方式指定,并作为方法参数注入测试中。
以下代码可以修改为测试MethodToTest的多个输入,如下所示:
[Theory]
[InlineData(1, 5)]
[InlineData(3, 10)]
[InlineData(5, 20)]
public void Test1(int testInput, int testOutput)
{
var myInstanceToTest = new ClassToTest();
Assert.Equal(testOutput,
myInstanceToTest.MethodToTest(testInput);
}
每个InlineData属性指定一个要注入方法参数的元组。由于可以将简单的常量数据作为属性参数包含在内,xUnit 还为您提供从实现IEnumerable的类中获取所有数据元组的能力,如下面的示例所示:
public class Test1Data: IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] { 1, 5};
yield return new object[] { 3, 10 };
yield return new object[] { 5, 20 };
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
...
...
[Theory]
[ClassData(typeof(Test1Data))]
public void Test1(int testInput, int testOutput)
{
var myInstanceToTest = new ClassToTest();
Assert.Equal(testOutput,
myInstanceToTest.MethodToTest(testInput);
}
使用ClassData属性指定提供测试数据的类的类型。
还可以从类的静态方法中获取数据,该静态方法返回一个IEnumerable,并使用MemberData属性,如下面的示例所示:
[Theory]
[MemberData(nameof(MyStaticClass.Data),
MemberType= typeof(MyStaticClass))]
public void Test1(int testInput, int testOutput)
{
...
MemberData属性将方法名称作为第一个参数传递,并将类类型作为MemberType命名参数。如果静态方法是同一测试类的一部分,则可以省略MemberType参数。
下一个部分将展示如何处理一些高级准备和清理场景。
高级测试准备和清理场景
有时准备代码包含非常耗时的操作,例如与数据库建立连接,这些操作不需要在每次测试之前重复,但可以在同一类中包含的所有测试之前执行一次。在 xUnit 中,这种测试准备代码不能包含在测试类构造函数中;由于在每次单个测试之前都会创建测试类的不同实例,因此必须将其分解到单独的类中,称为固定类。
如果我们还需要相应的清理代码,固定类必须实现 IDisposable。在其他测试框架中,如 NUnit,测试类实例仅创建一次,因此不需要将固定代码分解到其他类中。然而,像 NUnit 这样的测试框架,在每次测试之前不创建新实例,可能会因为测试方法之间不希望有的交互而产生错误。
以下是一个示例,展示了如何使用 xUnit 固定类打开和关闭数据库连接:
public class DatabaseFixture : IDisposable
{
public DatabaseFixture()
{
Db = new SqlConnection("MyConnectionString");
}
public void Dispose()
{
Db.Close()
}
public SqlConnection Db { get; private set; }
}
由于固定类实例仅在所有与固定类关联的测试执行之前创建一次,并且在测试完成后立即销毁,因此数据库连接仅在创建固定类时创建一次,并在销毁固定对象后立即销毁。
固定类通过让测试类实现空的 IClassFixture<T> 接口与每个测试类相关联,如下所示:
public class MyTestsClass : IClassFixture<DatabaseFixture>
{
DatabaseFixture fixture;
public MyDatabaseTests(DatabaseFixture fixture)
{
this.fixture = fixture;
}
...
...
}
固定类实例会自动注入到测试类构造函数中,以便使在固定类测试准备中计算的所有数据对测试可用。这样,例如,在我们的上一个例子中,我们可以获取数据库连接实例,以便类中的所有测试方法都可以使用它。
如果我们想在包含在测试类集合中的所有测试上执行一些测试准备代码,而不是单个测试类,我们必须将固定类关联到一个表示测试类集合的空类,如下所示:
[CollectionDefinition("My Database collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
// this class is empty, since it is just a placeholder
}
CollectionDefinition 属性声明了集合的名称,而 IClassFixture<T> 接口已被 ICollectionFixture<T> 接口所取代。
然后,我们通过将集合名称应用于 Collection 属性来声明测试类属于先前定义的集合,如下所示:
[Collection("My Database collection")]
public class MyTestsClass
{
DatabaseFixture fixture;
public MyDatabaseTests(DatabaseFixture fixture)
{
this.fixture = fixture;
}
...
...
}
Collection 属性声明了要使用哪个集合,而测试类构造函数中的 DataBaseFixture 参数提供了一个实际的固定类实例,因此它可以用于所有类测试。
下一节将展示如何使用 Moq 框架模拟接口。
使用 Moq 模拟接口
模拟功能不包括在本节中列出的任何测试框架中,因为它们不包括在 xUnit 中。因此,它们必须通过安装特定的 NuGet 包来提供。Moq NuGet 包中可用的 Moq 框架是 .NET 和 .NET Core 中最流行的模拟框架。它相当容易使用,本节将简要介绍。
一旦我们安装了 NuGet 包,我们需要在测试文件中添加一个 using Moq 语句。模拟实现很容易定义,如下所示:
var myMockDependency = new Mock<IMyInterface>();
可以通过 Setup/Return 方法对来定义模拟依赖对特定方法特定输入的行为,如下所示:
myMockDependency.Setup(x=>x.MyMethod(5)).Returns(10);
在Return之后,我们可以放置另一个Setup/Return对,该对定义了同一方法的相同输入的不同行为或不同方法的行为。这样,我们可以指定无限数量的输入/输出行为。
而不是使用特定的输入值,我们也可以使用通配符来匹配特定类型,如下所示:
myMockDependency.Setup(x => x.MyMethod(It.IsAny<int>))
.Returns(10);
一旦配置了模拟依赖项,我们可以从其Object属性中提取模拟实例,并像使用实际实现一样使用它,如下所示:
var myMockedInstance=myMockDependency.Object;
...
myMockedInstance.MyMethod(10);
然而,模拟方法通常由被测试的代码调用,所以我们只需要提取模拟实例并将其用作测试中的输入。
我们还可以按照以下方式模拟属性和异步方法:
myMockDependency.Setup(x => x.MyProperty)
.Returns(42);
...
myMockDependency.Setup(p => p.MyMethodAsync(1))
.ReturnsAsync("aasas");
var res=await myMockDependency.Object
.MyMethodAsync(1);
对于异步方法,Returns必须替换为ReturnsAsync。
每个模拟实例都会记录对其方法和属性的调用,因此我们可以在测试中使用这些信息。以下代码展示了示例:
myMockDependency.Verify(x => x.MyMethod(1), Times.AtLeast(2))
前面的语句断言MyMethod方法至少被调用过两次,并带有给定的参数。还有Times.Never、Times.Once(断言方法只被调用了一次)等更多选项。
到目前为止的 Moq 文档应该涵盖了您测试中可能出现的 99%的需求,但 Moq 还提供了更复杂的选择。进一步阅读部分包含了完整文档的链接。
下一节将展示如何在实践中定义单元测试以及如何在 Visual Studio 和 Azure DevOps 中运行它们,这得益于本书中的用例。
用例 - 在 DevOps Azure 中自动化单元测试
在本节中,我们将向我们在第十三章中构建的示例应用程序中添加一些单元测试项目,即展示 ASP.NET Core MVC。如果您没有,您可以从 GitHub 仓库中与本书相关的第十三章的展示 ASP.NET Core MVC部分下载它。GitHub 仓库中第四章的决定最佳云解决方案部分包含了本节将添加的代码以及所有添加这些代码的说明。
作为第一步,让我们复制解决方案文件夹并命名为PackagesManagementWithTests。然后打开解决方案并将其添加到名为PackagesManagementTest的 xUnit .NET Core C#测试项目中。最后,添加对 ASP.NET Core 项目(PackagesManagement)的引用,因为我们将要对其进行测试,并添加对Moq NuGet 包最新版本的引用,因为我们需要模拟功能。到这一点,我们就准备好编写测试了。
例如,我们将为ManagePackagesController控制器中带有[HttpPost]属性的Edit方法编写单元测试,如下所示:
[HttpPost]
public async Task<IActionResult> Edit(
PackageFullEditViewModel vm,
[FromServices] ICommandHandler<UpdatePackageCommand> command)
{
if (ModelState.IsValid)
{
await command.HandleAsync(new UpdatePackageCommand(vm));
return RedirectToAction(
nameof(ManagePackagesController.Index));
}
else
return View(vm);
}
在编写测试方法之前,让我们将测试项目中自动包含的测试类重命名为ManagePackagesControllerTests。
第一次测试验证,如果ModelState中存在错误,动作方法会渲染一个与作为参数接收的相同模型视图,以便用户可以纠正所有错误。让我们删除现有的测试方法,并编写一个空的DeletePostValidationFailedTest方法,如下所示:
[Fact]
public async Task DeletePostValidationFailedTest()
{
}
该方法必须是async,因为我们必须测试的Edit方法是async。在这个测试中,我们不需要模拟对象,因为不会使用注入的对象。因此,作为测试的准备,我们只需要创建一个控制器实例,并且我们必须向ModelState添加一个错误,如下所示:
var controller = new ManagePackagesController();
controller.ModelState
.AddModelError("Name", "fake error");
然后我们调用该方法,注入ViewModel和一个null命令处理程序作为其参数,因为命令处理程序将不会使用:
var vm = new PackageFullEditViewModel();
var result = await controller.Edit(vm, null);
在验证阶段,我们验证结果是否为ViewResult,并且它包含在控制器中注入的相同模型:
Assert.IsType<ViewResult>(result);
Assert.Equal(vm, (result as ViewResult).Model);
现在,我们还需要一个测试来验证如果没有错误,命令处理程序会被调用,然后浏览器会被重定向到Index控制器动作方法。我们调用DeletePostSuccessTest方法:
[Fact]
public async Task DeletePostSuccessTest()
{
}
这次准备代码必须包括命令处理程序模拟的准备,如下所示:
var controller = new ManagePackagesController();
var commandDependency =
new Mock<ICommandHandler<UpdatePackageCommand>>();
commandDependency
.Setup(m => m.HandleAsync(It.IsAny<UpdatePackageCommand>()))
.Returns(Task.CompletedTask);
var vm = new PackageFullEditViewModel();
由于处理程序HandleAsync方法不返回任何async值,我们不能使用ReturnsAsync,但我们必须使用Returns方法只返回一个完成的Task(Task.Complete)。要测试的方法使用ViewModel和模拟的处理程序调用:
var result = await controller.Edit(vm,
commandDependency.Object);
在这种情况下,验证代码如下:
commandDependency.Verify(m => m.HandleAsync(
It.IsAny<UpdatePackageCommand>()),
Times.Once);
Assert.IsType<RedirectToActionResult>(result);
var redirectResult = result as RedirectToActionResult;
Assert.Equal(nameof(ManagePackagesController.Index),
redirectResult.ActionName);
Assert.Null(redirectResult.ControllerName);
作为第一步,我们验证命令处理程序确实被调用了一次。更好的验证还应包括检查它是否使用传递给动作方法的ViewModel调用命令。这可以通过从commandDependency.Invocations中提取此信息来完成。我们将将其作为练习。
然后我们验证动作方法返回带有正确动作方法名称的RedirectToActionResult,并且没有指定控制器名称。
一旦所有测试都准备好了,如果测试窗口没有出现在 Visual Studio 的左侧栏中,我们只需从 Visual Studio Test 菜单中选择运行所有测试项。一旦测试窗口出现,进一步的调用可以从该窗口内启动。
如果测试失败,我们可以在其代码中添加一个断点,这样我们就可以通过在测试窗口中右键单击它并选择调试选定的测试来启动一个调试会话。
以下步骤显示了如何将我们的解决方案与 Azure DevOps 存储库连接起来,我们将定义一个 Azure DevOps 管道,该管道构建项目并启动其测试。这样,从那天起,每天开发人员推送更改后,我们都可以启动管道来验证存储库代码是否编译并通过所有测试:
-
作为第一步,我们需要一个免费的 DevOps 订阅。如果你还没有,请通过点击此页面的“开始免费”按钮来创建一个:
azure.microsoft.com/en-us/services/devops/。在这里,让我们定义一个组织,但在创建项目之前停止,因为我们将在 Visual Studio 内部创建项目。 -
确保你已使用 Azure 账户(与创建 DevOps 账户时使用的相同)登录到 Visual Studio。在此阶段,你可以通过右键单击解决方案并选择“配置持续交付到 Azure…”来为你的解决方案创建一个 DevOps 仓库。在出现的窗口中,一个错误消息会告知你你的代码尚未配置任何仓库:
- 点击“现在添加到源控制”链接。之后,DevOps 屏幕将出现在 Visual Studio Team Explorer 标签中:
- 点击“发布 Git 仓库”按钮后,系统会提示你选择你的 DevOps 组织和仓库名称。成功将代码发布到 DevOps 仓库后,DevOps 屏幕应如下所示:
DevOps 屏幕显示了你的在线 DevOps 项目的链接。未来当你打开你的解决方案时,如果链接没有出现,请点击 DevOps 屏幕上的“连接”按钮或“管理连接”链接( whichever appears)来选择并连接你的项目。
-
点击此链接访问在线项目。一旦到达那里,如果你点击左侧菜单中的“Repos”项,你会看到你刚刚发布的仓库。
-
现在,点击“管道”菜单项以创建一个用于构建和测试你的项目的 DevOps 管道。在出现的窗口中,点击按钮以创建一个新的管道:
- 你将被提示选择你的仓库位置:
- 选择“Azure Repos Git”然后选择你的仓库。然后你将被提示关于项目类型的问题:
- 选择 ASP.NET Core。将为你自动创建一个用于构建和测试项目的管道。通过将新创建的
.yaml文件提交到你的仓库来保存它:
- 可以通过选择“排队”按钮来运行管道,但由于 DevOps 标准管道是在仓库的 master 分支上触发的,因此每次提交此分支的更改或修改管道时,它都会自动启动。可以通过点击“编辑”按钮来修改管道:
-
一旦进入编辑模式,可以通过点击每个步骤上出现的“设置”链接来编辑所有管道步骤。可以按照以下方式添加新的管道步骤:
-
在新步骤必须添加的位置写上
task:,然后在你输入任务名称时接受出现的建议之一。 -
在你编写了一个有效的任务名称后,一个设置链接会出现在新步骤上方,点击它。
-
在出现的窗口中插入所需的任务参数并保存。
-
-
为了让我们的测试工作,我们需要指定定位包含测试的所有程序集的标准。在我们的案例中,因为我们有一个包含测试的唯一
.dll文件,所以指定其名称就足够了。点击VSTest@2测试任务的设置链接,并将自动建议的测试文件字段的内容替换为以下内容:
**\PackagesManagementTest.dll
!**\*TestAdapter.dll
!**\obj\**
- 然后点击“添加”以修改实际的管道内容。一旦你在“保存并运行”对话框中确认了你的更改,管道就会被启动,如果没有错误,测试结果将被计算。在特定构建期间启动的测试结果可以通过在管道历史记录标签页中选择特定的构建,并点击出现的页面上的“测试”标签来分析。在我们的案例中,我们应该看到以下截图类似的内容:
- 如果你点击管道页面的分析标签,你会看到关于所有构建的分析,包括关于测试结果的分析:
- 点击分析页面的测试区域,我们可以获得所有管道测试结果的详细报告。
摘要
在本章中,我们解释了为什么自动化软件测试是值得的,然后我们关注了单元测试的重要性。我们还列出了所有测试类型及其主要特性,主要关注单元测试。我们分析了 TDD 的优势以及如何在实践中使用它。有了这些知识,你应该能够生产出既可靠又易于修改的软件。
最后,我们分析了适用于.NET Core 项目的所有测试工具,重点关注 xUnit 和 Moq 的描述,并展示了如何在 Visual Studio 和 Azure DevOps 中通过本书用例的实际应用中使用它们。
下一章将探讨如何测试和衡量代码的质量。
问题
-
为什么自动化单元测试是值得的?
-
TDD 能够立即发现大多数错误的主要原因是什么?
-
[Theory]和[Fact]属性在 xUnit 中的区别是什么? -
在测试断言中使用了哪个 xUnit 静态类?
-
哪些方法允许定义 Moq 模拟的依赖项?
-
是否可以使用 Moq 模拟异步方法?如果是,如何?
进一步阅读
尽管章节中包含的 xUnit 文档相当完整,但它并没有包括 xUnit 提供的少数配置选项。完整的 xUnit 文档可在xunit.net/找到。MSTest 和 NUnit 的文档分别可在github.com/microsoft/testfx和github.com/nunit/docs/wiki/NUnit-Documentation找到。
Moq 的完整文档可在github.com/moq/moq4/wiki/Quickstart找到。
这里有一些针对 Web 应用程序的性能测试框架的链接:
第十六章:使用工具编写更好的代码
正如我们在第十四章中看到的,C# 8 编码的最佳实践,编码可以被认为是一门艺术,但编写可理解的代码更像是哲学。在前述章节中,我们讨论了作为软件架构师,你需要为你的开发者提供的实践。在本章中,我们将描述代码分析的技术和工具,以便你为你的项目编写高质量的代码。
本章将涵盖以下主题:
-
识别写得好的代码
-
理解在过程中可以使用哪些工具来使事情变得更简单
-
一本书的使用案例——在发布应用程序之前实施代码审查
到本章结束时,你将能够定义你打算将哪些工具纳入你的软件开发生命周期以实现代码分析。
技术要求
本章需要 Visual Studio 2017 或 2019 免费社区版或更高版本。您可以在github.com/PacktPublishing/Hands-On-Software-Architecture-with-CSharp-8/tree/master/ch16找到本章的示例代码。
识别写得好的代码
定义代码是否写得好的标准并不容易。在第十四章中描述的最佳实践,C# 8 编码的最佳实践,当然可以指导你作为软件架构师为你的团队定义一个标准。但是,即使有了标准,错误仍然会发生,你可能会在生产代码中才发现它们。仅仅因为代码没有遵循你定义的所有标准就决定在生产中重构代码,这是一个不容易做出的决定,尤其是如果这段代码运行正常的话。有些人认为,写得好的代码是那些在生产中运行良好的代码。然而,这无疑会损害软件的生命周期,因为开发者可能会受到那些非标准代码的启发。
因此,作为软件架构师的你,需要找到方法来预测你定义的编码标准可能不会被应用的情况。幸运的是,如今我们有许多工具选项可以帮助我们完成这项任务。它们被认为是静态代码分析的自动化;这项技术被视为提高开发的软件和帮助开发者的一个巨大机会。
你的开发者将通过代码分析而不断进步的原因是,你在代码审查过程中开始在他们之间传播知识。我们现在拥有的工具具有相同的目的。而且,有了 Roslyn,它们在你编写代码的同时完成这项任务。Roslyn 是.NET 的编译器平台,它使你能够开发一些用于分析代码的工具。这些分析器可以检查样式、质量、设计和其他问题。
例如,看看以下代码。它没有任何意义,但你可以看到有一些错误:
using System;
namespace SampleCodeChapter16
{
class Program
{
static void Main(string[] args)
{
try
{
int variableUnused = 10;
int variable = 10;
if (variable == 10)
{
Console.WriteLine("variable equals 10");
}
else
{
switch (variable)
{
case 0:
Console.WriteLine("variable equals 0");
break;
}
}
}
catch
{
}
}
}
}
这段代码的目的是展示一些工具的强大功能,以改善您交付的代码。让我们在下一个主题中逐一检查它们,包括如何设置它们。
理解和应用可以评估 C# 代码的工具
Visual Studio 中代码分析的发展是持续的。这意味着 Visual Studio 2019 确实比 Visual Studio 2017 等提供了更多此类目的的工具。
作为软件架构师,您需要处理的一个问题是团队的编码风格。这当然有助于更好地理解代码。例如,如果您转到 Visual Studio 菜单 | 工具 | 选项,您将找到设置不同代码风格模式的方法,并且它甚至将不良编码风格作为“代码风格”选项中的错误指示,如下所示:
例如,前面的屏幕截图表明“避免未使用参数”被视为错误。在此更改之后,本章开头展示的相同代码的编译结果不同,如以下屏幕截图所示:
您可以将您的编码风格配置导出并附加到项目中,这样它就会遵循您定义的规则。
Visual Studio 2019 提供的另一个好工具是分析和代码清理。在这个工具中,您可以设置一些可以从代码中清理出来的代码标准。例如,在以下屏幕截图中,它被设置为删除不必要的代码:
在解决方案资源管理器区域中选择代码清理的方式,正如您在下面的屏幕截图中所见。这个过程将在您所有的代码文件中运行:
解决了代码风格和代码清理工具指示的错误后,我们正在处理的示例代码有一些最小化简化,如下所示:
using System;
namespace SampleCodeChapter16
{
class Program
{
static void Main()
{
try
{
int variable = 10;
if (variable == 10)
{
Console.WriteLine("variable equals 10");
}
else
{
switch (variable)
{
case 0:
Console.WriteLine("variable equals 0");
break;
}
}
}
catch
{
}
}
}
}
值得注意的是,前面的代码有许多需要改进的地方。除此之外,Visual Studio 通过安装扩展到 IDE 中来启用额外的工具。这些工具可以帮助您提高代码质量,因为其中一些工具是专门用于代码分析的。本节将列出一些免费选项,以便您可以选择最适合您需求的选项。当然,还有其他选项,甚至付费选项。这里的想法不是指出一个特定的工具,而是给您一个它们能力的大致了解。
要安装这些扩展,您需要在 Visual Studio 中找到菜单。这里有一个扩展管理器的屏幕截图:
有许多其他酷炫的扩展可以提高您代码和解决方案的生产力和质量。您可以在该管理器中搜索它们。
在你选择要安装的扩展之后,你需要重新启动 Visual Studio。大多数扩展在安装后都很容易识别,因为它们会修改 IDE 的行为。
应用扩展工具分析代码
尽管在代码样式和代码清理工具之后提供的示例代码比我们在本章开头展示的要好,但它显然远未达到第十四章中讨论的最佳实践,即C# 8 编码最佳实践。在接下来的几节中,你将能够检查三个可以帮助你改进代码的扩展的行为:Microsoft Code Analysis 2019、SonarLint for Visual Studio 2019 和 Code Cracker for Visual Studio 2017。
使用 Microsoft Code Analysis 2019
这个扩展由 Microsoft DevLabs 提供,是对我们过去用来自动化的 FxCop 规则的升级。基本上,它有超过 100 条规则用于在编写代码时检测问题。
例如,只需启用扩展并重新构建我们在本章中使用的这个小示例,代码分析就找到了一个新的问题需要解决,如下面的截图所示:
值得注意的是,我们在第十四章中讨论了空try-catch语句作为反模式的使用。因此,如果能够像这样暴露这类问题,对代码的健康状况将大有裨益。
在 Visual Studio 2019 中应用 SonarLint
SonarLint 是 Sonar Source 社区发起的一个开源项目,旨在你在编码时检测错误和质量问题。它支持 C#、VB .NET、C、C++ 和 JavaScript。这个扩展的伟大之处在于它附带了解决检测到的问题的解释,这就是为什么我们说开发者在使用这些工具的同时学会了如何编写良好的代码。查看以下截图,其中展示了在示例代码中进行的分析:
我们可以验证这个扩展能够指出其他错误,并且正如它们承诺的那样,每个警告都有解释。这不仅仅对于检测问题很有用,而且对于培养开发者的良好编码实践也很有帮助。
将 Code Cracker 用于 Visual Studio 2017,作为编写更好代码的辅助工具
Code Cracker 是另一个使用 Roslyn 分析代码的工具,它是由一些 Microsoft MVP 发起的,也是开源的。根据 Visual Studio Marketplace,使用这个工具的人比使用其他任何工具的人都要多:
值得注意的是,这个工具中检测到了其他工具中没有发现的新规则。一个扩展到另一个扩展之间的代码分析差异的原因可能是编程的规则并不相同。
在分析后检查最终代码
在分析三个扩展之后,我们最终解决了所有提出的问题。我们可以检查最终的代码,如下所示:
using System;
namespace SampleCodeChapter16
{
static class Program
{
static void Main()
{
try
{
int variable = 10;
if (variable == 10)
{
Console.WriteLine("variable equals 10");
}
else
{
switch (variable)
{
case 0:
Console.WriteLine("variable equals 0");
break;
default:
Console.WriteLine("Unknown behavior");
break;
}
}
}
catch (Exception err)
{
Console.WriteLine(err);
}
}
}
}
如您所见,前面的代码不仅更容易理解,而且更安全,并且能够考虑编程的不同路径,因为switch-case的默认值已经编程。这种模式在第十四章编码 C# 8 的最佳实践中也进行了讨论,该章节总结说,最佳实践可以很容易地通过使用本章中提到的(或所有)扩展之一(或全部)来遵循。
用例 - 在发布应用程序之前评估 C#代码
在第三章,“使用 Azure DevOps 记录需求”,我们在平台上创建了 WWTravelClub 仓库。正如我们看到的,Azure DevOps 支持持续集成,这非常有用。在本节中,我们将讨论更多关于 DevOps 概念和 Azure DevOps 平台为何如此有用的原因。
目前,我们想要介绍的唯一事情是分析开发者在提交代码后但尚未发布之前代码的可能性。在当今的 SaaS 应用程序生命周期工具领域,这仅得益于我们拥有的一些 SaaS 代码分析平台。用例将使用 Sonar Cloud。
Sonar Cloud 对开源代码是免费的,并且可以分析存储在 GitHub、Bitbucket 和 Azure DevOps 中的代码。注册需要这些平台的一个用户。一旦您登录,您就可以按照以下文章中描述的步骤创建 Azure DevOps 和 Sonar Cloud 之间的连接:sonarcloud.io/documentation/analysis/scan/sonarscanner-for-azure-devops/。
通过在 Azure DevOps 项目和 Sonar Cloud 之间建立连接,您将拥有以下所示的构建管道:
值得注意的是,C#项目没有 GUID 编号,而 Sonar Cloud 需要这个编号。您可以使用此链接(www.guidgenerator.com/)轻松生成一个,并将其放置如下截图所示:
一旦您完成构建,代码分析的结果将在 Sonar Cloud 中展示,如下一截图所示。如果您想导航到该项目,可以访问:sonarcloud.io/dashboard?id=WWWTravelClub:
此外,到这个时候,已经分析的代码还没有发布。因此,这可以在发布系统之前获得下一步的质量提升非常有用。您可以将这种方法作为在提交代码时自动进行代码分析的参考。
摘要
本章介绍了可以用来应用第十四章中描述的编码最佳实践的工具体现,即《C# 8 编码最佳实践》。我们探讨了 Roslyn 编译器,它允许在开发者编码的同时进行代码分析,并探讨了用例——在发布应用程序之前评估 C#代码,这在使用 Sonar Cloud 的 Azure DevOps 构建过程中实现了代码分析。
一旦将本章所学的一切应用到你的项目中,代码分析将为你提供改进交付给客户代码质量的机会。这是软件架构师的一个非常重要的角色。
在下一章中,我们将使用 Azure DevOps 部署你的应用程序。
问题
-
如何描述软件为编写良好的代码?
-
Roslyn 是什么?
-
代码分析是什么?
-
代码分析的重要性是什么?
-
Roslyn 如何帮助进行代码分析?
-
什么是 Visual Studio 扩展?
-
展示的代码分析扩展工具是什么?
进一步阅读
这些是一些你可以找到本章主题更多信息的网站:
第五部分:持续且高质量地交付软件
本书最后一部分将指导你如何使用 DevOps 原则进行持续集成(CI)和持续部署(CD)来交付软件。在软件服务领域,最大的玩家在过去的几年中已经改变了软件交付的概念。因此,本节将讨论新兴的新颖和革命性的思维方式。
在第十七章,使用 Azure DevOps 部署您的应用程序中,我们将讨论软件交付的新哲学,以及 Azure DevOps 如何帮助你接受这种新方法,并掌握构建和部署管道原则。
在第十八章,理解 DevOps 原则中,我们将介绍 DevOps 的主要概念,这是大家都在学习和实践的过程。此外,本章还将展示使用 DevOps 开发和交付软件的工具。
然后,在第十九章,在 DevOps 中应用 CI 场景的挑战,重点将转向与持续集成和部署解决方案相关的挑战。目的是确保你了解风险并遵循最佳实践,在使用 CI 时。
第二十章,软件测试自动化专注于软件测试自动化。由于测试是一个不可避免(且重复)的过程,其自动化至关重要。本章将教你如何为项目编写自动化的功能测试用例。
本节包括以下章节:
-
第十七章,使用 Azure DevOps 部署您的应用程序
-
第十八章,理解 DevOps 原则
-
第十九章,在 DevOps 中应用 CI 场景的挑战
-
第二十章,软件测试自动化
第十七章:使用 Azure DevOps 部署你的应用程序
本章重点介绍所谓的服务设计思维,即牢记你正在设计的软件作为提供给组织/组织部分的服务。这种方法的主要收获是,最高优先级是软件为目标组织带来的价值。此外,你提供的不仅仅是可工作的代码和修复错误的协议,而是一套满足软件所设想的所有需求的解决方案。换句话说,你的工作包括满足这些需求所需的一切,例如监控用户满意度,并在用户需求发生变化时调整软件。
最后,监控软件以揭示问题和新的需求,并快速修改以适应不断变化的需求要容易得多。
服务设计思维严格与软件即服务(SaaS)模型相关联,我们在第四章中讨论了该模型,决定最佳基于云的解决方案。事实上,提供基于 Web 服务的解决方案最简单的方法是提供作为服务的 Web 服务使用,而不是销售实现它们的软件。
更具体地说,本章涵盖了以下主题:
-
理解 SaaS
-
为服务场景准备解决方案
-
用例 - 使用 Azure Pipelines 部署我们的包管理应用程序
到本章结束时,你将能够根据服务设计思维原则设计软件,并使用 Azure Pipelines 部署你的应用程序。
技术要求
本章需要 Visual Studio 2017 或 2019 免费社区版或更高版本,并安装所有数据库工具。它需要一个免费的 Azure 账户。如果你还没有创建一个,第一章中“理解软件架构的重要性”的创建 Azure 账户小节解释了如何创建。本章使用与第十五章相同的代码,使用单元测试用例和 TDD 测试你的代码,可在以下链接找到:github.com/PacktPublishing/Hands-On-Software-Architecture-with-CSharp-8。
理解 SaaS
将软件作为服务销售/使用与一套更广泛的服务设计思维原则相关联。服务设计思维不仅仅是一种软件开发技术/或软件部署方法,它还影响几个业务领域,即组织与人力资源、软件开发流程,最后是硬件基础设施和软件架构。
在接下来的小节中,我们将简要讨论我们列出的每个业务领域的含义,在最后一个小节中,我们将专注于 SaaS 部署模型。
使你的组织适应服务场景
第一个组织影响来自优化软件对目标组织价值的需要。这需要一个人力资源或团队——负责计划和监控软件在目标组织中的影响——以最大化软件带来的增值。这个战略角色不仅在初始设计阶段需要,在整个应用程序的生命周期中都需要。实际上,这个角色负责保持软件与目标组织不断变化的需求精确匹配。
另一个重要的影响领域是人力资源管理。事实上,由于主要优先考虑的是软件带来的增值,而不是利用现有资源和能力,因此人力资源必须适应项目需求。这意味着一旦需要就获取新资源,并通过新的人力资源和/或对现有资源的适当培训来发展所需的能力。
下一个子节将讨论所有涉及软件开发过程的含义。
在服务场景中开发软件
影响软件开发流程的主要约束是保持软件与组织需求精确匹配的需求。这一需求可以通过基于 CI/CD 方法的任何敏捷方法来满足。对于 CI/CD 的简要回顾,请参阅第三章“使用 Azure DevOps 组织工作”部分,文档化需求,而对于 CI/CD 的详细讨论,请参阅第十七章“使用 Azure DevOps 部署您的应用程序”,该章节完全致力于 CI/CD。值得注意的是,任何设计良好的 CI/CD 周期都应该包括处理用户反馈和用户满意度报告。
此外,为了优化软件带来的增值,组织开发团队(或其一部分)与系统用户紧密接触的阶段是一个好的做法,这样开发者可以更好地理解软件对目标组织的影响。
最后,在编写功能性和非功能性需求时,必须始终牢记软件带来的增值。因此,用考虑为什么和如何它们对增值做出贡献的原因来注释用户故事是有用的。收集需求的过程在第二章“功能性和非功能性需求”中讨论。
下一个子节将讨论更多技术影响。
服务场景的技术影响
在服务场景中,硬件基础设施和软件架构都受到以下三个主要原则的限制,这些原则是保持软件与组织需求精确匹配的必然结果,具体如下:
-
需要监控软件以发现可能由系统故障或软件使用和/或用户需求变化引起的任何问题。这意味着从所有硬件/软件组件中提取健康检查和负载统计信息。用户执行的操作统计信息也能提供有关组织需求变化的良好线索——更具体地说,用户和应用程序在每个操作实例上花费的平均时间以及每单位时间内(日、周或月)执行的操作实例数量。
-
还需要监控用户满意度。可以通过在每个应用程序屏幕上添加一个链接到易于填写的用户满意度报告页面来获取用户满意度的反馈。
-
最后,需要快速适应硬件和软件,既要适应每个应用程序模块接收到的流量,也要适应组织需求的变更。这意味着以下内容:
-
极端关注软件模块化
-
保持对数据库引擎变化的开放态度,并优先考虑基于 SOA 或微服务架构的解决方案,而不是单体软件
-
保持对新技术的开放态度
-
使硬件易于适应意味着允许硬件扩展,这反过来又意味着采用云基础设施、硬件集群或两者兼而有之。同时,保持对云服务供应商变化的开放态度也很重要,这反过来意味着将依赖云平台封装在少数几个软件模块中。
通过选择每个模块实现的最佳技术,可以最大化软件增加的价值,这反过来意味着能够混合不同的技术。这正是基于容器的技术,如 Docker,发挥作用的地方。Docker 和相关技术已在第五章中描述,即将微服务架构应用于您的企业应用。
总结来说,我们所列出的所有要求都汇聚于本书中描述的大多数先进技术,例如云服务、可扩展的 Web 应用、分布式/可扩展数据库、Docker、SOA 和微服务架构。
关于如何为服务环境准备软件的更多细节将在下一节中给出,而下一小节将专门讨论 SaaS 应用的优缺点。
采用 SaaS 解决方案
SaaS 解决方案的主要吸引力在于其灵活的支付模式,它提供了以下优势:
-
您可以避免为了更实惠的月度支付而放弃大额投资。
-
您可以从一个便宜的系统开始,然后在业务增长时再转向更昂贵的解决方案。
然而,SaaS 解决方案还提供了其他优势,具体如下:
-
在所有云解决方案中,您可以轻松扩展您的解决方案。
-
应用程序会自动更新。
-
由于 SaaS 解决方案是通过公共互联网提供的,因此可以从任何位置访问。
不幸的是,SaaS 的优势是有代价的,因为 SaaS 也存在不可忽视的劣势,具体如下:
-
您的业务与 SaaS 提供商紧密相连,这可能导致服务被终止或以您不再接受的方式修改。
-
通常,您无法实施任何类型的定制,只能限制于 SaaS 供应商提供的少数标准选项。然而,有时 SaaS 供应商也提供添加自定义模块的可能性,这些模块可以由他们或您编写。
总结来说,SaaS 解决方案提供了有趣的优势,但也存在一些劣势,因此作为软件架构师,您必须进行详细分析以决定如何采用它们。
下一个部分将解释如何将软件适应用于服务场景。
准备服务场景的解决方案
首先,为服务场景准备解决方案意味着专门为云和/或分布式环境设计它。反过来,这意味着在设计时考虑到可扩展性、容错性和自动故障恢复。
前三点的主要影响是关于如何处理状态。无状态模块实例易于扩展和替换,因此您应仔细规划哪些模块是无状态的,哪些有状态。此外,如第七章“如何在云中选择您的数据存储”中所述,您必须记住写入和读取操作以完全不同的方式扩展。特别是,读取操作可以通过复制更容易扩展,而写入操作在关系型数据库中扩展不佳,通常需要 NoSQL 解决方案。
在分布式环境中,高可扩展性防止了分布式事务和同步操作的使用。因此,数据一致性和容错性只能通过基于异步消息的更复杂技术来实现,如下所示:
-
一种技术是将所有要发送的消息存储在队列中,以便在发生错误或超时的情况下可以重试异步传输。消息可以在收到确认接收或模块决定终止产生消息的操作时从队列中删除。
-
另一个问题是处理同一消息被多次接收的可能性,因为超时导致同一消息被多次发送。
-
如果需要,使用乐观并发和事件溯源等技术来最小化数据库中的并发问题。乐观并发在第十三章的“数据层”子部分中进行了解释,该部分位于“展示 ASP.NET Core MVC”用例的末尾,而事件溯源则在第十章的“使用 SOLID 原则映射您的领域”部分中与其他数据层内容一起进行了描述。
前面的列表中的前两点与第五章的“如何.NET Core 处理微服务?”部分中讨论的其他分布式处理技术一起进行了详细讨论,该部分标题为“将微服务架构应用于您的企业应用程序”。
容错和自动故障恢复要求软件模块实现健康检查接口,云框架可能会调用这些接口以验证模块是否正常工作,或者是否需要将其终止并由另一个实例替换。ASP.NET Core 和所有 Azure 微服务解决方案都提供现成的基本健康检查,因此开发者不需要关心这些。然而,可以通过实现一个简单的接口添加更详细的自定义健康检查。
如果你的目标是可能更改某些应用程序模块的云提供商,那么难度会增加。在这种情况下,必须将云平台依赖封装在仅几个模块中,并且必须丢弃过于严格绑定到特定云平台解决方案。因此,例如,你应该避免使用有状态的/无状态的本地 Service Fabric 服务,因为它们的架构是特定于 Azure Service Fabric 的,所以它们不能移植到不同的云平台。
如果你的应用程序是为服务场景设计的,那么一切都必须自动化:新版本的测试和验证、创建应用程序所需的整个云基础设施,以及在该基础设施上部署应用程序。
所有云平台都提供语言和工具来自动化整个软件 CI/CD 周期,即构建代码、测试代码、触发手动版本批准、创建硬件基础设施以及部署应用程序。
Azure Pipelines 允许自动化所有列出的步骤。在第十五章的“使用单元测试用例和 TDD 测试您的代码”用例中展示了如何使用 Azure Pipelines 自动化包括软件测试在内的所有步骤。下一节的用例将展示如何自动化在 Azure Web 应用平台上的应用程序部署。
自动化在 SaaS 应用程序中扮演着更基本的角色,因为每个新客户的整个新租户的创建必须由客户订阅自动触发。更具体地说,多租户 SaaS 应用程序可以通过三种基本技术实现:
-
所有客户共享相同的基础设施和数据存储。这个解决方案最容易实施,因为它只需要实现一个标准 Web 应用程序。然而,它仅适用于非常简单的 SaaS 服务,因为对于更复杂的应用程序,始终更难确保存储空间和计算时间在用户之间平均分配。此外,随着数据库变得越来越复杂,始终更难确保不同用户的数据安全隔离。
-
所有客户共享相同的基础设施,但每个客户都有自己的数据存储。这个选项解决了之前解决方案中所有的数据库问题,并且由于创建一个新的租户只需要创建一个新的数据库,因此它很容易自动化。此解决方案提供了一种简单的方式来定义定价策略,通过将其与存储消耗相关联。
-
每个客户都有自己的私有基础设施和数据存储。这是最灵活的策略。从用户的角度来看,它的唯一缺点是价格更高。因此,它仅适用于每个用户所需的计算能力达到最低阈值以上。由于必须为每个新客户创建整个基础设施,并在其上部署应用程序的新实例,因此自动化更困难。
无论选择哪三种策略中的哪一种,你都需要有随着消费者增加而扩展你的云资源的能力。
如果你还需要确保你的基础设施创建脚本可以在多个云提供商之间工作,那么一方面,你不能使用过于特定于单个云平台的特性,另一方面,你需要一种独特的、可以翻译成更常见云平台本地语言的、用于创建基础设施的语言。Terraform 和 Ansible 是描述硬件基础设施的两个非常常见的选择。
用例 - 使用 Azure Pipelines 部署我们的包管理应用程序
在本节中,我们将配置一个自动部署到 Azure App Service 平台,用于我们已经在第十五章的用例中定义的 DevOps 项目,即“使用单元测试用例和 TDD 测试你的代码”。Azure DevOps 还可以自动创建一个新的 Web 应用程序,但为了避免配置错误(这可能会消耗掉你所有的免费额度),我们将手动创建它,并让 Azure DevOps 仅部署应用程序。所有必需的步骤都组织成以下各个子节。
创建 Azure Web 应用程序和 Azure 数据库
可以通过以下简单步骤定义 Azure Web App:
- 前往 Azure 门户,选择 App Services,然后点击“添加”按钮创建一个新的 Web App。按照以下方式填写所有数据:
-
显然,你可以使用你已有的资源组,以及对你来说最方便的区域。对于运行时堆栈,请选择你在 Visual Studio 解决方案中使用的相同的 .NET Core 版本。
-
现在,如果你有足够的信用额度,让我们为应用程序创建一个 SQL Server 数据库,并将其命名为
PackagesManagementDatabase。如果你没有足够的信用额度,不用担心——你仍然可以测试应用程序部署,但应用程序在尝试访问数据库时会返回错误。请参考第七章 7.7 的 关系数据库 子节,如何在云中选择你的数据存储,了解如何创建 SQL Server 数据库。
配置你的 Visual Studio 解决方案
一旦你定义了 Azure Web App,你需要按照以下简单步骤配置应用程序以在 Azure 中运行:
-
如果你定义了 Azure 数据库,你需要在 Visual Studio 解决方案中使用两个不同的连接字符串,一个用于本地数据库的开发,另一个用于 Web 应用的 Azure 数据库。
-
现在,在 Visual Studio 解决方案中打开
appsettings.Development.json和appsettings.json,如下所示:
- 然后,将
appsettings.json中的整个ConnectionStrings节复制到appsettings.Development.json中,如下所示:
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)....."
},
现在你已经有了开发设置中的本地连接字符串,因此你可以将 appsettings.json 中的 DefaultConnection 改为 Azure 数据库中的一个。
-
前往 Azure 门户中的数据库,复制连接字符串,并用你在定义数据库服务器时获得的用户名和密码填充它。
-
最后,在本地提交你的更改,然后与远程仓库同步。现在,你的更改已经在 DevOps 管道中,它正在处理这些更改以获取新的构建。
配置 Azure 管道
最后,你可以按照以下步骤配置 Azure 管道,以自动将你的应用程序部署到 Azure:
-
通过点击 Visual Studio Team Server 窗口的“连接”选项卡中的“管理连接”链接,将 Visual Studio 与你的 DevOps 项目连接起来。然后,点击 DevOps 链接进入你的在线项目。
-
通过在单元测试步骤之后添加一个额外的步骤来修改
PackagesManagementWitTests构建管道。实际上,我们需要一个步骤来准备所有要部署的文件,并将它们打包成一个 ZIP 文件。 -
点击
PackagesManagementWitTests管道的“编辑”按钮,然后前往文件末尾并写下以下内容:
- task: PublishBuildArtifacts@1
- 当新任务上方出现“设置”链接时,点击它以配置新任务:
-
接受默认的发布路径,因为它已经与将要部署应用程序的任务路径同步,只需插入工件名称,然后选择“Azure Pipeline”作为位置。一旦保存,管道将开始,新添加的任务应该会成功。
-
部署和其他发布工件被添加到不同的管道中,称为发布管道,以将它们与构建相关工件解耦。使用发布管道,您无法编辑
.yaml文件,但您将使用图形界面进行操作。 -
点击“发布”左侧菜单标签以创建新的发布管道。一旦您点击添加新管道,系统将提示您添加第一个管道阶段的第一项任务。实际上,整个发布管道由不同的阶段组成,每个阶段都包含一系列任务。虽然每个阶段只是一系列任务,但阶段图可以分支,我们可以在每个阶段之后添加几个分支。这样,我们可以部署到不同的平台,每个平台都需要不同的任务。在我们的简单示例中,我们将使用单个阶段。
-
选择“部署 Azure 应用服务”任务。一旦添加此任务,系统将提示您填写缺失信息:
- 点击错误链接并填写缺失的参数:
-
选择您的订阅,然后,如果出现授权按钮,请点击它以授权 Azure Pipelines 访问您的订阅。然后,选择“Windows”作为部署平台,最后,从“应用服务名称”下拉列表中选择您创建的应用服务。任务设置在您编写时自动保存,因此您只需点击“保存”按钮即可保存整个管道。
-
现在,我们需要将此管道连接到源工件。点击“添加工件”按钮,然后选择“构建”作为源类型,因为我们需要将新的发布管道与构建管道创建的 ZIP 文件连接。一个设置窗口将出现:
- 从下拉列表中选择我们之前的构建管道,并保持“最新”作为版本。最后,接受在“源别名”中建议的名称。
我们的发布管道已准备就绪,可以直接使用。您刚刚添加的源工件图像在其右上角包含一个触发图标,如下所示:
如果您点击触发图标,您可以选择在新构建可用时自动触发发布管道:
保持其禁用状态;我们可以在完成并手动测试发布管道后启用它。为了准备自动触发,我们需要在应用程序部署之前添加一个人工批准任务。
添加发布的人工批准
由于任务通常由软件代理执行,我们需要在手动工作中嵌入人工批准。让我们按照以下步骤添加:
- 点击阶段 1 标题右侧的三个点:
- 然后,选择添加一个无代理作业。一旦添加了无代理作业,点击其添加按钮并添加一个“人工干预”任务。以下截图显示了“人工干预”设置:
-
在“通知用户”字段中添加操作员的说明并选择你的账户。
-
现在,用鼠标拖动整个“无代理作业”,将其放置在应用程序部署任务之前。最终的截图应该是这样的:
- 完成!点击左上角的保存按钮保存管道。
现在,一切准备就绪,我们可以创建我们的第一个自动发布。
创建发布
一旦一切准备就绪,可以按照以下步骤准备和部署新版本:
- 让我们点击“创建发布”按钮开始创建新版本:
- 确认“源别名”是最后一个可用的,添加发布描述,然后点击创建。在短时间内,你应该会收到发布批准的电子邮件。点击其中的链接,进入批准页面:
- 点击“恢复/拒绝”按钮然后批准发布。等待部署完成。你应该看到所有任务都成功完成,如下面的截图所示:
你已经运行了第一个成功的发布管道!
在实际项目生活中,发布管道将包含更多任务。事实上,应用程序(在部署到实际生产环境之前)是在预发布环境中部署的,在那里它们进行 beta 测试。因此,在这次首次部署之后,可能会有一些手动测试,手动授权在生产中的部署,以及最终的在生产环境中的部署。
摘要
我们描述了服务设计思维原则和 SaaS 软件部署模型。现在,你应该能够分析这些方法对一个组织的影响,并且应该能够调整现有的软件开发流程和软硬件架构,以利用它们提供的机会。
我们还解释了软件周期自动化、云硬件基础设施配置和应用程序部署的必要性和涉及的技术。
一旦你在最后的使用案例部分实现了示例,你应该能够使用 Azure Pipelines 来自动化基础设施配置和应用程序部署。
下一章将更深入地探讨 DevOps,它与详细讨论在第十九章中的 CI/CD,即在 DevOps 中应用 CI 场景的挑战,在服务场景中起着基本的作用,尤其是在 SaaS 应用程序的维护方面。
问题
-
服务设计思维的主要目标是什么?
-
服务设计思维是否真的需要充分利用公司现有的所有能力?
-
为什么在 SaaS 应用程序的生命周期中,完全自动化是基本的?
-
是否可以使用平台无关的语言定义硬件云基础设施?
-
在整个应用程序生命周期自动化中,Azure 优先使用的工具是什么?
-
如果两个 SaaS 供应商提供相同的软件产品,你应该选择最可靠的还是最便宜的?
-
可扩展性是否是服务场景中唯一重要的要求?
进一步阅读
本章的主要参考文献是本书其他章节/部分的引用,并在本章中已给出。在此,我们仅提供 Azure Pipelines 文档的链接:docs.microsoft.com/en-us/azure/devops/pipelines/?view=azure-devops,以及本章中提到的两种基础设施描述语言,Terraform (www.terraform.io/) 和 Ansible (www.ansible.com/)。
第十八章:理解 DevOps 原则
DevOps 是当今每个人都正在学习和实践的过程。但作为一名软件架构师,你需要理解并推广 DevOps,不仅作为一个过程,而且作为一个哲学。本章将涵盖你需要用 DevOps 开发和交付软件的主要概念、原则和工具。
本章将涵盖以下主题:
-
描述 DevOps 是什么,并查看在 WWTravelClub 项目中如何应用的示例
-
理解 DevOps 原则和部署阶段以利用部署过程
-
学习可以与 Azure DevOps 一起使用以改进测试和反馈的 DevOps 工具
与其他章节不同,WWTravelClub 项目将在本章节的主题中介绍,我们将在章节末尾提供一个结论,给你机会了解这一哲学如何得到实施。所有展示 DevOps 原则的截图都来自本书的主要样本,因此你将能够轻松理解 DevOps 原则。
技术要求
本章需要 Visual Studio 2019 社区版或更高版本。你可能还需要一个 Azure DevOps 账户,如第三章中所述,使用 Azure DevOps 记录需求。
描述 DevOps
DevOps 一词来源于开发和运维的合并,因此这个过程简单地将这些领域的行动统一起来。然而,当你开始更深入地了解它时,你会意识到仅仅连接这两个领域还不足以实现这一哲学的真正目标。
我们也可以说,DevOps 是回答人类当前关于软件交付需求的流程。
唐纳·布朗(Donovan Brown),微软首席 DevOps 经理,对 DevOps 有一个精彩的定义:donovanbrown.com/post/what-is-devops。
使用流程、人员和产品持续地向我们的最终用户提供价值——这是 DevOps 哲学的最佳描述。我们需要开发和交付以客户为导向的软件。一旦公司的所有领域都明白关键点是最终用户,作为软件架构师的你,任务就是展示将促进交付过程的技术。
值得注意的是,本书的所有内容都与这一方法相关联。这从来不是知道一堆工具和技术的问题。作为一名软件架构师,你必须理解,这始终是一种将更快地满足最终用户需求的方式,与他们的实际需求相联系。因此,你需要学习 DevOps 原则,这些原则将在本章中讨论。
理解 DevOps 原则
将 DevOps 视为一个哲学,值得提到的是,有一些原则可以确保你的团队能够良好地工作。这些原则是持续集成、持续交付和持续反馈。
更多信息,请访问:azure.microsoft.com/en-us/overview/what-is-devops/。
在许多书籍和技术文章中,DevOps 概念用无穷大的符号表示。这个符号代表在软件开发生命周期中必须有一个持续的方法。在这个过程中,你需要规划、构建、持续集成、部署、运营、获取反馈,然后从头开始。这个过程必须是协作的,因为每个人的焦点都是相同的——向最终用户提供价值。与这些原则一起,作为软件架构师,你需要决定最适合这种方法的最佳软件开发流程。我们在第一章中讨论了这些流程,《理解软件架构的重要性》。
定义持续集成
当你开始构建企业级解决方案时,协作是更快完成任务和满足用户需求的关键。正如我们在第十四章中讨论的,《C# 8 最佳实践》中的版本控制系统对于这个过程至关重要,但工具本身并不能完成这项工作,尤其是如果工具配置不当的话。
作为一名软件架构师,持续集成(CI)将帮助你有一个具体的软件开发协作方法。当你实施它时,一旦开发者提交代码,主代码就会自动构建和测试。
当你应用它时,好处是可以激励开发者尽可能快地合并他们的更改,以最小化合并冲突。此外,他们可以共享单元测试,这将提高软件的质量。
在 Azure DevOps 中设置 CI 非常简单。在构建管道中,你可以通过编辑配置来找到它的选项,如下面的截图所示:
值得注意的是,如果你有一个包含单元测试和功能测试的解决方案集,一旦你提交代码,它就会自动编译和测试。这将使你的主分支在每次团队提交时都保持稳定和安全。
CI 的关键点是能够更快地识别问题。当你允许代码被他人测试和分析时,你将有机会这样做。DevOps 方法唯一能帮助的是确保这一切尽可能快地发生。
理解 Azure DevOps 中的持续交付和多阶段环境
一旦您的应用程序的每个提交都构建完成,并且这段代码经过单元和功能测试,您可能还希望进行持续部署。这样做不仅仅是配置工具的问题。作为一名软件架构师,您需要确保团队和流程已经准备好进入这一步骤。
与持续交付(CD)相关的方法需要保证每次新部署时生产环境的安全。为此,需要采用多阶段管道。以下截图显示了为此目的而采用的一些常见阶段的示例:
使用 Azure DevOps 的发布阶段
如您所见,这些阶段是通过 Azure DevOps 发布管道配置的。每个阶段都有自己的目的,这将最终提高交付产品的质量。让我们看看这些阶段:
-
开发/测试:这个阶段由开发人员和测试人员用来构建新的功能。这个环境肯定会是最容易暴露出 bug 和不完整功能的。
-
质量保证:这个环境向与开发和测试无关的团队区域提供新功能的简要版本。项目经理、市场营销、供应商和其他人可以使用它作为研究、验证甚至预生产的区域。此外,开发和质量团队可以保证新版本正确部署,考虑到功能和基础设施。
-
生产:这是客户运行其解决方案的阶段。根据 CD,良好生产环境的目标是尽可能快地更新。频率将根据团队规模而变化,但有一些方法,这个过程每天发生不止一次。
部署您的应用程序采用三个阶段将对解决方案的质量产生影响。此外,它将使团队能够有一个更安全的部署过程,风险更少,产品稳定性更好。这种做法一开始可能看起来有点昂贵,但如果没有它,不良部署的结果通常会比这种投资更昂贵。
除了所有的安全性之外,您还必须考虑多阶段场景。您可以设置管道,只有通过定义的授权才能从一个阶段过渡到另一个阶段:
如前一个截图所示,设置预部署条件相当简单,您可以在下面的截图看到,有多个选项可以自定义授权方法。这为您提供了细化 CD 方法的可能性,以满足您正在处理的项目需求。
以下截图显示了 Azure DevOps 为预部署批准提供的选项。你可以定义可以批准阶段的人员并为他们设置策略,即在进行过程之前重新验证审批者身份。作为软件架构师,你需要确定适合你使用这种方法创建的项目配置:
值得注意的是,尽管这种方法远优于单阶段部署,但 DevOps 管道会指导你,作为软件架构师,进入另一个监控阶段。持续反馈将是一个不可思议的工具,我们将在下一节中讨论这种方法。
定义持续反馈和相关 DevOps 工具
一旦你在上一节描述的部署场景中运行完美的解决方案,反馈对于你的团队理解发布的结果以及版本如何为顾客工作至关重要。为了获取这些反馈,一些工具可以帮助开发者和顾客,将这些人聚集在一起以加速反馈过程。让我们来看看这些工具。
使用 Application Insights 监控你的软件
Application Insights 确实是软件架构师进行持续反馈所需的工具。一旦你将应用程序连接到它,你就开始接收对软件每个请求的反馈。这使得你不仅可以监控请求,还可以监控数据库性能、应用程序可能遭受的错误以及处理时间最长的调用。
显然,你将会有与将此工具集成到你的环境相关的成本,但该工具提供的功能绝对物有所值。此外,你需要了解,由于所有存储数据到 Application Insights 的请求都在一个单独的线程中运行,因此性能成本非常小。以下截图显示了你在环境中创建工具的简易性:
例如,假设你需要分析你应用程序中耗时较长的请求。将 Application Insights 附接到你的 Web 应用程序的过程相当简单,考虑到它可以在你设置 Web 应用程序时立即完成。如果你不确定 Application Insights 是否是你的 Web 应用程序,你可以使用 Azure 门户来查找。导航到 App Services 并查看 Application Insights 设置,如下面的截图所示:
界面将为你提供创建或附加已创建的监控服务到你的 Web 应用程序的机会。值得注意的是,你可以将多个 Web 应用程序连接到同一个 Application Insights 组件。以下截图显示了如何将 Web 应用程序添加到已创建的 Application Insights 资源中:
一旦为您的 Web 应用程序配置了 Application Insights,您将在 App Services 中找到以下屏幕:
一旦连接到您的解决方案,数据收集将持续进行,您将在组件提供的仪表板中看到结果。您可以在配置 Application Insights 的同一位置找到此屏幕,在 Web 应用程序配置中,或者在 Azure 门户中,通过 Application Insights 资源进行导航:
此仪表板为您提供了关于失败请求、服务器响应时间和服务器请求的概览。您还可以开启可用性检查,这将从 Azure 数据中心中的任何一个向您选择的 URL 发送请求。
但 Application Insights 的美丽之处在于它如何深入分析您的系统。例如,在下面的截图中,它正在向您反馈网站上完成的请求数量。您可以通过对处理时间较长或被调用频率较高的请求进行排名来分析它:
考虑到这个视图可以以不同的方式过滤,并且您在 Web 应用程序中接收到的信息是事件发生后立即的,这确实是一个定义持续反馈的工具。这是您可以使用 DevOps 原则实现客户确切需求的最佳方式之一。
Application Insights 是一个技术工具,它完全符合您作为软件架构师在真实分析模型中监控现代应用程序的需求。它是一种基于您正在开发的系统用户行为的持续反馈方法。
使用测试和反馈工具来启用反馈
在持续反馈的过程中,另一个非常有用的工具是微软设计的测试和反馈工具,旨在帮助产品所有者和质量保证用户在分析新功能的过程中。
使用 Azure DevOps,您可以通过在每个工作项内部选择一个选项来向您的团队请求反馈,正如您在以下截图中可以看到的:
一旦收到反馈请求,您可以使用测试和反馈工具来分析和给出正确的反馈给团队。您可以将此工具连接到您的 Azure DevOps 项目,在分析反馈请求时获得更多功能。以下截图显示了如何为测试和反馈工具设置 Azure DevOps 项目 URL。您可以从marketplace.visualstudio.com/items?itemName=ms.vss-exploratorytesting-web下载此工具。
工具相当简单。您可以截图、记录一个过程,甚至做笔记。以下图片显示了您如何在截图内轻松地写消息:
好处在于你可以在会话时间线上记录所有这些分析。正如您可以在下一张截图中所见,您可以在同一个会话中获得更多反馈,这对分析过程是有益的:
一旦您完成了分析并且连接到 Azure DevOps,您将能够报告 bug、创建任务,甚至开始一个新的测试用例:
创建的 bug 的结果可以在 Azure DevOps 的工作项板上进行检查。值得一提的是,您不需要 Azure DevOps 的开发者许可证就可以访问这个环境区域。这使得您,作为一个软件架构师,可以将这个基本且非常有用的工具推广到您正在构建的解决方案的许多关键用户。
以下截图显示了您将工具连接到您的 Azure DevOps 项目后创建的 bug:
拥有这样一个工具以获得项目的好反馈无疑是重要的。但作为一个软件架构师,您可能需要找到最佳解决方案来加速这一过程。书中探讨的工具是这样做的好方法。您可能每次在开发过程中需要实施更多步骤时都会考虑这种方法。
WWTravelClub 项目方法
在本章中,WWTravelClub 项目的截图展示了实现良好的 DevOps 周期所需的步骤。WWTravelClub 团队决定使用 Azure DevOps,因为他们明白这个工具对于整个周期的最佳 DevOps 体验是必不可少的。
需求是用用户故事编写的,这些可以在 Azure DevOps 的工作项部分找到。代码放在 Azure DevOps 项目的仓库中。这两个概念在第三章,使用 Azure DevOps 记录需求中进行了解释。
用于完成任务的管理生命周期是 Scrum,在第一章第一章,理解软件架构的重要性中介绍。这种方法将实现分为 Sprint,这迫使每个周期结束时交付价值。使用我们在本章中学到的持续集成设施,每次团队将开发成果提交到仓库的 master 分支时,代码都会被编译。
一旦代码编译并测试,部署的第一阶段就完成了。第一阶段通常被称为开发/测试,因为您为内部测试启用了它。Application Insights 和测试与反馈都可以用来获取新版本的第一反馈。
如果新版本的测试和反馈通过,那么就是时候进入第二阶段——质量保证。Application Insights 和测试与反馈可以再次使用,但现在是在一个更稳定的环境中。
循环以在生产阶段部署授权结束。这当然是一个艰难的决定,但 DevOps 表明您必须持续这样做,以便从客户那里获得更好的反馈。Application Insights 仍然是一个非常有用的工具,因为您能够监控新版本在生产中的演变,甚至将其与过去版本进行比较。
这里描述的 WWTravelClub 项目方法可以用于许多其他现代应用程序开发生命周期。作为软件架构师,您负责使这一切发生。工具已经准备就绪,这取决于您来确保一切顺利进行!
摘要
在本章中,我们学习了 DevOps 不仅是一系列技术和工具的组合,用于持续交付软件,而且是一种哲学,旨在使您正在开发的项目最终用户能够持续获得价值。
考虑到这种方法,我们看到了持续集成、持续交付和持续反馈对于 DevOps 目的的重要性。我们还看到了 Azure、Azure DevOps 和 Microsoft 工具如何帮助您实现目标。
本章通过使用 WWTravelClub 作为示例,向您介绍了这种方法,在 Azure DevOps 内部实现 CI/CD,并使用 Application Insights 和测试与反馈工具进行技术和功能反馈。在现实生活中,这些工具将使您能够更快地了解您正在开发的系统的当前行为,因为您将对其有持续的反馈。
在下一章中,我们将详细了解持续集成。
问题
-
什么是 DevOps?
-
什么是持续集成?
-
什么是持续交付?
-
什么是持续反馈?
-
构建和发布管道之间的区别是什么?
-
在 DevOps 方法中,Application Insights 的主要目的是什么?
-
测试与反馈工具如何帮助 DevOps 的过程?
进一步阅读
这些是一些您可以找到本章涵盖主题更多信息的网站:
-
www.packtpub.com/networking-and-servers/devops-fundamentals-video -
docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview
第十九章:在 DevOps 中应用 CI 场景的挑战
持续集成(CI)(CI) 是 DevOps 的一步。在前一章中,我们讨论了持续集成(CI)的基础以及 DevOps 如何依赖于它。其实现也在第十八章,理解 DevOps 原则中介绍,但与其他实践章节不同,本章的目的是讨论如何在真实场景中启用持续集成(CI),考虑到你作为软件架构师需要应对的挑战。
本章涵盖的主题如下:
-
理解持续集成(CI)
-
理解使用持续集成(CI)时的风险和挑战
-
理解本章的 WWTravelClub 项目方法
正如前一章所述,在解释本章内容时,将展示 WWTravelClub 的示例,因为所有用于说明持续集成(CI)的屏幕截图都来自它。此外,我们将在本章末尾提供一个结论,以便你能够轻松理解持续集成(CI)的原则。
到本章结束时,你将能够决定是否在你的项目环境中使用持续集成(CI)。此外,你将能够定义成功使用此方法所需的工具。
技术要求
本章需要 Visual Studio 2019 Community Edition 或更高版本。你可能还需要一个 Azure DevOps 账户,如第三章使用 Azure DevOps 记录需求中所述。
理解持续集成(CI)
一旦你开始使用 Azure DevOps 等平台,启用持续集成(CI)在点击相应的选项时将变得非常容易,正如我们在第十八章,理解 DevOps 原则中看到的。因此,技术并不是实施此过程的阿基里斯之踵。
以下截图展示了使用 Azure DevOps 启用持续集成(CI)是多么容易。通过点击构建管道并编辑它,你将能够设置一个触发器,在几次点击后启用持续集成(CI):
事实上,持续集成(CI)将帮助你解决一些问题。例如,它将迫使你测试你的代码,因为你需要更快地提交更改,以便其他开发者可以使用你正在编写的代码。
另一方面,你仅仅通过点击前面的截图并不能启用持续集成(CI)。当然,一旦你完成提交并且代码已经完成,你将能够启动构建的可能性,但这远不能说明你在解决方案中已经有了持续集成(CI)。
作为软件架构师,你需要更加关注它的原因与对 DevOps 的真正理解有关。正如在第十八章理解 DevOps 原则中讨论的,向最终用户提供价值始终是决定和绘制开发生命周期的良好方式。因此,即使开启 CI 很容易,这个功能对最终用户的影响是什么?一旦你有了对这个问题的所有答案,并且你知道如何降低其实施的风险,那么你就可以说你已经实施了一个 CI 流程。
值得注意的是,CI 是一个可以使 DevOps 工作得更好、更快的原则,正如在第十八章理解 DevOps 原则中讨论的。然而,一旦你不确定你的流程是否足够成熟以实现代码的持续交付,DevOps 完全可以没有它。更重要的是,如果你在一个处理其复杂性还不够成熟的团队中开启 CI,你可能会对 DevOps 产生误解,因为你将在部署解决方案时开始承担一些风险。
这就是为什么我们专门用额外的一章来介绍 CI。你需要了解一旦开启 CI,作为软件架构师你将面临的风险和挑战。
使用 CI 时的风险和挑战理解
现在,你可能正在考虑风险和挑战作为避免使用 CI 的一种方式。但为什么我们要避免使用它,如果它可以帮助你更好地进行 DevOps 流程呢?这不是本章的目的。本节的想法是帮助你,作为软件架构师,通过良好的流程和技术来减轻风险,找到更好的方法来克服挑战。
本章将讨论的风险和挑战列表如下:
-
持续生产部署
-
生产中的不完整功能
-
测试中的不稳定解决方案
一旦你有了处理它们的技巧和流程,就没有不使用 CI 的理由。值得一提的是,DevOps 并不依赖于 CI。然而,它确实使 DevOps 工作更加顺畅。现在,让我们来看看它们。
禁用持续生产部署
持续生产部署是一个过程,在提交新代码片段和一些管道步骤之后,你将在生产环境中拥有这段代码。这并非不可能,但确实很难且成本高昂。此外,你需要一个真正成熟的团队。问题是,你将在互联网上找到的大多数演示和示例都会展示 CI 的快速部署路径。CI/CD 的演示看起来非常简单和容易操作!这种简单性可能会让你尽快开始实施。然而,如果你稍微思考一下,直接在生产环境中部署可能会很危险!在一个需要每天 24 小时、每周 7 天都可用解决方案中,这是不切实际的。因此,你需要担心这一点,并考虑不同的解决方案。
第一个方法是使用多阶段场景,如第十八章中所述的理解 DevOps 原则。多阶段场景可以为构建的部署生态系统带来更多安全性。此外,你将获得更多选项来避免错误的生产部署,例如预部署批准:
值得注意的是,你可以构建一个部署管道,其中所有代码和软件结构都将由这个工具更新。然而,如果你有超出这个场景的内容,比如数据库脚本和环境配置,错误的生产发布可能会对最终用户造成损害。此外,何时更新生产环境的决定需要计划,在许多场景中,所有平台用户都需要。在这些情况下,使用变更管理程序需要决定。
因此,将代码部署到生产环境所带来的挑战会让你考虑制定一个部署的时间表。无论是按月、按日,甚至是每次提交后,这都无关紧要。关键点在于你需要创建一个流程和管道,确保只有良好且经过批准的软件处于生产阶段。
不完整的特性
当你的团队中的开发者正在创建新功能或修复错误时,你可能会考虑生成一个分支,以避免使用为持续交付设计的分支。分支可以被视为代码仓库中的一项功能,它允许创建一个独立的开发线,因为它隔离了代码。正如你可以在下面的屏幕截图中所见,使用 Visual Studio 创建分支相当简单:
这似乎是一个好方法,但让我们假设开发者已经认为实现已经准备好部署,并且刚刚将代码合并到主分支。如果因为这个要求被遗漏,这个功能还没有准备好,会发生什么?如果错误导致了不正确的行为,结果可能是一个带有不完整功能或错误修复的发布。
避免在主分支中引入损坏的功能甚至错误的修复措施的好做法是使用拉取请求。拉取请求将让其他团队成员知道你开发的代码已准备好合并。以下截图显示了如何使用 Azure DevOps 创建一个针对你所做的更改的新拉取请求:
一旦创建了拉取请求并定义了审查者,每个审查者都将能够分析代码并决定这些代码是否足够健康,可以放入主分支。以下截图显示了如何通过使用比较工具来分析更改来检查它:
一旦所有审批都完成,你将能够安全地将代码合并到主分支,如下面的截图所示。要合并代码,你需要点击“完成合并”。如果 CI 触发器已启用,如本章前面所示,Azure DevOps 将启动构建管道:
没有办法争论,如果没有这样的流程,主分支将遭受许多糟糕的代码部署,这可能会与 CD 一起造成损害。值得一提的是,代码审查在 CI/CD 场景中是一种优秀的实践,并且被认为是在任何软件中创建良好质量的绝佳实践。
你需要关注这里的挑战是确保只有完整的功能会出现在最终用户面前。你可以使用功能标志原则来解决它,这是一种确保只有准备好的功能呈现给最终用户的技巧。再次强调,我们不是在谈论 CI 作为一个工具,而是在谈论一个需要在每次需要向生产交付代码时定义和使用的流程。
测试的不稳定解决方案
考虑到你已经缓解了本主题中提到的其他两个风险,你可能会发现,在 CI 之后编写出糟糕的代码相当困难。确实,考虑到你正在处理多阶段场景并在推送至第一阶段之前提交拉取请求,之前提出的担忧肯定会降低。
但是,有没有一种方法可以加速发布版本的评估,确保这个新版本已经准备好供利益相关者进行测试?是的,有!从技术上讲,你可以这样做的方式在第十五章的用例中描述,使用单元测试用例和 TDD 测试你的代码,以及第二十章的软件测试自动化中。
正如这两章所讨论的,考虑到实现自动化所需的努力,自动化软件的每一个部分都是不切实际的。此外,在用户界面或业务规则变化很大的场景中,自动化的维护可能更加昂贵。
为了举例说明,让我们看一下以下截图,它显示了当 WWTravelClub 项目启动时 Azure DevOps 创建的单元测试和功能测试:
在第九章“设计模式和.NET Core 实现”中介绍了某些架构模式,如 SOLID,以及一些质量保证方法,如同行评审,这些方法将比软件测试提供更好的结果。
然而,这些方法并没有使自动化实践失效。事实是,所有这些方法在获得稳定解决方案时都将是有用的,尤其是在你运行 CI 场景时。在这种情况下,你能做的最好的事情就是尽可能快地检测错误和不良行为。正如前面所展示的,单元测试和功能测试都将帮助你做到这一点。
单元测试在部署前发现业务逻辑错误时将非常有帮助,尤其是在构建管道期间。例如,在以下截图中,你将找到一个模拟的错误,由于单元测试未通过,该错误取消了构建:
获取这种错误的方法相当简单。你需要编写一些代码,这些代码不会根据单元测试所检查的内容做出响应。一旦你提交了它,考虑到你开启了持续部署的触发器,你的代码将在管道中构建。我们创建的 Azure DevOps 项目向导提供的最后一步是执行单元测试。因此,在代码构建之后,单元测试将会运行。如果代码不再匹配测试,你将得到错误。
同时,以下截图显示了在开发/测试阶段功能测试期间出现的错误。此时,开发/测试环境存在一个被功能测试快速检测到的错误:
但这并不是在 CI/CD 过程中应用功能测试的唯一好处,一旦你用这种方法保护了其他部署阶段。例如,让我们看一下以下来自 Azure DevOps 发布管道界面的截图。如果你查看 Release-9,你会意识到自从这个错误在开发/测试环境发布后发生,多阶段环境将保护部署的其他阶段:
持续集成(CI)过程成功的关键是将它视为加速软件交付的有用工具,并不要忘记团队始终需要向最终用户提供价值。采用这种方法,前面介绍的技术将提供实现团队目标结果的惊人方式。
理解 WWTravelClub 项目的方法
在本章中,WWTravelClub 项目的截图展示了在启用持续集成(CI)的同时获得更安全方法的过程。即使将 WWTravelClub 视为一个假设场景,在构建它时也考虑了一些担忧:
-
持续集成(CI)已启用,但多阶段场景也已启用。
-
即使是多阶段场景,拉取请求也是一种保证在第一阶段只展示高质量代码的方式。
-
要做好拉取请求的工作,需要进行同行评审。
-
例如,同行评审检查在创建新功能时是否存在功能标志。
-
同行评审检查在创建新功能期间开发的单元测试和功能测试。
之前提到的步骤不仅适用于 WWTravelClub。作为软件架构师,您需要定义保证安全持续集成(CI)场景的方法。您可以将此作为起点。
摘要
本章介绍了在软件开发生命周期中何时启用持续集成(CI)的重要性,并考虑了您一旦决定在解决方案中实施它,作为软件架构师将面临的潜在风险和挑战。
此外,本章还介绍了一些可以使此过程更容易的解决方案和概念,例如多阶段环境、拉取请求评审、功能标志、同行评审和自动化测试。理解这些技术和流程将使您能够引导项目在 DevOps 场景中的持续集成(CI)方面表现出更安全的行为。
在下一章中,我们将看到软件测试自动化的工作原理。
问题
-
什么是持续集成(CI)?
-
您可以在没有持续集成(CI)的情况下进行 DevOps 吗?
-
在一个不成熟的团队中启用持续集成(CI)有哪些风险?
-
多阶段环境如何帮助持续集成(CI)?
-
自动化测试如何帮助持续集成(CI)?
-
拉取请求如何帮助持续集成(CI)?
-
拉取请求是否仅与持续集成(CI)一起工作?
进一步阅读
这些是一些网站,您可以在这些网站上找到关于本章涵盖主题的更多信息:
第二十章:软件测试自动化
在前面的章节中,我们讨论了单元测试和集成测试在软件开发中的重要性,以及它们如何确保代码库的可靠性。我们还讨论了单元和集成测试是所有软件生产阶段的组成部分,并且每次修改代码库时都会运行。
还有其他重要的测试,称为功能/验收测试。它们只在每个冲刺结束时运行,以验证冲刺的输出实际上是否满足与利益相关者商定的规范。
本章专门介绍功能/验收测试以及定义和执行它们的技巧。更具体地说,本章涵盖了以下主题:
-
理解功能测试的目的
-
使用单元测试工具自动化 C#中的功能测试
-
用例 – 自动化功能测试
到本章结束时,你将能够设计手动和自动测试,以验证冲刺产生的代码是否符合其规范。
技术要求
鼓励读者在继续本章之前阅读第十五章,使用单元测试用例和 TDD 测试您的代码。
本章需要 Visual Studio 2017 或 2019 免费社区版或更高版本,并安装所有数据库工具。在这里,我们将修改第十五章的代码,使用单元测试用例和 TDD 测试您的代码,该代码可在github.com/PacktPublishing/Hands-On-Software-Architecture-with-CSharp-8/tree/master/ch20找到.
理解功能测试的目的
功能/验收测试使用与单元和集成测试类似的技术,但它们的不同之处在于它们只在每个冲刺结束时运行。它们的基本作用是验证当前版本的整个软件是否符合其规范。这种验证被转化为以下目的的正式流程:
-
功能测试代表了利益相关者和开发团队之间合同的最重要部分,另一部分是非功能规格的验证。这种合同的形式化方式取决于开发团队和利益相关者之间关系的本质。在供应商-客户关系的案例中,它们成为每个冲刺的供应商-客户商业合同的一部分,并由为客户工作的团队编写。如果测试失败,则冲刺将被拒绝,供应商必须运行一个补充冲刺来解决所有问题。如果没有正式的商业合同,测试的结果通常用于驱动下一个冲刺的规格。然而,在这种情况下,如果失败率很高,冲刺可能被拒绝并需要重新进行。
-
在每个冲刺结束时运行的正式功能测试可以避免之前冲刺中取得的结果被新代码破坏。
-
当使用敏捷开发方法时,维护一个最新的功能测试库是获取最终系统规格正式表示的最佳方式,因为在敏捷开发过程中,最终系统的规格并不是在开发开始之前就确定的,而是系统演化的结果。
由于早期阶段的第一轮冲刺的输出可能与最终系统有很大差异,因此不值得花费太多时间编写详细的手动测试和/或自动化测试。因此,你可以限制只添加一些示例到用户故事中,这些示例将同时作为软件开发输入和手动测试。
随着系统功能始终更加稳定,投入时间编写详细和正式的功能测试是值得的。对于每个功能规格,我们必须编写测试来验证它们在极端情况下的正确操作。例如,在一个支付用例中,我们必须编写测试来验证所有可能性:
-
资金不足
-
各种数字化错误
-
卡片过期
-
错误的凭证和重复的错误凭证
在手动测试的情况下,对于上述每个场景,我们必须提供每个操作中涉及的每个步骤的所有详细信息,以及每个步骤的预期结果。
一个重要的决定是是否要自动化所有或部分验收/功能测试,因为编写模拟与系统用户界面交互的人类操作员的自动化测试非常昂贵。最终的决定取决于测试实现的成本除以预期使用的次数。
在 CI/CD 的情况下,相同的功能测试可以执行多次,但不幸的是,功能/验收测试严格依赖于用户界面的实现方式,而在现代系统中,用户界面经常发生变化。因此,在这种情况下,相同的测试最多只能与完全相同的用户界面执行几次。
为了克服与用户界面相关的所有问题,功能测试可以实施为皮下测试,即作为绕过用户界面的测试。然而,皮下测试由于其本质是不完整的,因为它们无法检测用户界面本身的错误。此外,在 Web 应用的情况下,皮下测试通常还受到其他限制,因为它们绕过了整个 HTTP 协议。在 ASP.NET Core 应用的情况下,这意味着必须绕过整个 ASP.NET Core 管道,并将请求直接传递到 ASP.NET 控制器。因此,身份验证、授权、CORS 以及 ASP.NET Core 管道中其他模块的行为将不会被测试分析。
一个完整的 Web 应用功能自动测试应该执行以下操作:
-
在要测试的 URL 上启动一个实际的浏览器。
-
等待页面上的任何 JavaScript 完成其执行。
-
然后,向浏览器发送模拟人类操作员行为的命令。
-
最后,在每次与浏览器的交互之后,自动测试应该等待,以便任何由交互触发的 JavaScript 完成其执行。
虽然存在浏览器自动化工具,但如前所述,使用浏览器自动化实现的测试非常昂贵且难以实现。因此,ASP.NET Core MVC 建议的方法是使用.NET HTTP 客户端向实际的 Web 应用副本发送实际的 HTTP 请求,而不是使用浏览器。一旦 HTTP 客户端收到 HTTP 响应,它就会将其解析为 DOM 树,并验证它是否收到了正确的响应。
与浏览器自动化工具的唯一区别是 HTTP 客户端无法运行任何 JavaScript。然而,可以添加其他测试来测试 JavaScript 代码。这些测试基于特定的 JavaScript 测试工具,例如Jasmine和Karma。
下一个部分将解释如何使用.NET HTTP 客户端自动化 Web 应用的功能测试,而功能测试自动化的实际示例将在最后一部分展示。
使用单元测试工具来自动化 C#中的功能测试。
自动化的功能/验收测试使用与单元和集成测试相同的测试工具。也就是说,这些测试可以嵌入到我们在第十五章中描述的同一 xUnit、NUnit 或 MSTests 项目中,即使用单元测试用例和 TDD 测试您的代码。然而,在这种情况下,我们必须添加能够与用户界面交互和检查的进一步工具。
在本章剩余部分,我们将专注于网络应用程序,因为它们是本书的主要焦点。因此,如果我们正在测试网络 API,我们只需要HTTPClient实例,因为它们可以轻松地与 XML 和 JSON 格式的网络 API 端点进行交互。
在 ASP.NET Core MVC 应用程序返回 HTML 页面的情况下,交互更为复杂,因为我们还需要工具来解析和与 HTML 页面 DOM 树进行交互。AngleSharp NuGet 包是一个很好的解决方案,因为它支持最先进的 HTML 和最小化的 CSS,并为外部提供的 JavaScript 引擎(如 Node.js)提供了扩展点。然而,我们不建议你在测试中包含 JavaScript 和 CSS,因为它们严格绑定到目标浏览器,所以对于它们来说,最佳选项是使用可以在目标浏览器中直接运行的 JavaScript 特定测试工具。
使用HTTPClient类测试网络应用程序有两个基本选项:
-
一个
HTTPClient实例通过互联网/内网与实际的预发布网络应用程序以及所有正在测试软件的 beta 测试人员连接。这种方法的优势在于你正在测试真实的东西,但测试的构思更困难,因为你无法控制每个测试之前应用程序的初始状态。 -
一个
HTTPClient实例连接到一个在每次测试之前都配置、初始化和启动的本地应用程序。这种情况与单元测试场景完全类似。测试结果可以在每个测试的初始状态固定后重现,测试设计更容易,并且实际数据库可以被一个更快、更容易初始化的内存数据库所替代。然而,在这种情况下,你离实际系统的操作还相当远。
一个好的策略是使用第二种方法,即完全控制初始状态,用于测试所有极端情况,然后使用第一种方法测试真实环境中的随机平均情况。
下面的两个部分描述了两种方法。这两种方法的不同之处仅在于你定义测试固定值的方式。
测试预发布应用程序
在这种情况下,你的测试只需要一个HTTPClient实例,因此你必须定义一个高效的固定值,它提供HTTPClient实例,避免出现窗口连接耗尽的风险。我们在第十二章的“.NET Core HTTP 客户端”部分遇到了这个问题,使用.NET Core 应用服务架构。可以通过使用IHTTPClientFactory管理HTTPClient实例并将它们通过依赖注入注入来解决。
一旦我们有一个依赖注入容器,我们可以通过以下代码片段来丰富其处理HTTPClient实例的能力:
services.AddHTTPClient();
在这里,AddHTTPClient 扩展属于 Microsoft.Extensions.DependencyInjection 命名空间,并在 Microsoft.Extensions.HTTP NuGet 包中定义。因此,我们的测试固定项必须创建一个依赖注入容器,必须调用 AddHTTPClient,最后必须构建容器。以下固定类执行此操作(如果您不记得固定类,请参阅 第十五章 的 高级测试准备/测试清理场景 部分,使用单元测试用例和 TDD 测试您的代码,如果您不记得固定类):
public class HTTPClientFixture
{
public HTTPClientFixture()
{
var serviceCollection = new ServiceCollection();
serviceCollection
.AddHTTPClient();
ServiceProvider = serviceCollection.BuildServiceProvider();
}
public ServiceProvider ServiceProvider { get; private set; }
}
在前面的定义之后,您的测试应该如下所示:
public class UnitTest1:IClassFixture<HTTPClientFixture>
{
private ServiceProvider _serviceProvider;
public UnitTest1(HTTPClientFixture fixture)
{
_serviceProvider = fixture.ServiceProvider;
}
[Fact]
public void Test1()
{
using (var factory =
_serviceProvider.GetService<IHTTPClientFactory>())
{
HTTPClient client = factory.CreateClient();
//use client to interact with application here
}
}
}
在 Test1 中,一旦您获取了 HTTP 客户端,您可以通过发出 HTTP 请求并分析应用程序返回的响应来测试应用程序。有关如何处理服务器返回的响应的更多详细信息将在 用例 部分提供。
下一节解释如何测试在受控环境中运行的应用程序。
测试受控应用程序
在这种情况下,我们在测试应用程序中创建一个 ASP.NET Core 服务器,并使用 HTTPClient 实例对其进行测试。Microsoft.AspNetCore.Mvc.Testing NuGet 包包含我们创建 HTTP 客户端和运行应用程序的服务器所需的所有内容。我们还需要通过引用 Microsoft.AspNetCore.App NuGet 包来引用整个 Web 框架。
最后,我们必须通过以下步骤将测试项目转换为 Web 项目:
-
在 Visual Studio 解决方案资源管理器中单击测试项目图标,然后从上下文菜单中选择编辑项目项。
-
将根 XML 节点替换为
<Project Sdk="Microsoft.NET.Sdk">,改为<Project Sdk="Microsoft.NET.Sdk.web">。
Microsoft.AspNetCore.Mvc.Testing 包含一个固定类,该类负责启动本地 Web 服务器并为与之交互提供客户端。预定义的固定类是 WebApplicationFactory<T>。泛型 T 参数必须使用您的 Web 项目的 Startup 类进行实例化。
测试看起来像以下类:
public class UnitTest1
: IClassFixture<WebApplicationFactory<MyProject.Startup>>
{
private readonly
WebApplicationFactory<RazorPagesProject.Startup> _factory;
public UnitTest1 (WebApplicationFactory<MyProject.Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
....
public async Task MustReturnOK(string url)
{
var client = _factory.CreateClient();
// here both client and server are ready
var response = await client.GetAsync(url);
//get the response
response.EnsureSuccessStatusCode();
// verify we got a success return code.
}
...
---
}
如果您想分析返回页面的 HTML,您还必须引用 AngleSharp NuGet 包。我们将在下一节的示例中看到如何使用它。在这种类型的测试中处理数据库的最简单方法是使用内存数据库,这些数据库更快,并且每当本地服务器关闭和重新启动时都会自动清除。
这可以通过创建一个新的部署环境,例如AutomaticStaging,以及一个针对测试的特定配置文件来完成。在创建了这个新的部署环境之后,前往你的应用程序的Startup类中的ConfigureServices方法,找到添加你的DBContext配置的地方。一旦找到这个地方,就在那里添加一个if语句,如果应用程序正在AutomaticStaging环境中运行,就用类似以下的内容替换你的DBContext配置:
services.AddDbContext<MyDBContext>(options => options.UseInMemoryDatabase(databaseName: "MyDatabase"));
作为一种替代方案,你还可以在继承自WebApplicationFactory<T>的自定义固定工具的构造函数中添加所有必要的指令来清除标准数据库。
用例 - 自动化功能测试
在本节中,我们将向第十五章的 ASP.NET Core 测试项目添加一个简单的验收测试,该章节是使用单元测试用例和 TDD 测试你的代码。我们的测试方法基于Microsoft.AspNetCore.Mvc.Testing和AngleSharp NuGet 包。请创建整个解决方案的新副本。
作为第一步,我们必须将测试项目转换成一个 Web 项目,通过替换其项目文件根节点的sdk属性来实现,即Sdk="Microsoft.NET.Sdk.web"。
测试项目已经引用了位于test目录下的 ASP.NET Core 项目以及所有必需的 xUnit NuGet 包,因此我们只需要添加Microsoft.AspNetCore.Mvc.Testing和AngleSharp NuGet 包。
现在,让我们添加一个名为UIExampleTestcs.cs的新类文件。我们需要using语句来引用所有必要的命名空间。更具体地说,我们需要以下内容:
-
using PackagesManagement;: 这是为了引用你的应用程序类所必需的。 -
using Microsoft.AspNetCore.Mvc.Testing;: 这是为了引用客户端和服务器类所必需的。 -
using AngleSharp;和using AngleSharp.Html.Parser;: 这些是为了引用AngleSharp类所必需的。 -
System.IO: 这是为了从 HTTP 响应中提取 HTML 所必需的。 -
using Xunit: 这是为了引用所有xUnit类所必需的。
总结起来,整个using块如下:
using PackagesManagement;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Microsoft.AspNetCore.Mvc.Testing;
using AngleSharp;
using AngleSharp.Html.Parser;
using System.IO;
我们将使用之前在测试受控应用程序部分中引入的标准固定工具类,即以下内容:
public class UIExampleTestcs:
IClassFixture<WebApplicationFactory<Startup>>
{
private readonly
WebApplicationFactory<Startup> _factory;
public UIExampleTestcs(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
}
现在,我们已经准备好编写主页的测试了!这个测试验证了主页 URL 返回成功的 HTTP 结果,并且主页包含指向包管理页面的链接,这是一个/ManagePackages的相对链接。
理解这一点是基本的,即自动测试不应依赖于 HTML 的细节,而应仅验证逻辑事实,以避免在每次对应用程序 HTML 进行微小修改后频繁更改。这就是为什么我们只验证所需的链接存在,而不对其位置施加约束。
让我们将TestMenu称为主页测试:
[Fact]
public async Task TestMenu()
{
var client = _factory.CreateClient();
...
...
}
每个测试的第一步是创建一个客户端。然后,如果测试需要分析一些 HTML,我们必须准备所谓的AngleSharp浏览上下文:
//Create an angleSharp default configuration
var config = Configuration.Default;
//Create a new context for evaluating webpages
//with the given config
var context = BrowsingContext.New(config);
配置对象指定了诸如 cookie 处理和其他浏览器相关属性等选项。到目前为止,我们已经准备好要求主页:
var response = await client.GetAsync("/");
作为第一步,我们验证收到的响应包含成功状态码,如下所示:
response.EnsureSuccessStatusCode();
前面的方法调用在状态码不成功时抛出异常,从而导致测试失败。HTML 分析需要从响应中提取。以下代码展示了如何简单地进行操作:
var stream = await response.Content.ReadAsStreamAsync();
string source;
using (StreamReader responseReader = new StreamReader(stream))
{
source = await responseReader.ReadToEndAsync();
}
ReadAsStreamAsync返回Stream,我们可以使用它来构建StreamReader(一个专门用于读取文本的流),它可以读取整个响应体。
现在,我们必须将提取的 HTML 传递给之前的AngleSharp浏览上下文对象,以便它可以构建 DOM 树。以下代码展示了如何操作:
var document = await context.OpenAsync(req => req.Content(source));
OpenAsync方法使用context中包含的设置执行 DOM 构建活动。构建 DOM 文档的输入由传递给OpenAsync方法的 lambda 函数指定。在我们的情况下,req.Content(...)从传递给Content方法的 HTML 字符串构建 DOM 树,这是客户端收到的响应中包含的 HTML。
一旦获得document对象,我们就可以像在 JavaScript 中使用它一样使用它。特别是,我们可以使用QuerySelector来查找具有所需链接的锚点:
var node = document.QuerySelector("a[href=\"/ManagePackages\"]");
剩下的就是验证node不是 null:
Assert.NotNull(node);
我们做到了!如果你想要分析需要用户登录或其他更复杂场景的页面,你需要启用 HTTP 客户端中的 cookie 和自动 URL 重定向。这样,客户端将表现得像通常的浏览器,它会存储和发送 cookie,并在接收到Redirect HTTP 响应时移动到另一个 URL。这可以通过将选项对象传递给CreateClient方法来实现,如下所示:
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect=true,
HandleCookies=true
});
使用前面的设置,你的测试可以做到一个普通浏览器能做的所有事情。例如,你可以设计测试,其中 HTTP 客户端登录并访问需要身份验证的页面,因为HandleCookies=true允许客户端存储身份验证 cookie 并在所有后续请求中发送它。
摘要
本章解释了验收/功能测试的重要性,以及如何定义详细的手动测试,以便在每个冲刺的输出上运行。到目前为止,你应该能够定义自动和/或手动测试,以验证在每个冲刺结束时,你的应用程序符合其规范。
然后,本章分析了何时值得自动化某些或所有验收/功能测试,并描述了如何在 ASP.NET Core 应用程序中自动化它们。
一个最终的例子展示了如何使用AngleSharp编写 ASP.NET Core 验收/功能测试,以检查应用程序返回的响应。
问题
-
在快速 CI/CD 循环的情况下,自动化用户界面验收测试是否总是值得?
-
对 ASP.NET Core 应用程序的皮下测试有什么缺点?
-
建议用于编写 ASP.NET Core 验收测试的技术是什么?
-
建议如何检查服务器返回的 HTML?
进一步阅读
关于 Microsoft.AspNetCore.Mvc.Testing NuGet 包和 AngleSharp 的更多详细信息可以在它们各自的官方文档中找到:docs.microsoft.com/en-US/aspnet/core/test/integration-tests?view=aspnetcore-3.0 和 anglesharp.github.io/.
对 JavaScript 测试感兴趣的读者可以参考 Jasmine 文档:jasmine.github.io/.
第二十一章:评估
第一章
-
软件架构师需要了解任何可以帮助他们更快解决问题并确保他们有更好质量的技术的技术。
-
Azure 提供了,并且不断演进,许多软件架构师可以在解决方案中实现的功能组件。
-
流程模型可以帮助你了解你的团队,你将提供的解决方案类型,以及可用的预算。
-
软件架构师会关注任何可能影响性能、安全性、可用性等方面的用户或系统需求。
-
所有这些,但非功能性需求需要更多的关注。
-
设计思维是一种帮助软件架构师精确定义用户需求的工具。
-
当我们想要定义功能性需求时,用户故事是好的。
-
缓存、异步编程和正确的对象分配。
-
为了检查实现是否正确,软件架构师将其与已经设计和验证过的模型和原型进行比较。
第二章
-
垂直和水平。
-
是的,你可以自动部署到已定义的 Web 应用程序,或者直接使用 Visual Studio 创建一个新的。
-
通过最小化它们闲置的时间来利用可用的硬件资源。
-
代码行为是确定的,因此易于调试。不可能出现死锁,执行流程模仿顺序代码的流程,这意味着更容易设计和理解。
-
因为正确的顺序可以最小化填写表格所需的动作数量。
-
因为它允许以独立于操作系统的方式操作路径文件。
-
它可以与多个 .NET Core 版本以及多个经典 .NET 框架版本一起使用。
-
控制台、.NET Core 和 .NET 标准类库;ASP.NET Core、测试和微服务。
第三章
-
不,它适用于多个平台。
-
是的,开发/部署过程中的每一步都可以自动化,包括部署到生产环境。
-
自动、手动和负载测试计划。
-
是的,他们可以通过 Azure DevOps 源来做到这一点。
-
为了管理需求和组织整个开发过程。
-
史诗级工作项代表由多个功能组成的高级系统子部分。
-
一种父子关系。
第四章
-
当你从本地解决方案迁移或拥有基础设施团队时,IaaS 是一个好的选择。
-
PaaS 是在团队专注于软件开发时,快速且安全交付软件的最佳选择。
-
如果你打算交付的解决方案由知名玩家提供,例如 SaaS,你应该考虑使用它。
-
当你构建一个新系统,没有专门从事基础设施的人员,并且不想担心可伸缩性时,无服务器绝对是一个选项。
-
Azure SQL Server 数据库可以在几分钟内启动,之后你将拥有 Microsoft SQL Server 的全部功能。
-
Azure 提供了一组称为 Azure 认知服务的服务。这些服务提供视觉、语音、语言、搜索和知识方面的解决方案。
-
在混合场景中,你有灵活性来决定系统每个部分的最佳解决方案,同时尊重解决方案的路径并将其推向未来。
-
为了允许更新/写入并行性。
-
传递给
Create方法的第三个参数,它创建代理实例,允许我们指定通信的允许目标。通常,ServiceReplicaListener构造函数的第三个参数指定是否在辅助副本上创建监听器。
第五章
-
代码模块化和部署模块化。
-
不,其他重要优势包括很好地处理开发团队和整个 CI/CD 循环,以及轻松有效地混合异构技术的可能性。
-
一个帮助我们实现弹性通信的库。
-
在
HostBuilder方法中可以声明依赖注入和托管服务。 -
一旦你在开发机器上安装了 Docker,你就可以开发、调试和部署 Docker 化的 .NET Core 应用程序。你还可以将 Docker 镜像添加到由 Visual Studio 处理的服务 Fabric 应用程序中。
-
基于 Kubernetes
.yaml文件的那个。 -
面向集群外部的流量,并通过集群的 URI 访问的那个。
第六章
-
在数据库依赖提供者的帮助下。
-
要么通过调用它们
Id,要么通过使用Key属性装饰它们。 -
使用
MaxLength和MinLength属性。 -
类似于:
builder.Entity<Package>().HasIndex(m => m.Name);. -
类似于
builder.Entity<Destination>():.HasMany(m => m.Packages).WithOne(m => m.MyDestination).HasForeignKey(m => m.DestinationId).OnDelete(DeleteBehavior.Cascade); -
Add-Migration 和 Update-Database.
-
不,但你可以使用
IncludeLINQ 子句强制包含它们。 -
是的,它是,多亏了
SelectLINQ 子句。 -
通过调用
context.Database.Migrate()。
第七章
-
不,它是一个内存中的字典,可以用作缓存或满足其他内存存储需求。
-
是的,它们是。本章的大部分内容都是致力于解释为什么。
-
写操作。
-
NoSQL 数据库的主要弱点是其一致性和事务,而其主要优势是性能,尤其是在处理分布式写入时。
-
最终一致性、一致性前缀、会话、有界不一致性、强一致性。
-
不,它们在分布式环境中效率不高。基于 GUID 的字符串表现更好,因为它们的唯一性是自动的,不需要同步操作。
-
OwnsMany和OwnsOne. -
是的,它们可以。一旦你使用了
SelectMany,索引就可以用来搜索嵌套对象。
第八章
-
Azure Functions 是一个 Azure PaaS 组件,允许你实现 FaaS 解决方案。
-
你可以使用不同的语言编写 Azure Functions,例如 C#、F#和 Node。你还可以使用 Azure Portal 和 Visual Studio VS Code 创建函数。
-
Azure Functions 有两种计划选项。第一种计划是消费计划,你将根据使用的数量付费。第二种计划是 App Service 计划,你将与函数的需求共享 App Service 资源。
-
在 Visual Studio 中部署函数的过程与 Web 应用部署相同。
-
我们可以触发 Azure Functions 的方式有很多,例如使用 Blob 存储、Cosmos DB、Event Grid、Event Hubs、HTTP、Microsoft Graph 事件、队列存储、服务总线、定时器和 Webhooks。
-
Azure Functions v1 需要.NET Framework 引擎,而 v2 需要.NET Core。
-
每个 Azure Function 的执行都可以通过 Application Insights 进行监控。在这里,你可以检查处理所需的时间、资源使用情况、每个函数调用中发生的错误和异常。
第九章
-
设计模式是解决软件开发中常见问题的良好解决方案。
-
虽然设计模式为你提供了我们在开发中面临的典型问题的代码实现,但设计原则有助于你在实现软件架构时选择最佳选项。
-
建造者模式将帮助你生成复杂的对象,而无需在你要使用它们的类中定义它们。
-
工厂模式在有多种来自同一抽象的对象且不知道在开始编码时需要创建哪个对象的情况下非常有用。
-
当你需要一个在软件执行期间只有一个实例的类时,单例模式非常有用。
-
当你需要提供一个控制对另一个对象访问的对象时,使用代理模式。
-
当你需要执行将影响对象行为的命令时,使用命令模式。
-
当你需要向一组其他对象提供有关对象的信息时,发布/订阅模式非常有用。
-
如果你想实现依赖倒置原则,DI 模式非常有用。
第十章
-
专家使用的语言变化以及词语含义的变化。
-
领域映射。
-
不;整个通信都通过实体进行,即聚合根。
-
因为聚合体代表部分-部分层次结构。
-
只有一个,因为仓库是聚合中心。
-
应用层操作仓库接口。仓库实现注册在依赖注入引擎中。
-
为了在多个聚合体上协调单事务操作。
-
更新和查询的规范通常相当不同,尤其是在简单的 CRUD 系统中。其最强形式的原因主要是查询响应时间的优化。
-
依赖注入。
-
不;必须进行严重的影响分析,以便我们可以采用它。
第十一章
-
不,因为这种方法会有很多重复代码,这会在维护时造成困难。
-
代码重用的最佳方法是创建库。
-
是的。你可以在你之前创建的库中找到已经创建的组件,然后通过创建可以在未来重用的新组件来增加这些库。
-
.NET 标准是一个规范,它允许.NET 的不同框架之间实现兼容性,从.NET Framework 到 Unity。.NET Core 是.NET 的一种实现,并且是开源的。
-
通过创建.NET 标准库,你将能够在不同的.NET 实现中使用它,例如.NET Core、.NET Framework 和 Xamarin。
-
你可以使用面向对象的原则(继承、封装、抽象和多态)来启用代码重用。
-
泛型是一种复杂的实现,通过定义一个占位符来简化具有相同特性的对象在编译时被替换为特定类型的方式。
第十二章
-
不,因为这会违反服务对请求的反应必须依赖于请求本身,而不是之前与客户端交换的其他消息/请求的原则。
-
不,因为这会违反互操作性约束。
-
是的,它可以。POST 的主要操作必须是创建,但删除可以作为副作用执行。
-
三,即头和体的 Base64 编码加上签名。
-
从请求体中。
-
使用
ApiController属性。 -
ProducesResponseType属性。 -
使用
Route和Http<verb>属性。 -
类似于
services.AddHttpClient<MyProxy>()。
第十三章
-
开发者错误页面和开发者数据库错误页面、生产错误页面、主机、HTTPS 重定向、路由、身份验证和授权以及端点调用者。
-
不。
-
错误。同一个标签上可以调用多个标签助手。
-
ModelState.IsValid。 -
@RenderBody()。 -
我们可以使用
@RenderSection("Scripts", required: false)。 -
我们可以使用
return View("viewname", ViewModel)。 -
三。
-
不;还有
ViewState字典。
第十四章
-
可维护性为你提供了快速交付你设计的软件的机会。它还允许你轻松修复错误。
-
圈复杂度是一个检测方法节点数量的指标。数字越高,影响越差。
-
版本控制系统将保证源代码的完整性,并为你提供分析每个修改历史的机会。
-
Try-catch 是控制由你编写的代码引发的异常的一种方式。Try-finally 是一种保证即使在 try 块内部有异常,finally 块也会执行其过程的方式。当你想在同一块代码中解决这两种情况时,可以使用 try-catch-finally。
-
垃圾回收器是 .NET Core/.NET Framework 系统的一种,它监视你的应用程序并检测你不再使用的对象。它清理这些对象以释放内存。
-
IDisposable接口对于需要程序员进行清理的对象实例化的类来说很重要,因为垃圾回收器无法清理它们。 -
.NET Core 以一种可以保证更安全代码的方式封装了一些设计模式,例如依赖注入和 Builder。
第十五章
-
因为大多数测试在软件发生任何更改后都必须重新进行。
-
因为在单元测试及其相关应用程序代码中发生完全相同错误的概率非常低。
-
当测试方法定义了多个测试时使用
[Theory],而当测试方法只定义了一个测试时使用[Fact]。 -
Assert。 -
Setup、Returns和ReturnsAsync。 -
是的;使用
ReturnAsync。
第十六章
-
编写良好的代码是任何熟练掌握该编程语言的人都可以处理、修改和发展的代码。
-
Roslyn 是用于在 Visual Studio 内部进行代码分析的 .NET 编译器。
-
代码分析是一种在编译前检测不良实践的实践,它考虑了代码的编写方式。
-
代码分析可以发现即使在表面上看起来很好的软件中也会发生的问题,例如内存泄漏和不良编程实践。
-
Roslyn 可以用于代码分析。
-
Visual Studio 扩展是在 Visual Studio 内部运行的编程工具。这些工具可以在某些情况下帮助你,在这些情况下 Visual Studio IDE 没有为你提供适当的功能。
-
微软代码分析、SonarLint 和 Code Cracker。
第十七章
-
为了最大化软件为目标组织提供的价值。
-
不;它需要获取所有能够最大化软件增值所需的能力。
-
因为当新用户订阅时,其租户必须自动创建,并且因为新的软件更新必须分发到所有客户的基础设施中。
-
是的;Terraform 是一个例子。
-
Azure 管道。
-
你的业务依赖于 SaaS 供应商,因此其可靠性是基本的。
-
不;可扩展性同容错性和自动故障恢复一样重要。
第十八章
-
DevOps 是一种持续向最终用户提供价值的途径。为了成功做到这一点,必须进行持续集成、持续交付和持续反馈。
-
持续集成允许你在每次提交更改时检查你交付的软件的质量。你可以通过在 Azure DevOps 中启用此功能来完成此操作。
-
持续交付允许你在确信所有质量检查都通过了你设计的测试后部署解决方案。Azure DevOps 通过提供相关工具来帮助你完成此操作。
-
持续反馈是采用 DevOps 生命周期中的工具,以便在性能、可用性以及你正在开发的应用的其他方面快速获得反馈。
-
构建管道将允许你运行构建和测试应用程序的任务,而发布管道将为你提供定义应用程序在每个场景中如何部署的机会。
-
应用洞察(Application Insights)是监控你已部署的系统健康状况的有用工具,这使得它成为一款出色的持续反馈工具。
-
测试与反馈(Test & Feedback)是一个工具,允许利益相关者分析你正在开发的软件,并能够与 Azure DevOps 连接以打开任务甚至错误。
第十九章
-
这是一种确保每次提交到代码仓库的代码都经过构建和测试的方法。
-
是的,你可以先独立设置 DevOps,然后稍后启用持续交付。你的团队和流程需要准备好并关注这一变化的发生。
-
所有这些风险都可能对你的生产环境造成损害。例如,你可能有一个尚未准备好的功能却已经部署了,你可能在客户不利的时刻造成服务中断,或者你可能因为错误的修复而遭受不良的副作用。
-
多阶段环境可以保护生产环境免受不良发布的影响。
-
自动化测试可以预测预览场景中的错误和不良行为。
-
拉取请求允许在主分支提交之前进行代码审查。
-
不;拉取请求(pull requests)可以帮助你在任何有 Git 作为源控制的发展方法中。
第二十章
-
不;这取决于用户界面的复杂性和变化频率。
-
ASP.NET Core 管道不会执行,但输入会直接传递到控制器。
-
使用
Microsoft.AspNetCore.Mvc.TestingNuGet 包。 -
使用
AngleSharpNuGet 包。
89



被折叠的 条评论
为什么被折叠?



