Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Deployment

System.QueryException: List has no rows for assignment to SObject (in test context)

Your Apex test failed because it expects org data that doesn't exist in the test context. By default, tests run in **isolation** — they can't see existing org data. Either build the data the test needs in `@TestSetup`, or (less ideal) opt-in to `@isTest(SeeAllData=true)`.

Also seen astest class can't see records·no rows in test·Test class data isolation·@isTest SeeAllData

A developer pulls the latest from main, runs the Apex test suite locally, and one method fails with System.QueryException: List has no rows for assignment to SObject. The class itself works fine in production. The query in question selects a configuration record from a custom object, and that record definitely exists. Other tests pass. The developer wonders if their sandbox is broken; the answer is something more fundamental about how Apex tests run.

What the platform is checking

Apex test methods run inside an isolated data context. By default, every SELECT query inside a test method only sees records created by the test method itself or by its @TestSetup method. Real org data is invisible. This isolation is a deliberate design choice, not a bug or a sandbox quirk.

The platform enforces isolation because tests need to be deterministic. A test that depends on a particular record existing in the org would pass in one org and fail in another. A test that mutates real data would corrupt production-like environments. By default, Apex pretends the database is empty for the duration of a test, and the test is responsible for populating whatever data it needs to exercise.

There's an explicit opt-out: the @isTest(SeeAllData=true) annotation. A test marked this way sees real org data and can query existing records. The annotation was common in legacy code from the 2010s; modern Apex testing avoids it because it makes tests environment-dependent.

The query exception you see (List has no rows for assignment) is the natural consequence of running a query that expected a row and got zero rows. The test queried for a config record. The config record exists in the org, but the test can't see it. The query returns an empty list. The assignment to a singleton SObject variable throws.

The broken example

A service class that reads a Default_Settings custom metadata or custom setting record:

public class CommissionCalculator {
    public static Decimal applyRate(Decimal amount, String region) {
        Commission_Config__c config = [
            SELECT Default_Rate__c FROM Commission_Config__c
            WHERE Region__c = :region LIMIT 1
        ];
        return amount * config.Default_Rate__c;
    }
}

In production, a row exists for every region. The query returns one row. The calculation works. The test calls CommissionCalculator.applyRate(1000, 'EMEA'):

@isTest
private class CommissionCalculatorTest {
    @isTest
    static void testEMEARate() {
        Decimal result = CommissionCalculator.applyRate(1000, 'EMEA');
        System.assertEquals(120, result, '12% EMEA rate');
    }
}

The test fails. The query inside the service can't see the EMEA Commission_Config__c row because the test didn't create it. The exception fires.

A second shape: a trigger that reads a Setup-only object, like a custom metadata record that holds business rules. The trigger works because the org has the metadata. The test fires the trigger; the metadata lookup returns zero rows; the trigger throws.

A third shape: code that reads a User record by Profile name, expecting a particular profile to exist. In production, the profile exists. In a fresh scratch org without that profile created, the test fails.

The fix, three paths

Create the data the test needs inside the test method. The straight-forward answer is to insert whatever records the code expects. Modern Apex testing builds its own fixtures.

@isTest
private class CommissionCalculatorTest {
    @isTest
    static void testEMEARate() {
        insert new Commission_Config__c(Region__c='EMEA', Default_Rate__c=0.12);
        Decimal result = CommissionCalculator.applyRate(1000, 'EMEA');
        System.assertEquals(120, result);
    }
}

Use a TestDataFactory class. For projects with many tests that all need similar fixtures, extract the setup into a utility:

@isTest
public class TestDataFactory {
    public static Commission_Config__c createConfig(String region, Decimal rate) {
        Commission_Config__c c = new Commission_Config__c(Region__c=region, Default_Rate__c=rate);
        insert c;
        return c;
    }
}

