使用 ABAP 单元实施代码测试

学习目标

  • 实施测试类。

  • 运行 ABAP 单元测试。

ABAP 单元

模块测试

每当程序员编写或更改代码时,就有可能引入编程错误。因此,测试是每个开发项目的关键部分。

在现代编程中,代码以可重复使用的类和方法构建。要检测和本地化潜在的编程错误,需要对每个模块化单元进行全面测试。

让我们看一个示例。

视频:本节含 SAP 官方视频(视频 ID 1_3r1uwunc),需在 learning.sap.com 在线观看。

ABAP 单元 - 在 ABAP 中实施模块测试

ABAP 单元是一种允许您使用 ABAP 语言实施模块测试的技术。您可以在开发期间手动触发测试执行,也可以大规模定期自动触发测试执行。

ABAP 单元测试作为专门指定的 ABAP 类的方法实施。这些测试方法用作测试脚本,使用该脚本可以运行被测代码,并可用于评估结果。

我们继续看一下我们的示例。

视频:本节含 SAP 官方视频(视频 ID 1_ljcx88io),需在 learning.sap.com 在线观看。

测试完成后,您可以随时根据需要随时执行这些任务,这一点非常重要:在初始交付之前,将更改应用到生产代码之后,或者用于用户报告问题时的错误分析。执行测试只需单击几下鼠标即可完成。

ABAP 单元的重要功能

单元测试类

类定义中的附加项 FOR TESTING 将测试类与普通 ABAP 类区分开来。附加可用于局部类以及全局类。

附加(风险级别)用于为测试分配风险级别。如果缺少附加,则缺省使用风险级别 CRITICAL。

客户端级别的中央设置可能不允许执行特定风险级别的测试。

以下风险级别值可用:

CRITICAL

测试更改系统设置或定制数据

危险

测试更改持久数据

硬性

测试不会更改系统设置或持久数据。

加法持续时间指定预期执行时间。客户端级别的中央设置可用于定义三个值的运行时上限。

以下持续时间值可用:

短期

预计执行时间会短于几秒。

中等

预期明显的执行时间大约为一分钟。

LONG

预计执行时间会超过一分钟。

测试类可以有两种方法:

测试方法

测试方法在方法名称后使用附加项 FOR TESTING 定义。每种测试方法代表一个测试。ABAP 单元框架通过调用相关测试方法执行此测试。测试方法不得有任何参数。

辅助方法

辅助方法是测试类的普通方法。ABAP 单元框架不会调用它们。您可以使用助手方法构建测试方法的代码,或者如果要在多个测试方法中重用相同的功能。帮助程序方法可以具有任意数量的参数。

注意

ABAP 单元框架能够调用所有测试方法,即使其可见性设置为 PRIVATE。实际上,建议在测试类的专用部分定义测试方法,以确保不会直接在代码中的任何位置调用测试类。

测试类应至少包含一个测试方法。测试方法不得有任何参数。

执行测试类的测试时,ABAP 单元框架以未确定的顺序调用测试类中的所有测试。

如果要在全局 ABAP 类中定义局部测试类,则有一个专用位置执行此操作。在 局部类型 标签上定义普通局部类时,应在 测试类 标签上定义局部测试类

如图所示,要生成本地测试类的定义部分和实施部分,ADT 提供了代码模板 testClass。

在如何定义和实施测试类的演示中,您将了解如何调用此代码模板。

单元测试实施

服务类 CL_ABAP_UNIT_ASSERT

通常,测试方法的实施具有以下结构:

  1. 执行正在测试的生产编码

  2. 分析结果

  3. 向 ABAP 单元框架报告意外结果

对于步骤 3,ABAP 单元框架提供全局服务类 CL_ABAP_UNIT_ASSERT。测试方法调用此类的静态方法以报告错误并影响测试执行(例如,跳过一个或多个测试,因为不满足前提条件)。

  • 方法 failed() 报告无条件错误。通常,此方法的调用由控制结构(如 IF… ENDIF. 或 TRY…ENDTRY)包围,以确保仅在条件下到达。

  • 以 assert_ 开头的方法检查特定预期,如果未满足此预期,则报告错误。

  • 例如,方法 assert_equals() 比较两个数据对象的内容,如果它们不同,则报告错误。

  • 方法 assert_difference() 执行相同操作,但如果数据对象具有相同的内容,则会报告错误。

