微软-Dynamic365-自动化测试-全-
微软 Dynamic365 自动化测试(全)
原文:
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 处理程序。这三项技术将在你未来的测试自动化实践中具有无价的价值。
在下一章,我们将向你的测试工具包中添加两个工具。你将学习如何测试报告数据集以及如何处理更复杂的场景。
第八章:从客户需求到测试自动化——以及更多
现在掌握了吧?嗯,你已经做到这一步了,对吧?让我们再来一个 路上的最后一课,实际上是两个。我们将用最后一对测试工具补充你的工具箱,适用于 Microsoft Dynamics 365 Business Central。在这一章中,我们将扩展以下内容:
-
测试报告
-
设置更复杂的场景
测试示例 7 – 如何测试报告
报告一直是许多 Business Central 项目和解决方案的重要组成部分。考虑到这一点,了解如何以自动化方式测试它们是非常有意义的。那么,我们该如何进行呢?在这个例子中,我们将演示如何测试由报告创建的数据集。布局测试是另一项工作,属于测试框架之外的任务。
客户需求
我们客户的需求描述了客户的 Lookup Value Code 字段必须传递到各类销售单据中。尽管没有明确说明,合乎逻辑的推论是,这些单据的每个打印版本都必须添加此字段。请注意,在此时此刻,我们无法扩展标准报告。扩展标准报告只能通过将其克隆到我们的扩展中来实现。由于销售单据报告在数据集和布局方面都相当复杂,我们采取了一个更简单的例子。我们将克隆报告 101,Customer - List,并将 Lookup Value Code 字段添加到其中,如下一个截图所示:

