Waiting too long for Apex Tests results during Managed Package development

apexcode-coveragefailing-testsmanaged-packagepackage

I was asked to put my team's project into 2GP Managed Package. This project is relatively big, so passing Validating metadata step during sfdx force:package:version:create was really a struggle.

Now, I need to pass the package version creation with --codecoverage flag turned on, in order to promote the package version to release.

The first issue I'm facing is that, in my code there are a lot of lines like below:
Schema.getGlobalDescribe().get('Custom_Obj__c').getDescribe().fields.getMap().keySet())

It results in System.NullPointerException: Attempt to de-reference a null object.

Question 1:
Is renaming the arguments to namespace__Custom_Obj__c the only option? Can it be handled in a better way? Because this way, I'm restricting my development, to work only in Managed Package approach.

The other issue is that, for every Apex Testing (which I perform during version creation with --codecoverage flag on) results I have to wait around 2 hours. This is ridiculous!

Question 2:
Is there a better way to run my tests? How can I for example, check how just one test behaves in a managed package scenario? Without Managed Package approach, the tests are 100% successful and those NullPointerException are present only in MP approach.

I'm new to the 2GP MP world. Maybe I misunderstood some key concepts?

Best Answer

Is renaming the arguments to namespace__Custom_Obj__c the only option? Can it be handled in a better way? Because this way, I'm restricting my development, to work only in Managed Package approach.

Unless you're intentionally trying to keep the base object decoupled from the package, simply use:

sObjectType.Custom_Obj__c.fields.getMap().keySet();

If you are trying to keep the base object out of your package, use:

((sObject)Type.forName('Schema','Custom_Obj__c'))
  .newSObject().getSObjectType().getDescribe().fields.getMap().keySet();

The other issue is that, for every Apex Testing (which I perform during version creation with --codecoverage flag on) results I have to wait around 2 hours. This is ridiculous!

To be clear, Schema.getGlobalDescribe() can be very expensive and should only be used as a last resort (e.g. you need to list every object anyways). I've seen this method take up to 3 seconds on the first call, plus an additional second for each call thereafter.

Also, this is one example of why your tests are probably taking two hours. Tests run in serial mode only when testing code coverage this way, so if your tests are expensive, they will take a while to run. If you have a lot of smaller tests, they will likely take longer to run than fewer larger tests. That's just the nature of how transactions work in Salesforce.

Anecdotally, I once worked on a project that had a class with hundreds of "constants", and because of how they were working, that class, which was called from every other class, added a full 1 second of CPU time to every transaction, multiplied by the 500 or so unit tests we had to run. I refactored it so it lazy-loaded the data, and immediately reduced our deployment time by about 7 minutes. It also made every transaction in the database 1 second faster, which the users noticed. The point is, do some profiling and figure out how to optimize away your time killers.

Question 2: Is there a better way to run my tests? How can I for example, check how just one test behaves in a managed package scenario? Without Managed Package approach, the tests are 100% successful and those NullPointerException are present only in MP approach.

Properly written code won't have this problem. There are very few exceptions when a managed package and unmanaged code will behave differently, and most of the time, you can work around them by either checking your namespace or by using alternative methods.

I'm new to the 2GP MP world. Maybe I misunderstood some key concepts?

One important aspect is that you need to understand that everywhere you need namespaces, there are ways to get the code to behave correctly. Avoiding Schema.getGlobalDescribe() is one such example. Others include preferring to use tokens instead of strings (e.g. CustomObj__c c = new CustomObj__c() instead of CustomObj__c c = (CustomObj__c)Schema.getGlobalDescribe().get('CustomObj__c').newsObject()). Your code, if properly written, should be namespace agnostic.

Also, one final note: when you are developing, you can create a scratch org with the same namespace as your 2GP. See Link a Namespace to a Dev Hub Org for more information. This allows you to always work with a namespace, which provides you a more accurate way to determine why code is failing.