@isTest
private class CommissionCalculatorTest {
    @isTest
    static void testEMEARate() {
        TestDataFactory.createConfig('EMEA', 0.12);
        System.assertEquals(120, CommissionCalculator.applyRate(1000, 'EMEA'));
    }
}

Refactor the code to accept the config as an argument. Sometimes the right answer is to make the service less stateful. Pass the configuration in, rather than looking it up:

public class CommissionCalculator {
    public static Decimal applyRate(Decimal amount, Commission_Config__c config) {
        return amount * config.Default_Rate__c;
    }
}

The test calls the method with a stub config. The service is now easier to test, easier to mock, and arguably better designed. The trade-off is that every caller in production must now look up and pass the config, which can be a sizable refactor.

The fixed example

A @TestSetup method seeds shared fixtures once per test class:

@isTest
private class CommissionCalculatorTest {
    @TestSetup
    static void makeData() {
        insert new List<Commission_Config__c>{
            new Commission_Config__c(Region__c='EMEA', Default_Rate__c=0.12),
            new Commission_Config__c(Region__c='NA', Default_Rate__c=0.10),
            new Commission_Config__c(Region__c='APAC', Default_Rate__c=0.08)
        };
    }

    @isTest
    static void testEMEARate() {
        System.assertEquals(120, CommissionCalculator.applyRate(1000, 'EMEA'));
    }

    @isTest
    static void testNARate() {
        System.assertEquals(100, CommissionCalculator.applyRate(1000, 'NA'));
    }

    @isTest
    static void testAPACRate() {
        System.assertEquals(80, CommissionCalculator.applyRate(1000, 'APAC'));
    }
}

The @TestSetup method runs once before each test method in the class. The fixtures are visible to every test. Each test method is concise because the setup is shared.

Custom metadata is different

Custom metadata types are visible inside test methods without explicit setup, unlike custom objects or custom settings. A [SELECT ... FROM Region_Config__mdt] query inside a test returns whatever metadata records exist in the org. This is by design; custom metadata is meant to be configuration that ships with the code, so tests need to see it.

If your config is a custom metadata type, the original test would have passed. The fact that it failed implies your config is on a custom object or custom setting, both of which are isolated by default.

If you want config-like records visible across all tests without @TestSetup boilerplate, prefer custom metadata. The trade-off is that custom metadata can't be modified at runtime by user activity, so it's only suitable for configuration that's deployed alongside code.

The @isTest(SeeAllData=true) trap

Legacy code often uses @isTest(SeeAllData=true) to skip the data-isolation problem. The annotation lets tests see real org data, including the config record the production code expects.

The problem: the test now depends on the org's data state. It passes in your sandbox. It fails in production if the production org happens to lack the same data. It fails in a scratch org. It fails in CI. The test is no longer portable.

Modern style says: never use SeeAllData=true. Always create fixtures inside the test. The exception is for objects that genuinely cannot be created in a test context, like User records bound to specific licenses, but those are rare.

If you inherit a codebase with many SeeAllData=true tests, migrate them incrementally. The pattern is: each test that opts into real data is a future flakiness incident.

Test users and profiles

A test method runs as the user who launched it, unless the test explicitly runs as a different user via System.runAs. The running user's profile, permission sets, and roles apply.

If the production code depends on the running user having a specific profile (a System Administrator profile for queries against Setup-only objects, say), the test must run as such a user. Create one:

@isTest
static void testAsAdmin() {
    User admin = [SELECT Id FROM User WHERE Profile.Name = 'System Administrator' AND IsActive = true LIMIT 1];
    System.runAs(admin) {
        // Test code runs with admin profile
    }
}

