Apex 测试:编写单元测试

掌握 Apex 单元测试的核心模式:正向测试与负向测试、触发器集成测试、基于权限的测试,以及 mock 和 stub 对象。本文通过 AccountWrapper、AccountDeletionTrigger、ExternalSearch 等实例,讲解如何写出容错、可信、易维护的测试代码。...

📅 2026/9/24 ✍️ ponybai 🏷️ salesforce, apex, developer, testing, headless

学习目标

slide_2

完成本单元后,你将能够:

  • 解释正向测试为什么重要,并编写正向单元测试。
  • 解释什么是负向单元测试,并编写负向单元测试。

编写正向测试

slide_3

正向单元测试是指在向被测代码传入有效数据时,返回预期结果的测试。以经典计算器为例:加法方法的正向测试会用数据点 2 和 3 执行,并断言输出为 5。思想很简单——用有效数据执行时,被测代码应产生可预测的结果。

正向测试模式

slide_4

正向单元测试的核心是:当输入数据已知良好时,看到一组预期结果。标准模式包含以下步骤:

  • 使用数据工厂或 CSV 文件准备数据。
  • 做断言来验证数据。
  • 通过 Test.startTest() 重置 governor limits,将测试与测试数据准备隔离。
  • 执行被测代码。
  • 通过 Test.stopTest() 强制异步代码完成。
  • 断言被测代码的输出(例如记录存在、字段为预期值)。

以 AccountWrapper 类为例,它是 Account 对象之上的自定义逻辑层,用于计算账户关联商机的四舍五入平均金额。

安装 VS Code 并连接 Playground

slide_5

本模块中的测试通过 VS Code 执行。先完成「Apex Testing: Prepare for Unit Testing」徽章,把 VS Code 和 Trailhead Playground 设置好,并使用 VSCodeQuickstart 项目来完成本徽章。

安装非托管包

slide_6

在 Trailhead Playground 中安装一个包含可测试代码类的非托管包。包 ID 为 04taj000000BcRl。安装的包中包含一个名为 AccountWrapper.cls 的类,接下来为它编写正向测试。

代码要点:正向测试

slide_7

这个测试类使用 @TestSetup 方法加载测试数据,并调用工厂方法为每个账户生成五个商机。有了账户和商机,就可以测试 AccountWrapper 的 getRoundedAvgPriceOfOpps 方法——它返回该账户所有商机、四舍五入到最近千美元的平均价格。正向测试的数据在逻辑上与方法需求一致(商机金额为正值、平均值可预测)。正向测试的一条准则:给好的代码好的输入,它就会产生预期的结果。

编写负向测试

slide_8

负向单元测试不仅用于确保每一行代码都被测试,有时甚至比正向测试更重要。负向测试证明代码能正确处理无效数据、意外的用户输入,以及代码库其他部分的变化;更重要的是,它证明你的代码是容错的。

测试代码如何处理异常,与确保代码在有效条件下按预期工作同样重要。想象一个批处理进程:每天凌晨 2 点处理客户社区提交的账户更新。如果代码在批处理第一条记录上失败、又没有优雅地处理异常,剩余记录就不会被处理。负向测试能防止这种情况发生。

负向测试模式

slide_9

负向测试的一般模式如下:

  1. 生成或加载测试数据。
  2. 开启 try/catch 块。
  3. 调用 Test.startTest(),执行代码,再调用 Test.stopTest()。
  4. 在下一行加一个永远失败的断言(Assert.fail())——该语句不应被到达。
  5. 捕获预期的异常,并验证异常消息是否匹配。

AccountWrapper 类中有一行会抛出异常,测试正是针对它来验证:把商机金额设为 0,调用 getRoundedAvgPriceOfOpps 应抛出 AWException。

代码要点:负向测试

slide_10

负向测试基于 @TestSetup 创建的商机,用 for 循环把每个商机的金额字段设为 0。这样 getRoundedAvgPriceOfOpps 计算出四舍五入价格为 0,导致代码抛出 AWException(在 AccountWrapper 中定义的自定义异常)。

