Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Interview prep

Apex testing interview questions and answers

Test data, mocks, coverage that means something, and assertions.

12 questions · 4 of them scenarios

Junior round

Junior Testing questions

Testing junior

Where does a test get its data, and when would you use SeeAllData?

Testing How firmly you rule out the shortcut. This is where a suite starts depending on one org.

junior

The test creates it. By default a test cannot see the org's records, which is deliberate — it means the suite behaves the same in a sandbox, in production and in a scratch org. @isTest(SeeAllData=true) turns that off and lets the test read real data, and I avoid it, because a test that depends on a record somebody else can delete is a test that will fail for reasons unrelated to the code.

mid

I create it, ideally in a @TestSetup method so it is built once for the class and rolled back between test methods. SeeAllData=true is a last resort and the cases are narrow: something the test genuinely cannot insert, which in practice means a handful of setup objects and occasionally a pricebook. Even then I would rather find the supported way in — the standard pricebook has Test.getStandardPricebookId() precisely so nobody needs SeeAllData for it. The thing worth knowing is that isolation is not total anyway: User, Profile, RecordType and some setup objects are visible to tests regardless, so a test can still depend on org configuration without ever asking for it.

Testing junior scenario

A test passes in the sandbox and fails during the production deployment. Where do you start?

Testing Where you look first when the same code behaves differently in two orgs.

What they tell you

A test class passes every time it is run in the developer sandbox. The production deployment fails on it with System.DmlException: Insert failed. FIELD_CUSTOM_VALIDATION_EXCEPTION, naming a validation rule the developer had not heard of.

junior

The sandbox and production have drifted, and the validation rule is the proof — the test is building a record that production considers invalid. So the fix is in the test: set the fields the rule requires so the record is valid everywhere. I would also check whether the sandbox is missing the rule entirely, because if it is, every other test in that sandbox is passing against weaker rules than production has.

mid

A test that only passes in one org is telling me the test depends on that org's configuration. Here the specific cause is a validation rule missing or inactive in the sandbox, which surfaces as FIELD_CUSTOM_VALIDATION_EXCEPTION only where the rule exists, and the fix is to make the test's data genuinely valid rather than accidentally accepted. The broader question is how the two orgs diverged, because one rule is rarely alone — a sandbox refreshed a long time ago will differ in dozens of ways, and every test in it is a weaker check than it appears. So the immediate fix is the test data, and the thing I would raise is a refresh schedule, since deployment day is an expensive place to discover configuration drift.

Ask before you answer
  • Does that validation rule exist in the sandbox, and is it active there?
  • Is the test building its own records, or relying on something that happens to exist in the sandbox?
Do not say this

Deactivate the validation rule for the deployment and switch it back on afterwards.

It gets the release out and it means production briefly has no validation on that field, during a window when a data load could run. It also leaves the test still constructing invalid records, so the next developer meets the same failure with less context than you had.

Testing junior

What makes an Apex test worth having?

Testing Whether coverage is the goal in your head or a side effect of doing the job.

junior

An assertion that would fail if the code were wrong. Coverage only says a line ran, so a test that calls a method and asserts nothing gives me the same percentage as a real test and none of the protection. I set up the data, run the thing, and then assert the specific outcome I care about — the field value, the record count, the error message.

mid

The test has to be able to fail. That sounds obvious and it is the single most common defect in an inherited test suite: a method that inserts a record, calls the handler and ends, with 90% coverage and no Assert statement anywhere. My habit is to break the code deliberately and check the test goes red — if it does not, the test was decoration. Beyond that I want the negative cases, because those are where the value is: the validation that should reject, the limit that should be hit, the user who should not have access. A suite full of happy paths passes forever and catches nothing.

Deep dive on this
Mid-level round

Mid-level Testing questions

Testing mid

Why test with 200 records rather than one?

Testing The class of bug you are hunting. One record proves the logic; 200 proves the shape.

mid

Because the bugs that matter in Apex are bulk bugs, and one record cannot see them. A query inside a loop passes with one record and fails at 101; a handler that reads Trigger.new[0] passes with one record and silently ignores the other 199. Inserting 200 in the test is the cheapest way to catch both, and 200 specifically because that is the trigger batch size the platform uses, so it is the size real automation will meet.

