用 FFLIB 实现 Apex 企业设计模式

Apex 企业设计模式将应用分为服务层、模型层、选择逻辑层、工作单元几个部分。FFLIB 是一个开源的 Apex 框架,可以帮助开发者快速建立相关的功能。

FFLIB 的安装

FFLIB 可以直接部署到需要使用的 Salesforce 系统中。在其 GitHub 主页上可以点击 “Deploy to Salesforce” 按钮直接进行部署。

FFLIB 中的关键类

在 FFLIB 中,有一些关键的类可以帮助开发者实现 Apex 的企业设计模式。

  • fflib_SObjectSelectot:实现了 fflib_ISObjectSelector 接口,用于实现选择逻辑层,其中可以定义对象的查询逻辑等
  • fflib_SObjectDomain:实现了 fflib_ISObjectDomain 接口,用于实现模型层,其中可以定义某个对象的内部逻辑
  • fflib_SObjectUnitOfWork:实现了 fflib_ISObjectUnitOfWork 接口,用于实现工作单元模式,其中包含了数据增删修改的各种逻辑
  • fflib_Application:包含了初始化 Apex 企业设计模式各部分的函数,可以作为使用各个部分的统一入口

FFLIB 应用实例

下面我们通过一个简单的例子阐述如何实现设计模式的各个部分。

功能包括:

  1. 建立一个简单的关于 Account 的 Visualforce 页面,其中包含一个输入框和按钮。
  2. 当用户输入文字并点击按钮后,系统会首先查找名字和用户输入相同的 Account 记录,如果找不到则建立这样一个 Account 记录。
  3. 在每个 Account 记录创建之后,建立一个同名的 Contact 对象。

包含以下几个类:

  • App_Application:应用框架,使用 fflib_Application 中定义的工厂方法来初始化 Apex 企业设计模式的各部分
  • AccountSelector:选择逻辑层
  • AccountService:服务层
  • Accounts:模型层
  • AccountTrigger:Account 对象的触发器类

示例代码

App_Application 类:

public without sharing class App_Application {
    public static final fflib_Application.UnitOfWorkFactory unitOfWork = new fflib_Application.UnitOfWorkFactory(
        new List<SObjectType> {
			Account.SObjectType,
            Contact.SObjectType
        }
    );

    public static final fflib_Application.ServiceFactory service = new fflib_Application.ServiceFactory(
        new Map<Type, Type> {
			AccountService.IService.class => AccountService.class
        }
    );

    public static final fflib_Application.SelectorFactory selector = new fflib_Application.SelectorFactory(
        new Map<SObjectType, Type> {
			Account.SObjectType => AccountSelector.class
        }
    );

    public static final fflib_Application.DomainFactory domain = new fflib_Application.DomainFactory(
        App_Application.selector,
        new Map<SObjectType, Type> {
			Account.SObjectType => Accounts.class
        }
    );
}

App_Application 类中使用工厂方法给出了各个部分的初始化逻辑。注意每个工厂方法的参数,它们包含了对象类型和相应的类。在下面的各个部分中我们会直接调用 App_Application 类的成员。

AccountSelector 类:

public with sharing class AccountSelector extends fflib_SObjectSelector {
    public static AccountSelector newInstance() {
        return (AccountSelector) App_Application.selector.newInstance(Account.SObjectType);
	}
    
    /*
     * 实现了 fflib_SObjectSelector 中的抽象函数,用于返回当前类所关联的 SObject 对象类型
     */
    public Schema.SObjectType getSObjectType() {
        return Account.SObjectType;
    }
    
    /*
     * 实现了 fflib_SObjectSelector 中的抽象函数,用于提供默认搜索时得到的字段
     */
    public List<Schema.SObjectField> getSObjectFieldList() {
        return new List<Schema.SObjectField> {
            Account.Name,
            Account.Id
        };
	}
    
    /*
     * 实现了 fflib_SObjectSelector 中的抽象函数,通过一组 ID 的值来查询一组 Account 对象
     */
    public List<Account> selectById(Set<Id> idSet) {
        return (List<Account>) selectSObjectsById(idSet);
	}
    
    /*
     * 自定义函数,用于查找 Name 字段和给定的值相同的 Account 对象 
     */
    public List<Account> selectByName(String name) {
        return [SELECT Name FROM Account WHERE Name = :name];
	}
}

