4

Looking for best practices/suggestions on how to unit test a service layer method that creates new records based on input without having to issue the DML.

An example would be OpportunitiesServiceImpl.createInvoices in fflib apex sample code. Given a list of opportunities, new Invoice__c & InvoiceLine__c records are created. The method could be tested by mocking UnitOfWork, but how to verify that the data on Invoice__c & InvoiceLine__c is correct. In other words, how to determine that invoice__c.InvoiceDate__c was set to opportunityRecord.CloseDate instead of opportunityRecord.SomeOtherDate?

The challenge with DML is that in more complicated situations then the one presented in createInvoices, related data might to exist in the DB which means it all needs to be setup, inserted, etc. Also, domain logic (onBefore/onAfter/etc) will fire when records are inserted and complicates situations and takes away from a true unit test.

Some ideas:

  1. Stuck with DML - bad
  2. Create a InMemoryDB and build a UOW that will add to InMemoryDB instead of calling insert/update/etc. - Possibly an enhancement to fflib common?
  3. Have createInvoices return a wrapper class and inspect the wrapper class (e.g. InvoiceFactory concept with Invoices property). The challenge here is that you are validating the wrapper class not the Entity (SObject) and following this approach results in lots of wrapper classes when you have lots of service methods that do similar things in different areas of your app

Appreciate any thoughts/input, thank you!

Update - With some great insight from @andyinthecloud, the registerNew/registerDirty/etc. methods on the UOW itself can be verified to check the data resulted in the way expected. This avoids having to issue the DML. One note is to be aware of https://github.com/financialforcedev/fflib-apex-mocks/issues/8 which is what led me down the path of posting this since my attempts at this approach were not working as I expected. In summary, here's the approach:

// Create Mocks
fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectMocks.SObjectUnitOfWork(mocks);

// Set Mock
Application.UnitOfWork.setMock(uowMock);

// Call Service
MyService.doStuffThatWillInsertAccountRecord();

// create an account that should be the same as what
// registerNew was passed
Account expectedResult = new Account(Name = 'foo' .... );
((fflib_ISObjectUnitOfWork)
mocks.verify(uowMock, 1)).registerNew(expectedResult);
((fflib_ISObjectUnitOfWork)
mocks.verify(uowMock, 1)).commitWork();
1
  • +1 for apexmocks
    – cropredy
    Commented Dec 11, 2018 at 23:46

3 Answers 3

2

There is a 'smoking hot' answer provided by @sfdcfox that exploits Json.deserialize to build in-memory relationships.

You can place your JSON strings in a library of static resources to mimic many use cases and cycle through those static resources in your test class/testmethods

0

In some cases, some extra layers of indirection will provide the necessary independence to allow isolated unit testing. However, due to the way that queries return results that include related SObjects (e.g. Select Account.Name From Contact) and related SObject Lists (e.g. Select Id, (Select FirstName From Contacts) From Account) connecting uncommitted SObjects becomes extremely difficult.

Overcoming this becomes a ever-escalating test engineering exercise which takes away from the actual work that you are attempting to achieve.

In my experience, I now just opt to be stuck with DML. Making use of @testSetup methods can help with the performance of the Unit Tests, but you are still not doing "pure" Unit Testing.

0

This is general architectural question that requires thought and planning as it applies to more than just the type of service layer classes you're asking about. Some architects will include the ability to turn-off triggers when testing helper classes. Others that I've seen will use @testvisible as a means of creating a default set of values that will get passed through from one class to another when running tests of this sort or they perhaps may have something else happen.

There's a variety of means of handling these kinds of issues. The key here is that it's an architectural decision that needs to be made and then consistently applied/enforced regarding how you're going to handle them. You'll want to apply it to your application if you're an ISV or to your entire SF Org if you're not; something that's often much easier said than done in a large organization.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .