原文:
annas-archive.org/md5/45c346cbf4b7bfce8850faea0398b5b4译者:飞龙
第八章:从客户需求到测试自动化——以及更多
现在掌握了吧?嗯,你已经做到这一步了,对吧?让我们再来一个 路上的最后一课,实际上是两个。我们将用最后一对测试工具补充你的工具箱,适用于 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 仓库中的所有代码在技术上是没有问题的。
2241

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