The query for User WHERE Profile.Name is allowed even inside a test (Users are visible because of the test framework's special handling) but the running user might not match the profile your code expects.

Edge case: triggers on objects the test creates

When a test inserts a fixture, every trigger on that object fires. If the trigger reads config records that the test didn't create, the trigger throws and the insert fails.

The fix: create the config records before the records that trigger the trigger. Order matters in @TestSetup. Insert config records first, then the records under test.

Edge case: chained DML in tests

A test that inserts a parent record, expecting child records to be created by a flow or trigger, must let the chained DML complete. By default the chain runs synchronously in the test context. Future methods and queueables, however, run only after Test.startTest/Test.stopTest boundaries:

@isTest
static void testAsyncProcessing() {
    Test.startTest();
    insert new Lead(LastName='Test', Company='Acme');
    Test.stopTest();
    // Future methods and queueables triggered by the insert have now completed
}

The Test.stopTest call forces any pending async work to complete before assertions. Without it, the assertions run before the async work, and the test sees stale state.

A subtle case: User and Profile creation

Tests that need to run as a specific kind of user often query for an existing User record. The query works in most orgs, but a freshly-spun scratch org might not have a user of the required profile. The test fails with the same exception this article describes, sourced from the User query.

The pattern that survives across orgs is to create the user inside the test:

@isTest
static void testAsStandardUser() {
    Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
    User u = new User(
        ProfileId = p.Id,
        Username = 'test.' + System.currentTimeMillis() + '@example.com.test',
        Email = 'test@example.com',
        LastName = 'Test',
        Alias = 'test',
        TimeZoneSidKey = 'America/Los_Angeles',
        LocaleSidKey = 'en_US',
        EmailEncodingKey = 'UTF-8',
        LanguageLocaleKey = 'en_US'
    );
    insert u;
    System.runAs(u) {
        // Test code as the new user
    }
}

The Profile query still depends on the org having a 'Standard User' profile, which every Salesforce org does. The User itself is freshly created in each test, so the test doesn't depend on User records from real org data.

A subtle case: shared mocks

When multiple test classes need similar mocks (HTTP responses, external API stubs), a shared mock library keeps tests DRY. The library lives in a dedicated class with @isTest annotation so it's compiled but not counted against the production code base:

@isTest
public class MockResponses {
    public static HttpResponse paymentSuccess() {
        HttpResponse res = new HttpResponse();
        res.setStatusCode(200);
        res.setBody('{"status":"ok","transactionId":"tx_123"}');
        return res;
    }

    public static HttpResponse paymentDeclined() {
        HttpResponse res = new HttpResponse();
        res.setStatusCode(400);
        res.setBody('{"status":"declined","reason":"insufficient_funds"}');
        return res;
    }
}

Each test uses the mock without rebuilding the response shape. When the API contract changes, you update the mock library once and every dependent test reflects the new shape.

Defensive habits

Build a TestDataFactory class early in any project. Every test that needs fixtures uses the factory. The factory evolves to handle new objects, new trigger requirements, and new configuration shapes. Every test inherits the improvements.

Avoid @TestVisible shortcuts to read production data. If a test needs to inspect a private field for assertion, refactor to expose what's needed through a public method or return value. Tests that reach into implementation details get fragile when the implementation changes.

Treat SeeAllData=true as deprecated. If you find a test with the annotation, the next change should remove the annotation and create explicit fixtures. Treating the annotation as a smell catches the easy wins; treating it as forbidden catches the hard ones too.

Run the full test suite in a fresh scratch org weekly. The scratch org has no pre-existing data, so any test that secretly relies on org state fails. Catching the failure in scratch is much cheaper than catching it in production. A scheduled CI job that spins a scratch org, runs the suite, and tears down the org costs almost nothing and provides a constant guard against drift.

Quick recovery checklist

When a test fails with this exception:

  1. Read the query that failed; identify the object and the filter.
  2. Add fixtures for that object in @TestSetup or directly in the test method.
  3. Re-run the test.
  4. If the test still fails, check whether the code reads other data the test hasn't seeded.
  5. If the test passes in some orgs and fails in others, the test is environment-dependent. Search for SeeAllData=true or implicit data assumptions.

Most failures resolve in minutes. The longer ones reveal a codebase where production logic and configuration are tightly coupled, and those are worth refactoring for testability.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Deployment errors