原文:
annas-archive.org/md5/45c346cbf4b7bfce8850faea0398b5b4译者:飞龙
前言
随着最近的变化,尤其是 Microsoft Dynamics NAV 发展成 Dynamics 365 Business Central,你的开发实践需要变得更加规范。你的成功将比以往任何时候都更加依赖于你快速而一致地判断自己工作质量的能力。传统的手动测试方法已不再足够。因此,你需要学习自动化测试,以及如何高效地将其纳入日常工作中。在本书中,你将学习它如何从功能和技术上提升你的工作,并且希望你能够喜欢它。
本书的适用人群
本书面向开发人员、开发经理、测试人员、功能顾问以及使用 Microsoft Dynamics 365 Business Central 或 Dynamics NAV 的高级用户。
假设读者对 Dynamics 365 Business Central 作为一款应用程序及其如何扩展已有基本了解。尽管某些章节将专门讲解自动化测试的编写,但并不要求读者必须具备 Dynamics 365 Business Central 的编程能力。总体来说,能够从测试人员的角度进行思考无疑会给读者带来优势。
本书内容
第一章,自动化测试简介,将带你了解自动化测试:你为何需要使用它,它具体包含哪些内容,以及何时使用它。
第二章,可测试性框架,详细讲解了 Microsoft Dynamics 365 Business Central 如何支持自动化测试的执行,以及所谓的 可测试性框架 实际上是什么,通过描述其五大支柱进行阐述。
第三章,测试工具与标准测试,介绍了 Dynamics 365 Business Central 中的测试工具,帮助你运行测试。同时,我们将讨论 Microsoft 提供的标准测试和测试库。
第四章,测试设计,讨论了几个概念和设计模式,使你能够更有效率地设计测试。
第五章,从客户需求到测试自动化 - 基础知识,通过一个业务案例,教你如何从客户需求出发,实现自动化测试的实施,并允许你进行实践。在本章中,你将利用前几章中讨论的标准测试库和技术。本章中的示例将教你头 less 测试和 UI 测试的基础知识,以及如何处理正面和负面测试。
第六章,从客户需求到测试自动化 - 下一阶段,继续第五章,从客户需求到测试自动化 - 基础知识中的业务案例,并介绍了一些更高级的技巧:如何利用共享构造,如何对测试进行参数化,如何处理 UI 元素并将变量传递给这些所谓的 UI 处理器。
第七章,从客户需求到测试自动化 - 以及更多内容,包括两个额外示例,并继续使用前两章中的相同业务案例:如何进行报告测试以及如何为更复杂的场景设置测试。
第八章,如何将测试自动化集成到日常开发实践中,讨论了一些最佳实践,这些实践可能对您和您的团队在日常工作中启动和运行测试自动化有所帮助。
第九章,使 Business Central 标准测试在您的代码中正常工作,讨论了为什么要使用微软为 Dynamics 365 Business Central 提供的标准测试资料,以及当标准测试因您扩展标准应用程序而失败时如何修复错误。
附录 A,测试驱动开发,简要描述了测试驱动开发(TDD)是什么,并指出对您的日常开发实践也可能有价值的部分。
附录 B,设置 VS Code 并使用 GitHub 项目,关注 VS Code 和 AL 开发,以及在 GitHub 上的代码库中可以找到的代码示例。
为了从本书中获得最大收益
本书是《Dynamics 365 Business Central 测试自动化入门》。一方面,讨论了各种概念和术语,另一方面,我们还将通过编写测试代码进行实践。为了从本书中获得最大收益,您可能需要通过实现讨论过的代码示例来实践所倡导的内容。然而,由于本书未涉及如何针对 Business Central 编程,您可能首先需要阅读附录 B*,设置 VS Code 并使用 GitHub 项目中的提示。
如果你的学习方式是先了解原理、术语和概念,可以开始阅读第一章,《自动化测试简介》,然后逐步进入更实用的*第三部分:*为 Microsoft Dynamics 365 Business Central 设计和构建自动化测试。如果你的学习方式更倾向于通过实践来学习,你可以大胆地直接深入阅读第五章,《从客户需求到测试自动化——基础》,第六章,《从客户需求到测试自动化——进阶》,以及第七章,《从客户需求到测试自动化——更多内容》,并稍后或在学习过程中再阅读不同的背景知识。
下载示例代码文件
你可以从你的账户在www.packt.com下载本书的示例代码文件。如果你是在其他地方购买的本书,可以访问www.packt.com/support并注册,以便将文件直接发送到你的邮箱。
你可以通过以下步骤下载代码文件:
-
登录或注册www.packt.com。
-
选择“SUPPORT”标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载文件后,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Automated-Testing-in-Microsoft-Dynamics-365-Business-Central。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。
下载彩色图像
我们还提供了一份 PDF 文件,包含了本书中使用的截图/图表的彩色版本。你可以在此下载:www.packtpub.com/sites/default/files/downloads/9781789804935_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户。示例如下:“唯一需要采取的步骤是将以下代码添加到相关的Initialize函数中。”
代码块的表示方式如下:
fields
{
field(1; Code; Code[10]){}
field(2; Description; Text[50]){}
}
当我们希望特别指出代码块中的某部分时,相关的行或项目将以粗体显示:
[FEATURE] LookupValue Customer
[SCENARIO #0001] Assign lookup value to customer
[GIVEN] A lookup value
[GIVEN] A customer
粗体:表示新术语、重要词汇或屏幕上出现的文字。例如,菜单或对话框中的文字会像这样出现在文本中。以下是一个示例:“从管理面板中选择系统信息。”
警告或重要说明像这样呈现。
提示和技巧像这样呈现。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提到书名,并通过customercare@packtpub.com联系我们。
勘误表:尽管我们已尽力确保内容的准确性,但难免会出现错误。如果你在本书中发现了错误,我们将非常感激你向我们报告。请访问 www.packt.com/submit-errata,选择你的书籍,点击“勘误提交表单”链接,并输入相关信息。
盗版:如果你在互联网上发现我们作品的任何非法版本,我们将非常感激你能提供具体的地址或网站名称。请通过copyright@packt.com联系我们,并附上相关资料的链接。
如果你有兴趣成为作者:如果你在某个领域有专长,并且有兴趣撰写或参与编写一本书,请访问 authors.packtpub.com。
评价
请留下评论。阅读并使用本书后,为什么不在你购买本书的站点上留下评价呢?潜在的读者可以通过你的公正评价做出购买决策,我们 Packt 也能了解你对我们产品的看法,我们的作者也能看到你对他们书籍的反馈。谢谢!
若需了解更多有关 Packt 的信息,请访问 packt.com。
第一部分:自动化测试 - 总体概述
本节将介绍自动化测试。我们将讨论为什么需要使用它,它究竟包含哪些内容,以及什么时候应该使用它。
本节包含以下章节:
- 第一章,自动化测试简介
第一章:自动化测试简介
最终,我得到了一个关于写一本关于应用测试自动化的书的同意——这是我多年来一直想写的书,因为自动化测试似乎对很多人来说并不是一见钟情的话题。而且,像测试一样,在许多实现中,它往往是从属于需求规格和应用程序编码的,不论是项目还是产品。说实话,谁真正喜欢测试呢?这通常不是普通开发者热衷的事情。在我进行的许多自动化测试研讨会中,当我提出这个问题时,往往需要一些时间,功能顾问才会举手。
不可避免地,我问自己:我喜欢测试吗?我的答案是:是的,强烈的 YES。这接着引发了一些额外的问题,比如:是什么让我喜欢测试?我一直都喜欢测试吗?为什么我喜欢它,而其他人似乎不喜欢呢?这些问题的答案让我环游世界,宣扬测试自动化,固执地分享我的发现,并推动微软改进测试,使其变得更好、更可复用。这一切,都是因为我认为这很有趣——非常有趣!
曾在微软前任 Dynamics NAV 全球开发本地化 (GDL) 团队担任应用测试员的我,当然接触过测试的“病毒”。可以说,我必须学习如何进行测试,因为这是我的工作,而我也因此获得报酬。但这项工作对我来说也非常适合,显然我有一种特定的基因,使得我能成为一名测试员。喜欢打破东西,同时又喜欢证明它的健壮性——希望如此。最重要的是,敢于打破它,冒着开发者可能再也不喜欢你的风险。
在微软的一个下午,我的开发团队成员走进了我们的办公室,停在我的桌旁。他个子非常高,而我坐着,我不得不抬起头才能看见他。
“怎么了?”我问道。
“你不喜欢我了吗?”他回答道。
我:?
他:“你不喜欢我了吗?”
我:“不,还是喜欢你。怎么了?”
他:“你拒绝了我的代码,你不喜欢我了吗?”
我: “伙计,我还是喜欢你,但关于你的代码,我的测试显示它似乎没什么用。”
测试不是火箭科学。自动化测试也不是。它只是另一项可以学习的技能。然而,从开发者的角度来看,它需要转变思维方式,用与你习惯的目的完全不同的方式来编写代码。我们都知道,改变往往不是最容易实现的事情。正因如此,在我的研讨会中,参会者达到一定程度的挫败感并不罕见。
应用测试是一种思维方式,并且它也需要大量的纪律——做需要做的事情的纪律:验证功能是否构建正确;验证被测功能是否符合要求;以及在报告和修复 bug 后,执行整个测试过程,反复测试每个新 bug,以确保验证完整。
我倾向于把自己看作一个有纪律的专业人士。我一直是一个非常有纪律的测试员,报告 bug 的频率也很高。但,我总是喜欢做测试吗?你知道的,在那些日子里,我们的所有测试都是手动执行的,每当我找到一个 bug,我的纪律就会在某种程度上受到挑战。想象一下,在修复完一个 bug 后执行第五次测试时,我的内心在怎么想。现在是下午 4 点,我快完成了。早上,我向妻子承诺会按时回家,不管出于什么原因。我们随便选一个原因:我们的结婚纪念日。所以,作为一个有纪律的测试员,我承诺按时回家,下午 4 点,结果……我……又……碰到……一个……bug。知道修复它并重新运行测试至少要花几个小时,你觉得我当时的心情怎么样?没错:二进制。
我有两个选择:
-
报告 bug 会让我加班,至少会让我的妻子非常失望
-
不报告 bug 会让我按时回家,免去很多家庭麻烦
如果当时有自动化测试,选择就会变得非常简单:第一个选择,既没有家庭麻烦,也没有工作上的麻烦。
在本章中,我们将讨论以下主题:
-
为什么选择自动化测试?
-
何时使用自动化测试。
-
什么是自动化测试?
如果你更喜欢先看什么,你可能首先想跳到什么是自动化测试?
为什么选择自动化测试?
直白地说:归根结底,自动化测试可以为你节省很多麻烦。它没有情感,也没有耗时的执行来阻止你(重新)运行测试。只是按下按钮,测试就会自动执行。这是可重现的,快速的,且客观的。
为了澄清,自动化测试是以下内容:
-
容易重现
-
执行速度快
-
其报告是客观的
如果事情真这么简单,那为什么这些年我们在 Dynamics 365 Business Central 的世界里没有一直做这个呢?你可能能列出一些相关的论点,其中最突出的可能是:我们没有时间做这个。或者也许是:谁来为这个付费?
为什么不呢?
在详细阐述任何论点之前,让我先列出一个更完整的为什么不?列表。我们可以称之为非自动化测试的理由:
-
成本太高,会让我们失去竞争力。
-
Dynamics 365 Business Central 平台不支持此功能。
-
客户来做测试,那我们为什么还要费心呢?
-
谁来写测试代码?我们已经很难找到人手了。
-
我们的日常业务没有空闲时间来增加新的学科。
-
项目太多,无法实现测试自动化。
-
微软已经实现了测试自动化,但 Dynamics 365 Business Central 仍然不是无 bug 的。
为什么是这样呢?
Dynamics 365 Business Central 不像以前那么简单了,当时它叫做 Navigator、Navision Financials 或 Microsoft Business Solutions—Navision。周围的世界也不再单一。我们的开发实践变得更加规范化,随着这一点,测试自动化的需求几乎出于与非自动化测试的原因相同的原因,迫切需要。
-
推动测试向上游发展,节省成本
-
Dynamics 365 Business Central 平台支持测试自动化
-
依赖客户进行测试并不是一个好主意
-
找不到合适的人选?开始自动化你的测试吧
-
测试自动化将释放出更多时间用于日常业务
-
因为测试自动化继续处理不同的项目
-
自动化测试也是代码
推动测试向上游发展,节省成本
关于成本,我倾向于说,平均而言,Dynamics 365 Business Central 项目最终会超出预算的 25%,主要是因为上线后修复 bug。我不打算详细讨论由谁来支付这部分费用,但我的经验是,通常是实施伙伴承担。如果假设这是事实,那么数学很简单。如果你最终需要额外花费 25%的费用,为什么不把这笔钱推向上游,在开发阶段进行自动化测试并建立可重用的支持文件呢?
在我 2000 年代在微软工作期间,曾对在产品重大版本开发的不同阶段发现错误的成本进行过研究。如果我的记忆没错的话,研究发现,在发布后发现一个 bug 的成本大约是提前在需求规格阶段发现它的成本的 1000 倍。
把这个转化到独立软件供应商(ISV)的世界,可能大致是一个低十倍的因素。因此,发现一个 bug 在下游的成本将比在上游发现的成本高出 100 倍。对于一个做一次性项目的增值经销商(VAR)来说,成本可能再低十倍。不管这些因素是什么,任何上游的支出都比下游更具成本效益,无论是更规范的测试、更好的应用编程、代码检查,还是更详细的需求说明。
请注意,人们常常纠正我,说 25%的比例实际上偏低。
Dynamics 365 Business Central 平台支持测试自动化
说实话,这个问题不难理解,因为这正是本书的主题。但值得注意的是,平台内部的可测试性框架自 2009 年夏季发布的 2009 SP1 版本起就已经存在。因此,平台已经支持我们构建自动化测试超过九年了。如果我说我们在这段时间里一直处于“沉睡”状态,你觉得奇怪吗?至少,大多数人是这样的。
依赖客户进行测试并不是一个好主意
我同意客户可能最了解他们的功能,因此他们是可能的测试者。但你能百分之百地依赖他们的测试不被夹在实施的截止日期之间,并且还要在他们日常工作中的截止日期之间吗?而且,他们的测试将如何为未来更有效的测试工作做出贡献?它的结构性和可重现性如何?
提出这些问题本身就已经回答了这些问题。一般来说,如果你希望改进开发实践,依赖客户进行测试并不是一个好主意。话虽如此,这并不意味着客户不应该被纳入其中;无论如何,应将他们纳入自动化测试的设置过程中。我们稍后会详细阐述这一点。
找人很困难——开始自动化你的测试
就在此时,当我写下这些文字时,Dynamics 领域的所有实施合作伙伴都在为找到合适的人才以完成工作而苦恼。请注意,我故意没有在那句话中使用形容词right(正确)。我们都面临着这个挑战。即便人力资源充足,实践表明,随着业务的增长,无论是规模还是体量,所使用的资源数量并不会按比例增长。
因此,我们都必须投资改变日常实践,这往往会导致自动化的出现,例如使用 PowerShell 来自动化行政任务,使用 RapidStart 方法来配置新公司。同样,编写自动化测试以使测试工作变得更加轻松和快捷。确实,启动它需要一定的投入,但最终它会节省你的时间。
测试自动化将为日常业务释放时间
类似于用相对较少的资源完成工作,测试自动化最终会帮助释放时间来处理日常业务。这需要一定的初期投入,但随着时间的推移,它将带来回报。
当我讨论花时间进行测试自动化时,一个常见的问题是关于应用程序和测试编码所花时间的比例。通常,在微软 Dynamics 365 Business Central 团队中,这是 1:1 的比例,意味着每花一小时进行应用程序编码,就需要花一小时进行测试编码。
继续处理不同的项目,因为有了测试自动化
传统的 Dynamics 365 Business Central 实施合作伙伴通常忙于客户的一次性解决方案。因此,他们有专门的团队或顾问负责这些安装,测试与最终用户密切合作,每次测试都给参与者带来显著的负担。试想一下,如果有一个自动化的测试辅助工具,你将如何在业务扩展的同时继续服务这些一次性项目。
在任何主要的开发平台上,比如 Visual Studio,已经有很长一段时间的惯例是,应用程序会附带自动化测试。请注意,越来越多的客户已经意识到这些做法。越来越多的客户会问你为什么不为他们的解决方案提供自动化测试。
每个现有的项目都是一个门槛,拥有大量功能却没有自动化测试。在很多情况下,使用的主要功能是标准的 Dynamics 365 Business Central 功能,对于这些功能,微软自 2016 年起就提供了他们自己的测试。总的来说,最新版本的 Business Central 提供了超过 21,000 个测试。这是一个巨大的数字,你可以利用这一点,快速开始。稍后我们将讨论这些测试以及如何让它们在任何解决方案上运行。
自动化测试也是代码
不能否认的是:自动化测试也是代码。而任何一行代码都有一定的几率包含缺陷。
这是否意味着你应该避免进行测试自动化?如果是这样,那听起来就像是避免编码一样。挑战无疑是将测试设计作为需求和需求评审的一部分,像审查应用代码一样审查测试代码,并确保测试总是以适当数量的验证结束,将其纳入源代码管理,从而将缺陷概率控制在最低水平。
很久以前,我看过一部关于这个主题的纪录片,指出研究表明这种概率大约在 2%到 5%之间。具体的概率可能取决于相关开发者的编码技能。
更多的论据
还是不确定为什么你应该开始使用测试自动化?你需要更多的论据来在公司内部和向客户推销它吗?
以下是一些论据:
-
没有人喜欢测试
-
降低风险,提高满意度
-
一旦学习曲线过去,它通常会比手动测试更快
-
更短的更新周期
-
需要进行测试自动化
没有人喜欢测试
嗯,几乎没有人喜欢。当测试意味着今天、明天、明年反复进行时,它往往会变得令人厌烦,从而影响测试的纪律性。自动化那些让人感到无聊的任务,为更有意义的测试释放时间,在这些测试中,手动工作能够产生更大的影响。
降低风险,提高满意度
拥有自动化测试材料使你能够比以往更快速地洞察代码的状态。同时,在建立这些集合的过程中,回归性缺陷和新缺陷的引入将比以往更少。这一切都带来了更低的风险和更高的客户满意度,而你的管理层会喜欢这个。
一旦学习曲线过去,它通常会比手动测试更快
学习自动化测试这一新技能肯定需要一段时间,毫无疑问。但一旦掌握,构思和创建自动化测试的速度往往比手动执行要快得多,因为测试通常是彼此的变体。用代码复制粘贴是……嗯……你能用手动测试做到这一点吗?更不用说重新运行这些自动化测试或手动测试了。
更短的更新周期
随着敏捷方法和云服务的发展,更新以更短的时间间隔交付已成为常态,留给全面测试的时间更少了。而 Dynamics 365 Business Central 正是这一故事的一部分。如果微软不强制我们这么做,我们的客户会越来越多地向我们提出这一要求。参见前面的讨论。
需要进行测试自动化
最后但同样重要的是,这一段对为什么要进行测试自动化的讨论,也许是你读这本书的唯一理由:当你准备在AppSource上销售你的 Dynamics 365 Business Central 扩展时,微软要求进行自动化测试。这对我来说也是一个绝佳机会,可以与您分享我对讲学、举办研讨会、写这本书的激情。
说正经的,这是我们大家的一个巨大机会。是的,我们面临压力,但我们在这个话题上徘徊太久了。现在,让我们开始吧。
灵丹妙药?
然而,你或许有理由怀疑,测试自动化是否真的是解决一切问题的灵丹妙药。我不能否认这一点。然而,我可以告诉你,如果操作得当,它肯定会提高你开发工作的质量。如前所述,它具有以下优点:
-
易于重现
-
执行速度快
-
客观的报告
何时使用自动化测试
这些理由大概足以说服你为什么要使用自动化测试了吧。那么,何时使用它们呢?理想情况下,应该是在每次代码更改时,测试已经经过测试的功能是否仍然正常工作,以确保最近的修改不会影响现有的应用程序。
听起来很有道理,但如果没有自动化测试,这意味着什么呢?你该如何开始创建你的第一个自动化测试?基本上,我建议你使用以下两个标准:
-
哪种代码更改会在创建自动化测试时带来最高的投资回报率?
-
哪种代码更改会让你的测试自动化创建最能提升测试编码技能?
使用这两个标准,以下类型的代码更改通常是你首次尝试的理想候选:
-
上线后修复漏洞
-
有缺陷的代码
-
经常修改的代码
-
业务关键代码被更改
-
现有代码的重构
-
新功能开发
-
微软更新
上线后修复漏洞
上线后的缺陷揭示了初步测试工作中的遗漏,这种遗漏通常可以追溯到需求中的缺陷。它通常具有有限的范围,且不容忽视的是,清晰的重现场景。无论如何,这类缺陷应当防止再次出现。
有缺陷的代码
你有一个功能总是让你和客户感到烦恼。这个代码总是不断地出现缺陷,似乎永远也不会停止。你应该首先从上线后的缺陷修复方法入手,如前所述。但更重要的是,利用这些代码第一次创建一个完整的测试套件。
缺陷是一个特别有用的起点,因为它们通常会提供以下信息:
-
明确定义的期望
-
重现场景的步骤
-
清晰的代码失败定义
经常修改的代码
良好代码治理的基本规则之一是,代码只有在准备进行测试时才能更改。因此,如果代码经常修改,后果就是它也会频繁地被测试。自动化这些测试肯定会带来丰厚的投资回报。
关键业务代码被更改
彻底测试应该始终是常态,但鉴于实际情况,遗憾的是并不总是可行。然而,测试对关键业务代码所做的更改,应该始终是全面的。你绝对不能容忍这些代码出现任何故障。将发现即便是统计上总会存在的 2%到 5%的漏洞作为一种荣誉!
重构现有代码
重构代码可能让人紧张。删除、重写、重新排列。你怎么知道它仍然在完成它原本的任务?它不会破坏其他东西吧?它肯定需要经过测试。但是,当手动执行时,通常是在整个重构完成后才进行测试。那时可能已经太晚,因为许多部分已经被破坏。在任何重构之前,先为这些代码制定一个自动化测试套件,确保其有效性,从而让自己安心。每进行一步重构,就运行这个测试套件,再运行一次。这样,重构过程会变得轻松有趣。
新功能开发
从头开始,无论是在测试代码还是应用程序代码方面,都会是一个不可辩驳的经验。对一些人来说,这可能是最终的方式。而对其他人来说,这可能是一个过于遥远的桥梁,在这种情况下,之前的所有方案可能都是更好的选择。在第三部分,为 Microsoft Dynamics 365 Business Central 设计和构建自动化测试,我们将采用这种方法,并向你展示在应用代码旁边编写测试代码的价值。
微软更新
在 Microsoft 进行任何更新(无论是在本地还是在云端),都必须(重新)测试功能,以证明它们仍然像以前一样正常工作。如果您没有现有的自动化测试,请开始创建它们。基于对各种变更及其可能引入错误风险的分析进行此操作。
什么是自动化测试?
我们讨论了为什么要自动化您的测试以及何时进行此操作;或更具体地说,从哪里开始。但在结束本章之前,我们没有考虑过什么是自动化测试。所以,让我们在总结之前先做这件事。
通过自动化测试,我们解决了应用程序测试的自动化问题,脚本化手动应用程序测试以检验功能的有效性。在我们的情况下,这些是驻留在 Dynamics 365 Business Central 中的功能。您可能已经注意到,我们一直在使用略有不同的术语来描述它:
-
测试自动化
-
自动化测试
-
自动化测试
所有这些都意味着相同的事情。
一方面,自动化测试正在取代手动、通常是探索性的测试。它正在取代那些可重现但执行起来往往不那么有趣的手动测试。
什么是探索性测试?查看以下链接获取更多信息:
另一方面,它们是互补的。手动测试仍然有助于提升功能的质量,利用富有创造力和经验丰富的人类思维来发现当前测试设计中的漏洞。自动化测试也可能包括所谓的单元测试。这些测试验证组成功能的原子单元的工作。通常,这些单元将是单个的、全局的 AL 函数——这些单元永远不会手动测试。
最终,无论是手动测试还是自动化测试,都是为了验证测试对象是否符合要求。
更多关于单元测试和功能测试的信息,请访问以下链接:www.softwaretestinghelp.com/the-difference-between-unit-integration-and-functional-testing/。
摘要
在本章中,我们讨论了为什么要自动化您的应用程序测试,以及何时创建和运行它们。最后,我们简要描述了什么——什么是自动化测试?
在第二章,可测试性框架,您将了解到内建在 Dynamics 365 Business Central 平台中的技术特性,这些特性使我们能够运行和创建自动化测试。
第二部分:Microsoft Dynamics 365 Business Central 中的自动化测试
本节将讨论 Microsoft Dynamics 365 Business Central 如何通过可测试性框架和测试工具来实现自动化测试。接下来,我们将关注标准测试和测试库。
本节包含以下章节:
-
第二章,可测试性框架
-
第三章,测试工具和标准测试
第二章:可测试性框架
在 Dynamics NAV 2009 Service Pack 1 中,微软在平台中引入了可测试性框架。这使得开发人员能够使用 C/AL 编写测试脚本,运行所谓的无头测试;即不使用用户界面(UI)来执行业务逻辑的测试。这是对一个名为NAV 测试框架(NTF)的内部工具的跟进,已经使用并开发了几年。它允许用 C# 编写测试,并针对 Dynamics NAV UI 进行测试。这是一个非常精巧的系统,背后有着精巧的技术概念。然而,通过 UI 运行测试是放弃 NTF 的主要原因之一。我似乎记得,主要原因是通过 UI 访问业务逻辑太慢——太慢了。太慢了,以至于无法让微软 Dynamics NAV 开发团队在合理的时间内运行所有版本的测试。如今,微软支持五个主要版本(NAV 2015、NAV 2016、NAV 2017、NAV 2018 和 Business Central),并且每个国家版本每天至少会构建和测试一次。测试的任何延迟都会对这 100 个版本的构建产生巨大影响。
在本章中,我们将探讨我所称的可测试性框架的五个支柱。这五个构成该框架的技术特性如下:
-
测试代码单元和测试函数
-
断言错误
-
处理函数
-
测试运行器和测试隔离
-
测试页面
测试可测试性框架的五个支柱
在接下来的五个部分中,每个支柱将被讨论,并通过一个简单的代码示例进行说明。你可以随时亲自尝试。不过,作为一本实践性很强的书,我们稍后将会有更多相关的例子。
代码示例可以在 GitHub 上找到,链接是:github.com/PacktPublishing/Automated-Testing-in-Microsoft-Dynamics-365-Business-Central。
关于如何使用这个仓库以及如何设置 VS Code 的详细信息,请参阅附录 B,设置 VS Code 并使用 GitHub 项目。
支柱 1 – 测试代码单元和测试函数
目标:了解什么是测试代码单元和测试函数,并学习如何构建和应用它们。
可测试性框架最重要的支柱是测试代码单元和测试函数的概念。
测试代码单元
测试代码单元通过其Subtype来定义:
codeunit Id MyFirstTestCodeunit
{
Subtype = Test;
}
这与标准的代码单元有几方面不同:
-
它可以包含所谓的测试和处理函数,以及我们在编写应用程序代码时通常使用的常规函数。
-
执行测试代码单元时,平台将执行以下操作:
-
运行
OnRun触发器以及每个测试函数,该函数位于测试代码单元中,从上到下执行 -
记录每个测试函数的结果
-
测试函数
Test 函数由 FunctionType 标签定义:
[Test]
procedure MyFirstTestFunction()
begin
end;
这使得它与标准函数不同:
-
它必须是全局的
-
它不能有参数
-
它会返回一个结果,结果是
SUCCESS或FAILURE
当 SUCCESS 被返回时,意味着测试执行过程中没有发生错误。因此,当 FAILURE 被返回时,测试执行确实发生了错误。此错误可能由多种原因引起,例如以下情况:
-
代码执行触发了
TestField、FieldError或Error调用 -
由于版本冲突、主键冲突或锁定,数据修改未能完成
后者,返回 FAILURE 的 Test 函数,将我们带到了测试代码单元的另一个典型特征——当一个测试失败时,测试代码单元的执行并不会停止。它会继续执行下一个 Test 函数。
让我们构建两个简单的测试,一个返回 SUCCESS,另一个返回 FAILURE:
codeunit 60000 MyFirstTestCodeunit
{
Subtype = Test;
[Test]
procedure MyFirstTestFunction()
begin
Message('MyFirstTestFunction');
end;
[Test]
procedure MySecondTestFunction()
begin
Error('MySecondTestFunction');
end;
}
现在你可以运行它们了。
由于测试函数是从上到下执行的,MyFirstTestFunction 抛出的消息将首先显示以下截图:
之后,显示以下消息,作为整个测试代码单元执行的总结消息:
注意,错误并未以消息框的形式出现,而是被平台收集并记录为失败测试结果的一部分。
为了能够运行测试代码单元,我构建了一个简单的页面 MyTestsExecutor,其操作调用 MyFirstTestCodeunit:
page 60000 MyTestsExecutor
{
PageType = Card;
ApplicationArea = All;
UsageCategory = Tasks;
Caption = 'My Test Executor';
actions
{
area(Processing)
{
action(MyFirstTestCodeunit)
{
Caption = 'My First Test Codeunit';
ToolTip = 'Executes My First Test Codeunit';
ApplicationArea = All;
Image = ExecuteBatch;
RunObject = codeunit MyFirstTestCodeunit;
}
}
}
}
如果你跟随我使用 GitHub 上的代码并且在打开 MyTestsExecutor 页面时遇到困难,可以使用以下任何一种方法:
-
在
launch.json中将startupObjectType设置为Page,将startupObjectId设置为60000 -
在浏览器的地址栏中将
?page=6000添加到 web 客户端 URL 中:http://localhost:8080/BC130/?page=6000 -
在 web 客户端中使用 Alt + Q,告诉我你想要什么,并搜索
My Test Executor -
直接从 VS Code 启动页面,利用像 CRS AL 语言扩展这样的 VS Code AL 扩展
支柱 2 – asserterror
目标:理解 asserterror 关键字的含义,并学习如何应用它。
我们实施的大部分业务逻辑都指定了在某些条件下,用户操作或流程应该失败或停止继续执行。测试导致失败的情况与测试成功完成操作或流程同样重要。第二支柱允许我们编写测试,专注于检查是否发生错误;这是一种所谓的 正向-负向 或 雨天 路径 测试。例如,由于未提供过账日期,过账错误,或确实无法在销售订单行上输入负数折扣百分比。为了实现这一点,asserterror 关键字应该应用于 调用语句 之前:
asserterror <calling statement>
让我们在一个新的代码单元中使用它并运行:
codeunit 60001 MySecondTestCodeunit
{
Subtype = Test;
[Test]
procedure MyNegativeTestFunction()
begin
Error('MyNegativeTestFunction');
end;
[Test]
procedure MyPostiveNegativeTestFunction()
begin
asserterror Error('MyPostiveNegativeTestFunction');
end;
}
MyPostiveNegativeTestFunction函数被报告为SUCCESS,因此没有记录错误信息:
如果asserterror关键字后面的calling statement抛出错误,系统将继续执行后续语句。然而,如果calling statement没有抛出错误,asserterror语句将会抛出错误:
An error was expected inside an asserterror statement.
其中,asserterror使得测试可以继续执行下一条语句,它不会检查错误本身。正如我们稍后所看到的,是否发生了预期的结果由你来验证。如果在asserterror后没有验证特定的错误,任何错误都会导致测试通过。
如果成功的正负测试没有报告错误,这并不意味着错误没有发生。错误已经抛出,因此,当执行写入事务时,将会发生回滚。所有数据修改将会消失。
支柱 3 – 处理器函数
目标:了解什么是处理器函数,并学习如何构建和应用它们。
在我们的第一个测试代码单元示例中,Message语句会导致显示一个消息框。除非我们希望等待用户按下确认按钮,否则该消息框将一直存在,阻止我们的测试完全执行。为了能够进行完全自动化的测试,我们需要一种处理用户交互(如消息框、确认对话框、报告请求页面或模态页面)的方法。
为此,已设计了处理器函数,也称为UI 处理器。处理器函数是一种特殊类型的函数,只能在测试代码单元中创建,旨在处理代码中待测试的 UI 交互。处理器函数使我们能够完全自动化测试,而不需要真实用户进行交互。一旦发生特定的 UI 交互,并且为其提供了处理器,平台将自动调用该处理器,代替真实用户交互。
Test函数处理器由FunctionType标签定义。目前可用的值显示在下面的截图中:
每个处理器函数处理不同类型的用户交互对象,并需要不同的参数以便能与平台进行适当的交互。让 VS Code 和 AL 扩展成为你找到处理器函数正确签名的指南。以下截图展示了当你将鼠标悬停在函数名上时,MessageHandler的签名:
对于MessageHandler函数,签名是消息框显示给用户的文本。将该文本传递给MessageHandler使你能够确定是否触发了正确的消息。
关于每种处理程序类型的签名列表,请访问docs.microsoft.com/en-us/dynamics-nav/how-to--create-handler-functions。
所以,为了自动处理我们第一个测试代码单元中的Message语句,我们应该创建一个MessageHandler函数:
[MessageHandler]
procedure MyMessageHandler(Message: Text[1024])
begin
end;
但这只是工作的一半,因为这个处理程序需要与将执行调用Message的测试关联起来,某种方式或另一种方式。HandlerFunctions标签用来完成这一点。每个处理程序函数都需要在Test函数中调用,并且必须作为文本添加到HandlerFunctions标签中。如果需要多个处理程序,这些处理程序将组成一个以逗号分隔的字符串:
HandlerFunctions('Handler1[,Handler2,…]'*)*
让我们将此应用于新代码单元中的MyFirstTestFunction并运行它:
codeunit 60002 MyThirdTestCodeunit
{
Subtype = Test;
[Test]
[HandlerFunctions('MyMessageHandler')]
procedure MyFirstTestFunction()
begin
Message(MyFirstTestFunction);
end;
[MessageHandler]
procedure MyMessageHandler(Message: Text[1024])
begin
end;
}
即时显示,而不是先显示消息框,整个测试代码单元执行的简要信息会直接展示:
你添加到HandlerFunctions标签中的任何处理程序函数,必须至少在Test函数中被调用一次。如果该处理程序没有被调用,因为它应处理的用户交互没有发生,平台将抛出一个错误,提示:以下 UI 处理程序未执行,并列出未被调用的处理程序。
支柱 4 – 测试运行器和测试隔离
目标:理解什么是测试运行器及其测试隔离,并学习如何使用和应用它们。
鉴于前面的三个支柱,我们可以按照以下方式编写测试用例:
-
使用测试代码单元和测试函数
-
无论是晴天路径还是雨天路径,后者通过应用
asserterror关键字 -
通过应用处理程序函数,实现完全自动化的执行,解决任何用户交互
我们还需要更多内容吗?
事实上,确实需要,因为我们需要一种方式来执行以下操作:
-
运行存储在多个代码单元中的测试,控制它们的执行,收集并确保结果
-
在隔离环境中运行测试,以便我们能够实现以下目标:
-
编写事务,最终不修改我们运行测试的数据库
-
每次重新运行测试时,都会使用相同的数据设置
-
这两个目标可以通过使用所谓的TestRunner代码单元以及特定的测试隔离来完成。测试运行器代码单元由其Subtype定义,隔离由其TestIsolation定义:
codeunit Id MyTestRunnerCodeunit
{
Subtype = TestRunner;
TestIsolation = Codeunit;
}
测试运行器
像其他代码单元一样,测试运行器代码单元可以有一个OnRun触发器和正常的用户定义函数,但除了这些,你还可以添加两个特定于测试运行器的触发器,分别是OnBeforeTestRun和OnAfterTestRun。当从测试运行器的OnRun触发器调用测试代码单元时,OnBeforeTestRun和OnAfterTestRun将由系统如下触发:
-
OnBeforeTestRun:这是在调用测试代码单元之前触发的,测试代码单元的OnRun触发器被执行,并且每个测试函数也会运行 -
OnAfterTestRun:此触发器在每个测试函数运行完并且测试代码单元完成后触发。
使用OnBeforeTestRun触发器来执行测试运行前初始化,并控制整个测试代码单元和单个测试函数的执行。后者可以通过使用OnBeforeTestRun触发器的布尔返回值来实现。返回TRUE时,测试代码单元或测试函数将运行;返回FALSE时,则跳过。
使用OnAfterTestRun触发器执行后处理操作,例如记录每个测试的结果。当OnAfterTestRun触发器运行时,我们到目前为止看到的标准结果消息框将不会显示。
OnBeforeTestRun和OnAfterTestRun都在它们各自的数据库事务中运行。这意味着通过每个触发器对数据库所做的更改将在执行完成后提交。
进一步的阅读可以在以下链接找到:
OnBeforeTestRun:docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/triggers/devenv-trigger-onbeforetestrun OnAfterTestRun:docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/triggers/devenv-trigger-onaftertestrun
测试隔离
通过一个测试运行器使我们能够控制所有测试的执行,我们还需要控制在一个测试代码单元中创建的数据,以免影响下一个测试代码单元中的测试结果。为此,引入了测试代码单元的TestIsolation属性,该属性有三个可能的值:
-
Disabled:选择此值时,或者未显式设置TestIsolation属性时(因为这是默认值),任何数据库事务将被执行;在测试运行器触发的测试执行后,数据库将发生变化,和运行测试前相比。 -
Codeunit:选择此值时,当测试代码单元执行完成后,所有对数据库所做的数据更改将被还原/回滚。 -
Function:选择此值时,当单个测试函数完成后,所有对数据库所做的数据更改将被还原/回滚。
与此相关,分享一些关于运行测试及其隔离性的想法是有意义的:
-
测试隔离适用于数据库事务,但不适用于数据库外部的更改以及包括临时表在内的变量。
-
使用测试隔离、
Codeunit或Function时,所有数据更改将被回滚,即使它们已经通过 ALCommit语句显式提交。 -
在测试隔离之外运行测试代码单元,无论是
Codeunit还是Function的测试运行器都会执行任何数据库事务。 -
使用测试隔离时,
Function将比Codeunit带来额外的开销,导致执行时间更长,因为每个测试函数结束时,数据库的更改必须被回滚。 -
将测试隔离设置为
Function可能不合适,因为它完全禁用了测试函数之间的依赖关系,而这些依赖关系在扩展的测试场景中可能是需要的,尤其是当中间结果需要被报告时,这可以通过一系列相互独立但相互依赖的测试函数来实现。 -
使用测试运行器的
TestIsolation属性,我们可以以通用方式控制如何回滚数据更改;正如我们稍后会看到的,测试函数TransactionModel标签允许我们控制单个测试函数的事务行为。
支柱 5 – 测试页面
目标:了解什么是测试页面,并学习如何在测试 UI 时应用它们。
添加可测试性框架到平台的初衷是避免通过 UI 测试业务逻辑。可测试性框架使得无头测试(从而更快的测试)业务逻辑成为可能。这就是测试可用性框架在 NAV 2009 SP1 中的实现方式:纯无头测试。它包括了迄今为止讨论的四个支柱的所有内容,尽管测试隔离的实现方式与今天有所不同。此前无法测试 UI。
随着进展,逐渐清楚仅使用无头测试排除了太多内容。我们如何测试通常存在于页面上的业务逻辑呢?例如,考虑一个产品配置器,其中根据用户输入的值显示或隐藏选项。因此,在 NAV 2013 中,微软为可测试性框架添加了第五个支柱:测试页面。
测试页面是页面的逻辑表示形式,严格处理在内存中,不显示 UI。要定义测试页面,你需要声明一个TestPage类型的变量:
PaymentTerms: TestPage "Payment Terms";
TestPage变量可以基于解决方案中存在的任何页面。
测试页面允许你模拟用户执行以下操作:
-
访问页面
-
访问其子部分
-
在其上读取和更改数据
-
对其进行操作
你可以通过使用属于测试页面对象的各种方法来实现这一点。让我们构建一个小的代码单元,在其中使用其中的一些方法:
codeunit 60003 MyFourthTestCodeunit
{
Subtype = Test;
[Test]
procedure MyFirstTestPageTestFunction()
var
PaymentTerms: TestPage "Payment Terms";
begin
PaymentTerms.OpenView();
PaymentTerms.Last();
PaymentTerms.Code.AssertEquals('LUC');
PaymentTerms.Close();
end;
[Test]
procedure MySecondTestPageTestFunction()
var
PaymentTerms: TestPage "Payment Terms";
begin
PaymentTerms.OpenNew();
PaymentTerms.Code.SetValue('LUC');
PaymentTerms."Discount %".SetValue('56');
PaymentTerms.Description.SetValue(
PaymentTerms.Code.Value()
);
ERROR('Code: %1 \ Discount %: %2 \Description: %3',
PaymentTerms.Code.Value(),
PaymentTerms."Discount %".Value(),
PaymentTerms.Description.Value()
);
PaymentTerms.Close();
end;
}
请注意,强制出现错误以便获取一些关于测试代码单元的简历消息中的有用反馈。
因此,我们得到了以下结果:
要查看所有测试页面方法的完整列表,可以访问以下网址:
TestPage: docs.microsoft.com/zh-cn/dynamics365/business-central/dev-itpro/developer/methods-auto/testpage/testpage-data-type TestField: docs.microsoft.com/zh-cn/dynamics365/business-central/dev-itpro/developer/methods-auto/testfield/testfield-data-type TestAction: docs.microsoft.com/zh-cn/dynamics365/business-central/dev-itpro/developer/methods-auto/testaction/testaction-data-type
如果你在本地运行 Microsoft 365 Business Central,并且希望使用测试页面进行测试,请确保已安装页面可测试性模块:
摘要
本章讨论了测试框架是什么,描述了它包含的五个支柱:基本元素测试代码单元和测试函数、新的代码关键字asserterror、允许自动处理 UI 元素的处理函数、使我们能够隔离运行测试的测试运行器,最后是构建测试以检查页面行为的测试页面。
在第三章,测试工具和标准测试中,你将了解 Business Central 中的测试工具,以及 Microsoft 随产品发布的标准测试集。
第三章:测试工具与标准测试
自 NAV 2009 SP1 起,提供自动化测试已成为微软在应用程序工作中的重要组成部分。通过测试性框架,他们创建了以下内容:
-
一大堆用于验证标准应用的自动化测试
-
测试工具功能,已成为标准应用的一部分
-
大量的测试辅助库
在本章中,我们将更详细地讨论这三个问题。
请注意,这些都是由微软作为单独组件提供的;也就是说,产品 DVD 和 NAV 2016 及以后版本的 Docker 镜像中包含一个 .fob 文件。
测试工具也可以在 Business Central 在线版中使用,但你只能获取位于 CRONUS 中的扩展测试,而无法获取标准应用的测试。要获取免费的试用版,请访问:dynamics.microsoft.com/en-us/business-central/overview/。
有关 Docker 使用的详情,请访问:docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-running-container-development。
测试工具
目标:理解测试工具的内容,学习如何使用和应用它。
测试工具是一个标准应用功能,允许你管理和运行存储在数据库中的自动化测试,并收集其结果,无论是属于标准应用的测试代码单元,还是扩展的一部分。通过各种实际操作示例测试,我们将频繁使用这个工具。然而,在此之前,我们先详细了解一下它。
你可以通过在 Dynamics 365 Business Central 中使用告诉我你想做什么功能轻松访问测试工具,如下图所示:
在一个干净的数据库中,或者至少在一个尚未使用过测试工具的数据库或公司中,测试工具将如下所示。将出现一个名为 DEFAULT 的套件,其中没有任何记录,显示如下:
若要填充测试套件,请按以下步骤操作:
-
选择获取测试代码单元操作。
-
在打开的对话框中,你有以下两个选项:
-
选择测试代码单元:这将打开一个列表页面,显示数据库中所有存在的测试代码单元,您可以从中选择特定的测试代码单元;选择并点击确定后,这些代码单元将被添加到套件中
-
所有测试代码单元:这将把数据库中所有现有的测试代码单元添加到测试套件中
-
让我们选择第一个选项,选择测试代码单元。这将打开CAL 测试获取代码单元页面。不出所料,它显示了我们在第二章《测试性框架》中创建的四个测试代码单元,后面跟着超过 700 个标准测试代码单元的长列表:
- 选择四个测试代码单元 60000 至 60003,然后点击 OK。
现在,套件为每个测试代码单元显示一行,LINE TYPE = Codeunit,并且与此行关联并缩进显示所有的测试函数(LINE TYPE = Function),如以下截图所示:
-
要运行测试,请选择运行操作。
-
在接下来弹出的对话框中,选择选项“活动代码单元”和“全部”,选择“全部”并点击 OK。现在所有四个测试代码单元将被执行,每个测试将会产生结果,成功或失败:
如果我们选择了仅“活动代码单元”选项,则仅会执行所选的代码单元。
对于每个失败,“First Error” 字段将显示导致失败的错误。如您所见,First Error 是一个 FlowField。如果深入查看,它将打开 CAL 测试结果窗口,显示特定测试的整个测试运行历史。
请注意,MyFirstTestCodeunit 中的消息对话框会产生一个 Unhandled UI 错误。
选择运行后,标准测试运行器代码单元 CAL Test Runner(130400)将被调用,并确保以下事项发生:
-
从测试工具运行的测试将会在独立模式下运行
-
每个测试函数的结果将被记录
在这段简短的测试工具概述中,我们使用了以下功能:
-
获取测试代码单元
-
创建多个测试套件
在深入测试编码时,测试工具将是我们的伴侣。我们将在那里使用它的各种其他功能,包括以下内容:
-
运行所选测试
-
深入查看测试结果
-
引用调用栈
-
清除结果
-
测试覆盖率图
关于本地安装:测试工具可以通过终端用户许可证访问并执行。从 2017 年秋季开始,已经启用了此功能。
标准测试
目标:了解微软提供的标准测试基础。
自 NAV 2016 起,微软将他们自己的应用测试资料作为产品的一部分提供。大量的测试以 .fob 文件的形式提供在产品 DVD 中的 TestToolKit 文件夹中,亦可在 Docker 镜像中找到。实际上,这些测试尚未作为扩展交付。
标准测试套件主要包含测试代码单元。但在 .fob 文件中也包含了一些支持的表、页面、报告和 XMLport 对象。
对于 Dynamics 365 Business Central,整个测试集包含几乎 23,000 个测试,分布在 700 多个测试代码单元中,涵盖了每个微软发布的国家/地区的 w1 和本地功能。随着每个 bug 的修复和每个新功能的引入,测试数量不断增长。它在过去十年间逐步构建,涵盖了 Business Central 的所有功能领域。
让我们在测试工具中设置一个名为 ALL W1 的新套件:
-
在套件名称控制中点击助手编辑按钮
-
在 CAL 测试套件弹出窗口中选择“新建”
-
填充名称和描述字段
-
点击“确定”
打开新创建的测试套件:
现在,使用“获取测试代码单元”操作,让 Business Central 提取所有标准测试代码单元,如下截图所示。请注意,我已删除了我们测试代码单元 60000 至 60003:
阅读所有测试代码单元的名称会让你对它们的内容有一个初步了解,以下是一些示例:
-
企业资源管理(ERM)和供应链管理(SCM)代码单元:
-
这两个类别包含了将近 450 个代码单元,构成了标准测试资料的主要部分
-
ERM 测试代码单元涵盖了总账、销售、采购和库存
-
SCM 测试代码单元涵盖了仓库和生产
-
-
除了 ERM 和 SCM 外,还可以注意到其他几个类别,其中最大的有:
-
服务(大约 50 个测试代码单元)
-
O365 集成(大约 35 个)
-
作业(大约 25 个)
-
市场营销(大约 15 个)
-
这些测试代码单元大多包含功能性、端到端的测试。但也有一些代码单元包含单元测试(UT)。这些代码单元的名称中会加上“Unit Test”字样。以下是一些示例:
-
Codeunit 134155 - ERM 表字段 UT -
Codeunit 134164 - 公司初始化 UT II -
Codeunit 134825 - UT 客户表
由于无头测试是将可测试性框架引入平台的初始触发因素,因此标准测试代码单元中绝大多数都是无头测试。旨在测试用户界面(UI)的测试代码单元会在名称中标注UI或UX。以下是一些示例:
-
Codeunit 134280 - 简单数据交换 UI UT -
Codeunit 134339 - UI 工作流事实框 -
Codeunit 134711 - 自动付款登记.UX -
Codeunit 134927 - ERM 预算 UI
请注意,这些并不是唯一的 UI 测试代码单元。其他的代码单元也可能包含一个或多个 UI 测试,其中一般来说,大多数仍然是无头测试。
由于我经常被问及如何测试报告,值得一提的是,作为最后一类,我们有专门用于测试报告的测试代码单元。搜索名称中标有Report字样的测试代码单元,你会找到 50 个以上的例子。以下是几个示例:
-
Codeunit 134063 - ERM Intrastat 报告 -
Codeunit 136311 - 作业报告 II -
Codeunit 137351 - SCM 库存报告 – IV
按特性分类
通过检查标准测试代码单元的名称,我们对这些测试的内容有了一些初步的了解。然而,微软有一个更为结构化的分类方式,至今由于优先级较低,还没有明确与外界分享。随着自动化测试越来越多地被采用,微软现在面临着将此提升为更高优先级的压力。但目前为止,我们仍然可以在大多数测试代码单元中访问到它。你需要查找FEATURE标签。这个标签是验收测试驱动开发(ATDD)测试用例设计模式的一部分,我们将在第四章中讨论,测试设计。通过使用[FEATURE]标签,微软对其测试代码单元进行分类,在某些情况下,也会对单个测试函数进行分类。请注意,这个标记还远未完成,因为并非所有的测试代码单元都有它。
看一下以下代码单元的(部分)摘要:
-
代码单元 134000 - ERM 应用销售/应收款:-
OnRun:[FEATURE] [销售]
-
[测试] 程序 VerifyAmountApplToExtDocNoWhenSetValue:[FEATURE] [应用程序] [现金收款]
-
[测试] 程序 PmtJnlApplToInvWithNoDimDiscountAndDefDimErr:[FEATURE] [维度] [付款折扣]
-
-
代码单元 134012 - ERM 提醒 应用/撤销:-
OnRun:[FEATURE] [提醒] [销售]
-
[测试] 程序 CustomerLedgerEntryFactboxReminderPage:[FEATURE] [用户界面]
-
在后续章节中,我们将更详细地研究各种标准测试函数。你将看到如何将它们作为自己编写测试的示例(第四章,测试设计,第五章,从客户需求到测试自动化 - 基础,第六章,从客户需求到测试自动化 - 进阶,以及第七章,从客户需求到测试自动化 - 更多内容),以及如何让它们在你自己的解决方案上运行(第九章,让 Business Central 标准测试在你的代码上运行)。
目前,标准测试套件对象位于以下 ID 范围内:
134000 到 139999:w1 测试
144000 到 149999:本地测试
标准库
目标:了解微软提供的标准测试辅助库的基础知识。
为了支持标准测试,微软创建了一个非常有用的函数库,包含超过 70 个库代码单元。这些辅助函数涵盖了从随机数据生成、主数据生成到标准通用以及更具体的检查例程。
需要新项目吗?你可以使用Library - Inventory(代码单元 132201)中的CreateItem或CreateItemWithoutVAT辅助函数。
需要随机文本吗?使用Library – Random(代码单元 130440)中的RandText辅助函数。
想在验证测试结果时获得相同格式的错误信息吗?使用 Assert(codeunit 130000)中的一个辅助函数,如 IsTrue、AreNotEqual 和 ExpectedError。
在我的工作坊中,一个经常出现的问题是:
我怎么知道这些库中是否包含我在自己测试中需要的辅助函数?是否有一个概览列出了各种辅助函数?
不幸的是,Dynamics 365 Business Central 没有所有可用辅助函数的概览。然而,在 NAV 2018 之前,一个包含这些信息的 .chm 帮助文件被包含在产品 DVD 上的 TestToolKit 文件夹中。你可能想使用这个文件,但我总是使用一种非常简单的方法。由于我们的所有代码都存储在源代码管理系统中,我可以在标准测试对象文件夹中快速进行文件搜索。如果我需要一个能为我创建服务项的辅助函数,我可能会在该文件夹中打开 VS Code 并搜索 CreateServiceItem,如下图所示:
在本书的第三部分,为 Microsoft Dynamics 365 Business Central 设计和构建自动化测试中,构建测试时,我们将愉快地使用各种标准辅助函数,这将使我们的工作更加高效和一致。
目前,标准测试库对象位于以下 ID 范围:
130000 到 133999:w1 测试辅助库
请注意,所有测试工具对象也位于此范围的下部:
140000 到 143999:本地测试辅助库
想了解更多关于单元测试和功能测试的信息?请访问:
www.softwaretestinghelp.com/the-difference-between-unit-integration-and-functional-testing/
摘要
在本章中,我们简要讨论了什么是测试工具以及如何使用它来运行你的测试,甚至运行微软构建并提供给我们的测试集合。我们对这个庞大集合中的各种测试类别进行了简要概述。最后,我们简要介绍了包含 70 多个库的有用辅助函数,这些库可以支持你自己编写测试。
现在我们已经讨论了 Dynamics 365 Business Central 中存在的各种测试功能,我们准备开始设计和编写自己的测试了。我们将在第四章 测试设计 中介绍几种设计模式,以便更轻松、一致地编写测试用例。
第四章:为微软 Dynamics 365 Business Central 设计与构建自动化测试
在这一章节,你已经到达本书的核心部分。利用上一章节中讨论的功能和工具,我们将首先讨论各种概念和设计模式,并深入探讨自动化测试的实现。接着,我们将从无头和 UI 测试的基础知识开始,逐步过渡到更高级的技术,并讨论如何处理正向与负向测试。
本章节包含以下各章:
-
第四章,测试设计
-
第五章,从客户需求到测试自动化——基础篇
-
第六章,从客户需求到测试自动化——下一阶段
-
第七章,从客户需求到测试自动化——以及更多内容
第五章:测试设计
在查看了可测试性框架、测试工具、标准测试和库之后,我已经向你展示了平台和应用程序中可用的内容,允许你创建和执行自动化测试。这就是我们在本书这一部分要做的事情。但是,让我们退后一步,不要急于跳入代码编写。首先,我想介绍几个概念和设计模式,这些将帮助你更有效、更高效地构思你的测试。同时,这些概念将使你的测试不仅仅是技术性的练习,还将帮助你让整个团队参与进来。
我当然不会让你烦恼正式的测试文档和从上到下的八个阶段方法,从测试计划,到测试设计/用例规范,再到测试总结报告。这些远远超出了本书的范围,并且无论如何,这些也超出了大多数 Dynamics 365 Business Central 实施的日常实践。然而,事先花一些时间考虑设计,将为你的工作带来杠杆效应。
本章将涵盖以下主题:
-
没有设计,就没有测试
-
测试用例设计模式
-
测试数据设计模式
-
客户需求作为测试设计
如果我已经让你信息量过大,而你想直接开始编写代码,你可以跳到下一章。在那里,我们将练习到目前为止讨论的所有内容,以及本章后面内容。然而,如果你发现自己缺少背景信息,可以回到这一章,补充了解。
没有设计,就没有测试
目标:理解为什么测试应该在编写和执行之前就被构思出来。
我想我说得并不过分,大多数我们世界中的应用测试都可以归类为探索性测试。手动测试由经验丰富的测试人员进行,他们了解被测应用,并且有很好的理解和直觉,知道如何击破它。但是,这些测试没有明确的设计,也没有可重复、可共享、可重用的脚本。在这个世界里,我们通常不希望开发人员测试自己的代码,因为他们无论是有意还是无意,都知道如何使用软件并规避问题。他们的思维方式是如何让它(工作),而不是如何击破它。
但是,对于自动化测试,编写代码的将是开发人员。而且,往往是同一个开发人员负责应用程序的编码。所以,他们需要一个测试设计,设计出要编写的测试。这些测试将覆盖广泛的场景,包括晴天和雨天的场景,头 less 和 UI 测试。那单元测试和功能测试呢?
依我拙见,测试设计和其他交付物一样,是团队共同拥有的成果。它是一个团队合作的结果,大家共同达成一致的测试设计。这是产品负责人、测试人员、开发人员、功能顾问和关键用户之间的协议。如果没有设计,就没有测试。测试设计是一个帮助团队讨论测试工作、揭示思维漏洞并在工作中不断完善的工具。正如接下来所讨论的,它是一种将需求转化为测试和应用代码的有效方式。
完整的测试设计将描述应该执行的各种测试,如性能测试、应用测试和安全性测试;它们执行的条件;以及评判测试成功的标准。我们的测试设计将仅涉及应用测试,因为这是本书的重点:如何创建应用测试自动化。一旦我们的测试设计包含了完整的测试用例集,这些用例需要被详细描述,而这正是下一部分的内容。
如果你想了解更多关于正式测试文档的内容,以下的维基百科文章可能是一个起点:en.wikipedia.org/wiki/Software_test_documentation。
理解测试用例设计模式
目标:学习设计测试的基本模式。
如果你有过软件测试的经验,可能知道每个测试都有类似的整体结构。在你执行测试操作之前,例如文档的过账,首先需要对数据进行设置。然后,进行操作的测试将被执行。最后,需要对操作的结果进行验证。在某些情况下,还会有第四个阶段,所谓的拆解,用于将待测试系统恢复到其之前的状态。
测试用例设计模式的四个阶段如下所示:
-
设置
-
练习
-
验证
-
拆解
关于四阶段设计模式的简短清晰描述,请参考以下链接:
robots.thoughtbot.com/four-phase-test
这种设计模式通常是微软在 C/SIDE 测试编码初期使用的模式。比如以下这个测试功能示例,摘自代码单元 137295 - SCM 库存杂项 III,你将在大量旧的测试代码单元中遇到这种模式:
[Test] PstdSalesInvStatisticsWithSalesPrice()
// Verify Amount on Posted Sales Invoice Statistics
// after posting Sales Order.
// Setup: Create Sales Order, define Sales Price on Customer
Initialize();
CreateSalesOrderWithSalesPriceOnCustomer(SalesLine, WorkDate());
LibraryVariableStorage.Enqueue(SalesLine."Line Amount");
// Enqueue for SalesInvoiceStatisticsPageHandler.
// Exercise: Post Sales Order.
DocumentNo := PostSalesDocument(SalesLine, true);
// TRUE for Invoice.
// Verify: Verify Amount on Posted Sales Invoice Statistics.
// Verification done in SalesInvoiceStatisticsPageHandler
PostedSalesInvoice.OpenView;
PostedSalesInvoice.Filter.SetFilter("No.", DocumentNo);
PostedSalesInvoice.Statistics.Invoke();
验收测试驱动开发
现在,微软使用验收测试驱动开发(ATDD)设计模式。这是一个更完整的结构,并且更贴近客户,因为测试是从用户的角度来描述的。该模式通过以下所谓的标签来定义:
-
FEATURE:定义测试或测试用例集合正在测试的特性 -
SCENARIO:为单个测试定义所测试的场景 -
GIVEN:定义需要哪些数据设置;当数据设置更复杂时,一个测试用例可以有多个GIVEN标签 -
WHEN:定义测试中的动作;每个测试用例应仅有一个WHEN标签 -
THEN:定义动作的结果,或者更具体地说,定义结果的验证;如果适用多个结果,需要多个THEN标签
以下测试示例,取自测试代码单元 134141 - ERM 银行对账,展示了基于 ATDD 设计模式的测试:
[Test] VerifyDimSetIDOfCustLedgerEntryAfterPostingBankAccReconLine()
// [FEATURE] [Customer]
// [SCENARIO 169462] "Dimension set ID" of Cust. Ledger Entry
// should be equal "Dimension Set ID" of Bank
// Acc. Reconciliation Line after posting
Initialize();
// [GIVEN] Posted sales invoice for a customer
CreateAndPostSalesInvoice(
CustomerNo,CustLedgerEntryNo,StatementAmount);
// [GIVEN] Default dimension for the customer
CreateDefaultDimension(CustomerNo,DATABASE::Customer);
// [GIVEN] Bank Acc. Reconciliation Line with "Dimension Set ID" =
// "X" and "Account No." = the customer
CreateApplyBankAccReconcilationLine(
BankAccReconciliation,BankAccReconciliationLine,
BankAccReconciliationLine."Account Type"::Customer,
CustomerNo,StatementAmount,LibraryERM.CreateBankAccountNo);
DimSetID :=
ApplyBankAccReconcilationLine(
BankAccReconciliationLine,
CustLedgerEntryNo,
BankAccReconciliationLine."Account Type"::Customer,
'');
// [WHEN] Post Bank Acc. Reconcilation Line
LibraryERM.PostBankAccReconciliation(BankAccReconciliation);
// [THEN] "Cust. Ledger Entry"."Dimension Set ID" = "X"
VerifyCustLedgerEntry(
CustomerNo,BankAccReconciliation."Statement No.", DimSetID);
在进行任何测试编码之前,测试用例设计应该已经构思好。在前面的例子中,这些内容应该在编写测试代码之前就交给开发人员:
[FEATURE] [Customer]
[SCENARIO 169462] "Dimension set ID" of Cust. Ledger Entry
should be equal "Dimension Set ID" of Bank
Acc. Reconcilation Line after posting
[GIVEN] Posted sales invoice for a customer
[GIVEN] Default dimension for the customer
[GIVEN] Bank Acc. Reconcilation Line with "Dimension Set ID" =
"X" and "Account No." = the customer
[WHEN] Post Bank Acc. Reconcilation Line
[THEN] "Cust. Ledger Entry"."Dimension Set ID" = "X"
测试验证说明
在我的研讨会中,主要是开发人员参与。对于他们来说,在进行测试自动化时,必须克服的一个障碍就是验证部分。对于他们来说,数据设置(GIVEN部分)必须考虑到,更不用说在WHEN部分的测试动作了。然而,THEN部分是一个容易被忽视的环节,尤其是当他们需要自己设计GIVEN-WHEN-THEN时。有些人可能会问:如果代码成功执行,我为什么还需要验证呢?
因为你需要检查:
-
创建的数据是正确的数据,也就是预期数据
-
在正负测试的情况下抛出的错误是预期错误
-
确认处理的是预期的确认
充分的验证将确保你的测试经得起时间的考验。你可能想将下一句话作为海报挂在墙上:
没有验证的测试根本不算测试!
你可能会添加一些感叹号。
你可能注意到,ATDD 设计模式没有四阶段测试设计模式中的清理阶段。正如前面提到的,ATDD 是以用户为导向的,而清理阶段更多是技术性的操作。当然,如果需要,清理部分应该在测试结束时编写。
有关 ATDD 的更多信息,可以访问以下链接:
en.wikipedia.org/wiki/Acceptance_test%E2%80%93driven_development 或者 docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-extension-advanced-example-test#describing-your-tests.
理解测试数据设置设计模式
目标:学习设置测试数据的基本模式。
当你进行手动测试时,你会发现大部分时间都消耗在了设置正确的数据上。作为一个真正的 IT 专家,你会想出尽可能高效的方式来完成这一点。你可能已经考虑过确保以下几点:
-
提供了基本的数据设置,这将成为你即将执行的所有测试的基础。
-
对于每个待测试的功能,额外的测试数据预先存在
-
测试特定数据将即时创建
通过这种方式,你为自己创建了一些模式,帮助你高效地完成测试数据的设置。这些就是我们所称的测试数据设置设计模式,或者夹具或测试夹具设计模式,每种模式都有其特定的名称:
-
第一个是我们所称的预构建夹具。这是在任何测试运行之前创建的测试数据。在 Dynamics 365 Business Central 的上下文中,这将是一个准备好的数据库,比如 Microsoft 提供的演示公司
CRONUS。 -
第二个模式被称为共享夹具,或懒惰设置。这涉及到由一组测试共享的数据设置。在我们的 Dynamics 365 Business Central 环境中,这涉及到通用的主数据、补充数据和设置数据,比如客户和货币数据以及四舍五入精度,所有这些数据都是运行一组测试所必需的。
-
第三个也是最后一个模式是新鲜夹具,或新鲜设置。这涉及到单个测试特别需要的数据,比如一个空的位置、特定的销售价格或待发布的文档。
在自动化测试中,我们将使用这些模式,原因如下:
-
高效的测试执行:尽管自动化测试看起来似乎是光速执行,但随着时间的推移,建立测试资料的累积将增加总执行时间,这可能轻易地达到几个小时;自动化测试的运行时间越短,它就越能被频繁使用。
-
有效的数据设置:在设计测试用例时,数据需求和所需的阶段一目了然;这将加速测试编码的速度。
在这里阅读更多关于夹具模式的内容:
xunitpatterns.com/Fixture Setup Patterns.html
请注意,测试数据设置还有很多内容需要形式化。在接下来的章节中,我们的测试编码将使用更多前面提到的模式。
测试夹具、数据不可知和预构建夹具
如引言章节所述,自动化测试是可重现的、快速的和客观的。它们在执行时是可重现的,因为代码总是相同的。但这并不保证结果是可重现的。如果每次运行测试时,测试的输入——即数据设置——不同,那么测试的输出很可能也会不同。以下三点有助于确保你的测试是可重现的:
-
使一个测试在相同的夹具上运行
-
使一个测试遵循相同的代码执行路径
-
使一个测试根据相同且充分的标准集来验证结果
为了对夹具进行全面控制,强烈建议让你的自动化测试每次运行时都重新创建所需的数据。换句话说,不要依赖于测试运行前系统中已经存在的数据。自动化测试应该对被测系统中存在的任何数据保持独立。因此,在 Dynamics 365 Business Central 中运行测试不应依赖数据库中现有的数据,无论是CRONUS、你自己的演示数据,还是客户特定的数据。是的,你可能需要客户特定的数据来重现已报告的问题,但一旦修复,并且测试自动化得到了更新,它应该能够实现数据独立性地运行。因此,在我们的任何测试中,我们都不会使用预构建的夹具模式。
如果你曾经运行过标准测试,你可能注意到其中有相当一部分测试并非数据独立的。它们高度依赖于CRONUS中的数据。你也可能注意到,这种情况适用于较旧的测试。目前,标准测试力求实现数据独立性。
测试夹具和测试隔离
为了使每组测试都使用相同的夹具,我们将利用测试运行器代码单元的测试隔离功能,正如在第二章的可测试性框架部分中讨论的那样,支柱 4 - 测试运行器与测试隔离部分所述。通过使用标准测试运行器的测试隔离值代码单元,并将一组连贯的测试放入同一个测试代码单元中,为整个测试代码单元设置一个通用的清理操作。这将确保在每个测试代码单元终止时,夹具被恢复到初始状态。如果测试运行器使用函数级别的测试隔离,它将在每个测试函数中添加一个通用的清理操作。
共享夹具实现
你可能已经观察到,在作为四阶段和 ATDD 模式示例使用的两个 Microsoft 测试函数中,每个测试都在场景描述后开始调用一个名为Initialize的函数。Initialize包含了共享夹具模式的标准实现(接下来我们将详细介绍的通用新夹具模式),其实现如下:
local Initialize()
// Generic Fresh Setup
LibraryTestInitialize.OnTestInitialize(<codeunit id>);
<generic fresh data initialization>
// Lazy Setup
if isInitialized then
exit();
LibraryTestInitialize.OnBeforeTestSuiteInitialize(<codeunit id>);
<shared data initialization>
isInitialized := true;
Commit();
LibraryTestInitialize.OnAfterTestSuiteInitialize(<codeunit id>);
当在同一个测试代码单元中的每个测试函数开始时调用Initialize时,懒加载设置部分只会执行一次,因为只有在第一次调用Initialize时,布尔变量才会是false。请注意,Initialize还包含三个钩子,即事件发布者,以便通过将订阅者函数链接到它来扩展Initialize:
-
OnTestInitialize -
OnBeforeTestSuiteInitialize -
OnAfterTestSuiteInitialize
在第九章,让 Business Central 标准测试在你的代码上工作,我们将特别利用这些发布者。
Initialize中的懒加载设置部分就是 xUnit 模式中所称的 SuiteFixture 设置。
新的夹具实现
根据先前讨论的,可以在Initialize函数中设置通用的新装置(部分)。这是需要在每个测试开始时创建或清理的数据。特定于一个测试的新设置,即由GIVEN标签定义的实施测试函数中的内联设置。
Initialize中的通用新设置部分是 xUnit 模式称为隐式设置的一部分。测试特定的新设置称为内联设置。
以客户的期望作为测试设计
目标:了解为什么要以测试设计的形式描述需求。
过去的发展理念是阶段性的,每个阶段都会在下一个开始之前完成。就像瀑布一样,水从一个水平流向另一个水平。从需求收集到分析,再到设计、编码、测试,最后到运行和维护,每个阶段都有其截止日期,并且文档化的可交付成果会移交给下一个阶段。这种系统的一个主要缺点是对变化洞见的响应不足,导致需求的变动。另一个问题是产生的大量文档带来的显著开销。在最近的十年或两十年里,敏捷方法已经成为应对这些缺点的一般实践。
引入测试设计,作为你开发实践的额外文档,也许不是你一直在等待的,尽管我可以保证,你的开发实践会得到提升。但如果你的测试设计可以成为一种统一的文档呢?成为项目中每个学科的输入?每个层次共享同一真相?如果你可以一石五鸟?如果你可以以一种格式编写你的需求,作为所有实施任务的输入?
将需求定义为用户故事或用例是一种常见做法。但我个人认为它们的一个主要缺失是它们往往只定义了晴天路径,没有明确描述阴雨天的情景。在非典型输入下,你的功能应该如何表现?它应该如何报错?正如前面提到的,这是测试人员思维与开发者思维的分歧之处:如何使其出错而不是如何使其工作。这绝对是测试设计的一部分。那么,为什么不将测试设计提升为需求呢?或者从内而外:使用 ATDD 模式编写你的需求,就像编写测试设计一样。
这就是我们现在在我的主要雇主那里尝试的。这就是我在我的研讨会中提倡的,以及实施合作伙伴正在接受的。将每个愿望和每个功能拆分成像下面这样的测试列表,并将其作为我们的主要沟通工具:
-
详细说明你的客户期望
-
实施你的应用程序代码
-
结构化执行你的手动测试
-
编写你的测试自动化
-
更新你解决方案的文档
通过这样做,你的测试自动化将是前期工作的逻辑结果。新的洞见和需求更新将反映在这个列表中,并相应地体现在你的测试自动化中。虽然你当前的需求文档可能未必总是与实现的最新版本保持同步,但当你将测试设计推向需求时,它们会同步更新,因为你的自动化测试必须反映最新版本的应用程序代码。这样一来,你的测试自动化就是你最新的文档。真是一举多得。
正如我们在接下来的章节中将要做的那样,我们将把需求指定为测试设计,最初在功能和场景级别,使用FEATURE和SCENARIO标签。接着,将使用GIVEN、WHEN和THEN标签进行详细的规范。在接下来的例子中,先来看看它是如何展示的,这是一个关于我们将在下一个章节中处理的LookupValue扩展的场景:
[FEATURE] LookupValue UT Sales Document
[SCENARIO #0006] Assign lookup value on sales quote document page
[GIVEN] A lookup value
[GIVEN] A sales quote document page
[WHEN] Set lookup value on sales quote document
[THEN] Sales quote has lookup value code field populate
完整的 ATDD 测试设计作为 Excel 表格LookupValue存储在 GitHub 上。
总结
测试自动化将从结构化的方法中获益,为此,我们引入了一套概念和测试模式,如 ATDD 模式和测试夹具模式。
现在,在下一章节,第五章,从客户需求到测试自动化——基础篇,我们将利用这些模式,最终实现测试代码。
第六章:从客户需求到测试自动化 - 基础
我们从技术上已经完全准备好开始编写测试。这是因为我们了解测试框架的运作方式,知道测试工具包,了解标准测试库的存在,并且已经获得了多种模式,帮助我们设计高效且有效的测试。
但我们要测试什么呢?我们的业务案例是什么?我们将要实现的客户需求是什么?
在本章中,我们将开始应用前几章讨论的原则和技术,并将构建一些基本的自动化测试。
因此,本章涉及以下主题:
-
测试示例 1 – 第一个无头测试
-
测试示例 2 – 第一个正负测试
-
测试示例 3 – 第一个 UI 测试
-
无头与 UI
从客户需求到测试自动化
我们的客户希望扩展标准的 Dynamics 365 Business Central,增加一个基本功能:向 Customer 表中添加一个由用户填充的查找字段。该字段必须被传递到所有销售单据中,并且还需要包含在仓库发货中。
数据模型
尽管此字段的目的非常具体,我们将其通用命名为 Lookup Value Code。与 Business Central 中的任何其他查找字段一样,该 Lookup Value Code 字段将与另一个表(我们这里是一个名为 Lookup Value 的新表)有一个表关系(外键)。
以下关系图示意性地描述了此新功能的数据模型,其中新的表位于中间,扩展的标准表位于左右两侧:
Lookup Value Code 字段必须在所有表中可编辑,除了已过账的单据头表,例如:Sales Shipment Header、Sales Invoice Header、Sales Cr.Memo Header、Return Receipts Header 和 Posted Whse. Shipment Line。
业务逻辑
根据标准的 Business Central 行为,以下业务逻辑适用:
-
从客户模板创建客户时,
Lookup Value Code字段应从Customer Template继承到Customer -
在销售单据的
Sell-to Customer字段中选择客户时,Lookup Value Code字段应从Customer继承到Sales Header -
在过账销售单据时,
Lookup Value Code字段必须被填充 -
在过账销售单据时,
Lookup Value Code字段应从Sales Header继承到已过账单据的头部。也就是说,-
Sales Shipment Header -
Sales Invoice Header -
Sales Cr.Memo Header -
Return Receipt Header
-
-
在归档销售单据时,
Lookup Value Code字段应从Sales Header继承到Sales Header Archive -
在从销售订单创建仓库出货单时,
Lookup Value Code字段应从Sales Header继承到Warehouse Shipment Line。 -
在发布仓库出货单时,
Lookup Value Code字段应从Warehouse Shipment Line继承到Posted Whse. Shipment Line。
LookupValue 扩展
基于这些需求,将构建LookupValue扩展,包括自动化测试。
实现一个已定义的客户需求
既然我们的客户需求已清晰定义,我们就可以开始实现它。如前一章所述,我们将一箭双雕,即将每个需求分解为一系列测试,目的是:
-
客户需求的详细描述
-
应用代码的实现
-
手动测试的结构化执行
-
测试自动化的编码
-
最新的文档解决方案
因此,在接下来的基础测试案例示例中,以及第六章,从客户需求到测试自动化 - 下一步,和第七章,从客户需求到测试自动化 - 以及更多,我们将通过验收测试驱动开发(ATDD)模式描述客户需求。使用FEATURE、SCENARIO、GIVEN、WHEN和THEN标签,我们将讨论应用代码的实现,并更广泛地阐述测试自动化的编码。
手动测试和文档超出了本书的范围。
LookupValue扩展可以在 GitHub 上找到:github.com/PacktPublishing/Automated-Testing-in-Microsoft-Dynamics-365-Business-Central。这个仓库还包括一个 Excel 文件,列出了所有适用于LookupValue扩展的 ATDD 场景。虽然我们会选取特定场景作为示例进行详细说明,但请注意,整个场景列表是在一开始就设计好的,完整描述了客户需求的范围。
如何使用此仓库以及如何设置 VS Code 的详细信息,请参阅附录 B,设置 VS Code 并使用 GitHub 项目。
测试示例 1 - 第一个无头测试
现在,带着前几章交给我们的工具包,并且客户需求已定义,我们准备开始创建第一个自动化测试。
客户需求
让我们从完整客户需求的基础部分开始:在Customer表中扩展一个Lookup Value Code字段。
功能
我们正在通过扩展构建的功能称为LookupValue,而我们现在正在处理的特定部分是Customer表。这导致了以下[FEATURE]标签:
[FEATURE] LookupValue Customer
场景
要实现并测试的具体场景是将查找值分配给客户,因此[SCENARIO]标签应如下所示:
[SCENARIO #0001] Assign lookup value to customer
GIVEN
我们需要的固定数据以便分配查找值,是一个查找值记录和一个客户记录。因此,我们需要以下两个[GIVEN]标签:
[GIVEN] A lookup value
[GIVEN] A customer
WHEN
根据固定数据,我们可以在Customer记录上设置查找值代码,因此我们的[WHEN]标签可以定义如下:
[WHEN] Set lookup value on customer
THEN
现在,测试操作已经执行过了,是时候验证结果了。查找值代码字段是否确实从我们分配给客户记录的固定数据中获取了查找值?这就导致了以下[THEN]标签:
[THEN] Customer has lookup value code field populated
完整场景
因此,完整的场景定义将允许我们稍后在创建测试代码时进行复制:
[FEATURE] LookupValue Customer
[SCENARIO #0001] Assign lookup value to customer
[GIVEN] A lookup value
[GIVEN] A customer
[WHEN] Set lookup value to customer
[THEN] Customer has lookup value code field populated
应用程序代码
客户需求的第一部分,即[SCENARIO #0001],定义了对LookupValue的需求,这是一个新表,通过该表可以通过所谓的Lookup Value Code字段将一个值分配给客户。这已经通过以下.al对象实现:
table 50000 "LookupValue"
{
LookupPageId = "LookupValues";
fields
{
field(1; Code; Code[10]){}
field(2; Description; Text[50]){}
}
keys
{
key(PK; Code)
{
Clustered = true;
}
}
}
page 50000 "LookupValues"
{
PageType = List;
SourceTable = "LookupValue";
layout
{
area(content)
{
repeater(RepeaterControl)
{
field("Code"; "Code"){}
field("Description"; "Description"){}
}
}
}
}
tableextension 50000 "CustomerTableExt" extends Customer
{
fields
{
field(50000; "Lookup Value Code"; Code[10])
{
TableRelation = "LookupValue";
}
}
}
pageextension 50000 "CustomerCardPageExt" extends "Customer Card"
{
layout
{
addlast(General)
{
field("Lookup Value Code"; "Lookup Value Code"){}
}
}
}
在应用程序代码中,已经包含了最基本的内容以节省空间。像Caption、ApplicationArea、DataClassification、UsageCategory和ToolTip等属性已经省略。你可以从 GitHub 下载LookupValue扩展以获取完整的对象。
测试代码
随着客户需求的第一部分已经明确,我们有了一个整洁的结构来开始编写我们的第一个测试。
需要采取的步骤
以下是需要采取的步骤:
-
创建一个以
[FEATURE]标签为基础命名的测试代码单元 -
将客户需求嵌入到基于
[SCENARIO]标签命名的测试函数中 -
基于
[GIVEN]、[WHEN]和[THEN]标签编写你的测试故事 -
编写你的实际代码
创建一个测试代码单元
使用[FEATURE]标签命名时,我们的代码单元的基本结构将是这样的:
codeunit 81000 "LookupValue UT Customer"
{
Subtype = Test;
//[FEATURE] LookupValue UT Customer
}
正如你所看到的,添加到[FEATURE]标签的 UT 代表单元测试,标明这些测试是单元测试而非功能测试。
作为一个简单的开始:LookupValue扩展中的代码单元 81000 已经存在于 GitHub 上。
将客户需求嵌入到测试函数中
现在,我们基于SCENARIO描述创建一个测试函数,并在此函数中嵌入客户需求,GIVEN-WHEN-THEN。
我称之为“嵌入绿色”,即在你开始编写黑色的.al测试代码之前,先写下被注释掉的GIVEN-WHEN-THEN语句。看看现在代码单元已经变成了什么样:
codeunit 81000 "LookupValue UT Customer"
{
Subtype = Test;
//[FEATURE] LookupValue UT Customer
[Test]
procedure AssignLookupValueToCustomer()
begin
//[SCENARIO #0001] Assign lookup value to customer
//[GIVEN] A lookup value
//[GIVEN] A customer
//[WHEN] Set lookup value on customer
//[THEN] Customer has lookup value code field populated
end;
}
编写你的测试故事
对我来说,编写第一个黑色部分是写伪英语,定义我通过测试需要实现的目标。这使得任何非技术背景的项目成员都能读懂我的测试,如果我需要他们的支持,他们阅读测试的门槛比直接开始写真实代码时低得多。而且,或许更有力的论据是——我的代码将嵌入到可复用的辅助函数中。
那么,我们开始吧,让我们编写黑色部分:
codeunit 81000 "LookupValue UT Customer"
{
Subtype = Test;
//[FEATURE] LookupValue UT Customer
[Test]
procedure AssignLookupValueToCustomer()
begin
//[SCENARIO #0001] Assign lookup value to customer
//[GIVEN] A lookup value
CreateLookupValueCode();
//[GIVEN] A customer
CreateCustomer();
//[WHEN] Set lookup value on customer
SetLookupValueOnCustomer();
//[THEN] Customer has lookup value code field populated
VerifyLookupValueOnCustomer();
end;
}
在这个故事中,我设计了四个没有参数和返回值的辅助函数。这些将在下一步中定义。
请注意,辅助函数的名称与它所属标签的描述有多么接近。
构建实际代码
如果你是开发人员,直到目前为止,我可能一直在用伪代码挑战你,虽然没有真正的代码,只有一个结构。现在,准备好迎接真正的部分吧,我希望你和你的同事们将来也能在未来编写的测试中做到同样的事情。
在检查我们的第一个测试函数时,我已经得出结论,我需要以下四个辅助函数:
-
CreateLookupValueCode -
CreateCustomer -
SetLookupValueOnCustomer -
VerifyLookupValueOnCustomer
让我们构建并讨论这些。
CreateLookupValueCode
CreateLookupValueCode是一个多用途的可重用辅助函数,用于创建一个伪随机的LookupValue记录,如下所示。在后续阶段,我们可以将其提升为一个待创建的库代码单元:
local procedure CreateLookupValueCode(): Code[10]
var
LookupValue: Record LookupValue;
begin
with LookupValue do begin
Init();
Validate(
Code,
LibraryUtility.GenerateRandomCode(FIELDNO(Code),
Database::LookupValue));
Validate(Description, Code);
Insert();
exit(Code);
end;
end;
为了填充PK字段,我们利用标准测试库Library - Utility中的GenerateRandomCode函数,代码单元 131000。LibraryUtility变量将像 Microsoft 在他们的测试代码单元中做的那样全局声明,使其可以在其他辅助函数中重用。
伪随机意味着,每当我们的测试在相同上下文中执行时,GenerateRandomCode函数将产生相同的值,从而有助于测试的可重复性。
Description字段的值与Code字段相同,因为Description的具体值没有意义,这样做是最有效的。
我将在我的辅助函数中非常频繁地使用with…do结构;这使得辅助函数可以轻松地在相似的场景中重用,但只需要更新记录变量(以及它所引用的表),即可应用到其他表。
CreateCustomer
使用标准库代码单元Library - Sales中的CreateCustomer函数,代码单元 130509,我们的CreateCustomer函数创建了一个可用的客户记录,并使这个辅助函数成为一个直接的练习:
local procedure CreateCustomer(var Customer: record Customer)
begin
LibrarySales.CreateCustomer(Customer);
end;
和之前的LibraryUtility变量一样,我们将全局声明LibrarySales变量。
你可能会想,为什么我们要创建一个只有一行语句的辅助函数。如前所述,使用辅助函数可以使测试对非技术同事可读,同时使其可重用。它还使其更具可维护性/扩展性。如果我们需要对由Library - Sales代码单元中的CreateCustomer函数创建的客户记录进行更新,我们只需要将更新添加到我们的本地CreateCustomer函数中。
一般来说,我从不直接在测试函数中调用库函数。这有一些例外,我们稍后会看到。
SetLookupValueOnCustomer
看一下SetLookupValueOnCustomer的实现:
local procedure SetLookupValueOnCustomer(var Customer: record Customer;
LookupValueCode: Code[10])
begin
with Customer do begin
Validate("Lookup Value Code", LookupValueCode);
Modify();
end;
end;
在这里调用Validate是至关重要的。SetLookupValueOnCustomer不仅仅是将值分配给Lookup Value Code字段,还确保该值会与LookupValue表中已有的值进行验证。请注意,下面的Lookup Value Code字段的OnValidate触发器没有包含代码。
VerifyLookupValueOnCustomer
如第四章《测试设计》中提到的,没有验证的测试根本不是测试,我们需要验证分配给客户记录中Lookup Value Code字段的查找值代码,确实是Lookup Value表中创建的值。因此,我们从数据库中检索记录,如下所示:
local procedure VerifyLookupValueOnCustomer(CustomerNo: Code[20];
LookupValueCode: Code[10])
var
Customer: Record Customer;
FieldOnTableTxt: Label '%1 on %2';
begin
with Customer do begin
Get(CustomerNo);
Assert.AreEqual(
LookupValueCode,
"Lookup Value Code",
StrSubstNo(
FieldOnTableTxt
FieldCaption("Lookup Value Code"),
TableCaption())
);
end;
end;
为了验证预期值(第一个参数)和实际值(第二个参数)是否相等,我们使用标准库代码单元Assert中的AreEqual函数,130000。当然,我们也可以使用Error系统函数构建自己的验证逻辑,而AreEqual正是这么做的。看看这个:
[External] procedure AreEqual(Expected: Variant;Actual: Variant;Msg: Text)
begin
if not Equal(Expected,Actual) then
Error(
AreEqualFailedMsg,
Expected,
TypeNameOf(Expected),
Actual,
TypeNameOf(Actual),
Msg)
end;
通过使用AreEqual函数,我们确保在预期值和实际值不相等时会收到标准化的错误信息。随着时间的推移,通过阅读任何失败测试的错误,其中验证辅助函数使用了Assert库,您将能够轻松识别发生了哪种错误。
我们完整的测试代码单元将如下所示,准备执行。注意添加到测试代码单元和函数中的变量和参数:
codeunit 81000 "LookupValue UT Customer"
{
Subtype = Test;
var
Assert: Codeunit Assert;
LibraryUtility: Codeunit "Library - Utility";
LibrarySales: Codeunit "Library - Sales";
//[FEATURE] LookupValue UT Customer
[Test]
procedure AssignLookupValueToCustomer()
var
Customer: Record Customer;
LookupValueCode: Code[10];
begin
//[SCENARIO #0001] Assign lookup value to customer
//[GIVEN] A lookup value
LookupValueCode := CreateLookupValueCode();
//[GIVEN] A customer
CreateCustomer(Customer);
//[WHEN] Setlookup value on customer
SetLookupValueOnCustomer(Customer, LookupValueCode);
//[THEN] Customer has lookup value code field populated
VerifyLookupValueOnCustomer(
Customer."No.",
LookupValueCode);
end;
local procedure CreateLookupValueCode(): Code[10]
var
LookupValue: Record LookupValue;
begin
with LookupValue do begin
Init();
Validate(
Code,
LibraryUtility.GenerateRandomCode(FIELDNO(Code),
Database::LookupValue));
Validate(Description, Code);
Insert();
exit(Code);
end;
end;
local procedure CreateCustomer(var Customer: record Customer)
begin
LibrarySales.CreateCustomer(Customer);
end;
local procedure SetLookupValueOnCustomer(
var Customer: record Customer; LookupValueCode: Code[10])
begin
with Customer do begin
Validate("Lookup Value Code", LookupValueCode);
Modify();
end;
end;
local procedure VerifyLookupValueOnCustomer(
CustomerNo: Code[20]; LookupValueCode: Code[10])
var
Customer: Record Customer;
FieldOnTableTxt: Label '%1 on %2';
begin
with Customer do begin
Get(CustomerNo);
Assert.AreEqual(
LookupValueCode,
"Lookup Value Code",
StrSubstNo(
FieldOnTableTxt,
FieldCaption("Lookup Value Code"),
TableCaption())
);
end;
end;
}
测试执行
现在我们准备好进行第一次测试,可以将LookupValue扩展部署到我们的 Dynamics 365 Business Central 安装中。如果我们将测试工具页面设置为launch.json中的启动对象,我们可以立即将我们的测试代码单元添加到DEFAULT套件中,如下所示:
"startupObjectType": "Page",
"startupObjectId": 130401
通过选择“运行”操作来运行测试,将显示它成功执行。
恭喜,我们已经实现了第一个成功的测试,如上面的截图所示!
测试测试
在我的工作坊期间,一些怀疑的声音会挑战我,让我问:“确实,测试结果是成功的,但我怎么知道成功是真正的成功?我如何测试测试?”
您有以下两种方式来测试它:
-
测试创建的数据
-
调整测试,使验证出现错误
测试创建的数据
创建数据的测试可以通过两种方式进行:
-
在没有测试隔离的情况下运行测试,并查看
Customer表。发现一个新客户已创建,且Lookup Value Code字段已填充,当然,还在Lookup Value表中创建了相关记录。 -
调试您的测试,并使用 SQL Server Management Studio 在
Customer和Lookup Value表上运行 SQL 查询。确保您读取的是未提交的数据,在测试完成之前找到相同的记录。
后者是我更喜欢的方法,因为它使得可以在隔离的环境中运行测试,从而不会不可逆转地更改数据库。它还允许我们看到正在创建的数据。
请注意,第二个选项“调试你的测试并运行 SQL 查询”仅在本地或容器化安装的 Dynamics 365 Business Central 中可用。
调整测试,使验证出现错误
这可能是最简单和最可靠的选项:通过提供另一个期望结果值来确保验证失败。例如,在我们的测试中,可以使用你自己的名字:
//[THEN] Customer has lookup value code field populated
VerifyLookupValueOnCustomer(
Customer."No.",
'LUC');
如下截图所示,测试确实在验证部分失败:
抛出的错误告诉我们,期望值是LUC,而实际值是GU00000000:
Assert.AreEqual failed. Expected:<LUC> (Text). Actual:<GU00000000> (Text). Lookup Value Code on Customer.
如前所述,First Error 是一个 FlowField,你可以深入了解它。它将打开 CAL 测试结果窗口,显示特定测试的完整运行历史记录。正如下一个截图所示,在当前测试的情况下,它显示了我们迄今为止对AssignLookupValueToCustomer执行的两次测试结果:
请注意,如下图所示,测试工具窗口中的“清除结果”功能不会对测试运行历史产生影响,它仅清除在测试工具窗口中显示的结果:
测试示例 2 —— 第一个正负测试
这个测试示例与新的客户需求和新的应用程序代码无关,而是补充我们的第一个测试。它是相同客户需求的“雨天路径”版本,导致了新的场景:
[FEATURE] LookupValue UT Customer [SCENARIO #0002] Assign non-existing lookup value to customer
[GIVEN] A non-existing lookup value
[GIVEN] A customer
[WHEN] Set non-existing lookup value on customer
[THEN] Non existing lookup value error thrown
测试代码
让我们重新使用应用于测试示例 1 的配方。
需要采取的步骤
你可能已经记得,以下是需要采取的步骤:
-
创建一个测试代码单元,名称应基于
[FEATURE]标签 -
将客户需求嵌入到测试函数中,名称应基于
[SCENARIO]标签 -
根据
[GIVEN]、[WHEN]和[THEN]标签编写测试故事 -
构造你的实际代码
创建一个测试代码单元
与测试示例 1 共享相同的[FEATURE]标签值,我们的新测试用例将共享相同的测试代码单元,即代码单元 81000 LookupValue UT Customer。
将客户需求嵌入到测试函数中
嵌入绿色结果将导致代码单元 81000 中以下新的测试函数:
procedure AssignNonExistingLookupValueToCustomer()
begin
//[SCENARIO #0002] Assign non-existing lookup value to
// customer
//[GIVEN] A non-existing lookup value
//[GIVEN] A customer
//[WHEN] Set non-existing lookup value on customer
//[THEN] Non existing lookup value error thrown
end;
编写你的测试故事
填写第一个黑色“故事元素”会导致以下典型选择:
-
创建一个不存在的查找值只需提供一个在
Lookup Value表中没有相关记录的字符串常量。 -
为了将此值分配给客户的
Lookup Value Code字段,我们不需要数据库中的客户记录。一个本地变量足以触发我们希望发生的错误。 -
可以通过使用测试示例 1 中的
SetLookupValueOnCustomer来设置客户的查找值。
结果,测试故事 比我们之前的测试有更多细节:
procedure AssignNonExistingLookupValueToCustomer()
var
Customer: Record Customer;
LookupValueCode: Code[10];
begin
//[SCENARIO #0002] Assign non-existing lookup value to
// customer
//[GIVEN] A non-existing lookup value
LookupValueCode := 'SC #0002';
//[GIVEN] A customer record variable
// See local variable Customer
//[WHEN] Set non-existing lookup value on customer
asserterror SetLookupValueOnCustomer(
Customer,
LookupValueCode);
//[THEN] Non existing lookup value error thrown
VerifyNonExistingLookupValueError(LookupValueCode);
end;
构建实际代码
重用 SetLookupValueOnCustomer 函数,我们只需要创建一个新的辅助函数。
VerifyNonExistingLookupValueError
就像我们在第一个验证函数中一样,我们使用了来自标准库代码单元 Assert(130000)中的 ExpectedError 函数。我们只需要向 ExpectedError 提供预期的错误文本。实际的错误将通过 ExpectedError 使用 GetLastErrorText 系统函数来获取,如下所示:
local procedure VerifyNonExistingLookupValueError(
LookupValueCode: Code[10])
var
Customer: Record Customer;
LookupValue: Record LookupValue;
ValueCannotBeFoundInTableTxt: Label
'The field %1 of table %2 contains a value (%3) that
cannot be found in the related table (%4).';
begin
with Customer do
Assert.ExpectedError(
StrSubstNo(
ValueCannotBeFoundInTableTxt
FieldCaption("Lookup Value Code"),
TableCaption(),
LookupValueCode,
LookupValue.TableCaption()));
end;
注意如何通过结合使用 StrSubstNo 系统方法和 ValueCannotBeFoundInTableTxt 标签来构造预期的错误文本。
测试执行
让我们重新部署我们的扩展,并通过选择操作 | 函数 | 获取测试方法,将第二个测试添加到测试工具中。获取测试方法将通过将所有当前的测试函数作为行添加到测试工具中来更新选定的测试代码单元。请注意,结果列将被清空。现在,运行测试代码单元并查看两个测试都成功了:
测试 测试
如何验证 成功是真正的成功?我们可以像在测试示例 1 中那样通过为测试用例的验证函数提供不同的预期值来做到这一点。所以让我们来做吧。
调整测试使验证出错
让我们以与测试示例 1 类似的方式调整验证:
//[THEN] Non existing lookup value error thrown
VerifyNonExistingLookupValueError('LUC');
输出显示在以下截图中:
抛出的错误告诉我们预期值是 LUC,而实际值是 SC #0002,如下所示:
Assert.ExpectedError failed.
Expected: The field Lookup Value Code of table Customer contains a value (LUC) that cannot be found in the related table (Lookup Value)..
Actual: The field Lookup Value Code of table Customer contains a value (SC #0002) that cannot be found in the related table (Lookup Value)..
移除 asserterror
在雨路径场景中,我们通常使用 asserterror 来包裹 [WHEN] 部分以捕获错误,但我们还有另一种方法来 测试测试:通过移除 asserterror 并再次运行测试。现在,你将看到真正的错误:
The field Lookup Value Code of table Customer contains a value (SC #0002) that cannot be found in the related table (Lookup Value).
该错误消息允许我们在 .al 中构建文本标签来构造预期的错误。
在测试示例 2 中,由于我们没有从数据库中检索客户记录,因此 SetLookupValueOnCustomer 中的 Modify 严格来说是不可能的。然而,Validate 抛出的错误将阻止 Modify 被调用。
测试示例 2 基于以下假设:在 Customer 表的 Lookup Value Code 字段上设置的 TableRelation 使用该字段上 ValidateTableRelation 属性的默认设置。
测试示例 3 – 第一个 UI 测试
Lookup Value Code 字段已在 Customer 表上实现并经过测试。但当然,为了让用户管理它,必须使其在 UI 中可访问。因此,它需要被放置在 Customer Card 上。
客户愿望
客户需求的下一个阶段非常接近由 [SCENARIO #0001] 定义的上一部分。主要的区别是我们现在希望通过 UI 元素 Customer Card 来访问客户。通过模拟最终用户,我们的场景描述了创建一个新的 Customer Card(见第二个 [GIVEN])。请看一下:
[FEATURE] LookupValue UT Customer
[SCENARIO #0003] Assign lookup value on customer card
[GIVEN] A lookup value
[GIVEN] A customer card
[WHEN] Set lookup value on customer card
[THEN] Customer has lookup value code field populated
应用程序代码
基于客户需求的最后一个新增部分,Customer Card 需要按照以下 .al 对象扩展 Lookup Value Code 字段:
pageextension 50000 "CustomerCardPageExt" extends "Customer Card"
{
layout
{
addlast(General)
{
field("Lookup Value Code"; "Lookup Value Code"){}
}
}
}
测试代码
接下来,让我们一步步实现我们的 如何实现测试代码 配方。
创建测试代码单元
再次,使用与我们之前测试相同的 [FEATURE] 标签值,我们可以将新的测试用例放入相同的测试代码单元中,即代码单元 81000 LookupValue UT Customer。
将客户需求嵌入到测试函数中
将 绿色 部分封装成一个新的测试函数,并放入代码单元 81000 中,如下所示:
[Test]
procedure AssignLookupValueToCustomerCard()
begin
//[SCENARIO #0003] Assign lookup value on customer card
//[GIVEN] A lookup value
//[GIVEN] A customer card
//[WHEN] Set lookup value on customer card
//[THEN] Customer has lookup value field populated
end;
编写测试用例
新的测试用例版本与测试示例 1 平行,可以是:
[Test]
procedure AssignLookupValueToCustomerCard()
begin
//[SCENARIO #0003] Assign lookup value on customer card
//[GIVEN] A lookup value
CreateLookupValueCode();
//[GIVEN] A customer card
CreateCustomerCard();
//[WHEN] Set lookup value on customer card
SetLookupValueOnCustomerCard();
//[THEN] Customer has lookup value field populated
VerifyLookupValueOnCustomer();
end;
添加变量和参数后,代码变为:
[Test]
procedure AssignLookupValueToCustomerCard()
var
CustomerCard: TestPage "Customer Card";
CustomerNo: Code[20];
LookupValueCode: Code[10];
begin
//[SCENARIO #0003] Assign lookup value on customer card
//[GIVEN] A lookup value
LookupValueCode := CreateLookupValueCode();
//[GIVEN] A customer card
CreateCustomerCard(CustomerCard);
//[WHEN] Set lookup value on customer card
CustomerNo := SetLookupValueOnCustomerCard(
CustomerCard,
LookupValueCode);
//[THEN] Customer has lookup value field populated
VerifyLookupValueOnCustomer(CustomerNo, LookupValueCode);
end;
在自动化测试中访问 UI,我们需要使用第五个支柱:TestPage。如您在我们测试函数 AssignLookupValueToCustomerCard 的特定情况下所看到的,测试页面对象是基于 Customer Card 页面。
构建实际代码
我们可以利用已经存在的辅助函数 CreateLookupValueCode 和 VerifyLookupValueOnCustomer,但我们还需要构建以下两个新的辅助函数:
-
CreateCustomerCard -
SetLookupValueOnCustomerCard
CreateCustomerCard
要创建一个新的客户卡,我们只需调用任何可编辑 TestPage 都有的 OpenNew 方法:
local procedure CreateCustomerCard(
var CustomerCard: TestPage "Customer Card")
begin
CustomerCard.OpenNew();
end;
SetLookupValueOnCustomerCard
使用控件方法 SetValue 设置 Lookup Value Code 字段的值:
local procedure SetLookupValueOnCustomerCard(
var CustomerCard: TestPage "Customer Card";
LookupValueCode: Code[10]) CustomerNo: Code[20]
begin
with CustomerCard do begin
"Lookup Value Code".SetValue(LookupValueCode);
CustomerNo := "No.".Value();
Close();
end;
end;
由于 SetValue 模拟了用户设置值的操作,因此它会触发字段的验证。如果输入了一个不存在的值,它会根据我们在测试示例 2 中测试过的 Lookup Value 表中的现有记录验证该值。为了检索 No. 字段的值,我们使用控件方法 Value。我们确实需要关闭页面以触发系统,保存更改到数据库中的记录。
请注意,Value 有双重用途。它可以用于获取或设置字段(控件)的值。使用 Value 设置值与使用 SetValue 设置值的区别在于,Value 总是将一个字符串作为参数,而 SetValue 的参数应与字段(控件)的数据类型相同。
我们已经几乎准备好运行新的测试了。然而,SetLookupValueOnCustomerCard 辅助函数有一个主要的失败点。它会正常工作,但它没有考虑到我认为的一个设计缺陷:SetLookupValueOnCustomerCard 即使在“查找值代码”字段不可编辑时也会成功运行。SetValue 和 Value 都没有对此进行检查。因为我们测试的目的是检查用户是否可以设置“查找值代码”字段,我们需要添加一个小的验证,来判断该字段是否可编辑。因此,SetLookupValueOnCustomerCard 函数需要更新为以下内容,使用 Assert 代码单元中的另一个函数 IsTrue:
local procedure SetLookupValueOnCustomerCard(
var CustomerCard: TestPage "Customer Card";
LookupValueCode: Code[10]) CustomerNo: Code[20]
begin
with CustomerCard do begin
Assert.IsTrue("Lookup Value Code".Editable(), 'Editable');
"Lookup Value Code".SetValue(LookupValueCode);
CustomerNo := "No.".Value();
Close();
end;
end;
请注意,当我们尝试将 Value 和 SetValue 应用到一个不可见字段时,它们都会报错。
测试执行
让我们再次重新部署扩展,使用“获取测试方法”功能添加新的测试函数,并运行测试。请查看下一个截图:
糟糕,发生了一个错误。错误?让我们读一下并尝试理解:
Unexpected CLR exception thrown.: Microsoft.Dynamics.Framework.UI. FormAbortException: Page New - Customer Card has to close ---> Microsoft. Dynamics.Nav.Types.Exceptions.NavNCLMissingUIHandlerException: Unhandled UI: ModalPage 1340 ---> System.Reflect
我必须承认,每次看到那些技术性 CLR 异常抛出的消息时,我总是有点紧张,但我已经学会了扫描与我所知相关的内容。这里有两点:
-
NavNCLMissingUIHandlerException -
Unhandled UI: ModalPage 1340
显然,存在一个 ModalPage 实例没有被我们的测试处理。更具体地说,是页面 1340 被模态调用但没有处理。页面 1340?它是“配置模板”页面,当你要创建新客户时会弹出,如截图所示:
所以,我们需要构建一个 ModalPageHandler 并将其链接到我们的第三个测试:
[ModalPageHandler]
procedure HandleConfigTemplates(
var ConfigTemplates: TestPage "Config Templates")
begin
ConfigTemplates.OK.Invoke();
end;
将 HandlerFunctions 标签设置为与 ModalPageHandler 相关的测试函数:
[Test]
[HandlerFunctions('HandleConfigTemplates')]
procedure AssignLookupValueToCustomerCard()
现在测试成功运行。
测试测试
让我们测试测试并验证它是否是一个好的测试。
调整测试,使验证出错
一种经过验证的方法可以通过与我们在测试示例 1 中所做的完全相同的方式实现。
在这条注释旁边,我们在代码中添加了另一个验证:
Assert.IsTrue("Lookup Value Code".Editable(), 'Editable');
将 IsTrue 改为 IsFalse。你会看到测试失败,因为“查找值代码”字段是可编辑的。IsTrue 验证确保当“查找值代码”字段变为不可编辑时,测试会失败。
无头模式与 UI
如前所述,无头测试是自动化测试的首选模式,因为它比 UI 测试更快。在测试示例 1 和 3 中,我们实现了相同类型的测试:检查查找值是否可以分配给客户。测试示例 1 使用无头模式,而测试示例 3 使用 UI。运行这两个测试确实表明 UI 测试比无头测试慢。看看执行时长(以秒为单位)的图表。
UI 测试的平均执行时间为 1.35 秒,而无头模式的平均执行时间几乎快了 7 倍:0.20 秒。
总结
在本章中,我们将构建我们的第一个自动化测试。我们利用 ATDD 测试用例模式来设计每个测试,并将其作为我们四步法食谱的基础结构,用来创建测试单元,嵌入客户需求到测试中,编写测试故事,最后构建实际代码。
在下一章,我们将继续使用 ATDD 和四步法食谱来创建一些更高级的测试。
第七章:从客户需求到测试自动化——下一步
在上一章中,我们构建了我们在 Dynamics 365 Business Central 中的第一个基础测试自动化。我们查看了三个简单的示例,展示了如何应用验收测试驱动开发(ATDD)测试用例模式,并使用我们的4 步法将客户需求转化为应用程序和测试代码。在本章中,我们将使用相同的方法论创建更多的测试,这些测试:
-
使用共享固定设施
-
是参数化的
-
将变量交给 UI 处理器
销售文档、客户模板和仓库发货
在第五章《从客户需求到测试自动化——基础》中的三个例子中,我们将Lookup Value Code字段添加到Customer表中。 然而,这只是客户需求的一部分,因为它明确描述了……
“……这个字段必须传递到所有销售文档,并且同时需要包含在仓库发货中。”
因此,在深入以下测试示例之前,需要注意的是,在Customer表上实现Lookup Value Code字段的同时,必须在Sales Header表、Customer Template表、Warehouse Shipment Line表以及所有相关页面上实现相同的字段。ATDD 测试用例描述非常相似,应用程序和测试代码也是如此。复制和粘贴——任何 Business Central 开发人员的伟大美德。
让我们看看客户模板的 ATDD 测试用例描述是什么样的:
[SCENARIO #0012] Assign lookup value to customer template
[GIVEN] A lookup value
[GIVEN] A customer template
[WHEN] Set lookup value on customer template
[THEN] Customer template has lookup value code field populate
[SCENARIO #0013] Assign non-existing lookup value to customer template
[GIVEN] A non-existing lookup value
[GIVEN] A customer template record variable
[WHEN] Set non-existing lookup value to customer template
[THEN] Non existing lookup value error was thrown
[SCENARIO #0014] Assign lookup value on customer template card
[GIVEN] A lookup value
[GIVEN] A customer template card
[WHEN] Set lookup value on customer template card
[THEN] Customer template has lookup value code field populated
你是否看到了与场景#0001、#0002和#0003的相似性?
在 GitHub 上,你将找到完整的 ATDD 场景列表和完整的测试代码。
测试示例 4——如何设置共享固定设施
虽然没有明确提到,但我们为前面三个测试每个都创建了一个全新的固定设施,根据[GIVEN]标签定义,为每个创建了一个查找值记录和一个客户记录。然而,为了提高速度,确实有必要考虑你是否需要为每个测试创建一个全新的固定设施,还是可以为一组测试使用共享固定设施。就#0001和#0003这两个场景而言,我们完全可以使用相同的LookupValueCode,不需要为每个测试创建新的查找值记录。
客户需求
让我们使用客户需求中要求所有销售文档都具有Lookup Value Code字段的部分,来说明如何实现共享的固定设施。这将归结为以下八个场景,省略GIVEN-WHEN-THEN部分以节省空间:
[SCENARIO #0004] Assign lookup value to sales header
[SCENARIO #0005] Assign non-existing lookup value on sales header
[SCENARIO #0006] Assign lookup value on sales quote document page
[SCENARIO #0007] Assign lookup value on sales order document
page
[SCENARIO #0008] Assign lookup value on sales invoice document
page
[SCENARIO #0009] Assign lookup value on sales credit memo document
page
[SCENARIO #0010] Assign lookup value on sales return order
document page
[SCENARIO #0011] Assign lookup value on blanket sales order
document page
在第五章《从客户需求到测试自动化——基础》还历历在目时,你可能会注意到,场景#0001和#0004非常相似。场景#0003和#0006至#0011也是如此。因此,所有这些场景都共享以下相同的[GIVEN]部分:
[GIVEN] A lookup value
对这个需求的直接实现将导致创建七次查找值记录。因此,我们将采取懒汉式的共享夹具模式或懒惰设置模式。
应用程序代码
这部分客户需求导致了销售头中的Lookup Value Code字段的实现,并在每个销售文档页面上创建了该字段的页面控制。
下一个代码片段实现了销售头表的扩展,也就是场景#0004和#0005:
tableextension 50001 "SalesHeaderTableExt" extends "Sales Header"
{
fields
{
field(50000; "Lookup Value Code"; Code[10])
{
Caption = 'Lookup Value Code';
DataClassification = ToBeClassified;
TableRelation = "LookupValue";
}
}
}
此外,以下代码块将实现销售订单页面的扩展(参见场景#0007):
pageextension 50002 "SalesOrderPageExt" extends "Sales Order"
{
layout
{
addlast(General)
{
field("Lookup Value Code"; "Lookup Value Code")
{
ToolTip = 'Specifies the lookup value the
transaction is done for.';
ApplicationArea = All;
}
}
}
}
场景#0006、#0008、#0009、#0010和#0011会以类似的方式扩展销售报价、销售发票、销售贷项通知单、销售退货订单和长期销售订单文档页面。
测试代码
通过一些大的步骤,我们将为场景#0004、#0006和#0007创建测试代码,其余场景#0005、#0008、#0009、#0010和#0011留给你在 GitHub 上复习。
创建一个测试代码单元
codeunit 81001 "LookupValue UT Sales Document"
{
Subtype = Test;
//[FEATURE] LookupValue UT Sales Document
}
将客户需求嵌入到测试函数中
将三个场景#0004、#0006和#0007嵌入到测试函数中后,我们的新测试代码单元如下所示:
codeunit 81001 "LookupValue UT Sales Document"
{
Subtype = Test;
//[FEATURE] LookupValue UT Sales Document
[Test]
procedure AssignLookupValueToSalesHeader()
begin
//[SCENARIO #0004] Assign lookup value to sales header
// page
//[GIVEN] A lookup value
//[GIVEN] A sales header
//[WHEN] Set lookup value on sales header
//[THEN] Sales header has lookup value code field
// populated
end;
[Test]
procedure AssignLookupValueToSalesQuoteDocument()
begin
//[SCENARIO #0006] Assign lookup value on sales quote
// document page
//[GIVEN] A lookup value
//[GIVEN] A sales quote document page
//[WHEN] Set lookup value on sales quote document
//[THEN] Sales quote has lookup value code field populated
end;
[Test]
procedure AssignLookupValueToSalesOrderDocument()
begin
//[SCENARIO #0007] Assign lookup value on sales order
// document page
//[GIVEN] A lookup value
//[GIVEN] A sales order document page
//[WHEN] Set lookup value on sales order document
//[THEN] Sales order has lookup value code field populated
end;
}
编写测试故事
现在结构已明确,我们可以选择场景#0007来创建更多细节:
codeunit 81001 "LookupValue UT Sales Document"
{
Subtype = Test;
//[FEATURE] LookupValue UT Sales Document
[Test]
procedure AssignLookupValueToSalesOrderDocument()
begin
//[SCENARIO #0007] Assign lookup value on sales order
// document page
//[GIVEN] A lookup value
CreateLookupValueCode();
//[GIVEN] A sales order document page
CreateSalesOrderDocument();
//[WHEN] Set lookup value on sales order document
SetLookupValueOnSalesOrderDocument();
//[THEN] Sales order has lookup value code field populated
VerifyLookupValueOnSalesHeader();
end;
}
那么,我们如何设置共享的夹具呢?我们通过使用Initialize函数来实现,如第四章《测试设计》中所介绍的。这将把AssignLookupValueToSalesOrderDocument改为如下:
[Test]
procedure AssignLookupValueToSalesOrderDocument()
begin
//[SCENARIO #0007] Assign lookup value on sales order
// document page
//[GIVEN] A lookup value
Initialize();
//[GIVEN] A sales order document page
CreateSalesOrderDocument();
//[WHEN] Set lookup value on sales order document
SetLookupValueOnSalesOrderDocument();
//[THEN] Sales order has lookup value code field populated
VerifyLookupValueOnSalesHeader();
end;
构建实际代码
让我们构建一个简单的Initialize:
local procedure Initialize()
begin
if isInitialized then
exit;
LookupValueCode := CreateLookupValueCode();
isInitialized := true;
Commit();
end;
在这里,isInitialized和LookupValueCode分别是Boolean和Code[10]数据类型的全局变量。一旦调用了Initialize,isInitialized将变为true,并且每次调用Initialize时,if语句都会评估为true,始终直接退出Initialize。
关于场景#0007,我们的测试代码单元将如下所示,包括各种变量、参数和其他辅助函数:
codeunit 81001 "LookupValue UT Sales Document"
{
Subtype = Test;
var
Assert: Codeunit Assert;
LibrarySales: Codeunit "Library - Sales";
isInitialized: Boolean;
LookupValueCode: Code[10];
//[FEATURE] LookupValue UT Sales Document
procedure AssignLookupValueToSalesOrderDocument()
var
SalesHeader: Record "Sales Header";
SalesDocument: TestPage "Sales Order";
DocumentNo: Code[20];
begin
//[SCENARIO #0007] Assign lookup value on sales order
// document page
//[GIVEN] A lookup value
Initialize();
//[GIVEN] A sales order document page
CreateSalesOrderDocument(SalesDocument);
//[WHEN] Set lookup value on sales order document
DocumentNo := SetLookupValueOnSalesOrderDocument(
SalesDocument, LookupValueCode);
//[THEN] Sales order has lookup value code field populated
VerifyLookupValueOnSalesHeader(
SalesHeader."Document Type"::Order,
DocumentNo,
LookupValueCode);
end;
local procedure Initialize()
begin
if isInitialized then
exit;
LookupValueCode := CreateLookupValueCode();
isInitialized := true;
Commit();
end;
local procedure CreateLookupValueCode(): Code[10]
begin
//for implementation see test example 1; this smells like
//duplication ;-)
end;
local procedure CreateSalesOrderDocument(
var SalesDocument: TestPage "Sales Order")
begin
SalesDocument.OpenNew();
end;
local procedure SetLookupValueOnSalesOrderDocument(
var SalesDocument: TestPage "Sales Order";
LookupValueCode: Code[10])
DocumentNo: Code[20]
begin
with SalesDocument do begin
//for rest of implementation see test example 1
end;
end;
local procedure VerifyLookupValueOnSalesHeader(
DocumentType: Option Quote,Order,Invoice,
"Credit Memo","Blanket Order",
"Return Order";
DocumentNo: Code[20];
LookupValueCode: Code[10])
var
SalesHeader: Record "Sales Header";
FieldOnTableTxt: Label '%1 on %2';
begin
with SalesHeader do begin
Get(DocumentType, DocumentNo);
//for rest of implementation see test example 1
end;
end;
}
测试执行
运行完整的代码单元 81001 会产生一系列成功:
测试测试
到现在为止,我猜你已经知道该怎么做了:调整测试,使验证出现错误。试试看,或者使用 GitHub 上的完成代码作为备忘单。
测试示例 5——如何参数化测试
编写测试自动化,包括设计和编码,是一项相当大的工作,需要关注很多细节。然而,一旦你掌握了并且将其完成,你会享受它并从中受益。除非你在设计和编码阶段疏忽细节,导致不得不不断修复测试,否则你会更享受编写测试。如果你通过参数化测试来使测试更通用,你会更加喜欢编写测试。由于测试框架的性质,你无法直接参数化测试函数,但你可以通过将通用测试代码封装在辅助函数中来实现这一点。
客户需求
让我们通过另一个客户需求来说明:归档销售文档。由于 Business Central 允许用户归档销售报价单、销售订单和销售退货订单,因此我们需要将其包含在扩展中。以下是这三个场景的表达:
[FEATURE] LookupValue Sales Archive
[SCENARIO #0018] Archive sales order with lookup value
[GIVEN] A sales order with a lookup value
[WHEN] Sales order is archived
[THEN] Archived sales order has lookup value from sales order
[SCENARIO #0019] Archive sales quote with lookup value
[GIVEN] A sales quote with a lookup value
[WHEN] Sales quote is archived
[THEN] Archived sales quote has lookup value from sales quote
[SCENARIO #0020] Archive sales return order with lookup value
[GIVEN] A sales return order with a lookup value
[WHEN] Sales return order is archived
[THEN] Archived sales return order has lookup value from sales return order
应用代码
数据模型扩展由以下.al对象实现:
tableextension 50009 "SalesHeaderArchiveTableExt"
extends "Sales Header Archive"
{
fields
{
field(50000; "Lookup Value Code"; Code[10])
{
Caption = 'Lookup Value Code';
DataClassification = ToBeClassified;
TableRelation = "LookupValue";
}
}
}
然后,UI 根据场景#0019进行了扩展。场景#0018和#0020也将非常相似:
pageextension 50042 "SalesQuoteArchivePageExt"
extends "Sales Quote Archive"
{
layout
{
addlast(General)
{
field("Lookup Value Code"; "Lookup Value Code")
{
ToolTip = 'Specifies the lookup value the
transaction is done for.';
ApplicationArea = All;
}
}
}
}
pageextension 50045 "SalesQuoteArchivesPageExt"
extends "Sales Quote Archives"
{
layout
{
addfirst(Control1)
{
field("Lookup Value Code"; "Lookup Value Code")
{
ToolTip = 'Specifies the lookup value the
transaction is done for.';
ApplicationArea = All;
}
}
}
}
测试代码
现在应用程序代码已设置好,接下来我们来看一下测试代码。
创建、嵌入并编写
通过“创建、嵌入和编写”这一大步骤,测试故事#0018、#0019和#0020可能如下所示,当它们被放入新的测试代码单元中:
codeunit 81004 "LookupValue Sales Archive"
{
Subtype = Test;
//[FEATURE] LookupValue Sales Archive
[Test]
procedure ArchiveSalesOrderWithLookupValue();
begin
//[SCENARIO #0018] Archive sales order with lookup value
//[GIVEN] A sales order with a lookup value
CreateSalesOrderWithLookupValue();
//[WHEN] Sales order is archived
ArchiveSalesOrderDocument();
//[THEN] Archived sales order has lookup value from
// sales order
VerifyLookupValueOnSalesOrderArchive();
end;
[Test]
procedure ArchiveSalesQuoteWithLookupValue();
begin
//[SCENARIO #0019] Archive sales quote with lookup value
//[GIVEN] A sales quote with a lookup value
CreateSalesQuoteWithLookupValue();
//[WHEN] Sales quote is archived
ArchiveQuoteDocument();
//[THEN] Archived sales quote has lookup value from
// sales quote
VerifyLookupValueOnSalesQuoteArchive();
end;
[Test]
procedure ArchiveSalesReturnOrderWithLookupValue();
begin
//[SCENARIO #0020] Archive sales return order with lookup
// value
//[GIVEN] A sales return order with a lookup value
CreateSalesReturnOrderWithLookupValue();
//[WHEN] Sales return order is archived
ArchiveSalesReturnOrderDocument();
//[THEN] Archived sales return order has lookup value from
// sales return order
VerifyLookupValueOnSalesReturnOrderArchive();
end;
}
构建真实代码
当三个场景都在测试归档销售文档的过程时,它们归结为一个通用故事,唯一的变量是文档类型——报价单、订单或退货订单。因此,我们可以将其浓缩成一个测试故事:
[Test]
procedure ArchiveSalesDocumentWithLookupValue();
begin
//[SCENARIO #....] Archive sales document with lookup
// value
//[GIVEN] A sales document with a lookup value
CreateSalesDocumentWithLookupValue();
//[WHEN] Sales document is archived
ArchiveSalesDocumentDocument();
//[THEN] Archived sales document has lookup value from
// sales document
VerifyLookupValueOnSalesDocumentArchive();
end;
如前所述,我们无法参数化test函数,但我们可以将其转化为一个本地方法,从三个测试中调用该方法:
local procedure ArchiveSalesDocumentWithLookupValue(
DocumentType: Option
Quote,Order,Invoice,
"Credit Memo","Blanket Order",
"Return Order"): Code[20]
var
SalesHeader: record "Sales Header";
begin
//[GIVEN] A sales document with a lookup value
CreateSalesDocumentWithLookupValue(SalesHeader, DocumentType);
//[WHEN] Sales document is archived
ArchiveSalesDocument(SalesHeader);
//[THEN] Archived sales document has lookup value from sales
// document
VerifyLookupValueOnSalesDocumentArchive(
DocumentType,
SalesHeader."No.",
SalesHeader."Lookup Value Code",
1); // Used 1 for No. of Archived Versions
exit(SalesHeader."No.")
end;
这三个测试将变为:
[Test]
procedure ArchiveSalesOrderWithLookupValue();
var
SalesHeader: record "Sales Header";
begin
//[SCENARIO #0018] Archive sales order with lookup value
ArchiveSalesDocumentWithLookupValue(
SalesHeader."Document Type"::Order)
end;
[Test]
procedure ArchiveSalesQuoteWithLookupValue();
var
SalesHeader: record "Sales Header";
begin
//[SCENARIO #0019] Archive sales quote with lookup value
ArchiveSalesDocumentWithLookupValue(
SalesHeader."Document Type"::Quote)
end;
[Test]
procedure ArchiveSalesReturnOrderWithLookupValue();
var
SalesHeader: record "Sales Header";
begin
//[SCENARIO #0020] Archive sales return order with lookup value
ArchiveSalesDocumentWithLookupValue(
SalesHeader."Document Type"::"Return Order")
end;
复制并粘贴:一举三得。
前往 GitHub 查看其他辅助函数的实现以及额外的场景#00021。
测试执行
给我看看绿色的成功:
哎呀… 红色?
显然,正如测试工具中的测试错误所指示的,我们需要处理一个Confirm。让我们进入应用程序,尝试归档一个销售订单。
为了实现这一点,请按以下步骤操作:
-
使用Alt + Q,即“告诉我你想要什么”功能
-
输入
Sales Orders并选择“销售订单”超链接,打开Sales Orders页面 -
打开第一个销售订单的文档页面
-
选择操作 | 功能 | 归档文档
确实,这里会弹出一个对话框,要求用户确认(或不确认):
看看当我们在确认对话框中点击“是”时会发生什么:会出现一条消息,告知用户文档已被归档,如下图所示:
一旦用户在消息对话框中点击“确定”,文档的归档就完成了。对于我们的测试自动化,我们需要创建两个处理程序函数——一个处理确认对话框,另一个处理消息,如下所示:
[ConfirmHandler]
procedure ConfirmHandlerYes(Question: Text[1024]; var Reply: Boolean);
begin
Reply := true;
end;
[MessageHandler]
procedure MessageHandler(Message: Text[1024]);
begin
end;
两个处理程序实现得很简单;它们只会处理对话框,而不会检查任何内容。我将在下一个示例中对此做更详细的说明。
使用HandlerFunctions标签将它们链接到我们的测试:
[HandlerFunctions('ConfirmHandler,MessageHandler')]
场景#0018的测试代码单元将变成:
[Test]
[HandlerFunctions('ConfirmHandler,MessageHandler')]
procedure ArchiveSalesOrderWithLookupValue();
var
SalesHeader: record "Sales Header";
begin
//[SCENARIO #0018] Archive sales order with lookup value
ArchiveSalesDocumentWithLookupValue(
SalesHeader."Document Type"::Order)
end;
现在,再次运行它!请展示给我们绿色的结果:
测试测试
你知道该怎么做。是的,你知道,对吧?
漏掉的场景?
本书的一位重要评审者 Steven Renders 提醒我,客户需求的场景中存在一个空白,即在归档销售单据时,查找值应该被传递到归档后的销售单据中。在我进入具体细节之前,这正好是我在第四章中提到的一个完美例子,测试设计:“测试设计是一个帮助团队讨论他们测试工作、揭示思维漏洞的工具……”
那么,这个空白是什么?如果你有一个确认对话框,询问用户是否选择“是”或“否”,那么至少有两个场景需要测试,而我的场景只处理“是”。那么,“否”呢?这确实是一个用户场景,但我不认为它是我们客户需求范围内需要测试的场景。它是一个与归档销售单据的大功能相关的场景。因此,我们没有将此场景添加到我们的集合中,假设这将通过标准测试来处理。
然而,在未来的任何项目中,只要使用确认语句时都会触发它们,因为原则上,这些语句至少会导致两个场景。
测试示例 6 – 如何将数据交给 UI 处理程序
就像之前的测试示例中,我们遇到了需要两个对话框处理程序的情况,现在有必要讨论如何将数据交给 UI 处理程序,因为我们无法直接控制它。然而,平台是可以控制的,而且参数列表是固定的。
客户需求
在这个上下文中,我们提取了客户需求的另一部分——当通过点击功能区上的标准“新建”操作创建新客户时,用户必须选择一个模板来基于该模板创建新客户(或者通过选择“取消”来绕过此步骤),如下图所示:
我们已经在上一章的测试示例 3 中处理过 ModalPage 的显示。客户需求的这一部分告诉我们,用户可以选择的模板背后应该设置好配置模板,以便它会自动填充新创建客户的查找值代码字段。
这就是场景#0028的内容:
[FEATURE] LookupValue Inheritance [SCENARIO #0028] Create customer from configuration template with
lookup value
[GIVEN] A configuration template (customer) with lookup value
[WHEN] Create customer from configuration template
[THEN] Lookup value on customer is populated with lookup value of
configuration template
我们可以通过设置配置模板来实现这一点。无需任何应用程序代码。
测试代码
让我们将场景#0028包装在一个新的测试代码单元中。
创建、嵌入并写入
这将导致以下代码构建:
codeunit 81006 "LookupValue Inheritance"
{
Subtype = Test;
[Test]
procedure
InheritLookupValueFromConfigurationTemplateToCustomer();
begin
//[SCENARIO #0028] Create customer from configuration
// template with lookup value
Initialize();
//[GIVEN] A configuration template (customer) with lookup
// value
CreateCustomerConfigurationTemplateWithLookupValue();
//[WHEN] Create customer from configuration template
CreateCustomerFromConfigurationTemplate();
//[THEN] Lookup value on customer is populated with lookup
// value of configuration template
VerifyLookupValueOnCustomer();
end;
}
构建实际代码
包括所有技术细节,如变量和参数,这个代码单元将变成:
codeunit 81006 "LookupValue Inheritance"
{
Subtype = Test;
[Test]
[HandlerFunctions('HandleConfigTemplates')]
procedure
InheritLookupValueFromConfigurationTemplateToCustomer();
var
CustomerNo: Code[20];
ConfigTemplateHeaderCode: Code[10];
LookupValueCode: Code[10];
begin
//[SCENARIO #0028] Create customer from configuration
// template with lookup value
Initialize();
//[GIVEN] A configuration template (customer) with lookup
// value
ConfigTemplateHeaderCode :=
CreateCustomerConfigurationTemplateWithLookupValue(
LookupValueCode);
//[WHEN] Create customer from configuration template
CustomerNo :=
CreateCustomerFromConfigurationTemplate(
ConfigTemplateHeaderCode);
//[THEN] Lookup value on customer is populated with lookup
// value of configuration template
VerifyLookupValueOnCustomer(CustomerNo, LookupValueCode);
end;
}
我们需要创建以下四个辅助函数和一个 UI 处理程序:
-
Initialize -
CreateCustomerConfigurationTemplateWithLookupValue -
CreateCustomerFromConfigurationTemplate -
VerifyLookupValueOnCustomer -
HandleConfigTemplates
所需的五个过程中的两个可以继承自早期的测试示例:
-
Initialize负责处理 Lookup 值,可以从测试示例 4 中复制 -
VerifyLookupValueOnCustomer可以从测试示例 1 中获取
另外三个函数,CreateCustomerConfigurationTemplateWithLookupValue、CreateCustomerFromConfigurationTemplate和HandleConfigTemplates,将如下所示。函数名称准确地描述了该函数的作用。我会让你自己阅读并理解前两个函数的含义。在这个测试示例中,我们将更多地阐述HandleConfigTemplates:
local procedure CreateCustomerConfigurationTemplateWithLookupValue(
LookupValueCode: Code[10]): Code[10]
// Adopted from Codeunit 132213 Library - Small Business
var
ConfigTemplateHeader: record "Config. Template Header";
Customer: Record Customer;
begin
LibraryRapidStart.CreateConfigTemplateHeader(
ConfigTemplateHeader);
ConfigTemplateHeader.Validate("Table ID", Database::Customer);
ConfigTemplateHeader.Modify(true);
LibrarySmallBusiness.CreateCustomerTemplateLine(
ConfigTemplateHeader,
Customer.FieldNo("Lookup Value Code"),
Customer.FieldName("Lookup Value Code"),
LookupValueCode);
exit(ConfigTemplateHeader.Code);
end;
local procedure CreateCustomerFromConfigurationTemplate(
ConfigurationTemplateCode: Code[10]) CustomerNo: Code[20]
var
CustomerCard: TestPage "Customer Card";
begin
CustomerCard.OpenNew();
CustomerNo := CustomerCard."No.".Value();
CustomerCard.Close();
end;
[ModalPageHandler]
procedure HandleConfigTemplates(
var ConfigTemplates: TestPage "Config Templates")
begin
ConfigTemplates.GoToKey(
<provide the PK of the Config Template>);
ConfigTemplates.OK.Invoke();
end;
一旦在CreateCustomerFromConfigurationTemplate中创建了新的客户卡片,就需要通过ModalPageHandler的HandleConfigTemplates来处理Config Templates页面。在配置模板列表中,它应该选择由CreateCustomerConfigurationTemplateWithLookupValue创建的配置模板。通过TestPage的GoToKey方法,我们可以实现这一点,但需要提供模板的 PK 值,如前面代码中的三角括号所标注的。
一个简单的解决方案是创建一个名为ConfigTemplateCode的全局变量,并在我们的测试的[GIVEN]部分填充它,如下所示:
ConfigTemplateCode :=
CreateCustomerConfigurationTemplateWithLookupValue(
LookupValueCode);
这将依次被我们的ModalPageHandler捕获。这无疑是一个完全有效的解决方案。但想象一下,你不得不在一个测试代码单元中传递多个不同类型的数据值,不断堆叠全局变量。为了解决这个问题,微软为我们提供了一个很棒的功能,在代码单元中实现了Library - Variable Storage。它由一个包含 25 个变体元素的队列组成。通过使用Enqueue和Dequeue,你可以以先进先出的方式存储和检索变量。
Enqueue
在处理程序触发之前,在测试代码中调用Enqueue,如下所示:
//[GIVEN] A configuration template (customer) with lookup
// value
ConfigTemplateCode :=
CreateCustomerConfigurationTemplateWithLookupValue(
LookupValueCode);
//[WHEN] Create customer from configuration template
LibraryVariableStorage.Enqueue(ConfigTemplateCode);
CustomerNo :=
CreateCustomerFromConfigurationTemplate(
ConfigTemplateCode);
Dequeue
在处理程序中,调用Dequeue来检索变量,如下所示:
[ModalPageHandler]
procedure HandleConfigTemplates(
var ConfigTemplates: TestPage "Config Templates")
var
ConfigTemplateCode: Code[10];
"Value": Variant;
begin
LibraryVariableStorage.Dequeue("Value");
ConfigTemplateCode:= "Value";
ConfigTemplates.GoToKey(ConfigTemplateCode);
ConfigTemplates.OK.Invoke();
end;
测试执行
祈祷结果是绿色的:
成功!注意代码单元81006的第一行测试函数LookupValue Inheritance,它包含另一个场景#0024,由测试函数InheritLookupValueFromCustomerOnSalesDocument实现。
测试测试
到目前为止,你已经知道如何调整测试以便让验证失败。但是队列能否正确执行它的任务呢?试试将一个不存在的配置模板代码加入队列怎么样?我们随便选一个—LUC。
现在运行测试会抛出以下错误:
Unexpected CLR exception thrown.: Microsoft.Dynamics.Framework.UI.FormAbortException: Page New - Customer Card has to close ---> Microsoft.Dynamics.Nav.Types.Exceptions.NavTestRowNotFoundException: The row does not exist on the TestPage. ---> System.
错误消息没有提到行键值,但它明确告诉我们无法找到测试想要选择的行—LUC。
总结
通过构建三个额外的测试示例,我们学习了如何设置共享固定装置、如何参数化测试,以及如何将变量传递给 UI 处理程序。这三项技术将在你未来的测试自动化实践中具有无价的价值。
在下一章,我们将向你的测试工具包中添加两个工具。你将学习如何测试报告数据集以及如何处理更复杂的场景。
995

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