方法" "的参数失败( )

观看以下视频,了解更多信息。

视频:本节含 SAP 官方视频(视频 ID 1_wculzg7j),需在 learning.sap.com 在线观看。

断言方法的参数

所有 assert-methods 都具有可选导入参数 MSG、LEVEL 和 QUIT,这些参数始终与方法 fail() 中的含义相同。

此外,大多数断言方法都有一个用于待验证数据对象的导入参数 ACT。

比较 assert_equals()assert_difference() 等方法时,还具有预期值的参数 EXP。

如何定义和实施测试类

在此演示中,您将了解如何创建和实施本地测试类。

视频:本节含 SAP 官方视频(视频 ID 1_pu2xwcud),需在 learning.sap.com 在线观看。

单元测试执行

有两种方法可以执行 ABAP 单元测试:开发期间的交互式测试和使用 ABAP 测试主控室的批量测试。

观看以下视频,了解有关单元测试执行的详细信息。

视频:本节含 SAP 官方视频(视频 ID 1_0tmq4r7j),需在 learning.sap.com 在线观看。

ABAP 单元测试结果

在 ADT 中,最新的 ABAP 单元测试结果显示在 ABAP 单元视图中。

顶部的汇总显示已执行的测试方法的总数,以及测试的总持续时间(毫秒)。

使用摘要中的复选框过滤结果显示:仅失败测试、仅出现警告的测试、仅成功结束的测试等。

结果本身显示为树,其中资源库对象位于顶部,例如全局类。第二级别的节点表示局部测试类,端点对应于测试方法。图标可帮助您区分成功的测试、失败等。

左键单击选择测试方法并在右侧显示详细信息。在该示例中,您可以看到严重性(严重程度媒介的严重断言错误)、参数 MSG 的值("无异常")和参数 DETAILS 的值(详细信息下方展开的文本)。

如何运行单元测试并分析结果。

在此演示中,您将了解如何执行单元测试并分析结果。

视频:本节含 SAP 官方视频(视频 ID 1_mtx5oqh0),需在 learning.sap.com 在线观看。

测试固定设备和前提条件

测试固定设备的方法

有时,测试需要特定配置才能正常运行。此类测试配置称为测试固定装置。测试固定设备可能由测试数据、测试对象和资源组成。

要创建和移除固定设备,可以在测试类中实施附加方法。这些方法具有预定义名称,并且在执行测试时由 ABAP 运行时环境自动调用。

存在以下固定设备方法:

  • 设置 在每次测试类测试之前,都会调用此实例方法。针对要为每个测试重新创建的固定设备使用此方法。

  • TEARDOWN 每次测试类测试后,都会调用此实例方法。使用此方法撤销在方法 SETUP 中所做的更改。如果 SETUP 对持久数据(系统配置、定制、主数据等)进行更改,则使用 TEARDOWN 尤为重要。

  • CLASS_SETUP 此静态方法在测试类的首次测试之前执行一次。仅对耗时且您确定任何测试方法都不会更改设置的固定设备使用此方法。

  • CLASS_TEARDOWN 此静态方法在测试类的最后测试后执行一次。使用此方法撤消在方法 CLASS_SETUP 中所做的更改。

ABAP 单元测试的流逻辑

此图说明了单个测试类的 ABAP UNIT 程序流。

  • 首先,将测试类加载到程序内存中。如果测试类包含静态构造函数(静态方法 CLASS_CONSTRUCTOR),则照常执行此方法。

  • 然后,执行此测试类的测试。每个测试都从创建测试类的实例开始。如果测试类包含实例构造函数(方法 CONSTRUCTOR),则照常执行。

  • 然后,ABAP 单元框架调用测试方法。

  • 测试后,将放弃实例。

如果测试类中存在多个测试方法,则会为每个测试创建一个新实例。

在最后一个测试方法之后,此测试类的处理已完成,框架将继续处理下一个测试类(如果有)。

ABAP 单元测试的阶段模型

此图说明了 ABAP 单元测试的阶段模型,即框架调用固定设备方法的时间点。

CLASS_SETUP 仅在静态构造函数之后和第一次测试之前调用一次。

CLASS_TEARDOWN 在框架开始处理下一个测试类之前调用。

SETUP 在实例构造函数之后且测试执行之前调用。