try/catch 块若写得不好,catch 可能捕获任何异常实例或子类。最佳实践是:捕获异常后,检查异常类型、消息和详情是否符合预期。如果只捕获异常而不校验类型和消息,测试就可能「假通过」(false positive),侵蚀对测试的信任。

为动手挑战做准备

slide_11

在 VS Code 中,每次运行一个或多个测试时,都可以获取 Apex 类和触发器的代码覆盖率。编辑用户或工作区设置,搜索并勾选 retrieve-test-code-coverage,然后运行 Apex 测试。之后就能在 Output 面板中看到每个 Apex 类和触发器的覆盖率百分比,以及未被测试覆盖的行。

创建专门测试

slide_12

本单元将编写针对 Apex 触发器和基于权限场景的专门测试。

学习目标

slide_13

完成本单元后,你将能够:

  • 为触发单条记录操作的触发器编写测试。
  • 执行类中的所有测试方法。
  • 解释基于权限测试的重要性,并编写基于权限的单元测试。

测试 Apex 触发器

slide_14

部署触发器前,要编写测试来执行触发该触发器的操作并验证预期结果。触发器测试不完全是单元测试——它测的不是方法做了什么,而是 DML 操作发生时触发器的行为,因此更准确地说是集成测试。

最佳实践是把所有触发器逻辑封装到一个类(通常叫 trigger handler)中,在那里编写真正的单元测试;同时也要编写如下的触发器集成测试。以 AccountDeletion 触发器为例:当账户有关联商机时,阻止删除该记录。测试通过 Database.delete(acct, false) 删除账户,并断言返回的 DeleteResult 不成功、错误消息为「Cannot delete account with related opportunities.」。

代码要点:触发器测试

slide_15

测试方法先准备一个带商机的测试账户,然后删除该账户,触发 AccountDeletionTrigger。测试通过检查 Database.delete 的返回值(Database.DeleteResult 对象)验证触发器阻止了删除:断言删除未成功,并校验返回的错误消息。

测试不同条件

slide_16

一个测试方法不足以覆盖触发器的所有可能输入。还需要测试其他条件:例如删除没有商机的账户(应成功删除),以及用批量记录(而非单条)测试相同场景。更新测试类,加入这三个额外测试方法,然后对 AccountDeletionTriggerTests 类运行 Run All Tests。

测试基于权限的场景

slide_17

基于权限的测试可能是最复杂的测试模式:一方面权限本身容易混淆,另一方面一套好的权限测试需要同时用到正向和负向测试模式。写权限测试时,不仅要生成测试数据,还要创建一或多个测试用户;之后以这些测试用户身份(有或没有特定权限)运行正/负向测试。

基于权限的测试模式

slide_18

权限测试的模式如下:

  1. 生成或加载测试数据。
  2. 创建带相应权限集的用户。
  3. 开启 System.runAs(user) 块。
  4. 在 System.runAs(user) 块内执行负向或正向测试。

权限测试是唯一不需要创建自己测试数据的情况——权限集记录本质是元数据而非数据,属于组织配置,因此测试时使用现有权限集即可。测试里只需创建一个测试用户并为其分配现有权限集。

代码要点:权限测试

slide_19

示例中,@TestSetup 创建了一个 Private_Object__c 记录(默认共享设置为私有)。在测试方法中创建新用户,并以 System.runAs(user) 查询该对象——因为没有权限集,新用户看不到任何记录,断言结果数量为 0。这演示了没有权限集的用户无法访问私有对象记录。

使用 Mock 和 Stub 对象

slide_20

本单元介绍 mock 和 stub 对象——更高级的单元测试技巧,让测试更易编写、理解和维护。

学习目标

slide_21

完成本单元后,你将能够:

  • 解释什么是 mock 和 stub。
  • 描述何时使用 mock 和 stub。
  • 使用 mock 和 stub 编写单元测试。

理解 Mock 和 Stub 对象

slide_22

Mock 和 stub 是单元测试中较高级的话题,但极其实用——它们让测试更易编写、理解和维护,并把被测代码与代码库其他部分的变化隔离开来。

