First Apex Test Class | FIELD_INTEGRITY_EXCEPTION

apexunit-test

Struggling with building my first test class so I can deploy an Apex trigger.
The trigger converts an Opportunity/Opportunity Products into a Sales Order/Order Items.

Receiving the below error message:

System.DmlException: Insert failed. First exception on row 0; first error: FIELD_INTEGRITY_EXCEPTION, field integrity exception: PricebookEntryId, unknown (versions 3.0 and higher must specify pricebook entry id, others must specify product id): [PricebookEntryId, unknown]

Below is my test class:

@isTest
public class Create_Order_Test_Class {
    @isTest static void testSetup(){
        Account testAcct = new Account (Name = 'My Test Account');
        insert testAcct;

        Opportunity oppt = new Opportunity();
        oppt.Name ='New Test Deal';
        oppt.AccountID = testAcct.Id;
        oppt.StageName = 'Closed Won';
        oppt.Amount = 24000;
        oppt.CloseDate = System.today();
        insert oppt;
        
        Order ord = new Order();
        ord.OpportunityId = oppt.Id;
        ord.AccountId = oppt.AccountId;
        ord.EffectiveDate = oppt.CloseDate;
        ord.Status= 'Draft';
        ord.Pricebook2Id = oppt.Pricebook2Id;
        insert ord;
        
        Product2 p = new Product2();
        p.ProductCode = 'attribution_annual';
        p.Name = 'Attribution - Annual';
        insert p;
        
        String pbs = Test.getStandardPricebookId();
        
        List<PricebookEntry> pbes = new List<PricebookEntry>();
        pbes.add(new PricebookEntry(Pricebook2Id = pbs, Product2Id = p.Id, UnitPrice = 24000, IsActive = true, UseStandardPrice = false));
        insert pbes;
        
        Test.startTest();
        Map<String, String> productsMap = new Map<String, String>();
        for (PricebookEntry x : [Select Id, Product2.Name from PricebookEntry where Product2.ProductCode in ('Standard Price Book')]){
            productsMap.put(x.Product2.Name, x.Id);
        }
        update testAcct;
        update oppt;
        OpportunityLineItem oli = new OpportunityLineItem (OpportunityId = oppt.Id, Quantity = 1, UnitPrice = 24000, PricebookEntryId = productsMap.get('Standard Price Book'));
        insert oli;
        Map<string, string> items = new Map<string, string>();
        for(OpportunityLineItem x: [select id,PricebookEntryId from OpportunityLineItem where OpportunityId =: oppt.Id]){
            items.put(x.PricebookEntryId, x.id);
            
        OrderItem oi = new OrderItem(OrderId = items.get(oli.OpportunityId), Quantity = oli.Quantity, PricebookEntryId = oli.PricebookEntryId, UnitPrice = oli.UnitPrice);
        update oi;
        Test.stopTest();
        }
        
        }
    }

Best Answer

Yeah, working with OpportunityLineItems is one of the more tedious things to test because of all the required setup.

The issue(s)

The immediate issue I see here is with the following line (broken onto separate lines to make it easier to read)

OpportunityLineItem oli = new OpportunityLineItem (
    OpportunityId = oppt.Id, 
    Quantity = 1, 
    UnitPrice = 24000, 
    PricebookEntryId = productsMap.get('Standard Price Book')
);

Your productsMap keys are the product names, but you never create a product with the name "Standard Price Book". So productsMap.get('Standard Price Book') will return null. Given that OpportunityLineItem requires the PricebookEntryId, having that set to null is a problem.

I suspect that your productsMap is actually completely empty, because the query you're using to populate it contains where Product2.ProductCode in ('Standard Price Book'). The ProductCode that you're setting for your test product is "attribution_annual"

Fixing things

Basically, you just need to adjust your queries so that you're filtering (i.e. the stuff in the WHERE clause) on the appropriate things.

Instead of

for (PricebookEntry x : [Select Id, Product2.Name From PricebookEntry Where Product2.ProductCode in ('Standard Price Book')]){

To fetch the PBEs from a given Pricebook2, you'd want to use

// Filter based on the Pricebook's name, not the Product's ProductCode
for (PricebookEntry x : [Select Id, Product2.Name from PricebookEntry where Pricebook2.Name = 'Standard Price Book']){

Similarly, when creating your OLI, you need to use the Product Name (because that's how your previous loop built) instead of the Pricebook Name.

instead of

OpportunityLineItem oli = new OpportunityLineItem (
    OpportunityId = oppt.Id, 
    Quantity = 1, 
    UnitPrice = 24000, 
    PricebookEntryId = productsMap.get('Standard Price Book')
);

You want

OpportunityLineItem oli = new OpportunityLineItem (
    OpportunityId = oppt.Id, 
    Quantity = 1, 
    UnitPrice = 24000, 
    // Many parts of the Salesforce Platform are not case-sensitive
    // The keys of Maps, however, are one of the things that is case-sensitive
    PricebookEntryId = productsMap.get('Attribution - Annual')
);

Your work isn't done after you fix these issues

If I'm understanding you correctly, you shouldn't be creating an Order or an OrderItem in your test method at all. That's the work that your trigger is supposeed to be doing, so let the trigger do that. This test is to make sure that the trigger is, in fact, doing that work.

Tests have 3 general phases:

  1. Set up the test data
  2. Cause the code you're testing to execute
  3. Gather results and make sure your code did the thing it was supposed to do

I'm guessing that your trigger does this work when the Opportunity stage changes to "Closed Won". So your initial setup should be creating the Product2, PricebookEntry, Opportunity (in a non "closed won" stage), and OpportunityLineItem records.

Then, to actually cause your trigger to do work, you'd change your Opportunity StageName to "Closed Won" and perform a DML update.

Coverage is a side effect of unit tests.
The real utility of tests to us developers is to show that our code is behaving as expected.

To do that, you should be making assertions (System.assert(), System.assertEquals(), or System.assertNotEquals()). You'll generally have multiple assertions in a given test method, and the things that you want to assert against are the results of running your code.

In this case, you'd want to query for Order and OrderItem both before you perform the operation that causes your trigger to do the conversion, and after. Your assertions would then be along the lines of:

  • Ensure that this action caused a new Order to be created (check the size of the two Order lists)
  • Ensure that this action caused a new OrderItem to be created (check the size of the two OrderItem lists)
Related Topic