测试 Apex

Apex 提供了一个测试框架,允许您编写单元测试,运行 测试,检查测试结果,并有代码覆盖率结果。

让我们来谈谈单元测试、测试的数据可见性以及可用的工具 在 Lightning 平台上用于测试 Apex。我们还将介绍测试最佳实践 以及一个测试示例。

注意

若要保护数据的隐私,请确保测试错误消息和异常 详细信息不包含任何个人数据。Apex 异常处理程序和测试 框架无法确定敏感数据是否包含在用户定义的消息中,并且 详。要在自定义 Apex 例外中包含个人数据,我们建议您 创建一个 Exception 子类,其中包含保存个人数据的新属性。然后 不要在异常的消息字符串中包含子类属性信息。

  • 了解 Apex 中的测试
  • 在 Apex 中测试什么
  • 什么是 Apex 单元测试?
  • 了解测试数据
    Apex 测试数据是暂时性的,不会提交到数据库。
  • 运行单元测试方法
    若要验证 Apex 代码的功能,请执行单元测试。您可以在开发人员控制台、设置、Visual Studio Code 的 Salesforce 扩展中或使用 API 运行 Apex 测试方法。
  • 测试最佳实践
  • 测试实例
  • 测试和代码覆盖率
    Apex 测试框架为您的 Apex 类生成代码覆盖率数字,并在您每次运行一个或多个测试时触发。代码覆盖率指示测试方法已执行类和触发器中有多少行可执行代码。编写测试方法来测试触发器和类,然后运行这些测试以生成代码覆盖率信息。
  • 代码覆盖率最佳实践 请考虑以下代码覆盖率提示和最佳实践
  • 使用 Stub API 构建模拟框架 Apex 提供了一个用于实现模拟框架的存根 API
    。模拟框架有很多好处。它可以简化和改进测试,并帮助您创建更快、更可靠的测试。您可以使用它来单独测试类,这对于单元测试非常重要。使用存根 API 构建模拟框架也是有益的,因为存根对象是在运行时生成的。由于这些对象是动态生成的,因此不必打包和部署测试类。您可以构建自己的模拟框架,也可以使用其他人构建的模拟框架。

了解 Apex 中的测试

测试是长期成功发展的关键,是一种 开发过程的关键组成部分。强烈建议使用测试驱动开发过程,即同时进行的测试开发 时间即代码开发。

为什么要测试 Apex?

测试是应用程序成功的关键,尤其是当您的应用程序要 部署到客户。如果验证应用程序是否按预期工作,则有 没有意外行为,您的客户会更加信任您。

有两种方法可以测试应用程序。一种是通过Salesforce用户界面, 很重要,但仅仅通过用户界面进行测试并不能捕获所有用例 您的应用程序。另一种方法是测试批量功能:最多可以测试 200 条记录 如果代码是使用 SOAP API 或 Visualforce 标准集调用的,则通过代码进行传递 控制器。

应用程序很少完成。您将拥有它的其他版本,您可以在其中进行更改 并扩展功能。如果你已经编写了全面的测试,你可以确保 回归不会随任何新功能一起引入。在为 Salesforce 部署代码或打包代码之前 AppExchange,则必须满足以下条件。

  • 单元测试必须至少覆盖 75% 的 Apex 代码,并且所有这些测试都必须 成功完成。请注意以下事项。
    • 将 Apex 部署到生产组织时,每个单元测试 默认情况下,将执行 Organization 命名空间。
    • 调用不计入 Apex 代码的一部分 覆盖。System.debug
    • 测试方法和测试类不计为 Apex 代码的一部分 覆盖。
    • 虽然只有 75% 的 Apex 代码必须被测试覆盖,但不要专注于 所覆盖代码的百分比。相反,请确保每次使用 涵盖您的申请案例,包括正面和负面案例, 以及批量和单条记录。这种方法可确保 75% 或更多 的代码包含在单元测试中。
  • 每个触发器都必须具有一定的测试覆盖率。
  • 所有类和触发器都必须成功编译。

Salesforce 在具有 Apex 代码的所有组织中运行所有测试,以验证没有行为 已因任何服务升级而更改。

在 Apex 中测试什么

Salesforce 建议您为以下内容编写测试:单次操作测试以验证单个记录是否生成正确的预期结果。批量操作任何 Apex 代码,无论是触发器、类还是扩展,都可以为 1 到 200 调用 记录。您不仅必须测试单个记录案例,还必须测试批量案例。积极行为测试以验证预期行为是否通过每个预期的排列发生,即 是,用户正确填写了所有内容并且没有超出限制。负面行为您的应用程序可能存在限制,例如无法添加未来日期、 无法指定负数,依此类推。您必须测试阴性情况 并验证错误消息是否正确生成,以及对于正错误消息,在 限制情况。受限用户测试对代码中使用的 sObject 具有受限访问权限的用户是否看到 预期行为。也就是说,它们是否可以运行代码或接收错误消息。

注意

条件运算符和三元运算符不被视为执行,除非两者都为正 并执行负分支。

有关这些类型的测试的示例,请参阅测试 示例

什么是 Apex 单元测试?

为了促进健壮、无错误的代码的开发,Apex 支持创建和 执行单元测试。单元测试是验证 特定代码段工作正常。单元测试方法不带任何参数, 不向数据库提交任何数据,也不发送电子邮件。此类方法在方法定义中使用注释进行标记。 单元测试方法必须在测试类中定义,即用 批注的类。@IsTest@IsTest例如:

@IsTest
private class myClass {
    @IsTest
    static void myTest() {
        // code_block
    }
}

使用注解定义类和 仅包含用于测试应用程序的代码的方法。注释可以采用多个修饰符 括号内,并用空格分隔。@IsTest@IsTest

注意

上的注释 methods 等同于关键字。如 最佳做法,Salesforce 建议您使用 而不是 .关键字可以在 未来版本。@IsTesttestMethod@IsTesttestMethodtestMethod

此测试类示例包含两个测试方法。

@IsTest
private class MyTestClass {

   // Methods for testing
   @IsTest
   static void test1() {
      // Implement test code
   }

   @IsTest
   static void test2() {
      // Implement test code
   }

}

类和方法定义为 can be 或 。测试类方法的访问级别 无所谓。在定义测试类或 测试方法。Apex 中的默认访问级别是私有的。测试框架可以 始终找到测试方法并执行它们,无论其访问级别如何。@IsTestprivatepublic

定义为必须是顶级的类 类,不能是接口或枚举。@IsTest

测试类的方法只能从测试方法或测试调用的代码中调用 方法;非测试请求无法调用它。

此示例显示要测试的类及其对应的测试类。它包含两个 方法和构造函数。

public class TVRemoteControl {
    // Volume to be modified
    Integer volume;
    // Constant for maximum volume value
    static final Integer MAX_VOLUME = 50;    
    
    // Constructor
    public TVRemoteControl(Integer v) {
        // Set initial value for volume
        volume = v;
    }
        
    public Integer increaseVolume(Integer amount) {
        volume += amount;
        if (volume > MAX_VOLUME) {
            volume = MAX_VOLUME;
        } 
        return volume;
    }
    
    public Integer decreaseVolume(Integer amount) {
        volume -= amount;
        if (volume < 0) {
            volume = 0;
        }  
        return volume;
    }    
    
    public static String getMenuOptions() {
        return 'AUDIO SETTINGS - VIDEO SETTINGS';
    }
       
}

此示例包含具有四个测试方法的相应测试类。中的每个方法 调用上一个类。尽管有足够的测试覆盖率,但测试 Test 类中的方法执行额外的测试以验证边界条件。

@IsTest
class TVRemoteControlTest {
    @IsTest 
    static void testVolumeIncrease() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.increaseVolume(15);
        System.assertEquals(25, newVolume);
    }
    
    @IsTest
    static void testVolumeDecrease() {
        TVRemoteControl rc = new TVRemoteControl(20);
        Integer newVolume = rc.decreaseVolume(15);
        System.assertEquals(5, newVolume);        
    } 
        
    @IsTest
    static void testVolumeIncreaseOverMax() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.increaseVolume(100);
        System.assertEquals(50, newVolume);        
    }
    
    @IsTest
    static void testVolumeDecreaseUnderMin() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.decreaseVolume(100);
        System.assertEquals(0, newVolume);        
    }
    
    @IsTest
    static void testGetMenuOptions() {
        // Static method call. No need to create a class instance.
        String menu = TVRemoteControl.getMenuOptions();
        System.assertNotEquals(null, menu);
        System.assertNotEquals('', menu);
    }
}

单元测试注意事项

