If you need reliable email assertions in PHP tests, treat email as an external system with deterministic checks.

This guide shows a practical pattern:

  1. create test inboxes with the API
  2. send an email between those inboxes
  3. wait for delivery
  4. 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

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

Next guides