AccountSelector 类相对简单。除了实现 fflib_SObjectSelector 类中的抽象函数,我们自己定义了一个按名称查找的函数,可供服务层调用。

Accounts 类:

public with sharing class Accounts extends fflib_SObjectDomain {
    public Accounts(List<SObject> SObjectList) {
        super(SObjectList);
	}
    
    /*
     * 对于 fflib_SObjectDomain 类中的钩子函数的重写,在插入记录之后自动建立一个 Contact 对象
     */
    public override void onAfterInsert() { 
        fflib_ISObjectUnitOfWork uow = App_Application.unitOfWork.newInstance();
        
		createContact(uow);
        
        uow.commitWork(); 
    }
    
    /*
     * 自定义函数,对于每个 Account 记录,建立一个同名的 Contact 记录
     */
    public void createContact(fflib_ISObjectUnitOfWork uow) { 
        for (Account acc : (List<Account>) Records) // Records 变量是 fflib_SObjectDomain 中定义的 List<SObject> 类型的成员,表示此类包含的记录
		{
			Contact c = new Contact(LastName = acc.Name);
            
            // 将新建的 Contact 对象关联到 Account 对象中
            //  这个函数的第二个参数表明了 Contact 对象中和 Account 对象关联的字段
			uow.registerNew(c, Contact.AccountId, acc);
		}
    }
    
    // 每个继承了 fflib_SObjectDomain 类都必须有的内部类
    public class Constructor implements fflib_SObjectDomain.IConstructable {
        public fflib_SObjectDomain construct(List<SObject> SObjectList) {
            return new Accounts(SObjectList);
        }
	}
}

Accounts 类是模型层,里面定义了一个函数,用来在 Account 对象下面创建一个 Contact 对象。

对于内部类 Constructor,这是 FFLIB 中的一个约定,每一个继承了 fflib_SObjectDomain 类都必须有这一个内部类。

fflib_SObjectDomain 类实现了一个 TriggerHandler 函数,并提供了若干钩子函数,可以和触发器类结合使用,从而使得对象在被增删修改之后可以自动执行相应的逻辑。由于 Apex 缺乏完整的反射机制,在进行触发器操作时,模型层无法直接得到需要处理的记录。这个内部类实现了 fflib_SObjectDomain.IConstructable 接口的 construct 函数,从而可以将需要处理的数据传递给模型层。

AccountTrigger 类:

trigger AccountTrigger on Account (before insert, after insert) {
    fflib_SObjectDomain.triggerHandler(Accounts.class);
}

触发器类很简单,直接调用了 fflib_SObjectDomain 中的 triggerHandler 函数,将模型层的类作为参数传进去。

AccountService 类:

public class AccountService implements IService {
	public static IService newInstance() {
        return (IService) App_Application.service.newInstance(IService.class);
	}
    
    public interface IService {
        void createAccount(String name);
        void createAccount(fflib_ISObjectUnitOfWork uow, String name);
    }
    
    /*
     * 新建 Account 记录
     */
    public void createAccount(String name) {
        fflib_ISObjectUnitOfWork uow = App_Application.unitOfWork.newInstance();

        createAccount(uow, name);

        uow.commitWork(); // 将数据存入数据库
	}

    /*
     * 核心逻辑,重载 createAccount 函数,查找相应的 Account 记录,如果找不到则新建
     */
    public void createAccount(fflib_ISObjectUnitOfWork uow, String name) {
        AccountSelector selector = (AccountSelector) AccountSelector.newInstance();
        List<Account> accList = selector.selectByName(name);
        
        // 如果不存在相应的 Account 记录,才创建
        if (accList.isEmpty()) {
            Account newAcc = new Account(Name = name);
            
            uow.registerNew(newAcc); // 将 newAcc 记录标记为新记录,等待使用 commitWork 函数来存入数据库
        }
    }
}

