@HttpPost requires manually setting up RestContext.request and RestContext.response, populating URI parameters or request payloads, and executing positive and negative assertion tests against database state.
In Salesforce, the Apex programming language allows you to create powerful web services exposed to external systems for seamless data integration. A common scenario involves using the @HttpPost annotation within a @RestResource class to accept incoming REST requests. In this post, we'll walk through writing effective test classes for an @HttpPost Apex REST web service, setting up RestContext, and handling positive and negative scenarios to achieve robust code coverage.
Example Scenario
Consider a scenario where we track expenses using a custom object named Expense__c. We need an Apex REST endpoint exposed at /services/apexrest/expenseService that accepts incoming JSON payloads to insert new expense records.
1. The Web Service Class
Below is the implementation of our ExpenseWebService class containing the @HttpPost endpoint:
@RestResource(urlMapping='/expenseService')
global with sharing class ExpenseWebService {
@HttpPost
global static String createExpense(String expenseName, Decimal amount) {
if (String.isBlank(expenseName) || amount == null || amount < 0) {
return 'Error creating expense: Invalid input data';
}
Expense__c newExpense = new Expense__c(Name = expenseName, Amount__c = amount);
insert newExpense;
return 'Expense record created with ID: ' + newExpense.Id;
}
}
2. The Unit Test Class
To unit test an Apex REST service, we instantiate RestRequest and RestResponse objects, attach them to RestContext, and then invoke the static method directly.
- Initialize
RestRequestand setrequestURIandhttpMethod. - Pass parameters via request body (
Blob.valueOf(...)) or direct method call parameters. - Assign request/response instances to
RestContext.requestandRestContext.response. - Perform assertions on database state (
System.assertEquals) and returned response strings.
@isTest
private class TestExpenseWebService {
@isTest
static void testCreateExpenseSuccess() {
// Setup RestContext
RestRequest req = new RestRequest();
RestResponse res = new RestResponse();
req.requestURI = '/services/apexrest/expenseService';
req.httpMethod = 'POST';
req.requestBody = Blob.valueOf('{"expenseName": "Sample Expense", "amount": 100.00}');
RestContext.request = req;
RestContext.response = res;
// Execute service method
Test.startTest();
String result = ExpenseWebService.createExpense('Sample Expense', 100.00);
Test.stopTest();
// Verify result string
System.assert(result.startsWith('Expense record created'), 'Response should indicate successful creation.');
// Verify record insertion in database
List<Expense__c> expenses = [SELECT Id, Name, Amount__c FROM Expense__c WHERE Name = 'Sample Expense'];
System.assertEquals(1, expenses.size(), 'One expense record should be created.');
System.assertEquals(100.00, expenses[0].Amount__c, 'Amount should match the input parameter.');
}
@isTest
static void testCreateExpenseInvalidData() {
// Setup RestContext
RestRequest req = new RestRequest();
RestResponse res = new RestResponse();
req.requestURI = '/services/apexrest/expenseService';
req.httpMethod = 'POST';
req.requestBody = Blob.valueOf('{"expenseName": "", "amount": -50.00}');
RestContext.request = req;
RestContext.response = res;
// Execute service method
Test.startTest();
String result = ExpenseWebService.createExpense('', -50.00);
Test.stopTest();
// Assert failure response
System.assert(result.startsWith('Error creating expense'), 'Service should reject invalid inputs.');
// Assert no records were inserted
List<Expense__c> expenses = [SELECT Id FROM Expense__c];
System.assertEquals(0, expenses.size(), 'No expense records should be created on validation failure.');
}
}
RestContext.request or try to invoke the REST endpoint via Http.send() without a mock, your test will throw a System.NullPointerException or System.CalloutException.
- Rule: Set up
RestContext.requestandRestContext.responsebefore executing custom@RestResourcemethods in test classes. - Gain: Simulates real external inbound API requests isolated from actual web network dependencies.
- Price: Requires manually building mock JSON request payloads (
Blob.valueOf(...)) and request URIs. - Limits: Tests verify method execution and DML operations but do not validate external OAuth authentication or profile permission security policies.
Conclusion
Writing comprehensive test classes for @HttpPost REST endpoints in Salesforce requires setting up the execution context with RestContext. By covering success flows alongside validation failures, you ensure your endpoints remain reliable, resilient, and ready for production deployment.