这可能是 ATDD 测试用例描述的一个翻译:
[FEATURE] LookupValue Report
[SCENARIO #0029] Test that lookup value shows on CustomerList
report
[GIVEN] 2 customers with different lookup value
[WHEN] Run report CustomerList
[THEN] Report dataset contains both customers with lookup value
为什么是两个客户呢?你可能会问。因为报告应该能够列出多个客户,因此不只测试一个客户是有意义的。
应用代码
克隆报告的简化版本显示了我们在本页及下一页中添加了 Lookup Value Code 字段。前面的截图显示了该字段在布局中的位置:
Report 50000 "CustomerList"
{
//Converted from standard report 101 "Customer - List"
DefaultLayout = RDLC;
RDLCLayout = './Report Layouts/CustomerList.rdlc';
ApplicationArea = Basic, Suite;
Caption = 'Customer List';
UsageCategory = ReportsAndAnalysis;
dataset
{
dataitem(Customer; Customer)
{
...
column(Customer_No_; "No.")
{
IncludeCaption = true;
}
... }
column(Customer_Phone_No_; "Phone No.")
{
IncludeCaption = true;
}
column(Customer_Lookup_Value_Code;
"Lookup Value Code")
{
IncludeCaption = true;
}
...
}
}
requestpage
{
...
}
Labels
{
...
}
...
}
测试代码
看一下我们对场景 #0029 的 .al 实现。
创建、嵌入并编写
创建代码单元,嵌入 ATDD 场景并编写故事将产生以下结果:
codeunit 81008 "Lookup Value Report"
{
Subtype = Test;
//[FEATURE] LookupValue Report
[Test]
[HandlerFunctions('CustomerListRequestPageHandler')]
procedure TestLookupValueShowsOnCustomerListReport();
var
Customer: array[2] of Record Customer;
begin
//[SCENARIO #0029] Test that lookup value shows on
// CustomerList report
Initialize();
//[GIVEN] 2 customers with different lookup value
CreateCustomerWithLookupValue(Customer[1]);
CreateCustomerWithLookupValue(Customer[2]);
//[WHEN] Run report CustomerList
CommitAndRunReportCustomerList();
//[THEN] Report dataset contains both customers with
// lookup value
VerifyCustWithLookupValueOnCustListReport(
Customer[1]."No.", Customer[1]."Lookup Value Code");
VerifyCustWithLookupValueOnCustListReport(
Customer[2]."No.", Customer[2]."Lookup Value Code");
end;
}
构建真实的代码
看看这些辅助函数是如何呈现的。
Initialize 确保我们的报告只会选择两个新创建的客户,通过删除数据库中所有现有的客户记录;由于测试将在隔离状态下运行,这一删除操作将在后续被还原:
local procedure Initialize()
var
Customer: record Customer;
begin
if isInitialized then
exit;
Customer.DeleteAll();
isInitialized := true;
Commit();
end;
CreateCustomerWithLookupValue 和 CreateLookupValueCode 成为我们下一个邻居,几乎可以在所有场景中帮助我们:
local procedure CreateCustomerWithLookupValue(
var Customer: Record Customer)
begin
LibrarySales.CreateCustomer(Customer);
with Customer do begin
Validate("Lookup Value Code",CreateLookupValueCode());
Modify();
end;
end;
local procedure CreateLookupValueCode(): Code[10]
var
LookupValue: Record LookupValue;
begin
//for implementation see test example 1; this smells like
//duplication ;-) again
end;
测试报告数据集就是浏览其 XML 结构,因此,CommitAndRunReportCustomerList 在代码单元 Library - Report Dataset (131007) 中调用 RunReportAndLoad,将数据流式传输到临时 TempBlob 记录(表 99008535)中,以便在验证部分使用:
local procedure CommitAndRunReportCustomerList()
var
CustomerListReport: Report CustomerList;
RequestPageXML: Text;
begin
Commit(); // close open write transaction to be able to
// run the report
RequestPageXML := Report.RunRequestPage(
Report::CustomerList,
RequestPageXML);
LibraryReportDataset.RunReportAndLoad(
Report::CustomerList,
'',
RequestPageXML);
end;
[RequestPageHandler]
procedure CustomerListRequestPageHandler(
var CustomerListRequestPage:
TestRequestPage CustomerList)
begin
// Empty handler used to close the request page, default
// settings are used
end;
在VerifyCustWithLookupValueOnCustListReport中,我们看到FindRow读取客户号(列Customer_No_)和我们的查找值(列Customer_Lookup_Value_Code),并确定它们的行位置:
local procedure VerifyCustWithLookupValueOnCustListReport(
No: Code[20]; LookupValueCode: Code[10])
var
Row: array[2] of Integer;
begin
Row[1] := LibraryReportDataset.FindRow(
'Customer_No_',
No);
Row[2] := LibraryReportDataset.FindRow(
'Customer_Lookup_Value_Code',
LookupValueCode);
Assert.AreEqual(
13, Row[2] - Row[1],
'Delta between columns Customer_No_ and
Customer_Lookup_Value_Code')
end;
再多一些笔记:
-
报表数据集定义中的元素称为列。在
Library - Report Dataset代码单元中,因此在测试中,术语行也被用来指代生成的数据集 XML 中的行。 -
注意,行之间的差值被验证为
13,这意味着,参考前面的注释,列Customer_Lookup_Value_Code是数据集中列Customer_No_后的第 13 列。 -
出于合理的理由,您可能会想知道这是否是一个相关的检查;当行数计算不再正确时,它肯定会出错,正如我们稍后将看到的那样。
-
您可能已经注意到:全局变量声明已被省略。
测试执行
它正在成为一种习惯:绿色,绿色,绿色!

测试测试
来吧!我们自己控制它。让我们来做吧。
调整测试以使验证出错
改变一下怎么样…
-
在
VerifyCustWithLookupValueOnCustListReport中的硬编码的13为56,或 -
它的
LookupValueCode参数在从测试函数调用时
这些将分别引发以下错误:
Assert.AreEqual failed. Expected:<56> (Integer). Actual:<13> (Integer). Delta between columns Customer_No_ and Customer_Lookup_Value_Code.
Assert.AreEqual failed. Expected:<13> (Integer). Actual:<-4> (Integer). Delta between columns Customer_No_ and Customer_Lookup_Value_Code.
后者可能有些意外,因为它不像之前那样显示出可比性,指定LUC作为预期值等等。在这里,我们的验证是通过FindRow方法完成的,因为在数据集中找不到LUC,FindRow将返回-1,并且由于Customer_No_在行[3]上,数学运算将导致-4。
测试示例 8 - 如何构建一个广泛的场景
作为 Dynamics 365 Business Central 的终端用户,要实现您的目标,通常需要执行一系列连续动作。如何为此构建测试套件?如何创建可重用部分?以及如何利用 Microsoft 测试库中已经存在的辅助功能?
为了说明这一点,我们详细介绍客户愿望的另一部分。
客户愿望
在我们客户愿望的业务逻辑描述中提到:
在从销售订单创建仓库发货时,应该从销售头传递 Lookup Value Code 字段到仓库发货行。
这在以下两种情况中表达:
[FEATURE] LookupValue Warehouse Shipment
[SCENARIO #0030] Create warehouse shipment from sales order with
lookup value
[GIVEN] A lookup value
[GIVEN] A location with require shipment
[GIVEN] A warehouse employee for current user
[WHEN] Create warehouse shipment from released sales order with
lookup value and with line with require shipment location
[THEN] Warehouse shipment line has lookup value code field
populated
[SCENARIO #0031] Get sales order with lookup value on warehouse
shipment
[GIVEN] A lookup value
[GIVEN] A location with require shipment
[GIVEN] A warehouse employee for current user
[GIVEN] A released sales order with lookup value and with line
with require shipment location
[GIVEN] A warehouse shipment without lines
[WHEN] Get sales order with lookup value on warehouse shipment
[THEN] Warehouse shipment line has lookup value code field
populated
然而,这意味着Lookup Value字段已经存在于Warehouse Shipment Line和Posted Whse. Shipment Line表中,这是我们列表中的三个基本场景定义的结果,我们到目前为止跳过了:
[SCENARIO #0015] Assign lookup value to warehouse shipment line
[GIVEN] A lookup value
[GIVEN] A location with require shipment
[GIVEN] A warehouse employee for current user
[GIVEN] A warehouse shipment from released sales order with line
with require shipment location
[WHEN] Set lookup value on warehouse shipment line
[THEN] Warehouse shipment line has lookup value code field
populated
[SCENARIO #0016] Assign non-existing lookup value on warehouse
shipment line
[GIVEN] A non-existing lookup value
[GIVEN] A warehouse shipment line record variable
[WHEN] Set non-existing lookup value to warehouse shipment line
[THEN] Non existing lookup value error was thrown
[SCENARIO #0017] Assign lookup value to warehouse shipment line on
warehouse shipment document page
[GIVEN] A lookup value
[GIVEN] A location with require shipment
[GIVEN] A warehouse employee for current user
[GIVEN] A warehouse shipment from released sales order with line
with require shipment location
[WHEN] Set lookup value on warehouse shipment line on warehouse
shipment document page
[THEN] Warehouse shipment line has lookup value code field
populated
由于场景相关,当我们在制定所有五个场景时,发现一致的部分并不奇怪。这是一条愉快地通知我们,我们将能够构建可重用部分并节省时间的消息。
这些是显而易见的:
[GIVEN] A lookup value
[GIVEN] A location with require shipment
[GIVEN] A warehouse employee for current user
但你通常可以在代码中找到提示。比较下面从五个场景中的四个场景提取的内容:
[SCENARIO #0015]
[GIVEN] A warehouse shipment from released sales order with line
with require shipment location
[SCENARIO #0017]
[GIVEN] A warehouse shipment from released sales order with line
with require shipment location
[SCENARIO #0030]
[WHEN] Create warehouse shipment from released sales order with
lookup value and with line with require shipment location
[SCENARIO #0031]
[GIVEN] A released sales order with lookup value and with line
with require shipment location
这告诉我们,在所有这些情况下,我们需要一个已发布的销售订单,并带有查找值。
应用代码
针对场景#0015、#00016和#0017,Warehouse Shipment Line和Posted Whse. Shipment Line表格以及它们相关页面的扩展,通过以下代码实现(为了节省空间,这里仅展示最简代码):
tableextension 50007 "WhseShipmentLineTableExt"
extends "Warehouse Shipment Line"
{
fields
{
field(50000; "Lookup Value Code"; Code[10]){}
}
}
tableextension 50008 "PstdWhseShipmentLineTableExt"
extends "Posted Whse. Shipment Line"
{
fields
{
field(50000; "Lookup Value Code"; Code[10]){}
}
}
pageextension 50034 "WhseShipmentSubformPageExt"
extends "Whse. Shipment Subform"
{
layout
{
addlast(Control1)
{
field("Lookup Value Code"; "Lookup Value Code"){}
}
}
}
pageextension 50036 "PstdWhseShipmentSubformPageExt"
extends "Posted Whse. Shipment Subform"
{
layout
{
addlast(Control1)
{
field("Lookup Value Code"; "Lookup Value Code"){}
}
}
}
pageextension 50035 "WhseShipmentLinesPageExt"
extends "Whse. Shipment Lines"
{
layout
{
addlast(Control1)
{
field("Lookup Value Code"; "Lookup Value Code"){}
}
}
}
为了启用场景#0030和#0031,只需用以下代码扩展标准应用程序:
codeunit 50002 "WhseCreateSourceDocumentEvent"
{
[EventSubscriber(ObjectType::Codeunit,
Codeunit::"Whse.-Create Source Document",
'OnBeforeCreateShptLineFromSalesLine', '', false, false)]
local procedure OnBeforeCreateShptLineFromSalesLineEvent(
var WarehouseShipmentLine:
Record "Warehouse Shipment Line";
WarehouseShipmentHeader:
Record "Warehouse Shipment Header";
SalesLine: Record "Sales Line";
SalesHeader: Record "Sales Header")
begin
with WarehouseShipmentLine do
"Lookup Value Code" :=
SalesHeader."Lookup Value Code";
end;
}
订阅者OnBeforeCreateShptLineFromSalesLineEvent确保销售单据上Lookup Value Code字段的值被复制到仓库发货单行的Lookup Value Code字段。
测试代码
让我们实现四个重叠的场景#0015、#0017、#0030和#0031。
创建、嵌入和编写
codeunit 81005 "LookupValue Posting"
{
Subtype = Test;
//[FEATURE] LookupValue Warehouse Shipment
[Test]
procedure AssignLookupValueToWarehouseShipmentLine()
begin
//[SCENARIO #0015] Assign lookup value to warehouse
// shipment line on warehouse shipment
// page
//[GIVEN] A lookup value
//[GIVEN] A location with require shipment
//[GIVEN] A warehouse employee for current user
Initialize();
//[GIVEN] A warehouse shipment from released sales order
// with line with require shipment location
SalesOrderNo :=
CreateWarehouseShipmentFromSalesOrder(
DefaultLocation,
UseNoLookupValue());
//[WHEN] Set lookup value on warehouse shipment line on
// warehouse shipment page
FindAndSetLookupValueOnWarehouseShipmentLine(
SalesOrderNo,
LookupValueCode);
//[THEN] Warehouse shipment line has lookup value code
// field populated
VerifyLookupValueOnWarehouseShipmentLine(
SalesOrderNo,
LookupValueCode);
end;
[Test]
procedure
AssignLookupValueToLineOnWarehouseShipmentDocument()
begin
//[SCENARIO #0017] Assign lookup value to warehouse
// shipment line on warehouse shipment
// page
//[GIVEN] A lookup value
//[GIVEN] A location with require shipment
//[GIVEN] A warehouse employee for current user
Initialize();
//[GIVEN] A warehouse shipment from released sales order
// with line with require shipment location
SalesOrderNo :=
CreateWarehouseShipmentFromSalesOrder(
DefaultLocation,
UseNoLookupValue());
//[WHEN] Set lookup value on warehouse shipment line on
// warehouse shipment document page
SetLookupValueOnLineOnWarehouseShipmentDocumentPage(
SalesOrderNo);
//[THEN] Warehouse shipment line has lookup value code
// field populated
VerifyLookupValueOnWarehouseShipmentLine(
SalesOrderNo,
LookupValueCode);
end;
[Test]
procedure
CreateWarehouseShipmentFromSalesOrderWithLookupValue()
begin
//[SCENARIO #0030] Create warehouse shipment from sales
// order with lookup value
//[GIVEN] A lookup value
//[GIVEN] A location with require shipment
//[GIVEN] A warehouse employee for current user
Initialize();
//[WHEN] Create warehouse shipment from released sales
// order with lookup value and with line with
// require shipment location
SalesOrderNo :=
CreateWarehouseShipmentFromSalesOrder(
DefaultLocation,
UseLookupValue());
//[THEN] Warehouse shipment line has lookup value code
// field populated
VerifyLookupValueOnWarehouseShipmentLine(
SalesOrderNo,
LookupValueCode);
end;
[Test]
procedure GetSalesOrderWithLookupValueOnWarehouseShipment()
begin
//[SCENARIO #0031] Get sales order with lookup value on
// warehouse shipment
//[GIVEN] A lookup value
//[GIVEN] A location with require shipment
//[GIVEN] A warehouse employee for current user
Initialize();
//[GIVEN] A released sales order with lookup value and
// with line with require shipment location
CreateAndReleaseSalesOrder(
SalesHeader,
DefaultLocation,
UseLookupValue());
//[GIVEN] A warehouse shipment without lines
WarehouseShipmentNo :=
CreateWarehouseShipmentWithOutLines(
DefaultLocation."Code");
//[WHEN] Get sales order with lookup value on warehouse
// shipment
GetSalesOrderShipment(WarehouseShipmentNo);
//[THEN] Warehouse shipment line has lookup value code
// field populated
VerifyLookupValueOnWarehouseShipmentLine(
SalesHeader."No.",
LookupValueCode);
end;
}
构建实际的代码
在 GitHub 上的代码中,我们将留给你查看这些测试的主要实现部分。然而,在这里,我们将更详细地研究场景#0030,其中[GIVEN]和[THEN]部分与其他三个场景共享。回到我们开始这最后一个测试示例时提出的问题,目标是向你展示:
-
可以通过创建和使用可重用的部分来构建一个复杂的场景
-
在标准库中找到尽可能多的可重用部分
Initialize
由于Initialize是第一个被共享的可重用部分,部分数据设置已经在处理中了:
[GIVEN] A lookup value
[GIVEN] A location with require shipment
[GIVEN] A warehouse employee for current user
代码如下:
local procedure Initialize()
var
WarehouseEmployee: Record "Warehouse Employee";
begin
if isInitialized then
exit;
LookupValueCode := CreateLookupValueCode();
LibraryWarehouse.CreateLocationWMS(
DefaultLocation, false, false, false, false, true);
LibraryWarehouse.CreateWarehouseEmployee(
WarehouseEmployee, DefaultLocation."Code", false);
isInitialized := true;
Commit();
end;
到目前为止,Initialize的结构应该看起来很熟悉,包括使用全局布尔变量isInitialized、全局变量Code[10]的LookupValueCode,以及本地辅助函数CreateLookupValueCode,并且解释了为什么CreateLookupValueCode可以作为懒加载的一部分。
如何将创建位置(带有要求发货)和仓库员工的过程嵌入到Initialize中,因为这两个过程可以在四个场景之间轻松共享。为此,DefaultLocation被设置为一个全局记录变量(基于Location表)。仓库员工不需要存储,因为它将从数据库中检索。
如你所见,使用了两个辅助函数,通过一个基于标准代码单元Library - Warehouse的库代码单元变量LibraryWarehouse来调用。使用在第三章中提到的简单快速文件搜索方法,我查找了一个用于创建位置的辅助函数,搜索字符串为 CreateLocation,以及一个用于创建仓库员工的函数,搜索字符串为 CreateWarehouseEmployee。
运行测试函数后,我们最终会明确地实现场景#0015、#0017、#0030和#0031,这表明使用Initialize函数不仅能创建更易于维护和理解的代码,而且还大大提高了效率,因此对于自动化测试而言是必不可少的。
在单独运行所有四个测试 10 次(从而触发Initialize,仿佛是一个全新的设置)并且将四个测试一次性运行 10 次(现在触发Initialize以获得共享设置)之后,结果表明后一种方式的速度提高了超过 30%。
请注意,在这两种情况下,场景#0015的速度是一样快的,因为它总是让Initialize完全运行。

VerifyLookupValueOnWarehouseShipmentLine
使用VerifyLookupValueOnWarehouseShipmentLine,找到了第二个可重用的部分。它与之前示例中各种VerifyLookupValueOn辅助函数非常相似。因此,通过实践 Business Central 开发者的美德,编写VerifyLookupValueOnWarehouseShipmentLine的任务变得非常简单:复制、粘贴并调整。这个任务留给你完成。
CreateWarehouseShipmentFromSalesOrder
如果你有使用过 Business Central 仓库发货功能的经验,你会知道创建一个仓库发货需要执行一系列步骤。这不像创建采购发票那样是单一的操作。
以下过程图展示了场景#0030中[WHEN]部分需要执行的任务:

基于此,.al实现变成了:
local procedure CreateWarehouseShipmentFromSalesOrder(
Location: Record Location;
WithLookupValue: Boolean): Code[20]
var
SalesHeader: Record "Sales Header";
begin
CreateAndReleaseSalesOrder(
SalesHeader,
Location,
WithLookupValue);
LibraryWarehouse.CreateWhseShipmentFromSO(SalesHeader);
exit(SalesHeader."No.");
end;
local procedure CreateAndReleaseSalesOrder(
var SalesHeader: record "Sales Header"; Location: Record Location; WithLookupValue: Boolean)
var
SalesLine: record "Sales Line";
begin
LibrarySales.CreateSalesDocumentWithItem(
SalesHeader, SalesLine, SalesHeader."Document Type"::Order,
'', '', 1, Location."Code", 0D);
with SalesHeader do
if WithLookupValue then begin
Validate("Lookup Value Code", LookupValueCode);
Modify();
end;
LibrarySales.ReleaseSalesDocument(SalesHeader);
end;
除了已经遇到的全局代码单元变量LibraryWarehouse,我们还使用了LibrarySales,基于标准代码单元Library - Sales,以及LibraryRandom,基于标准代码单元Library - Random。这三者都是搜索的结果:
| 一个辅助方法 | 通过搜索字符串找到 |
|---|---|
| 从销售订单创建仓库发货 | 过程CreateWhseShipmentFrom |
| 创建一个包含项目和位置的销售单据 | 过程CreateSalesDocumentWith |
| 发布销售单据 | 过程ReleaseSalesDoc |
| 生成一个随机数 | 过程Random,接着是过程Rand |
我们创建了易于阅读、可重用、简约的函数。CreateWarehouseShipmentFromSalesOrder被场景#0015、#0017和#0030使用。CreateAndReleaseSalesOrder直接被场景#0031使用,并间接被#0015、#0017和#0030使用。
在调用LibrarySales.CreateSalesDocumentWithItem时,两个空字符串触发了客户和项目的创建。
测试执行
跑,跑,跑啊啊啊啊。咆哮~绿色!

测试测试
现在我们已经克服了这个更大的挑战,让我们最后一次测试这个测试。
调整测试,使验证出现错误。
现在让我们通过为场景#0015、#0017、#0030和#0031提供另一个期望结果值LUC来调整测试:

重构
在本章中,实际上也是第三部分的整个内容,我们讨论了设计和编写自动化测试的各个方面,针对 Dynamics 365 Business Central 的情况。我们讨论并应用了最佳实践,比如可重用性、可读性和简约性,但我们并没有完全实现这些实践。你可能还记得在某些地方提到过某些代码部分有重复的嫌疑。这通常是重构代码的提示,以使其更具可重用性。我们在本书的范围内不会对其进行操作。但你可以在 GitHub 上找到已经重构的完整代码,重构后的代码创建了两个可重用的帮助方法库。此外,它还包括所有完成完整客户需求的场景,这些场景在第五章开头提到的从客户需求到测试自动化——基础中进行了讨论,但它们并未作为示例在书中使用。
同时:接受挑战,重构你自己创建的代码,特别是在实现前八个测试示例的过程中。拥有完整的应用程序和测试代码,使你能够对需要重构的部分进行重构,无论是应用程序代码还是测试代码。但只能是其中之一。如果重构应用程序代码导致之前成功的测试失败了,那么改进你的重构代码,使所有测试都通过。如果重构测试代码导致它们失败,而之前没有失败过,那么恢复并做得更好。
除了时间紧张,也许还有懒散的心情之外,现在没有理由不进行重构。现在就让你的代码发挥最大的作用。
对于尚未覆盖的代码,应该在进行任何重构之前先编写测试。如果不这样做,破坏某些东西而没有察觉的可能性非常高。
总结
在本章中,我们学习了如何测试报告数据集,以及如何构建更广泛的场景,确保测试代码具有可读性、可重用性,并且最重要的是,保持简约,后者通过使用标准帮助函数来实现。
在下一章,第八章,如何将测试自动化融入日常开发实践,我们将进入本书的最后部分,讨论如何将测试自动化融入日常开发实践中,包括微软提供的测试。
第四部分:将自动化测试集成到你的日常开发实践中
本节详细阐述了一些最佳实践,这些实践可能对你和你的团队在日常工作中实现测试自动化有所帮助,以及如何通过利用 Microsoft 提供的标准测试工具扩展你的测试实践,这些工具适用于 Dynamics 365 Business Central。
本节包含以下章节:
-
第八章,如何将测试自动化集成到日常开发实践中
-
第九章,让 Business Central 标准测试在你的代码中运行
第九章:如何将测试自动化融入日常开发实践
你已经读到了这本书的这一部分,所以现在你对 Dynamics 365 Business Central 的测试自动化需求和好处有了明确的认识。你也已经开始根据第三部分,为 Microsoft Dynamics 365 Business Central 设计和构建自动化测试,进行测试设计和编写。下一步是将你学到的知识付诸实践。
通过阅读本书、理解所讨论的问题并完成第一次练习,虽然将其作为日常工作的一部分可能仍然是一个门槛。如前所述,测试自动化是团队的共同努力。因此,在本章中,我们将详细说明一些最佳实践,这些实践可能对你和你的团队在实现测试自动化方面非常有帮助:
-
将客户需求转化为 ATDD 场景
-
迈出小步伐
-
让测试工具成为你的朋友
-
与日常构建集成
-
维护你的测试代码
将客户需求转化为 ATDD 场景
将测试自动化融入日常开发实践的关键是团队的采纳。与需求和应用代码一样,测试和测试代码应该由开发团队负责;不仅仅是形式上的,而是要积极地承担责任。良好的应用代码并非来自一句简单的客户需求,而是来自于一个详细且正式的客户需求。测试和测试代码也是如此。
正如在第四章中讨论的,测试设计,通过使用 ATDD 设计模式来规范化你的需求。将客户需求转化为 ATDD 场景。将每个需求分解为测试列表,并使其成为你沟通的主要工具:(1)详细描述你的客户需求,(2)实现你的应用代码,(3)有结构地执行你的手动测试,(4)编写你的测试自动化,(5)为你的解决方案提供最新的文档。你的测试自动化将是所有前期工作的逻辑结果。
由于开发人员既要编写应用代码,也要编写测试代码,而通常他们并不是最了解客户需求的人,因此 ATDD 场景已经实现了一举两得。可以利用我在 GitHub 上的 ATDD 场景 Excel 表格,名为 Clean sheet.xlsx,让你的团队开始将客户需求转化为 ATDD 场景。这正是我在第五章到第七章编写测试示例时所做的。请参见下面的截图以获取印象:

前七列供产品负责人、功能顾问和关键用户填写:
-
功能 -
子功能 -
UI -
场景 -
Given-When-Then(标签) -
Given-When-Then(描述) -
场景 #
最后的两列,ATDD 格式和代码格式,会自动填充,正如以下截图所示。注意,后者列包含绿色,也就是已经格式化并注释掉的GIVEN-WHEN-THEN场景。这些场景已经准备好可以复制并粘贴到你的测试代码单元中,嵌入到你的测试函数里:
目前,Jan Hoek 和我正在一起努力推进这一步。看看我们的ATDD.TestScriptor,它可以根据定义的 ATDD 功能,构建关联的.al测试代码单元的框架:powershellgallery.com/packages/ATDD.TestScriptor。
采取小步走
正如俗话所说,罗马不是一天建成的。同样,逐步掌握测试自动化。通过以下方式学习和改进:
-
开始将客户的需求转化为你想到的场景。尽量保持简单。理想情况下,你希望立即获得完整的覆盖率,但由于这是团队合作,他们会帮助你识别漏洞并填补它们。
-
利用我的4 步流程——创建、嵌入、编写、构建——来构思测试代码:
1. 创建测试代码单元
2. 将客户的需求嵌入到测试函数中
3. 编写你的测试故事
4. 构建实际代码
-
在每一步操作时都运行测试,并在代码可以部署时尽快进行验证。不要等到完成后再验证,而是尽早验证你的工作成果。观察你的测试从红色变为绿色。
-
把测试测试作为完成它的最后一步来享受。要么验证已创建的数据,要么通常更简单地调整测试,以使验证出错。
-
一个接一个地实现场景,发现自己在重复代码部分。不要强迫自己立即将这些代码抽象成库中的辅助方法。这可以等到应用程序和测试代码准备好后再进行重构。
-
定期运行测试,一旦应用程序代码和测试完成,或者当功能进行下一次更新时。在实施过程中,不要等到代码准备好,验证每一个原子变化,运行测试,并为新旧场景添加新测试。
上述的ATDD.TestScriptor将帮助你完成我的 4 步流程中的步骤 1到步骤 3。
让测试工具成为你的朋友
在第三章,《测试工具与标准测试》中,我们向你介绍了测试工具,并在我们进行测试示例工作时频繁使用它。我们用它来测试测试,在插入一个 bug 并让验证错误后。除了 VS Code(你的编码工具)和调试器,测试工具是你最好的朋友。在开发时保持它的运行,正如之前提到的,“不要等到代码准备好,验证每一个原子变化,运行测试”。
创建一个特定的测试套件,用于存放与你正在处理的代码相关的测试代码单元。在你大多数的项目中,很可能会像LookupValue扩展那样,最终得到一堆测试代码单元,这些单元将在不到一分钟的时间内执行完毕。在编写新的测试代码单元时,创建一个新的测试套件,仅用于存放该代码单元,并反复执行,直到编写完成。
测试覆盖率图
测试工具还包含一个我们之前没有提到的强大功能,对于选择与你正在更新的代码相关的测试代码单元非常有帮助。这个功能叫做测试覆盖率图(TCM)。它结合了 Business Central 中代码覆盖工具的结果和测试工具。启用 TCM 后,它会为已展示的第三章中的获取测试代码单元功能添加两个额外选项,测试工具与标准测试。在那一章中,我们解释了获取测试代码单元提供了以下两个选项,让你能填充测试套件:
-
选择测试代码单元
-
所有测试代码单元
启用 TCM 后,会增加两个选项。
选择第三个选项,根据修改的对象获取测试代码单元,将选择那些会涉及你正在处理的应用对象的测试代码单元。第四个选项,根据选择的对象获取测试代码单元,让你从列表中选择你希望运行测试的应用对象。

此时,根据修改的对象获取测试代码单元选项仅考虑标准中存在的应用对象,即 C/SIDE 中的对象。不幸的是,它还未考虑到扩展中存在的对象。
第四个选项,根据选择的对象获取测试代码单元,仍然包括所有的应用对象。
为了能够使用 TCM,你需要先启用它。为此,在测试套件中勾选“更新测试覆盖率图”字段。如果在任何测试套件中没有启用此选项,则 TCM 将无法获取数据,无法让你选择根据修改的对象获取测试代码单元和根据选择的对象获取测试代码单元选项,如前所述。

为了让 TCM 能有足够的数据来完成其工作,已激活的测试套件应该首先运行。
扩展测试工具
几年前,当我们开始更密集地使用测试工具时,我们发现一个主要的遗漏问题,于是决定为其构建一个简单的扩展。一旦你设置好了测试套件并运行了所有测试,你可能会发现只有一部分测试成功通过,而逻辑上,其他部分没有通过。
在查找并修复失败原因时,您可能只想运行有问题的测试。标准的测试工具只允许通过选中/取消选中每个功能行的“Run”字段来启用/禁用测试。选中/取消选中代码单元行的“Run”选项,也会对其包含的所有功能行执行相同的操作。
测试工具扩展允许您选择以下内容的“Run”字段:
-
所有测试
-
仅失败的测试,从而禁用所有其他测试
-
对于没有失败的测试,禁用失败的测试
作为第四个选项,可以取消选择所有测试。

如前面截图所示,已添加以下四个操作:
-
选择“所有测试”
-
取消选择“所有测试”
-
选择“失败时”
-
选择“没有失败的测试”
因此,在使用“失败时选择”功能时,所有失败的测试将被选中“Run”选项,而其他所有测试则保持未选中状态,如下图所示:

已有的功能,如 TCM 和扩展测试工具的四个操作,使您可以轻松地选择有助于修复问题的测试,并扩大您正在构建的测试资源。
测试工具扩展的源代码可以从此 GitHub 仓库下载:github.com/fluxxus-nl/Test-Tool-Extension
与每日构建集成
在软件开发中,构建连接和自动化业务流程的应用程序,而现代软件开发将其扩展到自身的流程,如下所示:
-
在源代码仓库中共享代码,并且可以通过 API 从任何地方访问和管理
-
随时自动从头开始构建您的软件
-
运行由已完成构建触发的自动化测试,以展示重建软件的有效性
-
部署由自动化测试通过的构建将在预定时间自动进行
-
在仪表板上收集所有前述过程的结果和状态,以便向利益相关者通报软件的健康状况
现代开发工具,如 Microsoft Azure DevOps,使您能够实现这一点。以下截图展示了一个集成所有前述要点的 Azure DevOps 仪表板。

这就是我们在 Dynamics 365 Business Central 中迈进的世界,受到第一章中讨论的论点启发,自动化测试简介。尤其是微软为我们设定的要求,请不要低估我们客户今天所处的生态系统,他们每天都在听到关于计划构建、自动化测试运行和比以往更短的发布周期的信息。
持续集成(CI)和持续交付(CD)在 Business Central 开发中可能曾经显得遥不可及,但随着 AL 和扩展开发的推进,它们已经触手可及。自动化测试在这一过程中占据了至关重要的地位。
在过去十年里,只有少数一些 Business Central 开发合作伙伴在努力自动化他们的开发流程,而现在越来越多的合作伙伴也开始加入这一行列;并且微软 Dynamics 365 Business Central 开发团队最近也公开倡导这一做法。
阅读微软 FreddyDK(Freddy D. Kristiansen)关于 CI/CD 的博客系列:community.dynamics.com/business/b/freddysblog/archive/tags/Continuous+Integration。
但也可以关注像 Gunnar Gestsson 这样的专家:dynamics.is/,Kamil Sáček:dynamicsuser.net/nav/b/kine,James Pearson:jpearson.blog/,Richard Robberse:robberse-it-services.nl/blog/,以及 Michael Glue:navbitsbytes.com/。
尽管如此,你并不需要等到一个完全运作的自动化 CI/CD 管道才能从你的自动化测试中获得最大收益。通过一个简单的 PowerShell 脚本,由一个传统的 Windows 任务触发,你就可以在任何预定时间让你的测试在应用程序上运行。在我们开始在 Azure DevOps 上实现 CI/CD 管道之前,正是这样做了几年。这使我们能够每晚执行超过 18,000 个自动化测试,并且第二天早上通过测试报告邮件告知团队我们的代码的健康状况。测试运行的成功率偶尔出现下降时,会提醒我们应用程序中添加了某些意外的破坏性更改,并需要采取适当的措施。
维护你的测试代码
像应用程序代码一样,测试代码也是代码,因此应像处理应用程序代码一样处理测试代码,例如:
-
需要确保安全,理想情况下通过源代码管理工具来实现。
-
需要进行维护,并且由于任何新的客户需求都会导致应用程序代码的更改,因此测试代码很可能也会发生变化。
-
需要进行调试,无论你喜欢与否,因为开发人员编写的任何代码都有可能引入新的 bug。
-
需要进行审查,以确保它像应用程序代码一样符合编码标准。
扩展和测试
在我们结束本章之前,必须说明如何组织与应用程序和测试代码相关的扩展代码。
如果我们采用扩展的最严格要求,即微软为批准您的扩展发布到 AppSource 所设定的要求,那么应用程序和测试代码应放置在不同的扩展中。我猜你可能已经想到,测试扩展应依赖于应用程序扩展。不幸的是,这会阻碍应用程序和测试代码的并行开发,因为对应用程序扩展的任何更改都会导致其重新部署。这也可能导致测试扩展的更新和重新部署。在你意识到之前,你就会不断地在扩展之间进行 juggling,从而降低开发团队的效率。在开发过程中,最好的做法是将应用程序代码和测试代码放在同一个扩展中。准备好后,你可以通过一些自动化(构建)脚本或特定的合并策略将代码拆分,并创建这两个强制性的扩展。
如果你的扩展不打算发布到 AppSource,我仍然强烈建议不要在应用程序扩展中发布测试代码。原因与标准 CRONUS 不包含标准测试助手库和测试代码单元相同:为了避免在生产环境中运行自动化测试。当然,如果测试代码单元在测试运行器的隔离环境中运行,数据不会发生更改,最坏的情况是用户会遇到锁定问题,尤其是在 Object 表上。但是,如果测试代码单元不小心在测试运行器的隔离环境之外运行,并且提交变成了实际提交怎么办?您的客户可能会觉得他们度过了愉快的一天,营业额非常好。但当支付未完成且货物被退回发件人(因为地址无法识别)时,问题就会回到他们身上。
总结
在本章中,我们关注了如何将测试自动化嵌入到日常开发实践中的一些最佳实践。让你的功能同事编写 ATDD 场景,以便利用前面讨论过的 Excel 表格。不要让自己和团队过于负担重,采取小步骤前进。将测试工具与开发工具并行使用,并保持测试持续运行。自动化你的开发流程,包括运行测试。最后但同样重要的是,测试代码就是代码,因此要像维护应用程序代码一样维护测试代码。
我们即将进入最后一章,第九章,让 Business Central 标准测试在你的代码上工作,在这一章中,我们将更深入地研究微软提供的测试,以及如何将这一庞大的标准测试集合进行集成。
第十章:让 Business Central 标准测试在你的代码中运行
现在你知道如何编写自动化测试,并且已经将其集成到日常开发实践中,你如何从微软提供的庞大测试文档中受益呢?本章将展示如何将这些测试添加到你自己的文档中。我们将讨论:
-
为什么使用标准测试
-
执行标准测试
-
修复失败的标准测试
-
使你的代码可测试
为什么使用标准测试?
自从 2009 年引入可测试性框架以来,微软一直在构建其应用程序测试文档。正如第三章中已经提到的,测试工具与标准测试,它包含了大量的测试。这些测试涵盖了整个标准应用程序,从财务管理、销售和采购,到仓库和制造,再到服务管理。每次主要发布或累积更新时,都会添加新的测试,以覆盖新功能和最近的 bug 修复。这是我们所有人都可以受益的多年工作。如果你的代码扩展了标准应用程序,这对标准应用程序会有什么影响?
你可以选择编写你自己的测试,也可以选择运行标准测试并查看结果。当然,最终你也可以两者兼顾,因为你的扩展很可能不仅会改变标准行为,还会增加微软文档中没有覆盖的新的功能。
我们可以讨论很多并互相争论运行微软测试的有效性,但我们不如直接运行它们,看看我们的代码是否导致标准测试失败。这正是我多年前作为迈出自动化测试第一步所做的。那些一直关注我的博客的人可能记得,几年前,我专门写了一篇文章,标题是 如何:在你的代码上运行标准测试。正如标题所示,文章讨论了如何在你的解决方案上运行标准测试的步骤。去阅读它,执行并检查结果。几乎没有理由不这么做。30 分钟内,你就可以让测试开始运行。几小时内,你将看到结果。下图展示了我们的结果,其中只有 23%的标准测试成功(深色尖峰):

这是我文章的链接:dynamicsuser.net/nav/b/vanvugt/posts/how-to-run-standard-tests-against-your-code
执行标准测试
证明在实践中,所以我们按如下方式执行:
-
将我们在第三部分中构建和测试的解决方案,设计并构建自动化测试以适用于 Microsoft Dynamics 365 Business Central,部署到 Business Central
-
导入标准测试
-
使用测试工具中的“All Test Codeunits”选项设置测试套件,正如第三章中所讨论的,测试工具和标准测试
-
运行所有测试
由于这需要几个小时,我们将跳过时间并查看结果。在近 23,000 个测试中,有超过 3,000 个失败。
这告诉了我们什么?
首先,考虑到 3,000 个失败是一个相当大的数字,这应该主要与我们的扩展有关,原因如下:
-
在标准的 Business Central 上运行标准测试总是会抛出一些错误。
-
这个数字可能有几百个,其中一些与环境设置有关,比如没有权限将数据写入文件。
-
由于自动化测试本身也是代码,它们中也可能存在一些错误。然而,3,000 多个测试失败远远超出了几百个的范围。
其次,这意味着大量的标准测试确实会触及我们的代码。通过使用测试覆盖率图,我们可以找出哪些测试代码单元实际上会影响到我们的扩展,并将其添加到我们迄今为止构建的测试资源中。当然,前提是我们能够修复这些问题。
到目前为止,我们一直在使用 Business Central 的 Web 客户端来运行测试。在本章中,我们将使用 Windows 客户端和调试器,因为目前 Web 客户端在运行成千上万的测试时并不好用。而且,由于测试功能行的显示和操作,测试工具不幸的是并不是你最好的朋友。
修复失败的标准测试
让我们看看一些与扩展相关的错误。它们可能已经揭示出一些它们的秘密:
Lookup Value Code must have a value in Sales Header: Document Type=Invoice, No.=1004\. It cannot be zero or empty.
Assert.ExpectedError failed. Expected: . Actual: Lookup Value Code must have a value in Sales Header: Document Type=Order, No.=1001\. It cannot be zero or empty.
Assert.ExpectedError failed. Expected: The total amount for the invoice must be 0 or greater.. Actual: Lookup Value Code must have a value in Sales Header: Document Type=Order, No.=1001\. It cannot be zero or empty.
首个错误是最为显著的,约有 2,500 个命中。对于那些在 Business Central 领域有几年开发经验的人来说,它可能已经暗示了错误的原因:错误信息的格式通常是记录方法TestField抛出的错误。这显然是一个在任何失败的测试中都没有预期到的错误。
另外两个错误是由asserterror捕获的,并通过Assert库的ExpectedError方法在测试验证过程中被挑选出来。仔细查看实际错误信息,我们可以识别出它与第一个错误是相同类型的错误。很可能是同一个TestField。
现在让我们处理这些错误。这就是我所称的攻击协议:
-
打开测试工具并选择失败的测试
-
启动调试会话
-
使用“运行选定项”来运行单个测试
-
让调试器在错误处暂停
-
查看错误发生的位置
-
使用调用栈回溯代码,看看是否能分辨出原因,或者是否需要获取更多的细节
-
在代码中较早的地方设置一个断点
-
完成代码执行并重新运行测试
-
通过逐步调试代码来排除错误
-
实施修复并从步骤 1重新开始
好的,跟随我首先解决我们列表中最常见的错误。
解决错误
为了攻击这个错误,我们需要采取以下步骤:
-
打开测试工具并选择失败的测试。
在我们的案例中,剔除掉任何因为
Lookup Value Code must have a value…错误而失败的测试,这个错误是之前列出的三个错误中的第一个:

- 启动调试会话:

- 使用“运行选中的测试”运行单独的测试:

- 让调试器在错误处中断:

-
看看错误发生的位置。
这个问题显然发生在我们扩展中的一个对象里。你可能不太熟悉,因为我们还没有查看过这段代码。这是发布销售单据时,业务规则的实现,要求
Lookup Value Code字段必须被填写。看看调用TestField方法的地方:

-
使用调用栈回溯代码。
注意,我选择了调用栈中紧接着
RunTest所在行的上一行。RunTest是标准测试运行器代码单元(130400)中的主函数,用于调用每个测试代码单元。它上面的一行总是当前测试函数的调用。在这个特定的例子中是TestCustNoMatchOutsideThreshold:

跳转到TestCustNoMatchOutsideThreshold后,知道我的扩展是什么,我被CreateCustomer的语句行所触发,这一行在绿色指针上方的四行处。显然存在一个本地方法,它作为新鲜的测试环境创建了一个客户。我敢打赌,这个客户没有被分配Lookup Value。检查Customer变量,如下所示,证明了我的猜测是对的:

-
在代码中稍早的地方设置一个断点。
你可能已经猜到,在调用
CreateCustomer的那一行:

-
完成代码执行并重新运行测试。
按计划,它在
CreateCustomer调用处停止:

-
通过逐步调试代码来排查问题。
我们现在迈出了重要的一步,跳过了本地的
CreateCustomer方法,直接调用了Library - Sales代码单元中的通用辅助方法CreateCustomer:

通常我们会在这里修复问题。通常,这个辅助函数会在几乎所有需要新创建客户的测试中被调用。注意OnAfterCreateCustomer发布器。我们的修复将包含一个订阅器。
- 实施修复并从步骤 1重新开始。我们将在接下来的章节中详细说明这一点。
最近,微软才开始在其库中的辅助函数中添加像OnAfterCreateCustomer这样的发布者。你可能仍然会遇到一些尚未被批准的辅助函数,缺乏发布者。
修复错误
为了修复该错误,诀窍是向我们的扩展添加一个代码单元,并为OnAfterCreateCustomer发布者添加一个订阅者,该订阅者会在客户上设置查找值:
codeunit 80050 "Library - Sales Events"
{
[EventSubscriber(ObjectType::Codeunit,
Codeunit::"Library - Sales", 'OnAfterCreateCustomer',
'', false, false)]
local procedure OnAfterCreateCustomerEvent(
var Customer: Record Customer)
begin
SetLookupValueOnCustomer(Customer);
end;
local procedure SetLookupValueOnCustomer(
var Customer: record Customer)
var
LibraryLookupValue: Codeunit "Library - Lookup Value";
begin
with Customer do begin
Validate("Lookup Value Code",
LibraryLookupValue.CreateLookupValueCode());
Modify();
end;
end;
}
注意代码单元Library - Lookup Value的引用。这是第七章中关于重构讨论的结果,从客户需求到测试自动化——以及更多内容。Library - Lookup Value包含一个可重用的函数CreateLookupValueCode。可以去 GitHub 查看详细信息。
再次运行失败的测试
再次部署所有内容并重新运行所有失败的测试。使用我们的测试工具扩展中的“选择失败测试”功能,仅选择失败的测试。在 3,000 多个测试完成处理前,可能需要一些时间。结果是,失败的测试数量降至 559 个。显然,这个修复是一个值得的投资。
从测试工具查看调用堆栈
如果你没有调试失败的测试,但仍然希望从测试工具查看调用堆栈,可以从"First Error"字段进入 CAL 测试结果窗口,如第五章中所示,从客户需求到测试自动化——基础知识。然后选择调用堆栈操作。在接下来的截图中,调用堆栈显示了我们之前查看的TestCustNoMatchOutsideThreshold测试:

一切都与数据有关
根据我的经验,确保标准测试在代码中正常运行,主要是关于正确配置测试环境。就像之前的练习一样,修复我们列表上的第一个错误并非巧合。这是你在让标准测试在代码上运行时会做的一个简单例子:让测试环境处于正确状态。在这个具体的案例中,我们修复了新创建的测试环境。解决其余的错误同样需要更新测试环境。在这些情况下,涉及的是共享的测试环境。
更仔细地查看剩余的失败测试,似乎我们的第一个错误并没有完全消失。为了省去你逐一检查的麻烦,我挑选了以下三个失败的测试:

显然,我们为CreateCustomer辅助方法提供的通用修复并未对这些产生影响。调试每个失败的测试最终会发现,正在使用预构建的测试环境,也就是来自CRONUS演示公司的数据。以第一个测试PartialDisposalOfFA为例,来自测试代码单元 134450(ERM 固定资产日记账),很明显。来看一下:

在一组过滤条件下,从数据库中检索第一条客户记录。从 Watches 窗格可以看到,这是 CRONUS 中一个熟悉的客户,客户编号为 10000。由于我们没有修改预构建的测试数据,CRONUS 中的任何客户记录都必须没有查找值。
对于另外两个测试,它们的错误也源于一个不足的预构建测试数据:
-
测试代码单元 134640(
Sales E2E)中的SalesFromContactToPayment使用了来自CRONUS的客户模板 -
测试代码单元 136140(
Service Order Release)中的PullAndPostMixedHeadersUsingUseFilters从CRONUS中检索一个销售单据
结论是,在这三种情况下,我们必须更新预构建的测试数据,也就是创建一个共享的测试数据,确保 CRONUS 中已经存在的客户、客户模板和销售单据能够填充它们的 Lookup Value Code 字段。为了实现这一点,我们可以利用共享测试数据方法中的一个发布者 Initialize,该方法在大多数 Microsoft 测试函数中都有实现。你可能还记得在第四章中提到的结构:测试设计。
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>
Initialized := true;
Commit();
LibraryTestInitialize.OnAfterTestSuiteInitialize(<codeunit id>);
我们要么订阅 OnBeforeTestSuiteInitialize,要么订阅 OnAfterTestSuiteInitialize 发布者。一般来说,我选择订阅第一个,以确保在对测试数据进行任何标准更新之前完成此操作,并且利用 Commit 上已经存在的调用。
这是我们修复实现的样子:
codeunit 80051 "Library - Initialize"
{
[EventSubscriber(ObjectType::Codeunit,
Codeunit::"Library - Test Initialize",
'OnBeforeTestSuiteInitialize','', false, false)]
local procedure OnBeforeTestSuiteInitializeEvent(
CallerCodeunitID: Integer)
begin
Initialize(CallerCodeunitID);
end;
local procedure Initialize(CallerCodeunitID: Integer)
var
LibraryLookupValue: Codeunit "Library - Lookup Value";
LibrarySetup: Codeunit "Library - Setup";
begin
case CallerCodeunitID of
Codeunit::"ERM Fixed Assets Journal",
Codeunit::"ERM Fixed Assets GL Journal":
LibrarySetup.UpdateCustomers(
LibraryLookupValue.CreateLookupValueCode());
Codeunit::"Service Order Release":
LibrarySetup.UpdateSalesHeader(
LibraryLookupValue.CreateLookupValueCode());
Codeunit::"Sales E2E":
LibrarySetup.UpdateCustomerTemplates(
LibraryLookupValue.CreateLookupValueCode());
end;
end;
}
请注意,这段代码通过更新 Customer 表中现有的记录,修复了代码单元 134453(ERM 固定资产 GL 日志)中测试抛出的错误。
新库代码单元 Library - Setup 中引入的三个辅助函数正是它们名称所描述的功能:
-
UpdateCustomers更新CRONUS中已存在的所有客户记录,以便填充它们的Lookup Value Code字段 -
UpdateCustomerTemplates和UpdateSalesHeader对所有客户模板和销售单头做了相同的操作
前往 GitHub 学习这些函数的详细信息。
准备好了吗?差不多了。
使你的代码可测试
我们本来是想修复标准测试的,结果修复了。但是同时我们忽略了修复它们导致我们自己的一些测试失败了。你明白吗?

我们有两个失败的测试,它们位于同一个代码单元 LookupValue Posting 中:
-
PostSalesOrderWithNoLookupValue -
PostWarehouseShipmentFromSalesOrderWithNoLookupValue
这是他们的错误文本:
Microsoft.Dynamics.Nav.Types.Exceptions.NavNCLAssertErrorException: An error was expected inside an ASSERTERROR statement.\ at Microsoft.Dynamics.Nav.Runtime.NavMethodScope.AssertError(Action body)\ at Microsoft.Dynamics.Nav.BusinessApplication.C
在没有调试的情况下,这些信息已经讲述了一个完整的故事。第一测试方法的代码也确认了这一点:
procedure PostSalesOrderWithNoLookupValue();
//[FEATURE] LookupValue Posting Sales Document
var
SalesHeader: Record "Sales Header";
PostedSaleInvoiceNo: Code[20];
SalesShipmentNo: Code[20];
begin
//[SCENARIO #0027] Check posting throws error on sales
// order with empty lookup value
Initialize();
//[GIVEN] A sales order without a lookup value
CreateSalesOrder(SalesHeader, UseNoLookupValue());
//[WHEN] Sales order is posted (invoice & ship)
asserterror PostSalesDocument(
SalesHeader,
PostedSaleInvoiceNo,
SalesShipmentNo);
//[THEN] Missing lookup value on sales order error thrown
VerifyMissingLookupValueOnSalesOrderError(SalesHeader);
end;
asserterror被放置在这里,用来捕捉本应由PostSalesDocument抛出的错误。这正是测试错误信息所提示的。我们收到错误的事实意味着并没有发生错误。预期的错误应该是报告SalesHeader在发布时缺少查找值。这是PostSalesOrderWithNoLookupValue的单一目标(参见[THEN]标签)。
没有发生错误的真正原因,是因为SalesHeader记录中的Lookup Value Code字段为空。这是因为我们在若干个页面之前实现的通用修复,扩展了CreateCustomer辅助方法,并通过我们的订阅者OnAfterCreateCustomerEvent实现。虽然我把细节留给你在 GitHub 上查看,我在这里只提到,链接到SalesHeader记录的客户是通过标准的CreateCustomer辅助方法创建的。由于这是由我们的测试触发的,因此结果与预期相反。那么我们该如何解决这些错误呢?
移除OnAfterCreateCustomerEvent订阅者不是一个选项,因为它是为了让 2,500 个标准测试成功而存在的。然而,我们可以尝试在我们的测试运行时禁用该订阅者。或者换句话说,我们可以通过应用Handled 模式来模拟订阅者的行为。在解决我们的问题时,我们将基于这个模式做一些变体。
在这里阅读更多关于 Handled 模式的内容:
markbrummel.blog/2015/11/25/the-handled-pattern/ 和 community.dynamics.com/nav/b/navigateintosuccess/archive/2016/10/04/gentlemen-s-agreement-pattern-or-handling-the-handled-pattern
应用 Handled 模式
想要绕过订阅者影响的测试代码单元会在共享的测试环境中创建一条记录,并选中一个布尔字段,名为Skip OnAfterCreateCustomer。在OnAfterCreateCustomerEvent订阅者中勾选这个字段将使我们能够完全执行订阅者,或者直接跳过它。这就是订阅者的更新方式:
[EventSubscriber(ObjectType::Codeunit,
Codeunit::"Library - Sales", 'OnAfterCreateCustomer',
'', false, false)]
local procedure OnAfterCreateCustomerEvent(
var Customer: Record Customer)
var
LibraryTestsSetup: Codeunit "Library - Tests Setup";
begin
if LibraryTestsSetup.SkipOnAfterCreateCustomer() then
exit;
SetLookupValueOnCustomer(Customer);
end;
辅助函数SkipOnAfterCreateCustomer是一个新的库代码单元Library - Tests Setup的一部分,它将检查我们新设置表中的Skip OnAfterCreateCustomer字段:
procedure SkipOnAfterCreateCustomer(): Boolean
var
TestsSetup: Record TestsSetup;
begin
with TestsSetup do begin
Get();
exit("Skip OnAfterCreateCustomer");
end;
end;
唯一需要采取的步骤是将以下代码添加到相关的Initialize函数中:
local procedure Initialize()
var
LibraryTestsSetup: Codeunit "Library - Tests Setup";
begin
if isInitialized then
exit;
LibraryTestsSetup.SetSkipOnAfterCreateCustomer(true);
LocationSetup();
isInitialized := true;
Commit();
end;
这是一种让代码可测试的方法。在这个示例中,我们将其应用于测试代码,但以类似的方式,你也可以将其应用于应用程序代码。通过在代码中插入发布者,你使得订阅者能够控制流程,就像你测试代码中的表单一样。
正如你可能知道的,Dynamics 365 Business Central 允许你在合适的情况下动态绑定和解绑订阅者。遗憾的是,在我们的测试案例中,这并不可行,因为我们不能完全控制标准的测试代码单元。
阅读更多关于绑定订阅者的信息,请访问:docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/session/session-bindsubscription-method
这一切真的都是关于数据吗?
当然不是。但在大多数标准测试失败的案例中,结果通常是这样的。基于这个经验,我在解决标准测试失败时的最佳实践是以下步骤。尝试通过调整以下内容来修复错误:
-
共享夹具;如果这没有解决问题。
-
新的夹具;如果仍然不行。
-
测试代码,通常是验证部分,在这种情况下你遇到了测试代码的 bug;如果仍然不起作用。
-
在应用代码中,你发现了一个真正的 bug!
总结
在本章中,你了解了为什么你需要使用微软测试,如何在你的 Business Central 代码中应用它们,以及如何修复它们可能引发的错误。你了解到,大多数修复都与调整测试夹具有关,无论是共享的还是新的。
本章内容到此为止,本书也即将结束。但你在测试自动化的旅程才刚刚开始。对我而言,每次拿起它依然很有趣。它让我在应用代码中迷失方向,发现我的测试结果并提醒我。它让我可以随时重构代码。它还让我更容易反思代码质量,因为它可以被修改并立即检查。我可以继续称赞它,或多或少地重新写一遍本书的第一章。但我不会,因为现在是时候总结一下了。我希望你像我写书时一样享受阅读和实践的过程。
第十一章:测试驱动开发
正如本附录标题所示,我们将讨论测试驱动开发(TDD),这是我们在本书中到目前为止尚未提到的一个术语。我故意没有使用它。为什么?因为我希望我们专注于本书中我想讨论的以下主题:
-
为什么我们应该使用自动化测试
-
何时使用自动化测试
-
如何编写自动化测试
我不希望任何人因为他们对 TDD 的了解而被阻碍,尽管它是一种经过验证的方法论,但也常常伴随着怀疑。然而,关注 TDD 是有意义的,因为 TDD 有很多实际部分值得关注。因此,本附录不会全面揭示 TDD 是什么。相反,它将是一个简洁的描述,并附有注释,指出对您日常开发实践有价值的部分。
在本附录中,我们将讨论以下主题:
-
TDD 的简短描述
-
所谓的红绿重构法则(red-green-refactor)是实现代码的一种方式
-
我们的测试示例与 TDD 的接近程度
事实上,我们使用了验收测试驱动开发(ATDD)这一术语,该术语在第三章《测试工具与标准测试》中出现,包含了 TDD 这个词,我们确实采用了 ATDD 的一部分,但它并不等同于 TDD 方法论。
TDD 的简短描述
TDD 的最简短描述实际上就是这个术语本身。它是自包含的。它描述的是,通过将其作为软件应用开发的方法论,开发将由测试驱动。测试需要被定义以驱动应用代码的编写,通过这种方式,您的测试直接源自需求。这是用一句话概述 TDD 的常见表达:
"没有测试,就没有代码。"
最终的结果是,你永远不会编写没有测试的应用代码。而且,由于测试与需求是一对一对应的,您的应用代码不应包含任何未文档化的功能。
但这个描述实际上并不是 TDD 的基本定义。TDD 仅通过以下两条规则来定义,其他一切都由此推导出:
-
除非您有一个失败的自动化测试,否则绝不编写一行代码
-
消除重复
没有测试,就没有代码的这句简短语句直接源自这两条规则。以下是从这些规则中得出的可操作步骤:
-
通过定义测试,将需求转化为代码,这就是所谓的测试列表
-
一次编写一个测试,并仅针对它编写应用代码以使测试通过
-
重构代码,包括测试代码和应用代码,以清理并消除重复;重新运行测试以确保重构后的代码仍然有效
通过将这一做法融入到你的日常实践中,你将获得的巨大收益是:你最终会得到已经过测试的代码,你只实现所需的部分,同时你还有伴随的测试资料,允许你运行并检查应用代码的正确性。
如果你想了解更多关于 TDD 的内容,可以阅读我多年前在我的博客上写的关于 TDD 的系列文章,网址是:dynamicsuser.net/nav/b/vanvugt/posts/test-driven-development-in-nav-intro,在那里你还会找到一些有用的参考资料。
TDD,红绿重构
给定一个测试列表,可操作步骤通过已成为红绿重构的口号来描述。这里的红色和绿色分别指代失败(红色)和成功(绿色)的测试,这个口号告诉你要采取以下步骤:
-
从列表中选择一个测试并编写测试代码
-
编译测试代码,结果是红色,因为应用代码尚未到位
-
实现足够的应用代码,使测试代码能够编译通过
-
运行测试,看它可能失败,仍然是红色
-
调整应用代码,足够的以使其通过,即绿色
-
重构你的代码,无论是测试代码、应用代码还是两者,依次进行,每次变更后重新运行测试以验证所有代码仍然良好(绿色)
-
移动到列表上的下一个测试并从步骤 1开始重复
红绿重构口号促使你一步一步地高效完成任务,也就是尽可能简约。只实现所需的内容,每次做一件事。认真对待这个口号,你的第一个测试可能会非常基础。鉴于测试列表已经有足够的细节,即包含了足够数量的测试,实施下一个测试将为你的代码带来更多细节。
如果你需要一个红绿重构口号实际应用的例子,可以在以下网址找到:dynamicsuser.net/nav/b/vanvugt/posts/test-driven-development-in-nav-test-1
TDD 和我们的测试示例
如果我们将 TDD 应用到第三部分中的测试示例,设计和构建 Microsoft Dynamics 365 Business Central 的自动化测试,会怎么样呢?
老实说,它看起来也不会有太大不同,因为许多 TDD 原则已经隐性地被以下方式执行:
-
通过使用 ATDD 场景定义客户需求,我们创建了足够的测试集,也就是测试列表
-
通过使用这四步方法来实现我们的测试,我们做了以下事情:
-
我们采取了小步伐
-
我们为每个测试创建了一个基于
GIVEN-WHEN-THEN标签的结构 -
我们构建了实际的代码以使其工作
-
我们运行测试,如果是红色,我们调整代码直到测试通过,即绿色
-
-
尽管在测试示例中没有完全体现出来,正如在第七章的结尾部分所讨论的,从客户需求到测试自动化——以及更多内容,我确实重构了代码,提取了可复用的部分,并从我可用的自动化测试中获益良多。
唯一偏离 TDD 的地方是,在我们开始编写测试代码之前,应用程序代码已经被构思出来。顺便说一下,这也是我为本书准备 LookupValue 扩展的方式。
我个人的结论是,你完全可以在 Business Central 中使用 TDD。最大的挑战是让你的团队在 ATDD 场景中规范客户需求。请注意,以下内容描述了开始实践这一方法来解决 bug 的最简单方式:
-
以 ATDD 格式描述 bug:
-
从某种意义上说,这和你在重现 bug 时所做的非常相似
-
请注意,ATDD 场景描述了该功能的预期工作方式
-
-
基于 ATDD 场景,你构建测试代码
-
运行测试应该导致红色结果,因为 bug 仍然存在
-
修复应用程序代码,使测试通过:绿色
摘要
在总结本附录时,我想指出,TDD 的价值在于它表明以小步前进的方式进行工作、在编码之前定义测试(ATDD),并遵循红绿重构规则逐步进行,确实是非常有意义的。
第十二章:设置 VS Code 并使用 GitHub 项目
本书讲述的是在 Microsoft Dynamics 365 Business Central 中编写自动化测试,而不是如何使用 VS Code 和 AL 语言开发扩展。假设你已经知道如何将 VS Code 与 AL 开发语言结合使用,并且了解 Dynamics 365 Business Central 作为平台和应用程序的工作原理。基于此,我们直接进入了 第二章,可测试性框架 的编码,没有解释我们使用的开发工具,并且在接下来的章节中继续这样做。
然而,在本附录中,我们会关注一些 VS Code 和 AL 开发,并介绍在 GitHub 仓库中找到的代码示例。
VS Code 和 AL 开发
如果你是 AL 语言和 VS Code 扩展开发的新手,你可能需要先练习这一点。有许多资源可以参考,但一本内容详尽且不冗长的书是 Stefano Demiliani 和 Duilio Tacconi 编写的 Dynamics 365 Business Central 开发快速入门指南。
这里有一个链接到 Packt 页面,你也可以在这里订购这本书:www.packtpub.com/business/dynamics-365-business-central-development-quick-start-guide
VS Code 项目
在 Dynamics 365 Business Central 开发快速入门指南 中,Stefano Demiliani 和 Duilio Tacconi 以实用的方式解释了如何在 VS Code 中设置一个新的扩展项目,开始使用 AL 编程,并将扩展部署到 Business Central。你在本书的 GitHub 仓库中找到的项目也是以相同的方式创建的:每个章节都有一个单独的文件夹,其中包含 AL 代码对象和 app.json 文件。如果你已经在开发扩展,你会注意到缺少了一个重要的资源:launch.json。
请注意,我们将在接下来的章节中更详细地讨论 GitHub 仓库的结构。
launch.json
要能够将章节文件夹用作完整的 VS Code AL 扩展项目文件夹,你需要向其中添加一个 launch.json 文件。launch.json 文件通常存储在项目文件夹的 .vscode 子文件夹中,并包含有关将在其上启动扩展的 Business Central 安装信息。要获取一个包含 launch.json 文件的 .vscode 文件夹,请按照以下步骤操作:
-
打开安装了 AL 语言扩展的 VS Code
-
使用
AL: GO!命令创建一个新的 VS Code AL 项目 -
在这个项目中,配置
launch.json文件,以便与即将执行测试编码的 Business Central 安装建立关系 -
将这个
.vscode文件夹复制到你想要处理的章节项目文件夹中
你的 launch.json 可能会像这样:
{
"version": "0.2.0",
"configurations": [
{
"type": "al",
"request": "launch",
"name": "Your own server",
"server": "http://localhost",
"serverInstance": "BC130",
"authentication": "Windows",
"port": 7049,
"startupObjectId": 130401,
"startupObjectType": "Page",
"breakOnError": true,
"launchBrowser": true
}
]
}
与默认的 launch.json 文件相比,你会注意到我添加/更新了一些有用的关键词,例如 startupObjectId,启动页 130401,即 测试工具 页面。
你可以在Dynamics 365 Business Central 开发快速入门指南中找到更多详细信息,作者是 Stefano Demiliani 和 Duilio Tacconi。
app.json
app.json 文件,也叫做清单文件,定义了扩展的元描述,并已在 GitHub 上为每个章节文件夹提供。这里是第五章,第六章,第七章和第九章中仅列出相关部分的清单:
{
"id": "e26890f8-fafe-49c6-8951-2c1457921f9b",
"name": "LookupValue",
"publisher": "fluxxus.nl",
"brief": "LookupValue extension for book Automated Testing in
Microsoft Dynamics 365 Business Central",
"description": "LookupVale extension as basis to test examples
in chapters 5, 6, and 7 for book Automated
Testing in Microsoft Dynamics 365 Business
Central written by Luc van Vugt and published
by Packt",
"version": "1.0.0.0",
"platform": "14.0.0.0",
"application": "14.0.0.0",
"test": "14.0.0.0",
"idRange": {
"from": 50000,
"to": 81099
},
"runtime": "2.4",
"showMyCode": true
}
除了使用 GitHub 提供的 app.json 文件外,你还可以使用新创建的 VS Code AL 项目文件夹中的 app.json 文件,像之前创建的那样,以获得 launch.json 文件。如果你这么做,确保你的扩展具有不同的(且唯一的)名称和 ID,因为在编写本书时,我已经部署了 LookupValue 扩展无数次。
在 如何创建每租户扩展的十大技巧与窍门 博客的提示 #10 中(simplanova.com/top-tips-tricks-per-tenant-extensions/),Dmitry Katson 解释了如果你的扩展不是唯一的会发生什么,以及如何使其唯一。
GitHub 仓库
本书中使用的各种代码示例已经上传到一个专门的 GitHub 仓库中。可以通过以下链接访问该仓库:github.com/PacktPublishing/Automated-Testing-in-Microsoft-Dynamics-365-Business-Central。
GitHub 仓库结构
在该仓库的主页上,你会找到以下文件夹:
-
ATDD 场景 -
第二章
-
第五章 (LookupValue 扩展) -
第六章 (LookupValue 扩展) -
第七章 (LookupValue 扩展) - 重构并完成 -
第七章 (LookupValue 扩展) -
第九章 (LookupValue 扩展) -
LookupValue 扩展 (仅应用) -
LookupValue 测试扩展 (仅测试)
在本节中,我们将讨论各个文件夹的内容。请注意,GitHub 会按字母顺序排列它们。这与文件夹在书中使用的顺序无关。不过,我会详细阐述书中的顺序。
像其他任何 GitHub 仓库一样,我们的仓库也包含 LICENSE 和 README.md 文件。
第二章
该文件夹包含第二章的代码示例,可测试性框架,包括 MyTestsExecutor 页面和 app.json 文件,后者允许你将代码作为扩展部署。
ATDD 场景
该文件夹包含以下两个 Excel 文件:
-
清单.xlsx -
LookupValue.xlsx
LookupValue文件包含所有验收测试驱动开发(ATDD)场景的列表,按照GIVEN-WHEN-THEN格式详细描述了完整的客户需求,如第五章中介绍的从客户需求到测试自动化——基础篇,用于我们的LookupValue扩展。在第五章中已进一步详细说明,并延续至第七章。
Clean sheet.xlsx文件是一个现成的干净版本,用于编写你自己的 ATDD 场景。
LookupValue 扩展(仅应用)
本文件夹包含LookupValue扩展的应用代码,包含我们在第五章、第六章、第七章和第九章中编写测试代码的应用代码,包含app.json文件,允许你将代码作为扩展进行部署。请注意,它已包含一个Test Codeunits文件夹,里面有第一个代码单元结构,构建于第五章。
第五章(LookupValue 扩展)
从LookupValue 扩展(仅应用)文件夹中的代码开始,本文件夹将第五章中的完整代码示例,从客户需求到测试自动化——基础篇,添加到扩展中。它还包含app.json文件,允许你将代码作为扩展进行部署。
第六章(LookupValue 扩展)
从第五章(LookupValue 扩展)文件夹中的代码开始,本文件夹添加了第六章的完整代码示例,从客户需求到测试自动化——进阶篇。它还包含app.json文件,允许你将代码作为扩展进行部署。
第七章(LookupValue 扩展)
从第六章(LookupValue 扩展)文件夹中的代码开始,本文件夹添加了第七章的完整代码示例,从客户需求到测试自动化——以及更多内容。它还包含app.json文件,允许你将代码作为扩展进行部署。
第七章(LookupValue 扩展)- 重构并完成
本文件夹包含整个LookupValue扩展的重构和完成后的应用与测试代码,包括允许你将代码作为扩展部署的app.json文件。
第九章(LookupValue 扩展)
从第七章(LookupValue 扩展)- 重构并完成文件夹中的代码开始,本文件夹添加了第九章的完整代码示例,让 Business Central 标准测试在你的代码上工作。它还包含app.json文件,允许你将代码作为扩展进行部署。
LookupValue 测试扩展(仅测试)
如书中所述,在发布扩展时,最佳实践是将应用程序代码与测试代码分开。对于 AppSource,这甚至是一个要求。这意味着最终的测试扩展是依赖于真实扩展构建的。此文件夹包含一个单独的测试扩展,属于 LookupValue Extension (仅应用) 文件夹中的扩展。
关于 AL 代码的说明
现在,关于 GitHub 上的代码和书中的代码示例,最后再做几点说明。
VS Code 与 C/SIDE
本书完全聚焦于以扩展的方式开发 Microsoft Dynamics 365 Business Central 功能的现代方法。因此,GitHub 上的代码和书中的代码示例都是用 AL 语言编写的。但所有呈现和使用的原则同样适用于在 C/SIDE 中开发自动化测试,C/SIDE 是 Microsoft Dynamics NAV 的开发环境。目前,Business Central 的标准应用和测试代码仍由 Microsoft 提供,且是以 C/SIDE 对象形式存在。
前缀或后缀
在构建你自己的扩展时,最佳实践是使用所谓的前缀或后缀来命名你的对象、字段和控件。我们选择不使用前缀/后缀,原因如下:
-
LookupValue扩展不是一个注册过的扩展 -
使用前缀/后缀不会提高代码示例的可读性
关于使用前缀或后缀的更多信息,请参见:docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/compliance/apptest-prefix-suffix
自动换行
在书中添加代码示例时,始终面临如何整齐地格式化长行代码以保持代码可读性的问题。在本书中的代码示例中,采用了强制自动换行的方法,即使代码可能无法再编译。然而,GitHub 仓库中的所有代码在技术上是没有问题的。


浙公网安备 33010602011771号