AccountService 类的逻辑很简单,提供了 createAccount 函数,从而让外部代码可以调用并创建 Account 对象。

注意,我们重载了 createAccount 函数。

第一个函数只包含一个参数,即“名字”,让外部代码直接调用即可创建 Account 对象。

第二个函数包含了“名字”和“工作单元”两个参数,并且不包含 commitWork 函数,从而可以被外部代码单独调用,只实现创建记录的逻辑。外部代码可以调用其他的各种逻辑,也可以定义将数据写入数据库的时间。

Visualforce 页面:

<apex:page controller="AccountTestFflibController">
    <apex:form>
        <apex:inputText label="输入客户名字" value="{!name}"/> 
        <apex:commandbutton value="创建" action="{!create}" />
    </apex:form>
</apex:page>

Visualforce 控制器:

public class AccountTestFflibController {
    public String name {get; set;}
    
	public PageReference create(){
        AccountService service = (AccountService) AccountService.newInstance();
        
        service.createAccount(name);
        
        return null;
    }
}

可以看到,在 Visualforce 控制器中,我们只调用了一次服务层的 createAccount 函数,就完成了所有相关的逻辑。

小结

上述示例只是使用了 FFLIB 中的一些基本功能,实现了 Apex 企业设计模式的基本结构。

FFLIB 还提供了其他的功能和辅助函数,在实际应用中,可以和其他的框架或现有的代码结合,提高代码的维护和更新效率。

对于 FFLIB 的单元测试,可以使用 ApexMocks 框架

ApexMocks 框架

ApexMocks 框架是为 Apex 的单元测试开发,主要提供了模拟数据的功能。

在 Apex 开发中,我们始终要对开发的类做出单元测试,并且代码覆盖率要不小于75%。为了对功能进行全面的测试,我们往往需要准备很多数据,并将它们插入数据库(当然,在测试结束后 Salesforce 会自动将这些数据删除)。随之而来的问题就是单元测试的效率会随着准备数据的复杂度而降低。

ApexMocks 中提供了多种方法让我们来创建模拟数据,并可以和 Apex 企业设计模式结合,使得我们的单元测试不用真正的对数据库进行操作,从而提高测试效率。

安装

ApexMocks 可以直接部署到需要使用的 Salesforce 系统中。在其 GitHub 主页上可以点击 “Deploy to Salesforce” 按钮直接进行部署。

关键函数

在 ApexMocks 框架中,最关键的函数就是 setMock 函数。通过它,我们可以对要进行测试的类进行依赖注入,让我们的逻辑使用模拟的数据进行测试。

使用步骤

在使用 ApexMocks 进行模拟数据和测试的时候,一般遵循以下四个步骤:

  1. 建立模拟对象(Create mocks)
  2. 建立模拟数据(Given)
  3. 执行测试步骤(When)
  4. 检验测试结果(Then)

代码示例

让我们使用之前建立的 AccountService 类来进行单元测试。

测试类中包含两个函数,一个测试在没有任何 Account 存在时,新的 Account 对象可以被建立,另一个测试当已经存在同名的 Account 对象时,没有新的 Account 对象被创建。

