The Salesforce Summer ’14 release is right around the corner. This article details two new features related to products and pricing: creating PricebookEntry records in unit tests and PricebookEntry custom fields. Visit the Summer ’14 Release page for general information about the release.
SeeAllData=true Not Needed For PricebookEntry
PricebookEntry records can now be created in unit tests without using the @isTest(SeeAllData=true) annotation. This is a huge win for the best practice of isolating test data from org data. A PricebookEntry cannot be inserted unless there is a standard PricebookEntry already. If it is attempted, a DmlException will be generated with an error message “FIELD_INTEGRITY_EXCEPTION, No standard price defined for this product”. Prior to Summer ’14, the only way to create a standard PricebookEntry was to use the SeeAllData=true annotation, query for the standard pricebook to get its Id, and then set the PricebookEntry’s Pricebook2Id field to the standard Id. The method Test.getStandardPricebookId()
is new in Summer ’14 and can be used to get the standard pricebook Id. From now on, PricebookEntry records should be created in unit tests using this method to get the standard pricebook Id. The method is available for classes that are in previous API versions, as well, if you wish to refactor. If your code uses a single mechanism such as a TestUtils class to set up the data for unit tests, you can just modify it to start using the Test.getStandardPricebookId()
and then change the unit test methods to no longer specify SeeAllData=true. Here is an example of how the new method can be used in data set up for a unit test. The example has a contrived unit test that creates products, standard prices, and non-standard prices. A TestUtils class does the work of creating the records for the unit test. Note the use of the Test.getStandardPricebookId()
method to create the standard PricebookEntry records.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@isTest static void pricebookEntryTest() { List products = TestUtils.generateProducts(10, true); // Generate standard prices (note Test.getStandardPricebookId()) Map<Id, Decimal> standardProductPrices = TestUtils.generateProductPrices(products, 500, true); List standardEntries = TestUtils.generatePricebookEntries( standardProductPrices, Test.getStandardPricebookId(), true); // Generate non standard prices - OK to do since standard pricing for products exists Pricebook2 nonStandardPricebook = TestUtils.generatePricebooks(1, true).get(0); Map<Id, Decimal> nonStandardProductPrices = TestUtils.generateProductPrices(products, 400); List nonStandardEntries = TestUtils.generatePricebookEntries( nonStandardProductPrices, nonStandardPricebook.Id, true); Test.startTest(); // invoke method under test Test.stopTest(); // assert something } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
@isTest public class TestUtils { public static List generatePricebookEntries(Map<Id, Decimal> productPrices, Id pricebookId, Boolean doInsert) { List entries = new List(); for (Id productId : productPrices.keySet()) { entries.add(new PricebookEntry( Product2Id = productId, Pricebook2Id = pricebookId, UnitPrice = productPrices.get(productId), IsActive = true )); } if (doInsert) { insert entries; } return entries; } public static List generateProducts(Integer numProducts, Boolean doInsert) { List products = new List(); for (Integer i = 0; i < numProducts; i++) { products.add(new Product2(name='Test' + i)); } if (doInsert) { insert products; } return products; } public static Map<Id, Decimal> generateProductPrices(List products, Decimal baseAmount) { Map<Id, Decimal> productPrices = new Map<Id, Decimal>(); for (Integer i = 0; i < products.size(); i++) { productPrices.put(products.get(0).Id, baseAmount + i); } return productPrices; } public static List generatePricebooks(Integer numPricebooks, Boolean doInsert) { List pricebooks = new List(); for (Integer i = 0; i < numPricebooks; i++) { pricebooks.add(new Pricebook2( IsActive = true, Name = 'Unit Test' )); } if (doInsert) { insert pricebooks; } return pricebooks; } } |
PricebookEntry Custom Fields
The PricebookEntry object now supports custom fields. This is a huge win for data migration and integration or just general data loading, because an external Id field can be added to the PricebookEntry. Once added, systems that integrate with Salesforce to set prices can just specify the external Id in their updating process. The release notes describe a possible use case for custom fields on PricebookEntry as having start and end effective date fields. Entries in the pricebook could then be set to be valid during a certain time period. It would be up to the functionality using the PricebookEntry to enforce the validity via code or a validation rule. Be sure to understand what it isn’t before implementing it. Since a pricebook can only have one entry per product you cannot set up any pricing in advance for existing entries. For example, you cannot preload all special weekly sale prices for a subset of products, with start dates in the future. Doing so would require the same product to be in a pricebook more than once. Likewise, you could not have multiple prices in the same pricebook at the same time (i.e., overlapping sales). However, if your pricing is driven by integration the dates could be updated through the integration at the appropriate times, now that external Ids are supported. Overall, support for custom fields on PricebookEntry is a huge win and there are many uses cases that can now be supported easily. In addition to custom fields, validation rules page layouts, field sets, and search layouts are available for customization.
Determining the Standard PricebookEntries in Code Under Test
The Id of the standard pricebook is available in the unit tests, but the actual record is not. If you have code that needs to get all of the standard PricebookEntry records, you might consider something like
1 2 3 |
[SELECT Id FROM PricebookEntry WHERE Pricebook2.IsStandard = true] |
The problem is that in the unit test context the standard pricebook record is not available, so no record is returned from the “Pricebook2.” reference and the WHERE clause always evaluates to false and nothing is returned. You could change your code to detect if Test.isRunningTest() and branch based on that. Something like the following. But, that’s icky.
1 2 3 4 5 6 7 |
Id stdId = Test.isRunningTest() ? Test.getStandardPricebookId() : [SELECT Id From Pricebook2 WHERE IsStandard = true].Id; [SELECT ID FROM PricebookEntry WHERE Pricebook2Id = :stdId] |
A better solution is to add a checkbox formula field to the PricebookEntry called something like “Is Standard”. The value should be {!Pricebook2.IsStandard}. Your query to get all standard PricebookEntry records would then change to the following, which works in unit tests as well as normal execution contexts.
1 2 3 |
[SELECT ID FROM PricebookEntry WHERE Is_Standard__c = true] |
Try It Out
The ability to create PricebookEntry records without SeeAllData=true and custom fields on PricebookEntry records are valuable new features. If you are interested in trying the out now you can sign up for a Summer ’14 prerelease org. The release notes can be viewed in their HTML format (responsive design / mobile friendly!) and downloaded as a PDF.
Thanks Peter. This is really good news. SeeAllData=true has caused me some headaches in the past.