ApexSensei

Blog

Write Apex Tests That Actually Fail Before You Deploy

By Firus Hanov · · 8 min read

Coverage is not a test. Write Apex assertions that go red on purpose, isolate data, and read the deploy_test drills on ApexSensei. Check and Run on tests is part of Apex Path.

Write Apex Tests That Actually Fail Before You Deploy

Salesforce will not let you deploy Apex to production without 75% coverage. That rule trains a bad habit: writing tests that execute lines and never check a result.

A test that cannot fail is not a test. It is a coverage script. This article is about assertions, isolated data, bulk, negatives, and the deploy_test drills on ApexSensei that refuse a green bar you did not earn.

You can read the drills. Check and Run on Prove it with tests is part of Apex Path. You need a Developer org. The runner deploys your class and calls Salesforce runTests.

Coverage is a side effect

Coverage answers "did this line run?" An assertion answers "did the org do the right thing?" You can cover a method by calling it and discarding the return value:

@IsTest
static void coversTheMethod() {
    String label = Priority.label(80);
    // no assertion
}

That test is green if label throws nothing. Change the method to always return 'Standard'. The test stays green. Production routing is now wrong. Coverage may still clear 75%.

The smallest honest test names the expected value:

@IsTest
static void urgentScoreIsUrgent() {
    System.assertEquals('Urgent', Priority.label(80), 'score 80 should be Urgent');
}

Now a regression fails the deploy. That is the only useful outcome of a unit test.

Write the assertion first. Watch it fail. Then make the service tell the truth.

Fail first, on purpose

Before you implement the service, deploy a test that you believe is wrong for the current code. Salesforce should return System.AssertException. If it does not, the test is not hooked up: wrong class, wrong method, or an assertion that cannot fire.

ApexSensei's first tests drill, Assert service outcomes, is that muscle. You complete a verifier that calls a provided priority service and assertEquals the label. Calling the service without asserting does not pass. Returning a hardcoded 'Urgent' fails the other examples.

The signature of a useful assertion is boring:

System.assertEquals(expected, actual, 'why this matters');

Put expected first. Put a message third. When the test fails in a Metadata deploy, the message is what you read in the test result XML. "Assertion Failed" with no comment wastes the next ten minutes.

System.assert(actual == expected) works and is worse. Equality assertions print both values. Boolean asserts print "expected true, got false."

Tests own their data

Default Apex tests cannot see your org's Accounts. That is a gift. SeeAllData=true throws the gift away so a test can pass on a sandbox that happens to have a row named "Test Account" and fail on a scratch org that does not.

Insert what you need. Tag it. Query it back by Id.

@IsTest
static void insertsOneTaggedAccount() {
    Account row = new Account(Name = 'AS-T-' + Crypto.getRandomLong());
    insert row;

    Account roundTrip = [
        SELECT Name FROM Account WHERE Id = :row.Id
    ];
    System.assertEquals(row.Name, roundTrip.Name);
}

Create isolated test data is this habit as a drill. Build a test data factory is the next step: one helper so every test does not copy-paste Account construction.

A factory that always inserts the same Name will collide when two tests run in parallel. Include something unique, or let Salesforce generate Ids and query by those Ids. Do not query LIMIT 1 on Account and call it setup.

@TestSetup runs once per class and shares the rows with every test method via query. It does not share in-memory statics the way people hope. Share setup and run as another user covers that plus System.runAs, which is how you prove sharing and CRUD, not how you skip creating data.

Governor math belongs in Test.startTest

Setup DML counts toward limits unless you bracket the code under test:

@IsTest
static void bulkUpdateStaysInLimits() {
    List<Account> rows = new List<Account>();
    for (Integer i = 0; i < 200; i++) {
        rows.add(new Account(Name = 'AS-B-' + i));
    }
    insert rows;

    Test.startTest();
    AccountNameService.normalize(rows);
    Test.stopTest();

    System.assertEquals(1, Limits.getQueries(), 'service should query once');
}

startTest / stopTest reset governor counters and force async work to run. If you assert query counts without that bracket, you are measuring setup, not the service. Reset limits around the act exists because this is the bug that sneaks into otherwise careful classes.

Two hundred records is the minimum bulk proof for a trigger-shaped service. One record passing is not bulk. Prove bulk behavior is the drill. If your handler queries inside a loop, 200 rows will blow 100 SOQL in a way 1 row never will. That is the test doing its job.

Green paths are not enough

