Every Salesforce developer learns to write trigger test classes. Most write a test that creates one or two records, fires the trigger, and asserts on the result. The code coverage goes green, the deployment passes, and the trigger goes to production. Then a data migration runs, 10,000 records are loaded, and the trigger throws a LimitException on SOQL queries at record 101. The test was passing. The trigger was not bulk-safe. These two things are not contradictions — they are the expected result of testing with insufficient data volume.
The 200-record rule is the minimum meaningful bulk test for any Salesforce trigger. This article explains why 200 is the number, what commonly breaks when you actually test against it, and how SproutEzee removes the friction that causes developers to skip it.
Why Salesforce Processes Triggers in Batches of 200
Salesforce's execution model fires triggers once per DML operation, with all affected records in the Trigger.new and Trigger.old collections. When a user updates records through the UI, Salesforce processes them in batches of up to 200 per transaction. A data loader operation on 2,000 records fires the trigger 10 times — once per 200-record batch — each time with a fresh governor limit context.
This means 200 is not an arbitrary test size. It is the maximum number of records that will exist in Trigger.new during a single trigger execution in any production bulk operation. Writing a test with 200 records in a single DML call replicates exactly what happens when bulk data lands in your org.
A test with one record fires the trigger with one element in Trigger.new. Logic that loops over Trigger.new runs once. SOQL queries inside that loop execute once. DML statements inside that loop fire once. At 200 records, everything runs 200 times — and the difference between 1 iteration and 200 iterations is exactly where most trigger governor limit issues live.
What Actually Breaks at 200 Records
SOQL Inside Loops
The most common bulk trigger failure. A SOQL query inside a for loop fires once per iteration. At 200 records, that is 200 SOQL queries — double the governor limit of 100. The fix is to collect IDs first and query once before the loop:
// Wrong — fires once per record
for (Account a : Trigger.new) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id];
}
// Right — query once outside the loop
Set<Id> accountIds = new Map<Id, Account>(Trigger.new).keySet();
Map<Id, List<Contact>> contactMap = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactMap.containsKey(c.AccountId)) contactMap.put(c.AccountId, new List<Contact>());
contactMap.get(c.AccountId).add(c);
}
A test with one record will not catch the loop-SOQL pattern. A test with 200 will throw an exception immediately, which is exactly the feedback you need before deploying to production.
DML Inside Loops
The same pattern with DML. An update or insert inside a for loop fires once per record iteration. At 200 records, that is 200 DML statements — above the 150-statement governor limit. The fix is to collect records into a list and perform a single DML outside the loop:
// Wrong
for (Opportunity opp : Trigger.new) {
Task t = new Task(WhatId = opp.Id, Subject = 'Follow Up');
insert t; // fires once per record
}
// Right
List<Task> tasksToInsert = new List<Task>();
for (Opportunity opp : Trigger.new) {
tasksToInsert.add(new Task(WhatId = opp.Id, Subject = 'Follow Up'));
}
if (!tasksToInsert.isEmpty()) insert tasksToInsert;
Heap and CPU Accumulation
String concatenation, large map building, and complex per-record logic that looks harmless at one record can hit heap (6MB) or CPU time (10,000ms) limits at 200 records. These are harder to catch in code review than loop-SOQL but surface immediately in a proper bulk test.
Cascade Effects
When a trigger fires on 200 records, any automation those records trigger — flows, process builders, other triggers — also fires at bulk scale. A flow that sends an email or updates a related record fires once per record in scope, within the same governor limit context as your trigger. Bulk testing with realistic data reveals cascade limit issues that single-record tests miss entirely.
Writing the Bulk Trigger Test
The pattern is straightforward: create 200 records in a loop, insert them in a single DML call, then assert:
@isTest
static void testBulkAccountInsert() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
accounts.add(new Account(Name = 'Bulk Test Account ' + i, Industry = 'Technology'));
}
Test.startTest();
insert accounts;
Test.stopTest();
List<Account> inserted = [SELECT Id, Name FROM Account WHERE Name LIKE 'Bulk Test Account%'];
System.assertEquals(200, inserted.size(), 'Expected 200 accounts inserted');
// Add trigger-specific assertions here
}
The single insert call fires the trigger with all 200 records in Trigger.new simultaneously. This is the test. If the trigger has a SOQL in a loop, this test will fail. If it has DML in a loop, this test will fail. The failure in the test environment is the correct outcome — it means the trigger will also fail in production, and you have caught it before deployment.
Where SproutEzee Fits In
The most common reason developers skip the 200-record bulk test is test data setup friction. Creating 200 records with realistic field values — required lookups, valid picklist combinations, parent-child relationships, custom field constraints — takes meaningful setup time. Most developers use minimal data ('Test Account 1') because it is fast. The problem is that minimal data often masks bugs that realistic data would expose.
SproutEzee generates realistic, bulk-volume test data sets for Salesforce orgs, including: correctly populated required fields, valid picklist values for your specific org configuration, parent records for lookup relationships, and field-specific constraints that reflect your actual data model.
Test your triggers at production scale before deployment
SproutEzee generates 200-record bulk test data sets with realistic field values, valid relationship structures, and org-specific constraints — so your bulk tests reflect real-world data, not minimal test stubs.
See SproutEzee →The Coverage Trap
Salesforce requires 75% test coverage before deployment. This creates an incentive to write tests that maximise coverage rather than tests that verify correctness. A test with one record can achieve 80% coverage on a trigger. That test says nothing about whether the trigger handles 200 records safely.
Coverage is a floor, not a quality measure. A trigger with 80% coverage and no bulk test is not a tested trigger — it is a trigger with line coverage. The questions that coverage does not answer are: Does this trigger handle the maximum batch size without hitting governor limits? Does it handle realistic data shapes without unexpected null pointer exceptions? Does it interact correctly with other automation at scale?
These questions only have answers when you test with 200 records of realistic data. Coverage tells you which lines ran. A bulk test tells you whether the trigger is deployable.
The Standard This Should Be
Every trigger in Salesforce should have at minimum two tests: a single-record test (for the basic correctness assertion) and a 200-record bulk test (for governor limit safety). Neither replaces the other. The single-record test is fast and readable. The 200-record test is the production-safety check.
Teams that maintain this standard catch bulk trigger issues in development, not in production during a data migration at 11pm. The governor limit exception that surfaces in a test method is a two-minute fix. The same exception surfacing in a production data load is a rollback, a post-mortem, and an explanation to a stakeholder about why 50,000 records are now in a broken state.
The 200-record rule is not an advanced practice. It is the minimum viable trigger test for any Salesforce environment that handles real data volumes — which is all of them.