以下是有关单元测试的一些注意事项。

  • 从 Salesforce API 28.0 开始,测试方法不能再驻留在非测试中 类,并且必须是用 注释的类的一部分。请参阅 TestVisible 注解以了解 如何从测试类访问私有类成员。IsTest
  • 测试方法不能用于测试 Web 服务标注。相反,请使用 模拟标注。请参阅测试 Web 服务标注和测试 HTTP 标注。
  • 不能从测试方法发送电子邮件。
  • 由于测试方法不会提交在测试中创建的数据,因此 不必在完成后删除测试数据。
  • 如果测试类中静态成员变量的值在 testSetup 或 test 方法,则不会保留新值。其他测试方法 此类获取静态成员变量的原始值。此行为 当静态成员变量在另一个类中定义时也适用,并且 在测试方法中访问。
  • 对于某些具有具有唯一约束的字段的 sObject,插入重复项 sObject 记录错误结果。例如,插入 CollaborationGroup 具有相同名称的 sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。
  • Chatter 摘要中记录(FeedTrackedChange 记录)的跟踪更改不是 当测试方法修改关联记录时可用。FeedTracked更改 记录要求对与其关联的父记录进行更改 在创建数据库之前提交到数据库。由于测试方法没有 提交数据,它们不会导致创建 FeedTrackedChange 记录。 同样,无法在测试方法中创建字段历史记录跟踪记录 因为它们需要首先提交其他 sObject 记录。例如 无法在测试方法中创建 AccountHistory 记录,因为 Account 记录 必须首先提交。
  • 如果您的测试包括 DML,请确保不超过MAX_DML_ROWS 限制。请参阅执行调控器和限制中的“其他顶点限制”
  1. 访问私有测试类成员

访问私有测试类成员

测试方法是在测试类中定义的,与它们测试的类分开。这可以 当必须从测试中访问私有类成员变量时出现问题 方法,或者在调用私有方法时。因为这些是私有的,所以它们不是 对测试类可见。您可以修改类中的代码以公开公共 将使用这些私有类成员的方法,或者您可以简单地注释 这些带有 . 当您使用此批注对私有成员或受保护成员进行批注时,它们可以 由测试方法访问,并且仅在测试上下文中运行代码。TestVisible

此示例说明如何与 私有成员变量,带有构造函数的私有内部类,私有方法, 和私有自定义例外。所有这些都可以在测试类中访问,因为 它们带有 注释。这 类首先列出,后跟一个包含测试方法的测试类。TestVisibleTestVisible

public class VisibleSampleClass {
    // Private member variables
    @TestVisible private Integer recordNumber = 0;
    @TestVisible private String areaCode = '(415)';
    // Public member variable
    public Integer maxRecords = 1000;
    
    // Private inner class
    @TestVisible class Employee {
        String fullName;
        String phone;
        
        // Constructor
        @TestVisible Employee(String s, String ph) {
            fullName = s;
            phone = ph;
        }
    }
       
    // Private method
    @TestVisible private String privateMethod(Employee e) {
        System.debug('I am private.');
        recordNumber++;
        String phone = areaCode + ' ' + e.phone;
        String s = e.fullName + '\'s phone number is ' + phone;
        System.debug(s);
        return s;
    }
    
    // Public method
    public void publicMethod() {
        maxRecords++;
        System.debug('I am public.');    
    }
    
    // Private custom exception class
    @TestVisible private class MyException extends Exception {}
}
// Test class for VisibleSampleClass
@isTest
private class VisibleSampleClassTest {

    // This test method can access private members of another class 
    // that are annotated with @TestVisible.
    static testmethod void test1() {
        VisibleSampleClass sample = new VisibleSampleClass ();

        // Access private data members and update their values
        sample.recordNumber = 100;
        sample.areaCode = '(510)';
        
        // Access private inner class
        VisibleSampleClass.Employee emp = 
            new VisibleSampleClass.Employee('Joe Smith', '555-1212');
        
        // Call private method
        String s = sample.privateMethod(emp);
        
        // Verify result
        System.assert(
            s.contains('(510)') &&
            s.contains('Joe Smith') &&
            s.contains('555-1212'));
    }
    
    // This test method can throw private exception defined in another class
    static testmethod void test2() {
        // Throw private exception.
        try {
            throw new VisibleSampleClass.MyException('Thrown from a test.');
        } catch(VisibleSampleClass.MyException e) {
            // Handle exception 
        }
    }
    
    static testmethod void test3() {
        // Access public method.
        // No @TestVisible is used.
        VisibleSampleClass sample = new VisibleSampleClass ();
        sample.publicMethod();
    }   

}

当您 升级 Salesforce API 包含混合测试代码和非测试代码的现有类的版本。因为测试 从 API 版本 28.0 开始,非测试类中不允许使用方法,您必须 升级 API 版本 时,将测试方法从旧类移动到新的测试类(用 批注的类) 你的班级。在访问私有方法或 原始类的成员变量。在这种情况下,只需注释这些私有 具有 .TestVisibleisTestTestVisible

了解测试数据

Apex 测试数据是暂时性的,不会提交到数据库。

这意味着在测试方法完成执行后,测试插入的数据 不会保留在数据库中。因此,无需删除任何测试数据 测试的结论。同样,对现有记录的所有更改,例如更新或 删除,不要持久。测试数据的这种瞬态行为使得管理 数据更容易,因为您不必执行任何测试数据清理。同时,如果你 测试访问组织数据,这可以防止意外删除或修改现有数据 记录。

默认情况下,现有组织数据对测试方法不可见,但例外 某些设置对象。应尽可能为测试方法创建测试数据。 但是,针对 Salesforce API 版本 23.0 或更早版本保存的测试代码可以访问组织中的所有数据。数据可见性 下一节将更详细地介绍测试。

  • 在单元测试中将测试数据与组织数据隔离
  • 使用 isTest(SeeAllData=True) 批注 批注
    测试类或测试方法,以打开对组织中记录的数据访问。IsTest(SeeAllData=true) 批注适用于数据查询,但不适用于记录创建或更改,包括删除。即使在使用注释时,新记录和更改的记录仍会在 Apex 测试中回滚。IsTest(SeeAllData=true)
  • 加载测试数据 使用该方法,可以在测试方法中填充数据
    ,而无需编写许多代码行。Test.loadData
  • 用于创建
    测试数据的公共测试实用程序类 公共测试实用程序类是公共测试类,其中包含用于创建测试数据的可重用代码。
  • 使用测试设置方法 使用测试设置方法(用 批注的方法)创建一次测试记录,然后在测试类中的每个测试方法
    中访问它们。当您需要为所有测试方法创建参考或先决条件数据,或者需要为所有测试方法操作的一组通用记录创建时,测试设置方法可以节省时间。@testSetup

将测试数据与单元中的组织数据隔离 测试

默认情况下,Apex 测试方法(API 版本 24.0 及更高版本)不能 访问预先存在的组织数据,例如标准对象、自定义对象和自定义设置 数据。他们只能访问他们创建的数据。但是,用于管理 仍然可以在测试中访问您的组织或元数据对象。这些是一些 此类对象的示例。

  • 用户
  • 轮廓
  • 组织
  • Cron触发器
  • 记录类型
  • 顶点类
  • Apex触发器
  • Apex组件
  • ApexPage(顶点页面)

尽可能为每个测试创建测试数据。您可以通过以下方式禁用此限制 使用注释注释测试类或测试方法。IsTest(SeeAllData=true)

使用 Salesforce API 版本 23.0 或更早版本保存的测试代码 继续有权访问组织中的所有数据,其数据访问权限为 变。数据访问注意事项

  • 使用数据孤岛 Apex 测试时,不支持使用该关系的跨对象字段引用。由于 此限制在数据孤岛 Apex 测试中运行时返回 null。OwnerSELECT Owner.IsActive FROM Account
  • 如果使用 Salesforce API 版本 24.0 或更高版本保存的新测试方法调用方法 在使用版本 23.0 或更低版本保存的另一个类中,数据访问限制 调用方在被调用的方法中强制执行。调用的方法无法访问 组织数据,因为调用方无法访问它,即使它保存在 早期版本。
  • 注解具有 添加到使用 Salesforce API 版本 23.0 保存的 Apex 代码时不起作用,并且 早些时候。IsTest(SeeAllData=true)
  • 此对测试数据的访问限制适用于所有代码 在测试上下文中运行。例如,如果测试方法导致触发器执行 并且测试无法访问组织数据,触发器将无法 也。
  • 如果测试发出 Visualforce 请求,则执行的测试将停留在测试上下文中,但 在不同的线程中运行。因此,不再强制执行测试数据隔离。在 在这种情况下,测试将能够访问组织中的所有数据 发起 Visualforce 请求。但是,如果 Visualforce 请求执行 callback,例如 JavaScript remoting 调用,则回调插入的任何数据都不是 对测试可见。
  • 在 API 版本 27.0 及更早版本中,VLOOKUP 验证规则函数始终查找 当由正在运行的 Apex 测试触发时,除了测试数据外,还会向上组织数据。开头为 版本 28.0,VLOOKUP 验证规则函数不再访问组织 来自正在运行的 Apex 测试的数据。该函数仅查找测试创建的数据, 除非测试类或方法用 注释。IsTest(SeeAllData=true)
  • 在某些情况下,您可能无法从测试中创建某些类型的数据 由于特定的限制,方法。以下是此类限制的一些示例。
    • 某些标准对象不可创建。有关这些对象的详细信息, 请参阅对象参考 Salesforce的。
    • 对于某些具有具有唯一约束的字段的 sObject,插入重复项 sObject 记录错误结果。例如,插入 CollaborationGroup 具有相同名称的 sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。无论您的测试是否被批注,都会发生此错误 有 ,或没有。IsTest(SeeAllData=true)
    • 仅在将相关记录提交到 数据库,例如 Chatter 中跟踪的更改。记录的跟踪更改 (FeedTrackedChange 记录)在测试方法时不可用 修改关联的记录。FeedTrackedChange 记录需要更改为 之前要提交到数据库的与它们关联的父记录 它们被创造出来。由于测试方法不提交数据,因此它们不会导致 创建 FeedTrackedChange 记录。同样,字段历史记录跟踪记录 无法在测试方法中创建,因为它们需要其他 sObject 记录 首先承诺。例如,无法在测试中创建 AccountHistory 记录 方法,因为必须首先提交帐户记录。