A service with a try/catch that swallows DmlException and returns null can be fully covered by success data. Add the case that should throw:

@IsTest
static void blankNameIsRejected() {
    Boolean threw = false;
    try {
        AccountGuard.validate(new Account(Name = ''));
    } catch (AccountGuard.ValidationException e) {
        threw = true;
        System.assert(e.getMessage().contains('Name'));
    }
    System.assertEquals(true, threw, 'blank Name should throw');
}

Test success and exceptions pairs the happy path with the throw. Check partial DML results is the sibling: Database.insert(rows, false) can succeed for row 0 and fail for row 1 in one call. Assert both SaveResult values. A test that only checks results.size() will miss a silent partial failure.

Callouts and other people's code

Apex cannot make HTTP callouts in a test without a mock. If you skip the mock, the test fails at runtime with a callout-not-supported error, or worse, you start using Test.isRunningTest() to skip the callout and then you are not testing the parser.

@IsTest
static void parsesActiveStatus() {
    Test.setMock(HttpCalloutMock.class, new StaticJsonMock(
        200, '{"status":"ACTIVE"}'
    ));
    Test.startTest();
    String status = BillingClient.currentStatus('001xx');
    Test.stopTest();
    System.assertEquals('ACTIVE', status);
}

The mock is part of the test's oracle. Put the JSON you care about in the mock, not in a comment. Mock an HTTP callout is the drill. Use a fake collaborator is the same idea for Apex types you own: do not hit a real selector if the unit under test is a mapper.

Test.isRunningTest() in production code is a smell. Prefer dependency injection or HttpCalloutMock. If a branch exists only to keep tests green, the test is steering the product, not checking it.

How ApexSensei grades this

Prove it with tests is not anonymous Apex. The runner builds a Metadata zip, deploys your class plus any fixture classes, and asks Salesforce to run the named test class. You get compile errors, test failures, and coverage from the platform, not from a regex on System.debug.

That is why "it printed OK" does not pass. The assertion has to fail when the fixture is wrong. If you hardcode the expected string in the method under test so the assert stays green, later drills that vary inputs will still fail.

Anonymous Apex is the right tool for a first System.debug. It is the wrong tool for a test class. See Your First Salesforce Developer Org if you are still in Developer Console. Come back here when you can deploy a class.

After tests, the path continues into triggers, async, and Gate a release with test evidence. Production still requires 75% coverage. The drills are there so you do not confuse that number with correctness.

A checklist before you trust a green bar

  • Does at least one assertion name a business outcome, not "no exception"?
  • Can you break the production method in one obvious way and watch this test go red?
  • Did the test insert its own rows, or did it search the org?
  • Did you run 200 records through any DML-in-a-loop suspect?
  • Did you assert the exception or the failed SaveResult, not only the success?
  • Are callouts mocked with the payload you actually parse?

If the answer to "can this fail?" is no, delete the test and write a smaller one that can. A short class with three hard asserts beats a 400-line coverage wrapper.

What a useless test looks like in review

You will see this class, or a cousin of it, in a lot of orgs:

@IsTest
static void testTrigger() {
    Account acc = new Account(Name = 'x');
    insert acc;
    update acc;
    delete acc;
}

Every trigger line may run. Nothing is asserted. A handler that emails the wrong user, stamps the wrong status, or queries in a loop still deploys. Coverage is a vanity metric here. The test is a ritual to satisfy the 75% gate.

Replace it with one behavior:

@IsTest
static void insertStampsDescription() {
    Account acc = new Account(Name = 'AS-T-stamp');
    insert acc;
    acc = [SELECT Description FROM Account WHERE Id = :acc.Id];
    System.assertEquals('Reviewed', acc.Description);
}

If Description is wrong, the test is red. That is the whole design. Add bulk, negatives, and runAs only after this one assertion is honest.

Private methods do not need their own tests. Test the public entry the trigger or service actually calls. If you cannot reach a branch without SeeAllData or a 12-step UI setup, the code is hard to test — fix the code, do not weaken the test.

Practice with the same runner as a deploy

Then practice it where the runner is the same as a deploy: Assert service outcomes. Every drill is free to read, and running the test drills is part of Apex Path. The org you connected for anonymous Apex is the same org that runs these tests.

When the failures you care about are query-shaped instead of assertion-shaped, read SOQL Habits That Bite in Production next. Tests catch those only if you write bulk data and count queries. Most orgs never do. That is why the habit has to be drilled.

Practice Apex free on ApexSensei