@isTest(isParallel=true)
public class AccountServiceTest {
    @IsTest
    private static void shouldCreateAccount()
    {
        // Create mocks
        fflib_ApexMocks mocks = new fflib_ApexMocks();
        fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectMocks.SObjectUnitOfWork(mocks); // 建立工作单元的模拟
        AccountSelector selectorMock = (AccountSelector) mocks.Mock(AccountSelector.class); // 建立选择逻辑层的模拟
     
        // Given
        String testAccountName = 'Test Existing Account';

        App_Application.unitOfWork.setMock(uowMock); // 使用 setMock 设置选择逻辑层的模拟
        App_Application.selector.setMock(selectorMock); // 使用 setMock 设置工作单元的模拟
     
        // When
        AccountService service = (AccountService) AccountService.newInstance();
        service.createAccount(testAccountName);
     
        // Then
        fflib_ArgumentCaptor argument = fflib_ArgumentCaptor.forClass(fflib_ISObjectUnitOfWork.class);
		((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 1)).registerNew((Account) argument.capture()); // 验证 registerNew 函数被执行了一次,并且其中的参数是 Account 类型的

		((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 1)).registerNew((Account) fflib_Match.anyObject()); // 另一种验证,registerNew 函数被执行了一次,并且其中的参数是任意 Account 类型的任何对象
    }

    @IsTest
    private static void shouldNotCreateAccount()
    {
        // Create mocks
        fflib_ApexMocks mocks = new fflib_ApexMocks();
        fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectMocks.SObjectUnitOfWork(mocks);
        AccountSelector selectorMock = (AccountSelector) mocks.Mock(AccountSelector.class);
     
        // Given
        String testAccountName = 'Test Existing Account';

        /*
         * 下面这段代码使用 stub API 来模拟选择逻辑层的函数 selectByName 的执行结果:
         * 当其参数是变量 testAccountName 的值的时候,返回一个 Account 对象
         */
        mocks.startStubbing();
        List<Account> existingAccounts = new List<Account> { 
            new Account(
                Id = fflib_IDGenerator.generate(Account.SObjectType), // 建立随机的一个 ID 值
                Name = testAccountName) 
                };
        mocks.when(selectorMock.sObjectType()).thenReturn(Account.SObjectType);
        mocks.when(selectorMock.selectByName(testAccountName)).thenReturn(existingAccounts);
        mocks.stopStubbing();
        
        App_Application.unitOfWork.setMock(uowMock);
        App_Application.selector.setMock(selectorMock);
     
        // When
        AccountService service = (AccountService) AccountService.newInstance();
        service.createAccount(testAccountName);
     
        // Then
        fflib_ArgumentCaptor argument = fflib_ArgumentCaptor.forClass(fflib_ISObjectUnitOfWork.class);
		((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 0)).registerNew((Account) argument.capture()); // 验证 registerNew 函数没有被执行
    }
}

代码解释:

  1. 我们在 “Then” 部分进行验证的时候,使用了 fflib_ArgumentCaptor 类。这个类的作用是得到在模拟的工作单元中使用的参数。然后,我们可以使用 mocks.verify 函数来验证相应的函数 “registerNew” 是否在模拟工作单元 “uowMock” 中被调用了一次,并且参数是 “Account” 类型
  2. 在 shouldNotCreateAccount 函数中,为了模拟选择逻辑层的输出结果,我们使用了 Apex 提供的 stub API 功能。这样做的好处是我们不需要考虑选择逻辑层的正确与否(选择逻辑层有自己的测试函数来测试),只需要设置它的输出结果,然后用来测试当前的服务层函数即可。我们使用了 mocks.when(functionA(paramA)).thenReturn(B) 函数来设置当调用函数 functionA 并且其参数是 paramA 时,返回值是 B
  3. 注意这一行 “mocks.when(selectorMock.sObjectType()).thenReturn(Account.SObjectType);”,这是必须的设置,用来告诉模拟的选择逻辑层去和 Account 类型相关联。否则在接下来的执行中,模拟的选择逻辑层会给出 null 作为结果
  4. 在对 “Then” 部分进行验证时,不一定要使用 ApexMocks 提供的类或函数。我们也可以直接使用默认的 System.assert 系列函数来测试运行结果

小结

ApexMocks 框架提供了非常强大的模拟对象功能,我们在上文中只给出了很简单的示例。

将 ApexMocks 和 FFLIB 结合使用可以显著地提高单元测试的运行效率。

posted @ 2019-12-21 01:56  程程哥  阅读(442)  评论(0编辑  收藏  举报