在框架放弃测试类实例之前调用 TEARDOWN。

报告缺少前提条件

您已经学习了如何使用服务类 CL_ABAP_UNIT_ASSERT 的方法报告失败的测试。

但是,如果缺少前提条件(例如,缺少权限),您应该怎么做?或者 SETUP 方法在准备测试时遇到问题的情况?

对于这些情况,类 CL_ABAP_UNIT_ASSERT 包含方法 skip() 和以 ASSUME_ 开头的多个方法。

这些方法的工作原理与 failure() 和 assert-methods 非常相似,但在 UNIT 测试结果中它们看起来有所不同。这样可以更轻松地区分测试编码中的错误,以及系统配置或测试设计中的错误。

让我们看一个示例:

方法 test_with_fail( ) 调用方法 cl_abap_unit_assert=>fail( )。此方法计为失败测试,此方法的失败跟踪在消息文本前显示关键断言错误。

方法 test_with_skip( ) 调用方法 cl_abap_unit_assert=>skip( )。此方法计为已中止测试,此方法的失败跟踪在消息文本前显示 缺少前提条件 。

注意

方法 skip() 和 assume-methods 中没有参数 QUIT 和 LEVEL。如果缺少前提条件,则无需继续测试,也不会区分不同的严重性。

如何使用 SETUP 运行复杂单元测试

在此演示中,您将了解如何运行包含设置方法的更复杂的测试。

视频:本节含 SAP 官方视频(视频 ID 1_jq9iy2ms),需在 learning.sap.com 在线观看。

实施和运行 ABAP 单元测试

您注意到代码输出不正确。特别是,下一可用货物航班的日期似乎不正确。为本地类 lcl_carrier 的方法 find_cargo_flight 定义和实施 ABAP 单元测试以进一步分析此问题。

模板:

  • /LRN/CL_S4D401_ATS_CHECKED(全局类)

解决方案:

  • /LRN/CL_S4D401_ATS_UNIT_TEST(全局类)

任务 1: 复制模板(可选)

复制模板类 /LRN/CL_S4D401_ATS_CHECKED。如果已完成上一练习,则可以跳过此任务并继续编辑类 ZCL_##_SOLUTION