senior

Two hundred is the number that matches a trigger's batch, and it is a floor rather than a target. What it proves is that nothing in the path is per-record: the queries and DML statements stay flat as the list grows, which I can assert directly with Limits.getQueries() before and after rather than inferring it from the test passing. That assertion is the one I would add to an inherited suite first, because it fails on the exact regression everyone reintroduces. Above 200 gets more interesting for a different reason — a data load arrives in batches, so 400 records means two trigger invocations in one transaction sharing one limit budget, and a static guard written for a single batch behaves differently there. If the code has statics or does partial DML, I would test at 400 as well and assert the second batch was not skipped.

Testing mid

How do you test code that makes a callout?

Testing Which mocking approach you use, and whether Test.isRunningTest() appears anywhere in your answer.

mid

A mock, because a test cannot make a real callout — it throws System.CalloutException: You have uncommitted work pending or refuses outright. I implement HttpCalloutMock, return a canned HttpResponse with the status and body I want, and register it with Test.setMock(HttpCalloutMock.class, new MyMock()). Then I write more than one mock: the 200 case, a 500, and a malformed body, because the error handling is the part most likely to be wrong.

senior

The mechanism is straightforward and the design question underneath it is the interesting one. Test.setMock works, and it only intercepts at the Http.send boundary, so anything my code does with the response — parsing, retry decisions, mapping to records — is only reachable through a full callout test. I would rather that logic sat in its own class taking a response object, so the parsing and the retry rules can be tested directly with no mocking at all, and the callout test becomes a thin check that the request is shaped right. What I actively avoid is Test.isRunningTest() branching in production code: it means the tested path and the real path are different code, which is the one thing a test is supposed to rule out. I have seen a callout wrapped in that check pass every test and fail on first contact with the real endpoint, because nothing had ever executed the real branch.

Testing mid scenario

A test passes on its own and fails in the full run. What is going on?

Testing What you suspect when a failure depends on what else ran.

What they tell you

One test method asserts a Contact count of 3. Run alone it passes. In the full suite of 400 tests it intermittently fails, reporting 4 or 5, and which run fails changes.

mid

A count assertion that depends on what else ran means the query is not scoped to the test's own data. Tests are isolated from org data by default, but they are not isolated from each other when they run in parallel, so a query for all Contacts can pick up rows another test created in its own transaction. The fix is to make the assertion specific: filter on something the test set, like a LastName prefix or an external id, and assert on that. Counting everything is the actual defect.

senior

An intermittent failure that depends on the run is almost always shared state, and in Apex tests the shared state is usually a query with no filter or an ordering assumption. Both are worth checking. The count is the obvious one, and the fix is to scope the query to data this test created rather than to the object. The subtler variant is a test that asserts results[0] without an ORDER BY — that is not guaranteed to be stable even for the same data, so it fails on a different day rather than a different run. Once it is fixed I would look for the pattern rather than the instance, since a suite that grew this habit has it in many places, and a grep for count assertions and unordered indexing finds them faster than waiting for each to fail. If it turns out the class uses SeeAllData=true, that is the root cause and the fix is removing it, not scoping the query.

Ask before you answer
  • Is the assertion counting all Contacts, or only the ones the test created?
  • Does the class use SeeAllData=true, or query without a filter that scopes it to its own data?
Do not say this

Add a retry, or move the test into its own class so it stops colliding.

Isolating it makes the failure go away and leaves the assertion counting records it does not own. The test will pass forever and prove nothing about the code, and the same pattern is probably in a dozen other classes.

Testing mid

How do you test that a user without permission cannot do something?

Testing That System.runAs covers less than most people assume.

mid

System.runAs(someUser) around the block, with a user I created in the test carrying the profile and permission sets I want to exercise. That gives me the record sharing of that user, so I can assert they see nothing they should not. The part to be careful about is what it does not cover: runAs enforces record sharing, not object and field permissions, so a test that passes inside runAs is not evidence that field level security is being respected.

senior

