Is it worth it to try for 100% org coverage?
Yes, you should strive for 100%.
Actually, 100% is not a high enough bar. You should strive for 100% branch coverage. Do some reading on Cyclomatic Complexity to better understand.
Sometimes this type of coverage requires you to be more clever in how you design your code. One key strategy to achieve this goal is Separation Of Concerns.
Here is a simple example:
public static void complexOperation(List<SObject> records)
{
for (SObject record : records)
{
if (someCondition)
{
// data transformation
}
else
{
// other transformation
}
}
try
{
update records;
}
catch (Exception pokemon) // gotta catch em all!
{
// complex error handling
}
}
Hmm, testing that is going to be quite difficult! SOC to the rescue.
public static List<SObject> filter1(List<SObject> records)
{
List<SObject> filtered = new List<SObject>();
for (SObject record : records)
if (someCondition) filtered.add(record);
return filtered;
}
public static List<SObject> filter2(List<SObject> records)
{
List<SObject> filtered = new List<SObject>();
for (SObject record : records)
if (otherCondition) filtered.add(record);
return filtered;
}
public static List<SObject> dataTransformation1(List<SObject> records)
{
// data transformation
}
public static List<SObject> dataTransformation2(List<SObject> records)
{
// other transformation
}
public static void safeUpdate(List<SObject> records)
{
try
{
update records;
}
catch (DmlException dmx) // specificity yay!
{
// error handling
}
}
public static void complexOperation(List<SObject> records)
{
List<SObject> toUpdate = new List<SObject>();
toUpdate.addAll(dataTransformation1(filter1(records));
toUpdate.addAll(dataTransformation2(filter2(records));
safeUpdate(toUpdate);
}
Every specific chunk of functionality above is much easier to test directly. Testing the composition of these can then be somewhat more cursory.
Focussing on coverage in Apex is an example of what you measure is all you’ll get. Whether you aim for 75%, 80%, 90% or 100%, the number tells you next to nothing about whether the key behaviour of the code is being both exercised and confirmed. Developers should start with that aim in mind and the coverage will happen pretty much automatically.
100% coverage may not necessarily be possible, but you should be able to get close in most cases. I usually set the bar at 95%, because sometimes you just can't get all the way.
Typically, when I start from scratch, I'll usually write a test. If I'm at 100%, I'm done. Otherwise, I'll refactor branches to minimize uncovered areas. If I'm at 100% after an initial refactor, I'm done. Finally, I write unit tests to trip as many exception paths as I can. Thanks to @TestSetup, it's now a lot easier to reach 100%, but there are something things you simply can not cover.
They're impossible, because the language has no way to test them. For example, anything to do with row lock handling is impossible to reliably test, because you can't simulate a row being locked as a separate transaction.
What you must cover are all the primary paths through your code. Validate that they work correctly. The primary paths should be at least 75% of your code, because that will allow you to satisfy both the 75% minimum requirement, as well as the philosophy that you should verify the primary paths work correctly.
Using the initial unit test to gauge refactoring lets me not worry about unoptimized code ahead of time. The unit test will tell me what I did wrong. You shouldn't usually spend more time writing your tests than you did writing the initial code (e.g. 50% of your development time should be developing/fixing bugs).
Any more than that, and you'll start to get testing fatigue, as I like to call it. Each additional test beyond the first is going to yield smaller and smaller returns, to the point where you'll be writing 25 lines of code just to cover another 1 line of code, for example. If you're at 75%, you've covered all your branches, you've refactored, and you've reached a point where 1% takes more effort than the first 75%, that's usually the time to give up, or at least do it in phases.