它们统称 mock 对象,作用相同:都是替代真实对象实例的假对象,因此可以覆盖其功能、返回我们选择的数据。技术上有细微差别:mock 作用于对象层面,stub 替换单个方法。在 Salesforce Platform 上,开发者通过扩展平台接口来写 mock 和 stub,例如实现 HttpCalloutMock 接口创建 HTTP 响应 mock。创建 mock 对象是为了把被测代码与组织中其他代码(第三方代码、服务、其他类)隔离开。

Mock 和 Stub 测试模式

slide_23

Mock 对象有两个经典用例:第一个较专门——当从 Apex 向第三方 Web 服务发起 callout 时,需要 mock HTTP callout;第二个更通用——当测试依赖另一个对象内部状态或实现的代码时,用 stub 对象测试非常有用。

Mock 对象模式:HttpCalloutMock

slide_24

HttpCalloutMock 的模式:

  1. 创建一个实现 HttpCalloutMock 接口的类(例如 HTTPMockFactory)。
  2. 创建或加载测试数据。
  3. 创建 mock 实例,调用 Test.setMock(instance) 传入该实例。
  4. 调用 Test.startTest(),执行发起 callout 的代码,再调用 Test.stopTest()。
  5. 断言代码按预期工作。

Stub 对象模式

slide_25

使用 stub 对象的模式类似:

  1. 创建或加载测试数据。
  2. 创建实现 StubProvider 接口的类的实例。
  3. 调用 Test.startTest(),执行被测代码并传入 stub 实例,再调用 Test.stopTest()。
  4. 断言代码按预期工作。

创建 HTTPMockFactory 类

slide_26

创建一个 @IsTest 的 HTTPMockFactory 类,实现 HttpCalloutMock 接口。其构造函数接收 code、status、body 和 responseHeaders 参数,respond 方法用这些参数构造并返回一个 HttpResponse 对象。

代码要点:HTTPMockFactory

slide_27

该类的构造函数接收的参数会在 respond 方法中传回。就像数据用的 TestFactory 一样,这个工厂让我们能在测试中即时定义 mock 的返回内容。

测试:ExternalSearch 与 HTTP Mock

slide_28

安装的包中有一个 ExternalSearch.cls 类,它接收搜索字符串并执行 Web 搜索。为它写单元测试:创建 HTTPMockFactory 实例(返回 200、OK、「I found it!」),调用 Test.setMock(HttpCalloutMock.class, mock),然后调用 ExternalSearch.googleIt('epic search'),断言返回值为「I found it!」。

代码要点:ExternalSearch 测试

slide_29

这个测试的关键是调用 Test.setMock()——它确保代码永远不会真正发起 callout,而是返回你指定的 HttpResponse。在这里,googleIt 方法返回「I found it!」。把返回值注入 mock 工厂的能力,让你能轻松编写正向和负向测试。

Stubbing(存根)

slide_30

Stubbing 与 mocking 类似,但更灵活。安装的包中有一个 OpportunityDiscount.cls 类,其 getTotalDiscount 方法根据账户是否高优先级来确定商机的总折扣。因为 isHighPriority 的实现可能随时间变化,测试 getTotalDiscount 时不想依赖它的内部实现——这正是为 AccountWrapper 创建 stub 的绝佳场景。

测试通过 Test.createStub(AccountWrapper.class, new AccountWrapperMock()) 创建 stub,注入 OpportunityDiscount,分别验证低优先级(返回 .1)和高优先级(返回 .25)两种情况。

代码要点:Stub 测试

slide_31

这两个测试方法结构几乎相同:创建 AccountWrapper 的 mock 实例,注入 OpportunityDiscount 对象替代真实对象,于是我们能精确知道 isHighPriority 被调用时的响应。

关键区别在第二个测试的第一行:在 AccountWrapperMock 上设置静态变量。这是个实用技巧——当被 stub 的方法不接收参数时,可以用 stub 类中定义的静态变量在多个测试中有意改变 stub 的行为。

总结

slide_32

本模块信息量很大:你学习了正向测试、负向测试、权限测试,以及 mocking 和 stubbing。这些模式和工具共同构成了代码库中富有生产力的测试,也能帮你成为更好的开发者——因为写测试会开始塑造你写代码的方式。记住,内化这些思想和实践的最好方法,就是写更多的测试!


文章来源:Trailhead - Apex Testing: Write Unit Tests