使用 isTest(SeeAllData=True) 注解

对测试类或测试方法进行批注,以打开对组织中记录的数据访问。这 IsTest(SeeAllData=true) 批注适用于数据查询,但不适用于记录创建 或更改,包括删除。新记录和更改的记录仍会在 Apex 测试中回滚 即使在使用注释时。

IsTest(SeeAllData=true)

警告

通过用 注释类,可以允许测试方法访问所有组织记录。这 但是,最佳做法是使用 .取决于 API 版本 您正在使用时,默认注释可能会有所不同。@isTest(SeeAllData=true)@isTest(SeeAllData=false)此示例演示如何使用注释定义测试类。所有 此类中的测试方法可以访问 组织。

@IsTest(SeeAllData=true)

// All test methods in this class can access all data.
@IsTest(SeeAllData=true)
public class TestDataAccessClass {

    // This test accesses an existing account. 
    // It also creates and accesses a new test account.
    @IsTest
    static void myTestMethod1() {
        // Query an existing account in the organization. 
        Account a = [SELECT Id, Name FROM Account WHERE Name='Acme' LIMIT 1];
        System.assert(a != null);
        
        // Create a test account based on the queried account.
        Account testAccount = a.clone();
        testAccount.Name = 'Acme Test';
        insert testAccount;
        
        // Query the test account that was inserted.
        Account testAccount2 = [SELECT Id, Name FROM Account 
                                WHERE Name='Acme Test' LIMIT 1];
        System.assert(testAccount2 != null);
    }
       
    
    // Like the previous method, this test method can also access all data
    // because the containing class is annotated with @IsTest(SeeAllData=true).
    @IsTest
    static void myTestMethod2() {
        // Can access all data in the organization.
   }
  
}

第二个示例演示如何在测试上应用注释 方法。由于测试方法的类没有注释,因此必须注释 方法,以便能够访问该方法的所有数据。第二种测试方法没有 具有此注释,因此它只能访问它创建的数据。此外,它还可以 访问用于管理组织的对象,例如 用户。

@IsTest(SeeAllData=true)

// This class contains test methods with different data access levels.
@IsTest
private class ClassWithDifferentDataAccess {

    // Test method that has access to all data.
    @IsTest(SeeAllData=true)
    static void testWithAllDataAccess() {
        // Can query all data in the organization.      
    }
    
    // Test method that has access to only the data it creates
    // and organization setup and metadata objects.
    @IsTest
    static void testWithOwnDataAccess() {
        // This method can still access the User object.
        // This query returns the first user object.
        User u = [SELECT UserName,Email FROM User LIMIT 1]; 
        System.debug('UserName: ' + u.UserName);
        System.debug('Email: ' + u.Email);
        
        // Can access the test account that is created here.
        Account a = new Account(Name='Test Account');
        insert a;      
        // Access the account that was just created.
        Account insertedAcct = [SELECT Id,Name FROM Account 
                                WHERE Name='Test Account'];
        System.assert(insertedAcct != null);
    }
}

注释的注意事项@IsTest(SeeAllData=true)

  • 如果测试类是用注解定义的,则 未显式设置关键字的测试方法。@IsTest(SeeAllData=true)SeeAllData=trueSeeAllData
  • 注解用于打开 在类或方法级别应用时的数据访问。但是,如果 包含类已用 、 对于方法,将忽略对方法进行批注。 在这种情况下,该方法仍然可以访问 组织。使用重写对方法进行注释,对于该方法,对 类。@IsTest(SeeAllData=true)@IsTest(SeeAllData=true)@IsTest(SeeAllData=false)@IsTest(SeeAllData=true)@IsTest(SeeAllData=false)
  • @IsTest(SeeAllData=true)并且不能使用注释 一起使用相同的 Apex 方法。@IsTest(IsParallel=true)

加载测试数据

使用该方法,可以在测试方法中填充数据,而无需 编写多行代码。

Test.loadData请按照下列步骤操作:

  1. 在 .csv 文件中添加数据。
  2. 为此文件创建静态资源。
  3. 在测试中调用 方法,并向其传递 sObject 类型令牌和静态资源名称。Test.loadData

例如,对于帐户记录和静态资源名称 ,进行以下调用:

myResource

List<sObject> ls = Test.loadData(Account.sObjectType, 'myResource');

方法 返回与插入的每条记录相对应的 sObject 列表。Test.loadData

在调用此方法之前,必须创建静态资源。 静态资源是以逗号分隔的文件,以 .csv 扩展名结尾。 该文件包含测试记录的字段名称和值。这 文件的第一行必须包含字段名称,随后的行 行是字段值。若要了解有关静态资源的详细信息,请执行以下操作: 请参阅 Salesforce 联机帮助中的“定义静态资源”。创建后 .csv 文件的静态资源,静态资源将是 分配了 MIME 类型。支持的 MIME 类型包括:

  • 文本/CSV
  • 应用程序/vnd.ms-excel
  • application/octet-stream
  • 文本/纯文本

Test.loadData 示例

以下是步骤 用于创建示例 .csv 文件和静态资源,并调用以插入测试记录。

