If you need reliable email assertions in PHP tests, treat email as an external system with deterministic checks.
This guide shows a practical pattern:
- create test inboxes with the API
- send an email between those inboxes
- wait for delivery
- assert subject, body, sender, recipient, and extracted tokens
Why API-based inbox testing instead of local SMTP only
Local SMTP traps are useful for basic development, but API-driven inboxes are usually better for CI and integration tests because you can:
- create isolated inboxes per test run
- wait for specific messages instead of sleeping
- parse and assert dynamic content (codes, links, IDs)
- run the same flow in local, CI, and shared environments
Prerequisites
- PHP 8+
- Composer
- PHPUnit
- a MailSlurp account and API key from app.mailslurp.com
Set your API key as an environment variable:
export MAILSLURP_API_KEY="your-api-key"
Install dependencies
composer require --dev mailslurp/mailslurp-client-php phpunit/phpunit
End-to-end PHPUnit example
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PHPUnit\Framework\TestCase;
final class EmailFlowTest extends TestCase
{
private function config(): MailSlurp\Configuration
{
$apiKey = getenv('MAILSLURP_API_KEY');
if (!$apiKey) {
throw new RuntimeException('MAILSLURP_API_KEY is not set');
}
return MailSlurp\Configuration::getDefaultConfiguration()
->setApiKey('x-api-key', $apiKey);
}
public function test_can_send_and_receive_email_between_two_inboxes(): void
{
$inboxApi = new MailSlurp\Apis\InboxControllerApi(null, $this->config());
$waitApi = new MailSlurp\Apis\WaitForControllerApi(null, $this->config());
$sender = $inboxApi->createInbox();
$recipient = $inboxApi->createInbox();
$sendOptions = new MailSlurp\Models\SendEmailOptions();
$sendOptions->setTo([$recipient->getEmailAddress()]);
$sendOptions->setSubject('Verify your account');
$sendOptions->setBody('Your verification code is ABC-123');
$inboxApi->sendEmail($sender->getId(), $sendOptions);
$email = $waitApi->waitForLatestEmail(
$recipient->getId(),
30000, // timeout ms
true // unreadOnly
);
$this->assertEquals($sender->getEmailAddress(), $email->getFrom());
$this->assertEquals($recipient->getEmailAddress(), $email->getTo()[0]);
$this->assertEquals('Verify your account', $email->getSubject());
$this->assertStringContainsString('verification code', $email->getBody());
preg_match('/code is ([A-Z]{3}-[0-9]{3})/', $email->getBody(), $matches);
$this->assertEquals('ABC-123', $matches[1]);
}
}
Pattern for Laravel projects
For Laravel, keep transport and assertions separate:
- keep your app sending through your normal mail channel
- use MailSlurp in tests to observe what users would actually receive
- assert business-critical payloads (links, codes, locale strings, legal footer)
This avoids test-only mail internals leaking into production code.
Hardening checklist for CI
- create fresh inboxes per test to avoid cross-test contamination
- use explicit wait conditions instead of
sleep()calls - keep message assertions focused on business outcomes
- mask secrets and rotate API keys regularly



