All posts
    Tutorials

    How to Test Email in PHP with PHPUnit

    The Devmailr Team6 min read

    PHP apps send plenty of email — Laravel, Symfony, or plain PHP. To test that a verification or reset email really goes out, your PHPUnit suite can read it from a disposable inbox. Here’s a small Guzzle-based client and a test.

    A small client (Guzzle)

    tests/Devmailr.php
    <?php
    use GuzzleHttp\Client;
    
    class Devmailr
    {
        private Client $http;
    
        public function __construct()
        {
            $this->http = new Client([
                'base_uri' => 'https://api.devmailr.app/api/',
                'headers'  => ['Authorization' => 'Bearer ' . getenv('DEVMAILR_API_KEY')],
            ]);
        }
    
        public function createInbox(string $prefix): array
        {
            $res = $this->http->post('mailboxes', ['json' => ['prefix' => $prefix]]);
            return json_decode((string) $res->getBody(), true);
        }
    
        public function waitForEmail(string $id, string $query = ''): array
        {
            // /wait long-polls — give Guzzle a read timeout above the wait
            $res = $this->http->get("mailboxes/{$id}/wait?timeout=60&{$query}", ['timeout' => 65]);
            return json_decode((string) $res->getBody(), true);
        }
    
        public function deleteInbox(string $id): void
        {
            $this->http->delete("mailboxes/{$id}");
        }
    }

    The test (PHPUnit)

    tests/SignupEmailTest.php
    <?php
    use PHPUnit\Framework\TestCase;
    
    final class SignupEmailTest extends TestCase
    {
        public function test_signup_sends_verification_code(): void
        {
            $dm = new Devmailr();
            $inbox = $dm->createInbox('php-' . time());
    
            // trigger your app to email a code to $inbox['address']
            $this->signUp($inbox['address']);
    
            $email = $dm->waitForEmail($inbox['_id'], 'has_code=true');
    
            $this->assertStringContainsString('noreply@myapp.com', $email['from']);
            $this->assertMatchesRegularExpression('/^\d{6}$/', $email['extractedCodes'][0]);
    
            $dm->deleteInbox($inbox['_id']);
        }
    }
    php
    $email = $dm->waitForEmail($inbox['_id'], 'subject=' . urlencode('reset'));
    $resetLink = $email['primaryLink'];

    Tips

    • Set DEVMAILR_API_KEY via phpunit.xml <env> or a CI secret.
    • Give Guzzle a read timeout above the wait timeout (65s above).
    • Unique prefix per test (time() or uniqid()).
    • Laravel? Swap the trigger for your app’s HTTP test client or a queued mailable dispatch.

    Related reading

    Try it in two minutes

    Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.