runAs is the right tool for sharing and the wrong tool for the thing people usually mean by permissions, and being precise about that gap is the answer. For field and object access I test the enforcement directly: run the query with WITH USER_MODE or the DML with AccessLevel.USER_MODE inside the runAs block, and assert it throws or returns nothing for the restricted user. That is testing the mechanism the production code relies on rather than a proxy for it. Two practical notes. Creating the test user is where these tests go wrong — a user inserted in the same transaction as other setup data hits the mixed DML restriction, so the user creation goes in its own System.runAs block or in @TestSetup. And the assertion has to be that the operation failed, not that it returned fewer rows, because a query returning nothing is indistinguishable from a query filtering nothing when the data setup is wrong.

Deep dive on this
Testing mid

How do you test a Batch class or a Queueable?

Testing What Test.stopTest() does for you, and whether you know what it does not.

mid

Enqueue or execute the job between Test.startTest() and Test.stopTest(). The stopTest call is what makes the async work run synchronously before the next line, so I can assert on the results immediately afterwards. It also resets the governor limits at startTest, which means the data setup I did before it does not eat the budget the tested code needs. Without that pair the job is queued and nothing has happened by the time the assertions run.

senior

The pair does two things people conflate: it gives the tested code a fresh set of limits, and it forces the queued async work to complete at stopTest. What it does not do is run a chain. A Queueable that enqueues another Queueable will not run the child in a test — the platform stops after the first, which is deliberate and means chaining logic is untested unless I restructure to call the next job's execute directly with a constructed context. Batch has its own version of the same gap: only one execute batch runs unless I keep the scope under the batch size, so a test with 250 records and a scope of 200 will silently exercise one batch and the stateful behaviour across batches goes unverified. So for anything stateful I test the pieces directly and use the async path to prove the wiring, rather than expecting one test to cover both.

Deep dive on this
Senior round

Senior Testing questions

Testing senior scenario

An org has 78% coverage and almost no assertions. Where do you start?

Testing How you get value early from a job too big to finish, rather than proposing the full version again.

What they tell you

Around 300 Apex classes, org-wide coverage 78%, and a sampling of the test classes shows methods that insert records, call the handler and end. Nobody has confidence in the suite, and a rewrite has been proposed twice and never started.

mid

I would not start with coverage at all. The first useful thing is knowing which classes matter: the ones handling money, the ones an integration calls, the ones users touch daily. Those get real tests with assertions, written as we change them rather than in a separate project. The second useful thing is finding the dead code, because a meaningful share of 300 classes will be unreferenced, and deleting one is faster and safer than testing it.

senior

A rewrite has been proposed twice and not started, which tells me the framing is wrong rather than the intent. So I would make it incremental and tie it to work that is happening anyway: any class we touch gets its tests made real before the change lands, which spreads the cost across delivery and never needs its own budget. Alongside that, two cheap things with disproportionate value. Adding a pipeline check for test methods with no assertions makes the problem visible and stops it growing, and it is a few lines. And measuring coverage per class rather than org-wide reveals what the 78% is hiding — almost certainly a payment or integration class near zero while a formatter carries the average. Then I would pick the three highest-consequence classes and write proper tests for those specifically, as a demonstration that this is achievable, because the reason a rewrite stalls twice is that nobody has seen the first slice land. What I would say plainly to whoever is asking is that 78% was never a measure of safety, and the number may well go down as dead code is deleted and honest tests replace decorative ones. That needs saying before it happens rather than after.

Ask before you answer
  • Which of those classes are actually invoked in production, and which are dead?
  • Is there a change coming that needs confidence, or is this a standing improvement with no deadline?
Do not say this

Set a policy that all new code needs 90% coverage with assertions, and leave the existing suite alone.

It sounds like progress and it changes nothing about the 300 classes, which is where the risk is. It also makes the coverage number rise, which reduces the pressure to do the real work — the metric improves while the exposure stays exactly where it was.

Testing senior scenario

Your release is blocked by a failing test in code you did not touch. What do you do?

Testing Whether the answer is only about tonight, or also about why one team's test can hold another's release.

What they tell you

A production deployment fails on a test in a class owned by another team. It has been failing for six days. Your change is unrelated, the release window closes tomorrow, and the other team's lead is on leave.

mid

