automated email testing

Test signup email verification with Pytest and Selenium

Drive signup in Selenium, wait for the matching message through InboxRhino’s API, then assert on the code or link your application sent.

By Wesley Copley·October 14, 2026·4 min read
What matters here
  1. A wait-for-message request can hold for up to 180 seconds while filtering by subject or sender.
  2. A separate inbox per concurrent test prevents one signup flow from consuming another test’s message.
  3. InboxRhino inboxes receive mail only; Selenium should submit the signup form, not send test email.

Email verification tests often fail for a mundane reason: the browser submits a signup form, then the test has no reliable way to retrieve the message that arrives. A receive-only inbox API closes that gap. Selenium drives the application; Pytest asks the inbox for the email and checks what it contains.

The important design choice is to match one test run to one inbox. Shared inboxes make parallel tests compete for messages, and a subject filter alone may not distinguish two identical signups. Keep the inbox identifier and address with the test that created them, and use a unique test account for each run.

InboxRhino provides receive-only test inboxes through an API. Its wait-for-message request can wait up to 180 seconds and filter by subject or sender. For a broader comparison of what to check when selecting this kind of service, see the guide to choosing a lightweight test inbox API.

Set up the test boundary

Keep the API key in an environment variable, not in the test file. Create an inbox before submitting the signup form, then use the returned inbox ID for the message request. The API quickstart documents the create-inbox request at POST https://api.inboxrhino.in/v1/inboxes; the response includes the inbox ID. Use the returned address in the signup form, taking the address field from the documented response schema.

A small adapter keeps response-shape details out of the test itself. For example, define create_test_inbox() to make the documented request and return the inbox ID and address. Define message_text(message) to expose the parsed message body using the field documented by the API. Those two helpers are the only places that need to know the response schema.

InboxRhino’s free tier allows 11 active inboxes and 33 inbound emails per month for one user. That is enough to wire a focused flow, but it is a real limit: avoid creating an inbox for every assertion, and plan cleanup using the documented inbox lifecycle. Messages are retained for 30 days before deletion.

Submit the signup form with Selenium

Make the browser step ordinary UI testing. The selectors and fields below are illustrative; replace them with your application’s selectors. The test should submit the form once and then hand off to the inbox API.

In a Pytest fixture, create an inbox and expose its address to the test. In the test body, use Selenium to enter that address and submit signup: driver.find_element(...).send_keys(inbox_address), then driver.find_element(...).click() on the submit control. Assert the application’s immediate confirmation if it has one, but do not treat that as proof that the verification email arrived.

Keep the browser and inbox steps separate. If signup fails before submission, the failure belongs to the UI path. If the form succeeds but no matching email arrives, the failure belongs to delivery or the message wait. That split makes CI failures easier to diagnose.

Wait once, then assert the message

InboxRhino’s documented message request uses GET https://api.inboxrhino.in/v1/inboxes/{id}/messages with wait_seconds, limit, include, and optional subject or sender filters. A Python request can follow the same shape:

response = requests.get(f"https://api.inboxrhino.in/v1/inboxes/{inbox_id}/messages", headers={"Authorization": f"Bearer {api_key}"}, params={"wait_seconds": 180, "limit": 1, "include": "content", "subject": "Verify"})

Read the returned message list from data, as in the API quickstart, and fail clearly if it is empty. Then assert against the parsed body, not just the subject. For a six-digit OTP, for example, serialize the returned message object and search it with a regular expression such as r"\b(\d{6})\b". Tune the pattern to the format your application actually sends. To check explanatory copy, use the body accessor from your adapter and assert that it includes an expected phrase.

Using the documented parsed-body field matters. A subject match proves only that a message with that subject arrived; it does not prove the OTP or verification instructions are present. Keep the expected content specific enough to catch a broken template, but avoid asserting on incidental wording that changes often.

Keep parallel runs isolated

One inbox per concurrent signup flow is the safest default. If several tests share an inbox, a wait can return another test’s message, even when each test is otherwise correct. Filtering by sender or subject narrows the match, but it does not replace isolation when messages look alike.

InboxRhino inboxes are receive-only: they cannot send, reply, or forward. That is appropriate for this flow. Selenium sends the signup request through the application, while the API only retrieves the resulting email. The mailbox viewer is sandboxed and blocks scripts, forms, popups, and remote images; use it when visual inspection helps debug a template, not as a substitute for the automated assertion.

With that boundary in place, the test has a clear contract: Selenium submits a unique signup, the wait-for-message call returns its verification email, and Pytest checks the content that matters. The browser verifies the user journey; the inbox verifies the message.

More from InboxRhino News