1

I want to write some tests to check a logic, related to OpportunityPartner object. The logic reads OpportunityPartner for Account, then reads it's Contacts.

    partners = [
    SELECT OpportunityId, AccountToId 
    FROM Partner
    WHERE OpportunityId IN :createdOpps
  ];
System.assertEquals(numberOfElementsToCreate, partners.size());

List<OpportunityPartner> opportunityPartners = [
    SELECT OpportunityId, AccountToId 
    FROM OpportunityPartner
    WHERE OpportunityId IN :createdOpps
  ];
System.assertEquals(numberOfElementsToCreate, opportunityPartners.size());

As you may assume, the last line is failing.

Is Salesforce insert OpportunityPartner object not right after Partner object is created?

While writing a question I thought... Why do we need another object if we can read all information from Partner object?

1
  • Can you confirm that your queries are correct? Use something like Force.com explorer to see if the proper data is there
    – EricSSH
    Sep 19 '14 at 17:18
1

This is kind of interesting.

I used anonymous APEX to create an Account and Opportunity - then built a Partner between them using an existing PartnerRole. (The anonymous Apex cleans up its previous run you you can run repeatedly. Use in sandbox so you don't create PROD accounts/oppos/partners

// Clean up last anonymous apex execution
Opportunity[] oList = [select id from Opportunity where name = 'sfse oppo'];
delete oList;
Account[] aList = [select id from account where name IN : new List<String> {'sfse0', 'sfse1'}];
delete aList;

// create 2 Accounts
aList = new List<Account> {new Account(name = 'sfse0'), new Account(name = 'sfse1')};
insert aList;
// Create Oppo on second Account
Opportunity o = new Opportunity(name = 'sfse1 oppo', accountId = aList[1].id, stageName = 'foo', closeDate = Date.today(), type='foo');
insert o;

// create a Partner between first account and the Oppo on the second Account
Partner p = new Partner(accountToId = aList[0].id, opportunityId = o.id, role='Advertiser', isPrimary = true);
insert p;

// see what we got
system.debug(LoggingLevel.INFO,'Partners=' + 
     [select id, accountFromId, accountToId, OpportunityId, isDeleted, isPrimary, role 
         from Partner where Opportunityid IN: new List<ID> {o.id}]);
system.debug(LoggingLevel.INFO,'opportunityPartners=' + 
     [select id, accountToId, opportunityId, isprimary, role 
         from OpportunityPartner where Opportunityid IN: new List<ID> {o.id}]);

The results are interesting. Even though I inserted only 1 Partner with role=Advertiser (and the log shows only 1 record inserted), SFDC built two Partners:

 14:09:27:226 USER_DEBUG [11]|INFO|Partners=
  (Partner:{OpportunityId=006J000000GeLGmIAN, IsDeleted=false, Role=Advertiser, Id=00IJ0000002Z6bFMAS, AccountToId=001J000001SNUqpIAH, IsPrimary=true, AccountFromId=001J000001SNUqqIAH}, 
   Partner:{OpportunityId=006J000000GeLGmIAN, IsDeleted=false, Role=Vendor, Id=00IJ0000002Z6bGMAS, AccountToId=001J000001SNUqqIAH, IsPrimary=false, AccountFromId=001J000001SNUqpIAH}) 

The second Partner has the defined ReverseRole (Advertiser > Vendor) from the Force.com UI PartnerRole setup page

SFDC also did, as expected by the doc, built an OpportunityPartner record as shown here (the second debug) - but, as with Partner, used the ReverseRole to build a second OpportunityPartner

14:09:27:229 USER_DEBUG [12]|INFO|opportunityPartners=(
 OpportunityPartner:{OpportunityId=006J000000GeLGmIAN, Role=Advertiser, Id=00IJ0000002Z6bFMAS, AccountToId=001J000001SNUqpIAH, IsPrimary=true}, 
 OpportunityPartner:{OpportunityId=006J000000GeLGmIAN, Role=Vendor, Id=00IJ0000002Z6bGMAS, AccountToId=001J000001SNUqqIAH, IsPrimary=false})

With the above, I'm going to surmise that part of your issue is related to the Reverse Role which SFDC uses to build a parallel Partner and parallel OpporutnityPartner

0

I've done the very same thing:

private static void setupTestOpportunityPartner(String opportunityId, String partnerAccountId) {
    Partner partner = new Partner();
    partner.AccountToId = partnerAccountId;
    partner.OpportunityId = opportunityId;
    partner.IsPrimary = true;
    partner.Role = PARTNER_DEVELOPMENT_ROLE;
    insert partner;
}

I called this from the @testSetup method in a test class (annotated with is @isTest(seeAllData=false)).

And, in a test method, I wrote:

List<Partner> partners = [select Id, AccountFromId, AccountToId, Role from Partner];
System.assertEquals(2, partners.size());

But the assertion failed! Only 1 Partner was inserted.

I tried to insert the reverse relationship record programmatically in many different ways, but Salesforce doesn't allow us to set AccountFromId and OpportunityId at the same time.

However, when this is done manually through the Opportunity UI, two records are inserted. One is set with isPrimary = true and the other one is set with isPrimary = false.

As usual, Salesforce's documentation doesn't explain any of this, and I find myself incapable of putting the database in the right state before running automated tests for my APEX classes.

0

As of V39.0, inserting Partner is not good enough in a testmethod to get OpportunityPartner

Here is how you need to mock OpportunityPartner in testmethods:

This is based on the Separation Of Concerns pattern as implemented in fflib

Assume you have some AccountsService layer class/method that looks like this:

public virtual void syncWithOpportunityPartners(set<ID> oPartnerIds) {
    OpportunityPartner[] opList = OpportunityPartnersSelector.newInstance().selectById(oPartnerIds);
    // do work with these OpportunityPartners ...
}

You need to use a Selector Layer (as shown above) that allows the selector results to be mocked by the testmethod as there won't be any real OpportunityPartners you can get out of a normal SOQL invocation within a testmethod.

Here's the Selector Layer:

public virtual with sharing class OpportunityPartnersSelector extends fflib_SObjectSelector implements IOpportunityPartnersSelector {
   public List<Schema.SObjectField> getSObjectFieldList() {
        return new List<Schema.SObjectField> {  
                OpportunityPartner.ID,
                OpportunityPartner.AccountToId,
                OpportunityPartner.IsPrimary,
                OpportunityPartner.LastModifiedDate,
                OpportunityPartner.OpportunityId,
                OpportunityPartner.Role
        };
    }

    public Schema.SobjectType getSObjectType() {return OpportunityPartner.SObjectType;}

    //  Callers needing a Selector instance should use this method as it supports mocking framework
    public static IOpportunityPartnersSelector newInstance()    {
        return (IOpportunityPartnersSelector) Application.Selector.newInstance(OpportunityPartner.SObjectType);
    }


    public virtual List<OpportunityPartner> selectById(Set<ID> idSet){
        return idSet.isEmpty() ? new List<OpportunityPartner>() : (List<OpportunityPartner>) selectSObjectsById(idSet);
    }
}

You'll need an Interface and the Application class so things can be mocked and mock implementations found at runtime.

public interface IOpportunityPartnersSelector {

    OpportunityPartner[] selectById(set<ID> ids);
}

public class Application {
 ...
public static final fflib_Application.SelectorFactory Selector = 
        new fflib_Application.SelectorFactory(
            new Map<SObjectType, Type> {
                    Account.SObjectType => AccountsSelector.class,
                    ...
                    OpportunityPartner.SObjectType => OpportunityPartnersSelector.class,
                    });

OK, now for the testmethod

First, define a mock class for the selector that queries OpportunityPartners. This is typically an inner class to your test class.

public class OpportunityPartnersSelectorMock extends OpportunityPartnersSelector {
        OpportunityPartner[] mockedOpportunityPartners;
        public override OpportunityPartner[] selectById(set<ID> idSet) {
            return mockedOpportunityPartners;
        }

        public OpportunityPartnersSelectorMock(OpportunityPartner[] opList) {this.mockedOpportunityPartners = opList;}
    }

Now you need to tell the SoC pattern to use your mock selector

ID opId00   = fflib_IDGenerator.generate(Partner.SObjectType);  // keyprefix of OpportunityPartner = Partner
ID opId01   = fflib_IDGenerator.generate(Partner.SObjectType);

Application.Selector.setMock(new OpportunityPartnersSelectorMock(
                                (OpportunityPartner[])Json.deserialize(

                                    JsonTestData.makeArray(new List<JsonTestData> {
                                        JsonTestData.getInstance()
                                            .newObj()
                                            .withVal('Id',opId00)
                                            .withVal('AccountToId',aList[2].Id)
                                            .withVal('OpportunityId',oList[0].Id)
                                            .withVal('IsPrimary',true)
                                            .withVal('Role','Foo')
                                            .make()
                                        }),
                                            List<OpportunityPartner>.class
                                )
                                ));

Note that OpportunityPartner sobjects can't be set up using new OpportunityPartner(AccountToId = someAccountId, ...) as the fields aren't writeable.

So, you have to mock the SObjects using Json.deserialize. Here, I'm using a fluent Json generator helper class but you could construct the JSON string the old fashioned way with string constant assembly.

And, ta-da - you can now test your service layer class as the Mock Selector will return mocked OpportunityPartner objects that can point at real mocked Accounts and Opportunities

Test.startTest();
AccountsService.syncWithOpportunityPartners(new set<ID> {opId00});
Test.stopTest();

Asides

  1. This approach also works well when you want to mock ProcessInstanceSteps and related objects - thus avoiding having your testmethods forcing objects into approval processes that must be active.
  2. The approach can, of course, be used for AccountPartner
  3. The approach also can be used to mock custom metadata type rows that otherwise can't be inserted using Apex.
  4. The Separation of Concerns pattern is hugely useful in unexpected ways such as these edge cases where normal DML in testmethods doesn't or can't create the sobjects your code wants

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.