First I would find out what the test is actually telling me, because six days of red means either a real defect nobody is looking at or a test that depends on something that changed. Reading the failure usually settles it in minutes: a FIELD_CUSTOM_VALIDATION_EXCEPTION or INSUFFICIENT_ACCESS points at drift, and a wrong assertion value points at a code change. If it is a genuine defect in their code, the release should not go out on top of it and that is a conversation with whoever is covering. If it is a broken test, fixing the test in their class is a small, reviewable change and better than routing around it.

senior

The immediate decision depends on what the failure means, so I would not reach for a deployment option before reading it. An INSUFFICIENT_ACCESS or validation error suggests configuration drift and the test is fixable in place; a failed assertion on a value suggests their code changed and the test is correctly reporting it, which is a release blocker for a real reason. Either way I would rather fix it than exclude it — a one-line change in their test class, raised as a pull request to them, is honest and reversible, and it keeps the signal alive. Excluding the class is the option I would take only with their team's agreement and a written follow-up, because it disables the one check that was working. The thing I would put more weight on afterwards is why this was possible: production deployments run the whole org's tests, so any team can block any other, and six days without anyone owning it is the actual finding. A failing test needs an owner within a day, and if the org is large enough for this to recur then unlocked packages or a shared on-call rota for the pipeline is the structural answer rather than better manners.

Ask before you answer
  • Is the test failing because of a real defect, or because of data or configuration drift in production?
  • Has anything been deployed in those six days, or has the release train been stopped the whole time?
Do not say this

Deploy with a specified test level that excludes the failing class, and raise a ticket.

That gets tonight's release out and asserts that the failing test does not matter, which nobody has established. If the test is failing because the other team's code is genuinely broken in production, skipping it ships your change on top of a live defect and removes the only signal anyone had.

Testing senior

How would you set a testing strategy for an org with 900 Apex classes?

Testing Where you spend a fixed budget, given you cannot test everything well.

mid

I would stop treating all code as equal. The classes that carry money, entitlement or an integration contract get real tests with negative cases; the rest get enough to deploy. Then I would fix the suite's honesty — find the test methods with no assertions, because those are the coverage that is not protection, and they are usually concentrated in a few old classes. Beyond that, the run time matters: a suite taking 90 minutes gets skipped, and a skipped suite is worth nothing.

senior

The strategy is mostly about where confidence is actually needed, because 900 classes will never all be well tested and pretending otherwise produces a uniform layer of weak tests. So I would rank by consequence — what costs money if it breaks, what a customer sees, what an integration depends on — and put the real effort there, including the negative and permission cases that most suites skip entirely. Two structural things matter as much as the ranking. First, the suite has to be fast enough to run on every change, because a test that runs quarterly is documentation; shared @TestSetup, no SeeAllData, and pushing logic out of triggers into classes that can be tested without DML all buy that. Second, I would put a check in the pipeline for assertion-free test methods and for coverage measured per class rather than org-wide, since the 75% org number lets a critical class sit at zero while a utility class carries the average. And I would be honest in the plan that some of those 900 classes are dead — finding and deleting them is the cheapest coverage work available.

Deep dive on this
Testing senior

What makes an Apex test flaky, and how do you deal with one?

Testing Whether a flaky test is a defect to you or an inconvenience to be retried.

mid

Usually a dependency on something the test does not control. Time is the common one — a test asserting on System.today() behaves differently on the last day of a month, and one asserting on System.now() can straddle a boundary. After that: queries with no filter picking up other tests' data, assertions on results[0] with no ORDER BY, and unique field collisions when two parallel tests generate the same value. My response is to fix the cause, because a retried test is a test nobody trusts.

senior

I treat a flaky test as a genuine defect, on the grounds that it is either testing something non-deterministic or the code under test is non-deterministic — and the second possibility is the one worth ruling out before touching the test. Dates are where I look first, and the fix is to inject the date rather than call System.today() in the middle of the logic, which makes the code better as well as the test. Ordering and unrelated-data assertions are the next layer, and both are fixed by being specific about what the test owns. Uniqueness collisions in parallel runs need a value derived from something per-transaction rather than a hardcoded string. The one I would push back hardest on is disabling parallel test execution to make a suite stable: it works, it hides every instance of this class of bug at once, and the suite gets slower forever. If a team has already done that, turning it back on is a project rather than a switch, which is a reason to fix these as they appear rather than in a batch later.