步骤

  1. 将类 /LRN/CL_S4D401_ATS_CHECKED 复制到您自己的包中的类(建议名称:ZCL_##_SOLUTION,其中 ## 代表组编号)。

  2. 在项目浏览器中,右键单击类 /LRN/CL_S4D401_ATS_CHECKED 以打开上下文菜单。

  3. 从上下文菜单中,选择复制....

  4. 在包字段中输入的名称。在名称字段中,输入名称 ZCL_##_SOLUTION,其中 ## 代表组编号。

  5. 选择下一步

  6. 确认传输请求并选择完成

  7. 激活复制。

  8. Ctrl + F3 激活类。

任务 2: 定义局部测试类

在全局类中,定义本地测试类(建议名称:ltcl_find_flights)。定义并实施类 **lcl_**carrier 的方法 find_cargo_flight 的测试。

步骤

  1. 创建本地测试类 ltcl_find_flights

  2. 导航至*测试类(不存在)*标签。

  3. 如果 创建测试类... 按钮显示在 标签上,请选择该按钮。

  4. 将光标置于第一个源代码行中的注释末尾,然后按回车

  5. 在新的源代码行中,键入 test 并按 **Ctrl + 空格**键以调用代码完整化。

  6. 选择源代码模板 testClass - Test class (ABAP Unit) 并按 Enter

  7. 在突出显示初始类名 ltcl_ 的同时,键入完整的类名 ltcl_find_flights

  8. 使用快速修复重命名预定义的测试方法(建议名称:test_find_cargo_flight)。

  9. 右键单击 first_test,然后选择快速修复。从显示的列表中,选择重命名 first_test。当名称突出显示时,键入新名称 test_find_cargo_flight

  10. 实施方法 test_find_cargo_flight。从数据库表 /LRN/CARGOFLIGHT 中读取可用容量至少为 1 千克的任何货物航班的关键字段(carrier_id、connection_id、flight_date)和两个机场(airport_from_id 和 airport_to_id)。将结果存储在合适的数据对象中(建议名称:some_flight_data)

提示

可用容量计算为两个表字段 maximum_load 和 actual_load 之间的差异。

  1. 如下调整代码:

METHOD test_find_cargo_flight.
SELECT SINGLE
FROM /lrn/cargoflight
FIELDS carrier_id, connection_id, flight_date,
airport_from_id, airport_to_id
WHERE maximum_load - actual_load >= 1
INTO @DATA(some_flight_data).
ENDMETHOD.
  1. 如果表 /LRN/CARGOFLIGHT 中不存在合适的数据集,则将测试报告为失败。

  2. 如下调整代码:

METHOD test_find_cargo_flight.
SELECT SINGLE
FROM /lrn/cargoflight
FIELDS carrier_id, connection_id, flight_date,
airport_from_id, airport_to_id
WHERE maximum_load - actual_load >= 1
INTO @DATA(some_flight_data).
IF sy-subrc <> 0.
cl_abap_unit_assert=>fail( `No suitable data in table /LRN/CARGOFLIGHT` ).
ENDIF.
ENDMETHOD.
  1. 如果能够从表 /LRN/CARGOFLIGHT 中读取合适的记录,则使用此记录的 carrier_id 值创建类 lcl_carrier 的实例(参考的建议名称:the_carrier)。

  2. 在方法的末尾添加以下代码:

DATA(the_carrier) = NEW lcl_carrier( i_carrier_id =
some_flight_data-carrier_id ).
  1. 如果类 lcl_carrier 的构造函数引发异常报表,则测试失败。

  2. 如下调整代码:

TRY.
DATA(the_carrier) = NEW lcl_carrier( i_carrier_id =
some_flight_data-carrier_id ).
CATCH cx_abap_invalid_value.
cl_abap_unit_assert=>fail( `Unable to instantiate lcl_carrier` ).
ENDTRY.
  1. 如果实例化成功,则调用 lcl_carrier 实例的方法 find_cargo_flight。使用所选货物航班的机场和航班日期作为输入。将最小免费货物(参数 i_goods)设置为 1。将结果存储在合适的数据对象中(建议名称:flightdays_later)。

  2. 在方法的末尾添加以下代码:

the_carrier->find_cargo_flight(
EXPORTING
i_airport_from_id = some_flight_data-airport_from_id
i_airport_to_id   = some_flight_data-airport_to_id
i_from_date       = some_flight_data-flight_date
i_cargo           = 1
IMPORTING
e_flight          = data(flight)
e_days_later      = data(days_later)
).
  1. 分析方法的输出。调用适当的 assert 方法以确保参数 e_flight 返回有效的对象参考,并且参数 e_days_later 返回零。

注意

参数 e_days_later 返回值 0 表示该方法直接在请求日期找到合适的航班。如果我们使用现有航班的属性作为输入,这就是我们的预期。

  1. 在 方法的末尾,添加以下代码:

cl_abap_unit_assert=>assert_bound(
act = flight
msg = `Method find_cargo_flight does not return a result`
).
cl_abap_unit_assert=>assert_equals(
act = days_later
exp = 0
msg = `Method find_cargo_flight returns wrong result`
).
  1. 激活类。

  2. Ctrl + F3 激活类。

任务 3: 执行单元测试

执行单元测试。如果测试返回错误,则分析结果并调整方法 find_cargo_flight 的实施。

警告

如果单元测试已中止,并显示消息超过允许的最大持续时间 60 秒,则增加预期执行时间。为此,将测试类的 CLASS … DEFINITION 语句中的 DURATION 附加项从 DURATION SHORT 更改为 DURATION MEDIUM。

步骤

  1. 在全局类 ZCL_##_SOLUTION 中执行单元测试。

  2. 在项目浏览器中,右键单击类 ZCL_##_SOLUTION 并选择运行方式ABAP 单元测试

  3. 测试完成后,分析 ABAP 单元视图中的结果。

  4. 如果尚未显示,请单击屏幕底部的 ABAP 单元标签。

  5. 您应该会在失败/错误图标旁边看到值 1

  6. 在树中,选择测试方法名称 ( zcl_##_solutionltcl_find_flightstest_find_cargo_flight) 以显示错误的详细信息。

  7. 分析方法 find_cargo_flight 的代码并修复错误。

  8. 在树中,双击 test_find_cargo_flight 以导航到测试方法的实施。

  9. 向下滚动至方法 find_cargo_flight 的调用。将光标置于方法名称上,然后按 F3 导航到其实施。

  10. 查找 days_later 的计算。在此,从提供的日期中减去航班日期。这应该是相反的。

  11. 要修复错误,请在此语句前面添加注释符号:

*  DATA(days_later) = i_from_date - flight->flight_date.

将其替换为此代码:

DATA(days_later) = flight->flight_date - i_from_date.
  1. 激活类并重新运行单元测试以确认错误现在已消失。

  2. Ctrl + F3 激活类。

  3. ABAP 单元标签上的工具栏中选择重新运行测试

  4. 确保测试现在显示零失败/错误。

  5. 以控制台应用形式执行全局类,以确保下次可用货物航班的日期在相当短的时间内完成。

任务 4: 实现 CLASS_SETUP 方法(可选)

您计划向测试类添加更多测试方法,这些测试方法都需要一个 lcl_carrier 的实例。为提高测试性能,将类 lcl_carrier 的耗时实例化移动到 class_setup 方法,因为此方法仅针对测试类的所有测试执行一次。

步骤

  1. 在测试类 ltcl_find_flights 中定义私有静态方法 class_setup(不含参数),并使用快速修复添加方法实施。

  2. 将以下代码添加到类 ltcl_find_flights 的专用部分。

CLASS-METHODS class_setup.
  1. 右键单击 class_setup,然后选择快速修复

  2. 从建议清单中,选择 添加 class_setup 的实施

  3. 在测试类中,声明一个私有静态属性,可在其中存储类 lcl_carrier 的实例(建议名称:the_carrier)的引用。

  4. 将以下代码添加到类 ltcl_find_flights 的专用部分。

CLASS-DATA the_carrier TYPE REF TO lcl_carrier.
  1. 声明一个私有静态属性,您可以在其中存储数据库表 /LRN/CARGOFLIGHT 中的一个记录(建议名称:some_flight_data)。

  2. 将以下代码添加到类 ltcl_find_flights 的专用部分。

CLASS-DATA some_flight_data TYPE /lrn/cargoflight.
  1. 将 SELECT 语句和类 lcl_carrier 的实例化从方法 test_find_cargo_flight 移动到方法 class_setup。在数据库中将数据存储在静态属性 some_flight_data 中,并将对 lcl_carrier 实例的参考存储在静态属性 the_carrier 中。

  2. 从方法 test_find_cargo_flight 的实施中剪切以下代码,然后将其粘贴到方法 class_setup 的实施中。

SELECT SINGLE
FROM /lrn/cargoflight
FIELDS carrier_id, connection_id, flight_date,
airport_from_id, airport_to_id
WHERE maximum_load - actual_load >= 1
INTO @DATA(some_flight_data).
IF sy-subrc <> 0.
cl_abap_unit_assert=>fail( `No suitable data in table /LRN/CARGOFLIGHT` ).
ENDIF.
TRY.
DATA(the_carrier) = NEW lcl_carrier( i_carrier_id =
some_flight_data-carrier_id ).
CATCH cx_abap_invalid_value.
cl_abap_unit_assert=>fail( `Unable to instantiate lcl_carrier` ).
ENDTRY.
  1. INTO @DATA(some_flight_data) 替换为 INTO CORRESPONDING FIELDS OF @some_flight_data

  2. DATA(the_carrier) 替换为 the_carrier

  3. 现在,方法 class_setup 的代码应如下所示:

METHOD class_setup.
SELECT SINGLE
FROM /lrn/cargoflight
FIELDS carrier_id, connection_id, flight_date,
airport_from_id, airport_to_id
WHERE maximum_load - actual_load >= 1
INTO CORRESPONDING FIELDS OF @some_flight_data.
IF sy-subrc <> 0.
cl_abap_unit_assert=>fail( `No suitable data in table /LRN/CARGOFLIGHT` ).
ENDIF.
TRY.
the_carrier = NEW lcl_carrier( i_carrier_id =
some_flight_data-carrier_id ).
CATCH cx_abap_invalid_value.
cl_abap_unit_assert=>fail( `Unable to instantiate lcl_carrier` ).
ENDTRY.
ENDMETHOD.
  1. 激活类并再次运行单元测试以确保其仍然有效。

  2. Ctrl + F3 激活类。

  3. 像以前一样运行单元测试。