Test.loadData

  1. 创建一个包含测试记录数据的 .csv 文件。此示例 .csv 文件有三个 帐户记录。您可以使用此示例内容创建 .csv 文件。Name,Website,Phone,BillingStreet,BillingCity,BillingState,BillingPostalCode,BillingCountry sForceTest1,http://www.sforcetest1.com,(415) 901-7000,The Landmark @ One Market,San Francisco,CA,94105,US sForceTest2,http://www.sforcetest2.com,(415) 901-7000,The Landmark @ One Market Suite 300,San Francisco,CA,94105,US sForceTest3,http://www.sforcetest3.com,(415) 901-7000,1 Market St,San Francisco,CA,94105,US
  2. 为 .csv 文件创建静态资源:
    1. 在“设置”中,输入“快速查找”框,然后选择“静态资源”。Static Resources
    2. 单击“新建”。
    3. 将静态资源命名为 。testAccounts
    4. 选择您创建的文件。
    5. 点击保存
  3. 呼入 用于填充测试帐户的测试方法。Test.loadData@isTest private class DataUtil { static testmethod void testLoadData() { // Load the test accounts from the static resource List<sObject> ls = Test.loadData(Account.sObjectType, 'testAccounts'); // Verify that all 3 test accounts were created System.assert(ls.size() == 3); // Get first test account Account a1 = (Account)ls[0]; String acctName = a1.Name; System.debug(acctName); // Perform some testing using the test records } }

测试数据的通用测试实用程序类 创造

常见的测试实用程序类是公共测试类,它们 包含用于创建测试数据的可重用代码。

公共测试实用程序类使用注释进行定义,因此被排除在组织代码大小限制之外并执行 在测试上下文中。它们可以由测试方法调用,但不能由非测试代码调用。IsTest

公共测试实用程序类中的方法的定义方式与非测试中的方法相同 类。它们可以接受参数并返回值。必须声明这些方法 作为公共或全局,以便对其他测试类可见。这些常用方法可以是 由 Apex 类中的任何测试方法调用,以在运行 测试。虽然您可以在常规 Apex 中创建用于创建测试数据的公共方法 类,如果没有注解,你 不要从组织代码大小中排除此代码的好处 限制。IsTest

这是测试实用程序类的一个示例。它包含一个方法,该方法接受 要创建的帐户数和每个帐户的联系人数。 下一个示例演示一个测试方法,该方法调用此方法以创建 一些数据。createTestRecords

@IsTest
public class TestDataFactory {
    public static void createTestRecords(Integer numAccts, Integer numContactsPerAcct) {
        List<Account> accts = new List<Account>();
        
        for(Integer i=0;i<numAccts;i++) {
            Account a = new Account(Name='TestAccount' + i);
            accts.add(a);
        }
        insert accts;
        
        List<Contact> cons = new List<Contact>();
        for (Integer j=0;j<numAccts;j++) {
            Account acct = accts[j];            
            // For each account just inserted, add contacts
            for (Integer k=numContactsPerAcct*j;k<numContactsPerAcct*(j+1);k++) {
                cons.add(new Contact(firstname='Test'+k,
                                     lastname='Test'+k,
                                     AccountId=acct.Id));
            }
        }
        // Insert all contacts for all accounts
        insert cons;
    }
}

此类中的测试方法调用测试实用程序方法 ,以创建 5 个 测试帐户,每个帐户有三个联系人。createTestRecords

@IsTest
private class MyTestClass {
    static testmethod void test1() {
        TestDataFactory.createTestRecords(5,3);
        // Run some tests
    }
}

使用测试设置方法

使用测试设置方法(带有 注释的方法)创建一次测试记录,然后再创建一次 在测试类的每个测试方法中访问它们。测试设置方法可以是 当您需要为所有测试方法创建参考或先决条件数据时,可以节省时间, 或所有测试方法都操作的一组通用记录。@testSetup

测试设置方法可以减少测试执行时间,尤其是在 您正在处理许多记录。测试设置方法使您能够创建通用测试 轻松高效地获取数据。通过为班级设置一次记录,您不需要 为每个测试方法重新创建记录。此外,因为回滚的记录 在测试设置期间创建,发生在整个类执行结束时, 减少回滚的记录数。因此,系统资源是 与创建这些记录并回滚这些记录相比,使用效率更高 每种测试方法。

如果测试类包含测试设置方法,则测试 Framework 首先执行测试设置方法,然后再执行类中的任何测试方法。 在测试设置方法中创建的记录可用于 测试类,并在测试类执行结束时回滚。如果测试方法 更改这些记录,例如记录字段更新或记录删除,这些更改 在每个测试方法完成执行后回滚。下一个执行测试 方法获取对这些记录的原始未修改状态的访问。

语法

测试设置方法是在测试类中定义的,不带任何参数,也不返回 价值。以下是测试设置方法的语法。

@testSetup static void methodName() {

}

以下示例演示如何创建一次测试记录,然后在 多种测试方法。此外,该示例还显示了如何在 第一个测试方法将回滚,并且不适用于第二个测试 方法。

@isTest
private class CommonTestSetup {

    @testSetup static void setup() {
        // Create common test accounts
        List<Account> testAccts = new List<Account>();
        for(Integer i=0;i<2;i++) {
            testAccts.add(new Account(Name = 'TestAcct'+i));
        }
        insert testAccts;        
    }
    
    @isTest static void testMethod1() {
        // Get the first test account by using a SOQL query
        Account acct = [SELECT Id FROM Account WHERE Name='TestAcct0' LIMIT 1];
        // Modify first account
        acct.Phone = '555-1212';
        // This update is local to this test method only.
        update acct;
        
        // Delete second account
        Account acct2 = [SELECT Id FROM Account WHERE Name='TestAcct1' LIMIT 1];
        // This deletion is local to this test method only.
        delete acct2;
        
        // Perform some testing
    }

    @isTest static void testMethod2() {
        // The changes made by testMethod1() are rolled back and 
        // are not visible to this test method.        
        // Get the first account by using a SOQL query
        Account acct = [SELECT Phone FROM Account WHERE Name='TestAcct0' LIMIT 1];
        // Verify that test account created by test setup method is unaltered.
        System.assertEquals(null, acct.Phone);
        
        // Get the second account by using a SOQL query
        Account acct2 = [SELECT Id FROM Account WHERE Name='TestAcct1' LIMIT 1];
        // Verify test account created by test setup method is unaltered.
        System.assertNotEquals(null, acct2);
        
        // Perform some testing
    }

}

测试设置方法注意事项

  • 测试设置方法仅支持 测试类的默认数据隔离模式。如果测试类或测试 方法可以使用注释访问组织数据, 此类不支持测试设置方法。因为数据隔离 for tests 可用于 API 版本 24.0 及更高版本的测试设置方法 也仅适用于这些版本。@isTest(SeeAllData=true)
  • 每个测试类只能有一种测试设置方法。
  • 如果在执行测试设置方法期间发生致命错误,例如 由 DML 操作或断言失败导致的异常,整个 测试类失败,并且不会执行该类中的其他测试。
  • 如果测试设置方法调用另一个类的非测试方法,则无代码 覆盖率是针对非测试方法计算的。

运行单元测试方法

若要验证 Apex 代码的功能,请执行单元测试。您可以运行 Apex 在开发人员控制台、设置和 Salesforce Visual 扩展中测试方法 Studio Code,或使用 API。可以运行这些单元测试分组。

  • 特定类中的部分或全部方法
  • 一组类中的部分或全部方法
  • 预定义的类套件,称为测试套件
  • 组织中的所有单元测试

若要运行测试,请使用以下任一方法:

  • Salesforce 用户界面
  • 适用于 Visual Studio 的 Salesforce 扩展 法典
  • 闪电平台开发人员 安慰
  • The API

从 Salesforce 用户界面启动的所有 Apex 测试(包括 Developer Console)异步并行运行。Apex 测试类放置在 用于执行的 Apex 作业队列。可以运行的最大测试类数 每 24 小时是 500 或 10 乘以测试类数的较大值 在组织中。对于沙盒和 Developer Edition 组织,此限制更高,并且是 500 或 20 中的较大值乘以组织中的测试类数。

注意

作为部署的一部分运行的 Apex 测试始终同步运行,并且 串行。

通过 Salesforce 用户运行测试 接口

您可以在 Apex 测试执行页面上运行单元测试。在此页面上开始测试 异步运行,也就是说,您不必等待测试类执行 完成。“Apex Test Execution”(Apex 测试执行)页面刷新测试状态,并显示 测试完成后的结果。

Apex 测试执行页面
  1. 在“设置”中,输入“快速查找”框,然后选择“Apex 测试” 执行Apex Test Execution
  2. 单击“选择测试…”。注意如果你 具有从托管包安装的 Apex 类,您必须 首先通过单击“全部编译”来编译这些类 Apex 类页面上的类,以便它们显示在 列表。
  3. 选择要运行的测试。测试列表仅包括 包含测试方法。
    • 若要从已安装的托管包中选择测试,请选择 下拉列表中托管包的相应命名空间。 仅具有所选命名空间的托管包的类 出现在列表中。
    • 若要选择组织中本地存在的测试,请从下拉列表中选择“[我的命名空间]”。 只有不是来自托管包的本地类才会出现在 列表。
    • 要选择任何测试,请从下拉列表中选择[所有命名空间]。将显示组织中的所有类, 无论它们是否来自托管包。
    注意当前正在运行测试的类不会出现在 列表。
  4. 若要选择不在测试运行期间收集代码覆盖率信息,请选择“跳过代码覆盖率”。
  5. 单击运行

使用 Apex Test Execution 页面运行测试后,您可以查看代码覆盖率 开发者控制台中的详细信息。

在“设置”中,输入“快速查找”框,选择“Apex 测试执行”,然后单击“查看” 测试历史记录,用于查看组织的所有测试结果,而不是 只是您运行的测试。测试结果在测试后保留 30 天 完成运行,除非清除。Apex

使用 Salesforce 扩展运行测试 针对 Visual Studio Code

可以使用 Visual Studio Code 执行测试。请参阅适用于 Visual Studio 的 Salesforce 扩展 代码。

使用 Lightning 运行测试 平台开发人员控制台

在开发者控制台中,您可以在特定的测试类中执行部分或全部测试, 设置和运行测试套件,或运行所有测试。开发者控制台运行测试 在后台异步运行,除非测试运行仅包含一个类和 你尚未在“测试”菜单中选择“始终异步运行”。 通过异步运行测试,您可以在开发者控制台的其他区域工作 在测试运行时。测试完成执行后,可以检查测试 结果在开发者控制台中。此外,还可以检查整体代码覆盖率 对于测试涵盖的课程。

有关更多信息,请参阅 Salesforce 中的开发人员控制台文档 帮助。

使用 API 运行测试

您可以使用来自 SOAP 的调用 用于运行测试的 API 同步。

runTests()

RunTestsResult[] runTests(RunTestsRequest ri)

此调用允许您运行所有类中的所有测试,以及特定类中的所有测试 命名空间,或指定特定命名空间中类子集中的所有测试 在 RunTestsRequest 对象中。它返回以下内容。

  • 运行的测试总数
  • 代码覆盖率统计
  • 每个失败测试的错误信息
  • 每个成功的测试的信息
  • 运行测试所花费的时间

有关 的更多信息,请参见 SOAP API 中的 runTests() 开发人员指南。runTests()

还可以使用工具 REST API 运行测试。使用 和 终结点运行测试 异步或同步。有关用法的详细信息,请参阅工具 API:REST 资源。/runTestsAsynchronous//runTestsSynchronous/

使用 ApexTestQueueItem 运行测试

可以使用 ApexTestQueueItem 和 ApexTestResult 异步运行测试。这些对象允许您 将测试添加到 Apex 作业队列,并检查已完成的测试运行的结果。 此过程不仅使你能够异步启动测试,还使你能够安排 使用 Apex 调度程序在特定时间执行的测试。有关详细信息,请参阅 Apex Scheduler。

插入一个对象以放置其 Apex 作业队列中相应的 Apex 类以供执行。Apex 作业执行 类中的测试方法。作业执行后,包含每个测试方法的结果 作为测试的一部分执行。ApexTestQueueItemApexTestResult

要中止 Apex 作业队列中的类,请在 ApexTestQueueItem 对象,并将其 Status 字段设置为 。Aborted

如果在单个批量操作中插入多个 Apex 测试队列项,则队列 项目共享相同的父作业。这意味着测试运行可以包括 如果插入了所有测试队列项,则执行多个类的测试 在相同的批量操作中。

可以在 Apex 作业队列是 500 或 10 乘以测试类数中的较大值 在组织中。对于沙盒和 Developer Edition 组织,此限制更高 并且是 500 或 20 中的较大值乘以 组织。此示例使用 DML 操作插入和查询 和 对象。该方法为以 测试。然后,它返回一个队列项的父作业 ID,该 ID 对于所有队列项都是相同的 对项目进行排队,因为它们是批量插入的。该方法检索对应于 指定的作业 ID。然后,它查询并输出名称、作业状态和传递 每个班级的费率。该方法获取作为 工作。

ApexTestQueueItemApexTestResultenqueueTestscheckClassStatuscheckMethodStatus

public class TestUtil {

    // Enqueue all classes ending in "Test". 
    public static ID enqueueTests() {
        ApexClass[] testClasses = 
           [SELECT Id FROM ApexClass 
            WHERE Name LIKE '%Test'];
        if (testClasses.size() > 0) {
            ApexTestQueueItem[] queueItems = new List<ApexTestQueueItem>();
            for (ApexClass cls : testClasses) {
                queueItems.add(new ApexTestQueueItem(ApexClassId=cls.Id));
            }

            insert queueItems;

            // Get the job ID of the first queue item returned.
            ApexTestQueueItem item = 
               [SELECT ParentJobId FROM ApexTestQueueItem 
                WHERE Id=:queueItems[0].Id LIMIT 1];
            return item.parentjobid;
        }
        return null;
    }

    // Get the status and pass rate for each class
    // whose tests were run by the job.
    // that correspond to the specified job ID.
    public static void checkClassStatus(ID jobId) {
        ApexTestQueueItem[] items = 
           [SELECT ApexClass.Name, Status, ExtendedStatus 
            FROM ApexTestQueueItem 
            WHERE ParentJobId=:jobId];
        for (ApexTestQueueItem item : items) {
            String extStatus = item.extendedstatus == null ? '' : item.extendedStatus;
            System.debug(item.ApexClass.Name + ': ' + item.Status + extStatus);
        }
    }

    // Get the result for each test method that was executed.
    public static void checkMethodStatus(ID jobId) {
        ApexTestResult[] results = 
           [SELECT Outcome, ApexClass.Name, MethodName, Message, StackTrace 
            FROM ApexTestResult 
            WHERE AsyncApexJobId=:jobId];
        for (ApexTestResult atr : results) {
            System.debug(atr.ApexClass.Name + '.' + atr.MethodName + ': ' + atr.Outcome);
            if (atr.message != null) {
                System.debug(atr.Message + '\n at ' + atr.StackTrace);
            }
        }
    }
}
  1. 使用 runAs 方法
  2. 使用 Limits、startTest 和 stopTest
  3. 将 SOSL 查询添加到单元测试

使用 runAs 方法

通常,所有 Apex 代码都运行在系统模式下,其中权限和记录共享 的当前用户不被考虑在内。使用系统方法,可以编写更改的测试方法 现有用户或新用户的用户上下文,以便用户的记录 强制执行共享。该方法强制执行 记录 共享。 用户权限和字段级权限将应用于新的上下文用户,因为 在强制执行对象和字段权限中进行了描述。runAsrunAs

注意

无论测试类的共享模式如何,都会在块内强制执行用户的共享权限。如果 在块中调用用户定义的方法, 强制执行的共享模式是定义方法的类的共享模式。runAsrunAs

您只能在测试方法中使用。这 完成所有测试方法后,将再次启动原始系统上下文。runAsrunAs

该方法忽略用户许可证限制。你 能 创造 即使你的组织没有 其他用户许可证。runAsrunAs

注意

每个调用都很重要 相对于流程中发出的 DML 语句总数。runAs

在以下示例中,将创建一个新的测试用户,然后以该用户身份运行代码,其中 该用户的记录共享访问权限:

@isTest
private class TestRunAs {
   public static testMethod void testRunAs() {
        // Setup test data
        // Create a unique UserName
        String uniqueUserName = 'standarduser' + DateTime.now().getTime() + '@testorg.com';
        // This code runs as the system user
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        User u = new User(Alias = 'standt', Email='standarduser@testorg.com',
        EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
        LocaleSidKey='en_US', ProfileId = p.Id,
        TimeZoneSidKey='America/Los_Angeles',
         UserName=uniqueUserName);

        System.runAs(u) {
              // The following code runs as user 'u'
              System.debug('Current User: ' + UserInfo.getUserName());
              System.debug('Current Profile: ' + UserInfo.getProfileId());
          }
    }
}

可以嵌套多个方法。为 例:runAs

@isTest
private class TestRunAs2 {

   public static testMethod void test2() {

      Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
      User u2 = new User(Alias = 'newUser', Email='newuser@testorg.com',
         EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
         LocaleSidKey='en_US', ProfileId = p.Id,
         TimeZoneSidKey='America/Los_Angeles', UserName='newuser@testorg.com');

      System.runAs(u2) {
         // The following code runs as user u2.
         System.debug('Current User: ' + UserInfo.getUserName());
         System.debug('Current Profile: ' + UserInfo.getProfileId());

         // The following code runs as user u3.
         User u3 = [SELECT Id FROM User WHERE UserName='newuser@testorg.com'];
         System.runAs(u3) {
            System.debug('Current User: ' + UserInfo.getUserName());
            System.debug('Current Profile: ' + UserInfo.getProfileId());
         }

         // Any additional code here would run as user u2.
      }
   }
}

runAs 的其他用途

您还可以使用该方法来执行 通过将 DML 操作封闭在块中,在测试中混合 DML 操作。这样,您就可以绕过混合 DML 在插入或更新安装对象时返回的错误 其他 sObjects。请参见不能一起使用的 sObjects 在 DML 操作中。runAsrunAs

runAs 方法 (runAs(System.Version)) 的另一个重载将包版本作为参数。这 方法导致使用特定版本的托管包的代码。为 有关使用 runAs 方法和 指定包版本上下文,请参阅测试包中的行为 版本。

使用 Limits、startTest 和 stopTest

Limits 方法返回特定调控器的特定限制,例如数字 方法的调用次数或剩余的堆大小量。

每种方法都有两个版本。第一个版本返回具有 在当前上下文中使用。第二个版本包含“限制”一词 并返回可用于该上下文的资源总量。例如,将标注数返回到 已在当前上下文中处理的外部服务,而返回标注的总数 在给定的上下文中可用。getCalloutsgetLimitCallouts

除了 Limits 方法之外,还可以使用 和 方法来验证代码的接近程度 是达到调速器限制。startTeststopTest

该方法标记测试中的点 在测试实际开始时编写代码。每个测试方法只允许调用此方法 一次。此方法之前的所有代码都应用于初始化变量、填充数据 结构等,允许您设置运行测试所需的一切。任何代码 在调用之后执行,并在调用之前执行,分配了一组新的调控器 限制。startTeststartTeststopTest

该方法不会刷新 测试:它为测试添加上下文。例如,如果您的类进行 98 次 SOQL 查询 在它调用之前,第一个重要的 后面的语句是 DML 语句, 程序现在可以进行额外的 100 次查询。但是,一旦被调用,程序就会返回到原始上下文,并且 在达到 100 个限制之前,只能进行 2 个额外的 SOQL 查询。startTeststartTeststartTeststopTest

当测试结束时,该方法在测试代码中标记该点。stopTest在 与方法结合使用。每次测试 方法只允许调用此方法一次。在为方法分配原始限制后执行的任何代码 实际上之前被调用了。都 在该方法之后进行的异步调用是 由系统收集。startTeststopTeststartTeststartTest执行时,将运行所有异步进程 同步。stopTest

将 SOSL 查询添加到单元测试

为确保测试方法始终以可预测的方式运行,任何 Salesforce 对象 添加到 Apex 测试方法的搜索语言 (SOSL) 查询将返回一组空的 测试方法执行时的搜索结果。如果您不希望查询返回 结果列表为空,您可以使用系统方法定义记录 ID 列表,该 由搜索返回。稍后在测试方法中发生的所有 SOSL 查询 返回方法指定的记录 ID 列表。此外,测试方法可以多次调用 为不同的 SOSL 查询定义不同的结果集。如果未在测试方法中调用该方法,或者 如果在未指定记录 ID 列表的情况下调用此方法,则任何 SOSL 查询 place later in the test 方法返回一个空的结果列表。Test.setFixedSearchResultsTest.setFixedSearchResultsTest.setFixedSearchResultsTest.setFixedSearchResults

该方法指定的记录 ID 列表将替换通常 如果 SOSL 查询不受任何 OR 子句的约束,则由 SOSL 查询返回。如果这些 子句存在于 SOSL 查询中,它们应用于固定搜索结果列表。为 例:Test.setFixedSearchResultsWHERELIMIT

@isTest
private class SoslFixedResultsTest1 {

    public static testMethod void testSoslFixedResults() {
       Id [] fixedSearchResults= new Id[1];
       fixedSearchResults[0] = '001x0000003G89h';
       Test.setFixedSearchResults(fixedSearchResults);
       List<List<SObject>> searchList = [FIND 'test' 
                                         IN ALL FIELDS RETURNING 
                                            Account(id, name WHERE name = 'test' LIMIT 1)];
    }
}

注意

ContentDocument (File) 或 ContentNote (Note) 实体的 SOSL 查询需要与 ContentVersion ID 一起使用,以便与 Salesforce 索引的方式保持一致 并搜索文件和笔记。setFixedSearchResults

尽管 ID 为 的客户记录可能与 FIND 子句 () 中的查询字符串不匹配,但该记录将传递到 SOSL 语句的子句中。如果记录 如果 ID 与子句筛选器匹配,则返回记录。如果是这样 不匹配子句,无记录 返回。001x0000003G89h‘test’RETURNING001x0000003G89hWHEREWHERE

测试最佳实践

好的测试可以做到以下几点:

  • 覆盖尽可能多的代码行。在部署 Apex 或 将其打包为 AppExchange,则必须满足以下条件。重要
    • 单元测试必须覆盖至少 75% 的 Apex 代码,以及所有 这些测试必须成功完成。
    • 每个触发器都必须具有一定的测试覆盖率。
    • 所有类和触发器都必须成功编译。
    请注意以下事项。
    • 将 Apex 部署到生产组织时,每个 组织命名空间中的单元测试由 违约。
    • 调用不计入 Apex 代码的一部分 覆盖。System.debug
    • 测试方法和测试类不计入 Apex 代码覆盖率。
    • 虽然只有 75% 的 Apex 代码必须被 测试,不要关注代码的百分比 覆盖。相反,请确保你的每个用例 涵盖应用,包括正面和负面 案例,以及批量和单条记录。这种方法 确保 75% 或更多的代码被单元覆盖 测试。
  • 如果代码使用条件逻辑(包括三元运算符),请执行每个分支。
  • 使用有效和无效输入调用方法。
  • 成功完成而不会引发任何异常,除非这些错误是预期的 并被困在一个街区。try…catch
  • 始终处理捕获的所有异常,而不仅仅是捕获 异常。
  • 使用方法证明代码 行为正常。System.assert
  • 使用该方法测试应用程序 在不同的用户上下文中。runAs
  • 练习批量触发器功能 – 在测试中使用至少 20 条记录。
  • 使用关键字确保 记录按预期顺序返回。ORDER BY
  • 不假定记录 ID 按顺序排列。记录 ID 不是在 升序,除非您使用同一请求插入多条记录。例如 如果您创建了一个帐户 A,并接收了 ID,则创建帐户 B,则帐户 B 的 ID 可能会,也可能不会 依次走高。001D000000IEEmT
  • 设置测试数据:
    • 在测试类中创建必要的数据,以便测试不必依赖数据 在特定组织中。
    • 在调用方法之前创建所有测试数据。Test.startTest
    • 由于测试不提交,因此无需删除任何数据。
  • 写评论,不仅要说明应该测试的内容,还要说明假设 测试人员对数据、预期结果等进行了测试。
  • 单独测试应用程序中的类。永远不要测试你的整个应用程序 一次测试。

注意

若要保护数据的隐私,请确保测试错误消息和异常 详细信息不包含任何个人数据。Apex 异常处理程序和测试 框架无法确定敏感数据是否包含在用户定义的消息中,并且 详。要在自定义 Apex 例外中包含个人数据,我们建议您 创建一个具有新属性的 Exception 子类,用于保存个人数据。然后 不要在异常的消息字符串中包含子类属性信息。

如果要运行许多测试,请测试组织中的类 在 Salesforce 用户界面中单独使用,而不是使用“全部运行” “测试”按钮将它们一起运行。

并行测试执行的最佳实践

从 Salesforce 用户界面(包括开发人员控制台)启动的测试 并行运行。并行测试执行可以加快测试运行时间。有时,并行 测试执行会导致数据争用问题,您可以在 那些情况。具体而言,在以下情况下可能会发生数据争用问题和错误:

UNABLE_TO_LOCK_ROW

  • 当测试同时更新相同的记录时。更新相同的记录 通常在测试不创建自己的数据并关闭数据时发生 隔离以访问组织的数据。
  • 当并行运行并尝试创建 具有重复索引字段值的记录。当两个正在运行的测试发生时,会发生死锁 等待彼此回滚数据。如果插入两个测试,则可能会发生这种等待 以不同顺序具有相同唯一索引字段值的记录。

您可以通过在 Salesforce 用户界面:

  1. 在“设置”中,输入 。Apex Test
  2. 单击“选项…”。
  3. 在“Apex 测试执行选项”对话框中,选择“禁用并行 Apex” “测试”,然后单击“确定”。

带批注的测试类指示测试类可以同时运行,并且超过默认数量 并发测试类。此批注将覆盖默认设置。IsTest(IsParallel=true)

测试实例

以下示例包括以下类型的案例 测试:

  • 具有单条和多条记录的阳性病例
  • 单条和多条记录的负数大小写
  • 与其他用户一起测试

该测试与简单的里程跟踪应用程序一起使用。现有代码 应用程序验证一天内输入的里程不超过 500 英里。这 主要对象是名为 Mileage__c 的自定义对象。该测试创建一条包含 300 的记录 英里并验证仅记录了 300 英里。然后循环创建 200 条记录 每人一英里。最后,它验证总共记录了 500 英里( 原来的 300 加上新的)。这是整个测试类。以下各节 单步执行代码的特定部分。

@isTest
private class MileageTrackerTestSuite {

    static testMethod void runPositiveTestCases() {
        
        Double totalMiles = 0;
        final Double maxtotalMiles = 500;
        final Double singletotalMiles = 300;
        final Double u2Miles = 100;
  
        
        //Set up user
        User u1 = [SELECT Id FROM User WHERE Alias='auser'];
        
        //Run As U1
        System.RunAs(u1){

            
        System.debug('Inserting 300  miles... (single record validation)');
        
        Mileage__c testMiles1 = new Mileage__c(Miles__c = 300, Date__c = System.today());
        insert testMiles1;
        
        //Validate single insert
        for(Mileage__c m:[SELECT miles__c FROM Mileage__c 
            WHERE CreatedDate = TODAY
            and CreatedById = :u1.id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        
        Assert.areEqual(singletotalMiles, totalMiles);
    
    
        //Bulk validation   
        totalMiles = 0; 
        System.debug('Inserting 200 mileage records... (bulk validation)');
        
        List<Mileage__c> testMiles2 = new List<Mileage__c>();
        for(integer i=0; i<200; i++) {
            testMiles2.add( new Mileage__c(Miles__c = 1, Date__c = System.today()) );
        }
        insert testMiles2;
       
        for(Mileage__c m:[SELECT miles__c FROM Mileage__c
            WHERE CreatedDate = TODAY
            and CreatedById = :u1.Id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        
        Assert.areEqual(maxtotalMiles, totalMiles);

        }//end RunAs(u1)


       //Validate additional user:
       totalMiles = 0;
       //Setup RunAs
       User u2 = [SELECT Id FROM User WHERE Alias='tuser'];
       System.RunAs(u2){
        
        Mileage__c testMiles3 = new Mileage__c(Miles__c = 100, Date__c = System.today());
        insert testMiles3;
        
                for(Mileage__c m:[SELECT miles__c FROM Mileage__c
            WHERE CreatedDate = TODAY
            and CreatedById = :u2.Id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        //Validate 
        Assert.areEqual(u2Miles, totalMiles);
        
       } //System.RunAs(u2)

      
    } // runPositiveTestCases()
   
    static testMethod void runNegativeTestCases() {

        User u3 = [SELECT Id FROM User WHERE Alias='tuser'];
        System.RunAs(u3) {

           System.debug('Inserting a record with 501 miles... (negative test case)'); 

           Mileage__c testMiles3 = new Mileage__c( Miles__c = 501, Date__c = System.today() );

           try {
               insert testMiles3;
               Assert.fail('DmlException expected');
           } catch (DmlException e) {
               //Assert Status Code
               Assert.areEqual('FIELD_CUSTOM_VALIDATION_EXCEPTION', e.getDmlStatusCode(0));
            
               //Assert field
               Assert.areEqual(Mileage__c.Miles__c, e.getDmlFields(0)[0]);
                        
               //Assert Error Message
               Assert.isTrue(e.getMessage().contains(
                'Mileage request exceeds daily limit(500): [Miles__c]'),
                'DMLException did not contain expected validation message:' + e.getMessage() );
            
             } //catch
           } //RunAs(u3) 
    } // runNegativeTestCases() 
  
    
} // class MileageTrackerTestSuite

阳性测试用例

以下步骤通过上面的代码,特别是, 单条记录和多条记录的阳性测试用例。

  1. 将文本添加到调试日志中,指示代码的下一步:System.debug('Inserting 300 more miles...single record validation');
  2. 创建一个Mileage__c对象并将其插入到数据库中。Mileage__c testMiles1 = new Mileage__c(Miles__c = 300, Date__c = System.today() ); insert testMiles1;
  3. 通过返回插入的记录来验证代码:for(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :createdbyId and miles__c != null]) { totalMiles += m.miles__c; }
  4. 使用该方法验证 预期结果是 返回:Assert.areEqualAssert.areEqual(singletotalMiles, totalMiles);
  5. 在进入下一个测试之前,请设置总里程数 返回 0:totalMiles = 0;
  6. 通过创建 200 条记录的批量插入来验证代码。首先,在调试日志中添加文本,指示下一步 法典:System.debug('Inserting 200 Mileage records...bulk validation');
  7. 然后插入 200 条Mileage__c记录:List<Mileage__c> testMiles2 = new List<Mileage__c>(); for(Integer i=0; i<200; i++){ testMiles2.add( new Mileage__c(Miles__c = 1, Date__c = System.today()) ); } insert testMiles2;
  8. 用于验证预期结果 是 返回:Assert.areEqualfor(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :CreatedbyId and miles__c != null]) { totalMiles += m.miles__c; } Assert.areEqual(maxtotalMiles, totalMiles);

阴性测试用例

以下步骤通过上面的代码,特别是, 阴性测试用例。

  1. 创建一个名为 :runNegativeTestCasesstatic testMethod void runNegativeTestCases(){
  2. 将文本添加到调试日志中,指示代码的下一步:System.debug('Inserting 501 miles... negative test case');
  3. 创建 501 英里的Mileage__c记录。Mileage__c testMiles3 = new Mileage__c(Miles__c = 501, Date__c = System.today());
  4. 将语句放在 / 块中。这允许您捕获验证异常和 断言生成的错误消息。使用该方法明确断言您期望 验证异常。inserttrycatchAssert.failtry { insert testMiles3; Assert.fail('DmlException expected'); } catch (DmlException e) {
  5. 现在使用 和 方法进行测试。 将以下代码添加到之前的块中 创建:Assert.areEqualAssert.isTruecatch//Assert Status Code Assert.areEqual('FIELD_CUSTOM_VALIDATION_EXCEPTION', e.getDmlStatusCode(0)); //Assert field Assert.areEqual(Mileage__c.Miles__c, e.getDmlFields(0)[0]); //Assert Error Message Assert.isTrue(e.getMessage().contains( 'Mileage request exceeds daily limit(500): [Miles__c]'), 'DMLException did not contain expected validation message:' + e.getMessage() );

以第二用户身份进行测试

以下步骤通过上面的代码,特别是, 以第二个用户身份运行。

  1. 在进入下一个测试之前,请设置总里程数 返回 0:totalMiles = 0;
  2. 设置下一个用户。User u2 = [SELECT Id FROM User WHERE Alias='tuser']; System.RunAs(u2){
  3. 将文本添加到调试日志中,指示代码的下一步:System.debug('Setting up testing - deleting any mileage records for ' + UserInfo.getUserName() + ' from today');
  4. 然后插入一条Mileage__c记录:Mileage__c testMiles3 = new Mileage__c(Miles__c = 100, Date__c = System.today()); insert testMiles3;
  5. 通过返回插入的记录来验证代码:for(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :u2.Id and miles__c != null]) { totalMiles += m.miles__c; }
  6. 使用该方法验证 预期结果是 返回:Assert.areEqualAssert.areEqual(u2Miles, totalMiles);

测试和代码覆盖率

Apex 测试框架为您的 Apex 类生成代码覆盖率数字,并且 每次运行一个或多个测试时触发。代码覆盖率指示有多少可执行行 类和触发器中的代码已由测试方法执行。将测试方法写入 测试触发器和类,然后运行这些测试以生成代码覆盖率 信息。

测试方法涵盖的顶点触发器和类

测试方法涵盖的顶点触发器和类

除了确保代码的质量外,单元测试还使您能够满足代码要求 部署或打包 Apex 的覆盖率要求。要部署 Apex 或将其打包为 Salesforce AppExchange,单元测试必须至少覆盖 75% 的 Apex 代码,并且这些测试 必须通过。

代码覆盖率是测试有效性的一个指标,但不能保证测试 有效性。测试的质量也很重要,但您可以使用代码覆盖率作为工具来 评估是否需要添加更多测试。虽然您需要满足最低代码覆盖率 部署或打包 Apex 代码的要求,代码覆盖率不应是唯一的目标 您的测试。测试应断言应用的行为,并确保代码的质量。

代码覆盖率是如何计算的?

代码覆盖率百分比是计算 覆盖线除以覆盖线数和未覆盖线数之和。只 包括可执行代码行。(注释和空行不计算在内。 语句和大括号在以下情况下被排除在外 它们单独出现在一行上。一行上的多个语句计为一行 代码覆盖率的目的。如果语句由多个表达式组成,这些表达式写在 多行,每行都计入代码覆盖率。System.debug()

下面是一个具有一个方法的类的示例。这门课的测试是 run,并在开发人员控制台中为此类选择了显示代码覆盖率的选项。 蓝线表示测试覆盖的线。未突出显示的行 被排除在代码覆盖率计算之外。红线表示未覆盖的线 通过测试。为了实现全面覆盖,需要更多的测试。测试必须使用不同的输入进行调用,并验证返回的 价值。getTaskPriority()

这是测试方法部分涵盖的类。对应的测试类 未显示。

开发者控制台中具有代码覆盖率视图的示例类

测试类(带有 注释的类) 从代码覆盖率计算中排除。此排除项适用于所有测试类 无论它们包含什么 – 用于测试的测试方法或实用方法。@isTest

注意

Apex 编译器有时会优化语句中的表达式。例如,如果多个 字符串常量与运算符 编译器在内部将这些表达式替换为一个字符串常量。如果字符串 串联表达式位于单独的行上,附加行不计为 优化后的代码覆盖率计算。为了说明这一点,字符串变量是 分配给两个连接的字符串常量。第二个字符串常量位于 分开 线。+

String s = 'Hello'
    + ' World!';
String s = 'Hello World!';

检查代码覆盖率

运行测试后,可以在“开发人员”的“测试”选项卡中查看代码覆盖率信息 安慰。代码覆盖率窗格包括每个 Apex 类的覆盖率信息,以及 全面覆盖组织中的所有 Apex 代码。

此外,代码覆盖率存储在两个 Lightning Platform Tooling API 对象中: ApexCodeCoverageAggregate 和 ApexCodeCoverage。ApexCodeCoverageAggregate 存储 在检查测试类的所有测试方法后,为该类覆盖了行。ApexCodeCoverage 存储 每个单独的测试方法覆盖和未覆盖的线。出于这个原因,一个 类在 ApexCodeCoverage 中可以有多个覆盖率结果,每个测试方法一个覆盖率结果 这已经测试了它。可以使用 SOQL 和工具 API 查询这些对象以检索 覆盖范围信息。将 SOQL 查询与工具 API 结合使用是检查代码的另一种方法 覆盖范围和获取更多详细信息的快速方法。

例如,此 SOQL 查询获取类的代码覆盖率。覆盖率是从执行 此类中的方法。TaskUtil

SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered 
FROM ApexCodeCoverageAggregate 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'

注意

此 SOQL 查询需要工具 API。您可以使用查询编辑器运行此查询 ,然后选中 Use Tooling API

下面是测试部分覆盖的类的示例查询结果:

ApexClassOrTrigger.NameNumLines覆盖NumLines未覆盖
任务实用性82

下一个示例演示如何确定哪些测试方法涵盖了该类。查询 从存储覆盖范围的其他对象 ApexCodeCoverage 获取覆盖率信息 按测试类和方法列出的信息。

SELECT ApexTestClass.Name,TestMethodName,NumLinesCovered,NumLinesUncovered 
FROM ApexCodeCoverage 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'

下面是一个示例查询结果。

ApexTestClass.NameTestMethodName (测试方法名称)NumLines覆盖NumLines未覆盖
TaskUtilTesttestTaskPriority(测试任务优先级)73
TaskUtilTesttestTaskHighPriority(测试任务高优先级)64

ApexCodeCoverage 中的 NumLinesUncovered 值与 聚合结果为 ApexCodeCoverageAggregate,因为它们表示与 测试方法各。例如,测试方法覆盖了整个班级的 7 行,总共 10 行 行,因此未覆盖的行数为 3 行 (10–7)。因为聚合覆盖率存储在 ApexCodeCoverageAggregate 包括所有测试方法的覆盖率,其中 和 的覆盖率被包括在内,只剩下 2 行未被覆盖 任何测试方法。testTaskPriority()testTaskPriority()testTaskPriority()testTaskHighPriority()

代码覆盖率最佳实践

请考虑以下代码覆盖率提示和最佳做法。

代码覆盖率一般提示

  • 运行测试以刷新代码覆盖率。在以下情况下不会刷新代码覆盖率数字 除非重新运行测试,否则将对组织中的 Apex 代码进行更新。
  • 如果组织自上次测试运行以来已更新,则代码覆盖率估计可以 不正确。重新运行 Apex 测试以获得正确的估计值。
  • 组织中的总体代码覆盖率百分比不包括 托管包测试。唯一的例外是托管包测试导致触发器 火灾。有关更多信息,请参见托管包测试。
  • 覆盖率基于组织中的代码行总数。添加或删除 代码行更改覆盖率百分比。例如,假设一个组织有 50 个 测试方法涵盖的代码行。如果添加的触发器包含 50 行代码,则不 通过测试,代码覆盖率从 100% 下降到 50%。触发器增加 组织中的总代码行从 50 行到 100 行,其中只有 50 行由 测试。

为什么沙盒和生产环境之间的代码覆盖率数字不同

当 Apex 部署到生产环境或作为软件包的一部分上传到 Salesforce 时 AppExchange、Salesforce 在目标组织中运行本地测试。沙盒和生产 环境通常不包含相同的数据和元数据,因此代码覆盖率结果不会 始终匹配。如果代码在生产环境中的覆盖率低于 75%,请增加覆盖率以便能够 以部署或上传代码。以下是代码差异的常见原因 开发或沙盒环境与生产环境之间的覆盖率。这 信息可以帮助您排查和协调这些差异。测试失败如果一个环境中的测试结果不同,则总体代码覆盖率百分比 不匹配。在比较沙盒和生产环境之间的代码覆盖率之前,请使 确保要部署或打包的代码的所有测试都在组织中通过 第一。参与代码覆盖率计算的测试必须全部通过 部署或包上传。数据依赖关系如果测试使用批注访问组织数据,则测试结果可以 根据组织中可用的数据而有所不同。如果 测试中引用的记录不存在或已更改,测试失败或 在 Apex 方法中执行不同的代码路径。修改测试,以便 他们创建测试数据,而不是访问组织数据。@IsTest(SeeAllData=true)元数据依赖关系元数据中的更改(例如用户配置文件设置中的更改)可能会导致测试 失败或执行不同的代码路径。确保沙盒和生产环境中的元数据 匹配,或确保元数据更改不是导致不同测试执行的原因 行为。托管包测试在用户界面中运行所有 Apex 测试后计算的代码覆盖率,例如 开发人员控制台,可能与在 部署。如果运行所有测试,包括托管包测试,请在 用户界面,组织中的整体代码覆盖率没有 包括托管包代码的覆盖率。虽然托管包测试 覆盖托管包中的代码行,此覆盖率不是 组织的代码覆盖率计算为总行数和覆盖行数。 相比之下,在运行所有 测试通过测试 级别包括托管包代码的覆盖率。如果您运行的是托管 通过测试级别在部署中打包测试,建议运行此 首先在沙盒中部署或执行验证部署以验证 代码覆盖率。RunAllTestsInOrgRunAllTestsInOrg部署导致整体覆盖率低于 75%将具有 100% 覆盖率的新组件部署到生产环境时,部署失败 如果新代码和现有代码之间的平均覆盖率未达到 75% 的阈值。如果 在目标组织中测试运行返回的覆盖率结果小于 75%,修改 现有的测试方法或编写额外的测试方法以提高代码覆盖率 75%.单独部署修改后的或新的测试方法,或使用具有 100% 的新代码部署 覆盖。生产环境中的代码覆盖率降至 75% 以下有时,生产中的整体覆盖率会下降到75%以下,即使至少是 当组件从沙盒部署时,为 75%。依赖于 组织的数据和元数据可能会导致代码覆盖率下降。如果数据和元数据 已经进行了充分的更改以改变相关测试方法的结果,某些方法可能会失败 或行为不同。在这种情况下,某些行将不再被覆盖。

为生产环境匹配代码覆盖率数字的推荐过程

  • 使用完整沙盒作为生产部署的暂存沙盒环境。一个完整的 沙盒模拟生产中的元数据和数据,并有助于减少代码差异 两个环境之间的覆盖数。
  • 若要减少对沙盒和生产组织中数据的依赖性,请在 您的 Apex 测试。
  • 如果由于代码覆盖率不足而导致部署到生产环境失败,请编写更多测试 将整体代码覆盖率提高到尽可能高的覆盖率或 100%。重试 部署。
  • 如果在沙盒中提高代码覆盖率后部署到生产环境失败, 从生产组织运行本地测试。确定低于 75% 的类 覆盖。在沙盒中为这些类编写其他测试以提高代码覆盖率。

使用 Stub API 构建模拟框架

Apex 提供了一个用于实现模拟框架的存根 API。模拟框架 有很多好处。它可以简化和改进测试,并帮助您更快、更快速地创建更多 可靠的测试。您可以使用它来单独测试类,这对单元很重要 测试。使用存根 API 构建模拟框架也是有益的,因为 存根对象在运行时生成。由于这些对象是动态生成的,因此 不必打包和部署测试类。您可以构建自己的模拟框架,或者 您可以使用其他人构建的。

您可以定义存根对象的行为,这些对象在运行时创建为匿名对象 Apex 类的子类。存根 API 由接口和方法组成。System.StubProviderSystem.Test.createStub()

注意

此功能适用于高级 Apex 开发人员。使用它需要彻底 了解单元测试和模拟框架。如果你认为嘲笑 框架是取笑你的东西,你可能想多做一点 在进一步阅读之前进行研究。

让我们看一个示例来说明存根 API 的工作原理。此示例不是故意的 来演示模拟框架的广泛可能用途。它 有意简单专注于使用 Apex 存根 API 的机制。

假设我们想在下面的类中测试格式化方法。

public class DateFormatter {    
    // Method to test    
    public String getFormattedDate(DateHelper helper) {
        return 'Today\'s date is ' + helper.getTodaysDate();
    }
}

通常,当我们调用此方法时,我们会传入一个帮助程序类,该类具有一个方法 返回今天的日期。

public class DateHelper {   
    // Method to stub    
    public String getTodaysDate() {
        return Date.today().format();
    }
}

下面的代码调用该方法。

DateFormatter df = new DateFormatter();
DateHelper dh = new DateHelper();
String dateStr = df.getFormattedDate(dh);

为了进行测试,我们希望隔离该方法以确保格式有效 适当地。该方法的返回值通常因日期而异。然而,在这个 情况下,我们希望返回一个恒定的、可预测的值,以将我们的测试隔离到 格式。而不是编写类的“假”版本,该方法返回 一个常量值,我们创建类的存根版本。创建存根对象 在运行时动态,我们可以指定其方法的“存根”行为。getFormattedDate()getTodaysDate()要使用 Apex 类的存根版本,请执行以下操作:

  1. 通过实现接口来定义存根类的行为。System.StubProvider
  2. 使用该方法实例化存根对象。System.Test.createStub()
  3. 从测试类中调用存根对象的相关方法。

实现 StubProvider 接口

下面是接口的实现。StubProvider

@isTest
public class MockProvider implements System.StubProvider {
    
    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, 
        Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, 
        List<Object> listOfArgs) {
        
        // The following debug statements show an example of logging 
        // the invocation of a mocked method.
       
        // You can use the method name and return type to determine which method was called.
        System.debug('Name of stubbed method: ' + stubbedMethodName);
        System.debug('Return type of stubbed method: ' + returnType.getName());
        
        // You can also use the parameter names and types to determine which method 
        // was called.
        for (integer i =0; i < listOfParamNames.size(); i++) {
            System.debug('parameter name: ' + listOfParamNames.get(i));
            System.debug('  parameter type: ' + listOfParamTypes.get(i).getName());
        }
        
        // This shows the actual parameter values passed into the stubbed method at runtime.
        System.debug('number of parameters passed into the mocked call: ' + 
            listOfArgs.size());
        System.debug('parameter(s) sent into the mocked call: ' + listOfArgs);
        
        // This is a very simple mock provider that returns a hard-coded value 
        // based on the return type of the invoked.
        if (returnType.getName() == 'String')
            return '8/8/2016';
        else 
            return null;
    }
}

StubProvider是一个回调接口。它 指定需要实现的单个方法:。当调用存根方法时,将调用。您可以定义 此方法中存根类的行为。该方法具有以下特点 参数。handleMethodCall()handleMethodCall()

  • stubbedObject:存根对象
  • stubbedMethodName:调用方法的名称
  • returnType:被调用方法的返回类型
  • listOfParamTypes:参数类型的列表 invoked 方法
  • listOfParamNames:参数名称的列表 invoked 方法
  • listOfArgs:传递到此参数的实际参数值 方法在运行时

您可以使用这些参数来确定调用了类的哪个方法,以及 然后,您可以定义每个方法的行为。在这种情况下,我们检查退货 用于标识它并返回硬编码值的方法的类型。

实例化类的存根版本

下一步是实例化该类的存根版本。以下实用程序 类返回一个可用作模拟的存根对象。

public class MockUtil {
    private MockUtil(){}

    public static MockProvider getInstance() {
        return new MockProvider();
    }
    
     public static Object createMock(Type typeToMock) {
        // Invoke the stub API and pass it our mock provider to create a 
        // mock class of typeToMock.
        return Test.createStub(typeToMock, MockUtil.getInstance());
    }
}

该类包含方法, 调用该方法。 该方法采用 Apex 类 类型和我们之前创建的接口的实例。它返回一个存根对象,我们可以在 测试。createMock()Test.createStub()createStub()StubProvider

调用 Stub 方法

最后,我们从测试中调用存根类的相关方法 类。

@isTest 
public class DateFormatterTest {   
    @isTest 
    public static void testGetFormattedDate() {
        // Create a mock version of the DateHelper class.
        DateHelper mockDH = (DateHelper)MockUtil.createMock(DateHelper.class);
        DateFormatter df = new DateFormatter();
        
        // Use the mocked object in the test.
        System.assertEquals('Today\'s date is 8/8/2016', df.getFormattedDate(mockDH));
    }
}

在这个测试中,我们调用 创建类的存根版本。然后,我们可以在存根对象上调用该方法,该方法返回我们的 硬编码日期。使用硬编码日期允许我们在 隔离。createMock()DateHelpergetTodaysDate()getFormattedDate()

Apex 存根 API 限制

使用 Apex 存根 API 时,请记住以下限制。

  • 被模拟的对象必须与对方法的调用位于同一命名空间中。但是, 接口的实现可以在另一个命名空间中。Test.createStub()StubProvider
  • 您无法模拟以下 Apex 元素。
    • 静态方法(包括将来的方法)
    • 私有方法
    • 属性(getter 和 setter)
    • 触发器
    • 内部类
    • 系统类型
    • 实现接口的类Batchable
    • 只有私有构造函数的类
  • 迭代器不能用作返回类型或参数类型。