Questions on code coverage are very common here, and they are commonly closed because the general advice is the same every time.
I myself have some words that I repeat quite often regarding unit testing.
The golden rule of unit testing
In unit testing, you only gain coverage for code that is executed as part of a test method. It really is that simple.
You are completely in control of your unit test; so, if you're not getting coverage for part of your code, and want coverage for it, you need to ask yourself "how can I cause this code to be executed?"
- If you're not gaining coverage for a method, then you need to have a test that actually calls the method.
- If you're not gaining coverage for an
else
block, then you need to have a test where you ensure that you do not satisfy the conditions to enter the corresponding if
block.
- If you're not gaining coverage for a loop, you need to make sure that your test actually has records to loop over.
Most times, fixing coverage issues comes down to making sure that you create/insert appropriate test data (ideally, in a method with the @testSetup
annotation).
The most important part of unit testing
Coverage is what Salesforce requires, but code coverage alone is pretty meaningless.
The way I see things, the real use of unit tests is both to verify that your code behaves the way you think it should, and produces the correct output, as well as acting as a guard to alert you when changes or additions you make in the future break existing code.
For both of those purposes, you need to use assertions. Without assertions, you're only checking to make sure that your code doesn't crash/fail catastrophically. Your code could, for example, tell you that 2 + 2 = banana (joking side note: everyone knows that 2 + 2 = fish), or that red is equal to blue, you don't know unless you make assertions.
What to assert in unit tests?
The answer here is the always helpful "it depends". You could write an assertion for almost everything under the sun, but it takes time to write assertions (and to write code that satisfies them). After a while, you'll learn what things are important to assert, and which things don't need assertions.
Personally, the things I find myself writing assertions for most frequently are:
- Initial values - When I create a new instance of a class, are the class variables initialized to something useful?
- Return values - When a method finishes and returns, did I get the value that I expected?
- Changes to object state - If I perform some action (most often, calling a method that does not return any data), does the object instance that I performed the action on have its internal state modified? Was the modification what I expected?
- Changes to the database - If I call a method that's supposed to insert an
Opportunity
, was a new Opportunity
really inserted?
- Consumption of governor limits - This one is less frequent, and starts to matter when you have triggers that cause other triggers to fire. What I assert here is that my code only uses a certain number of queries, or that it consumes less than X amount of the CPU governor limit
You can (and likely should) have more than one unit test
A unit test is supposed to be just that, a test of a single "unit" of code. The definition of a "unit" of code can vary from person to person, and situation to situation, but for me it's usually a single method of the class I want to test.
Trying to cram everything into a single unit test makes the test setup harder, and makes it more complicated to actually test things (and also harder to debug after a test fails).
Beyond breaking up tests to be per-method, I like to further break up tests to test specific, unique conditions.
I'll have one or two tests to see what happens on the "happy path", where all required input is present and well-formed.
I'll have several tests to see what happens when a null input is given, or if a record I expect to be able to query doesn't exist, or if someone who doesn't have permission to perform an action tries to perform that action.
These additional tests help to ensure that code is robust (i.e. able to gracefully handle varied and less than ideal conditions).
A nice thing about writing a variety of tests is that if you have unit tests to cover a variety of situations, you can largely stop worrying about getting enough code coverage. If you test your code against enough different situations, you will naturally gain coverage.
Your specific case (finally)
Code that relies on standard Salesforce history objects are hard to test, because we don't have the ability to insert history records explicitly, and inserting/updating normal records (like an Account
) does not cause a history record to be generated when executing a unit test.
In "Is it possible to test apex that relies on field history tracking?", I explain that you can use Test.loadData()
to create otherwise uncreate-able data. The big caveat is that if you try to query the resulting records, you may not get back some (or any) of the data.
To gain the coverage you're looking for, you'll need to call your class's execute(TxnSecurity.Event)
method, and you'll need to set the loginHistoryId
of TxnSecurity.Event.data
.
I think the general flow of your test will look something like
- Call
Test.loadData()
, store the result in a list/map
- Instantiate a
TxnSecurity.Event
object
e.data.put('loginHistoryId',
`)
- Create an instance of the class you're trying to test
- Call the
execute()
method, and pass in your object from step 2
- Assert that the response is true/false (depending on which one of your, at least 2, test methods you are in)
You'll need to experiment with whether the queried results from what you inserted via Test.loadData()
actually contains the field(s) you need. If that doesn't work, then I'd try writing some test-specific code to inject the data you need, something like...
global class BlockIEPolicyCondition implements TxnSecurity.PolicyCondition {
// The private modifier means that nothing else outside of this class can
// see/modify/use the variable
// The @testVisible annotation makes an exception to the above, allowing
// a test class to get/set the value
@testVisible
private LoginHistory testHistory;
public boolean evaluate(TxnSecurity.Event e) {
//<existing code unchanged and omitted>
// We still want to run this query so we get coverage
LoginHistory eObj = [SELECT Browser FROM LoginHistory WHERE Id = :e.data.get('LoginHistoryId')];
// ...but in a test, we want to use our specific record
if(Test.isRunningTest()){
eObj = this.testHistory;
}
//<remaining code is unchanged, and omitted>
}
}
I hope that's enough to put you on the right path.