SMS Delivery Testing for ASP.NET Applications: Ensuring Message Reliability

SMS Delivery Testing for ASP.NET Applications: Ensuring Message Reliability

Getting an SMS provider to accept your API call is not the same as getting a message into someone’s hands. Your ASP.NET application can return HTTP 200 on every send request and still silently fail to deliver OTP codes to healthcare login flows or order confirmations to e-commerce customers.

This guide walks you through building a structured, repeatable SMS delivery testing strategy that covers mock provider setup, delivery receipt webhook handling, failure simulation, and CI pipeline isolation.

Quick Answer: To test SMS delivery in ASP.NET, abstract your SMS sender behind an ISmsService interface, use a provider sandbox or mock implementation, write xUnit tests for delivery receipt webhook handling, and simulate failure scenarios using provider test numbers or controlled mock responses.

What SMS Delivery Testing Actually Covers

There are three distinct layers to test. Most developers only test the first one.

  • Outbound send logic — your application calls the provider API with the correct parameters
  • Delivery receipt handling — your application processes the asynchronous webhook callback that confirms carrier delivery
  • Failure response behavior — your application handles rejected, expired, and undelivered messages without silently swallowing errors

The gap between send confirmation and delivery confirmation is real. Research published by Zerfos et al., UCLA Computer Science Dept. / Deutsche Telekom Laboratories (IMC’06) found that in a nationwide cellular network test, 94.9% of messages were successfully delivered while 5.1% expired or were denied delivery. That’s more than one in twenty messages failing. Manual testing won’t catch that pattern. A structured test strategy will.

The stakes get higher in critical-use contexts. Data from the Cornell University Division of Public Safety Communications Center showed that 85% of over 44,900 emergency SMS messages were delivered within 71 seconds, but 3.58% of targeted users were never messaged at all. For emergency alerts, authentication codes, or appointment reminders, that gap matters.

Choosing a Testable SMS Provider Integration

Provider Comparison for .NET Testability

Three providers dominate .NET SMS integrations: Twilio, Vonage, and AWS SNS. Each handles testability differently.

  • Twilio — offers dedicated test credentials that route to a sandbox, not live carrier networks. The Twilio.Rest NuGet package exposes clean interfaces that work well with Moq. Delivery receipts arrive via configurable webhook callbacks with a MessageSid and MessageStatus field.
  • Vonage (formerly Nexmo) — provides a sandbox mode through their developer dashboard. The Vonage NuGet package is testable but requires more ceremony to mock than Twilio’s SDK.
  • AWS SNS — no dedicated sandbox for SMS, but the AWSSDK.SimpleNotificationService package works with LocalStack for local integration testing. Delivery receipt data requires enabling delivery status logging to CloudWatch, which adds setup overhead.

For most ASP.NET projects, Twilio’s sandbox credentials give you the fastest path to isolated test runs without live carrier traffic.

Synchronous vs. Asynchronous Delivery Status

Twilio and Vonage return a synchronous response confirming the API accepted your message, then deliver status updates asynchronously via webhook. AWS SNS behaves similarly. Your test strategy needs to cover both paths. Don’t assume a successful API response means delivery succeeded.

Setting Up a Mock SMS Service in Your Test Project

Define the Interface First

Wrap your provider SDK behind an ISmsService interface. This is the dependency injection pattern that makes SMS logic testable without touching live APIs.

public interface ISmsService
{
    Task<SmsResult> SendAsync(string toNumber, string message);
}

public class SmsResult
{
    public string MessageId { get; set; }
    public SmsDeliveryStatus Status { get; set; }
}

public enum SmsDeliveryStatus
{
    Queued, Sent, Delivered, Failed, Undelivered
}

Register your real provider implementation in Program.cs using services.AddScoped<ISmsService, TwilioSmsService>(). Your test project registers a mock instead, without touching production startup configuration.

Configure the Mock in xUnit

Use Moq (install Moq from NuGet) to configure controlled delivery responses.

[Fact]
public async Task SendSms_ValidNumber_ReturnsDeliveredStatus()
{
    var mockSms = new Mock<ISmsService>();
    mockSms.Setup(s => s.SendAsync("+15005550006", It.IsAny<string>()))
           .ReturnsAsync(new SmsResult 
           { 
               MessageId = "SM_test_001", 
               Status = SmsDeliveryStatus.Delivered 
           });

    var service = new OrderNotificationService(mockSms.Object);
    var result = await service.NotifyOrderShippedAsync("+15005550006", "ORD-9912");

    Assert.Equal(SmsDeliveryStatus.Delivered, result.Status);
    mockSms.Verify(s => s.SendAsync("+15005550006", It.IsAny<string>()), Times.Once);
}

After completing this setup, your application’s SMS send logic is fully testable without network calls, provider costs, or non-deterministic CI behavior.

Handling Delivery Receipt Webhooks in ASP.NET

Create the Webhook Controller

Delivery receipts arrive as HTTP POST callbacks. Create a dedicated controller to receive them.

[ApiController]
[Route("webhooks/sms")]
public class SmsWebhookController : ControllerBase
{
    private readonly IDeliveryReceiptService _receiptService;

    [HttpPost("status")]
    public async Task<IActionResult> ReceiveStatus([FromForm] TwilioStatusPayload payload)
    {
        if (!ValidateTwilioSignature(Request))
            return Unauthorized();

        var status = MapToInternalStatus(payload.MessageStatus);
        await _receiptService.RecordDeliveryAsync(payload.MessageSid, status);
        return Ok();
    }
}

Map provider status strings to your internal SmsDeliveryStatus enum. Twilio sends values like delivered, failed, undelivered, and queued. Normalize these before they touch your domain logic.

Test the Webhook Endpoint with WebApplicationFactory

Use WebApplicationFactory<Program> from the Microsoft.AspNetCore.Mvc.Testing NuGet package to run integration tests against your webhook controller without deploying anywhere.

[Fact]
public async Task WebhookController_ValidPayload_RecordsDeliveredStatus()
{
    var factory = new WebApplicationFactory<Program>();
    var client = factory.CreateClient();

    var payload = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("MessageSid", "SM_test_001"),
        new KeyValuePair<string, string>("MessageStatus", "delivered")
    });

    var response = await client.PostAsync("/webhooks/sms/status", payload);
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Validate webhook authenticity using Twilio’s X-Twilio-Signature header before processing any payload. Skipping signature validation is a real production vulnerability, not a theoretical one.

Simulating Delivery Failures and Carrier Rejection

The failure scenarios worth testing are not edge cases. Carrier filtering, invalid numbers, and message expiry happen in production at measurable rates.

  1. Invalid phone number — the carrier rejects the message immediately. Your mock should return SmsDeliveryStatus.Failed and your application should log the MessageId and destination number for diagnosis.
  2. Carrier filtering — the message is accepted by the provider but blocked by the carrier, typically returning undelivered with an error code like 30007. Test that your application distinguishes this from a clean failure.
  3. Provider timeout — simulate by configuring your mock to throw TaskCanceledException. Verify your retry logic attempts the configured number of retries before giving up.
  4. Missing webhook callback — the delivery receipt never arrives. Test that your application marks messages as Unconfirmed after a configurable timeout window rather than leaving them in a Queued state indefinitely.

Add these failure simulation tests to your CI pipeline configuration using a [Trait("Category", "SmsFailure")] attribute on each test method. This lets you run them selectively without slowing down your fast unit test suite.

Measuring Delivery Rates and Setting Thresholds

A delivery rate of 95% or above is a reasonable baseline for transactional SMS. Below that, you have a provider configuration problem, a carrier filtering issue, or a data quality problem with your phone number list. Track delivery status in a database table with columns for MessageId, Status, Timestamp, and ErrorCode. Query it weekly to calculate your rate.

In Application Insights, log a custom event on each delivery receipt callback: telemetry.TrackEvent("SmsDeliveryReceived", new { status, messageId, errorCode }). Set an alert rule that fires when your failed-plus-undelivered rate exceeds 5% over a rolling 24-hour window.

Isolating SMS Test Traffic from Production

Use environment-specific configuration in appsettings.Development.json and appsettings.Production.json to switch provider credentials. Never share production credentials with CI pipelines.

Implement a phone number allowlist in non-production environments. If the destination number isn’t in the allowlist, route the call to your mock service and log a warning. This prevents accidental sends to real customers during staging runs. In your CI pipeline definition (GitHub Actions or Azure DevOps), inject sandbox credentials as environment variables and use a feature flag or ASPNETCORE_ENVIRONMENT check to activate mock routing automatically.

Running SMS Delivery Tests in a CI Pipeline

Separate your test categories cleanly. Unit tests using mocked ISmsService implementations run on every commit. Integration tests that hit Twilio sandbox endpoints run on pull requests and nightly builds only.

In your pipeline YAML, exclude integration tests from the fast run using the dotnet test filter: dotnet test --filter "Category!=SmsIntegration". Store sandbox credentials in GitHub Actions secrets or Azure DevOps variable groups, never in source control.

Frequently Asked Questions

How do I test SMS delivery without sending real messages in ASP.NET?

Define an ISmsService interface and inject a Moq-based mock in your test project’s DI container. Configure the mock to return controlled SmsDeliveryStatus values. Your production code never changes; only the registered implementation differs between environments.

What is the best way to mock an SMS provider in xUnit?

Install the Moq NuGet package, create a Mock<ISmsService>, and use Setup() to define return values for specific inputs. Use Verify() assertions to confirm your application called the send method with the expected parameters.

How do I handle SMS delivery receipts in ASP.NET Core webhooks?

Create an [ApiController] with an [HttpPost] action that accepts the provider’s form payload. Validate the provider signature header, map the status string to your internal enum, and persist the result. Test the endpoint using WebApplicationFactory and HttpClient in your integration test project.

What is a good SMS delivery rate benchmark?

Target 95% or above for transactional SMS. Track your rate by recording every delivery receipt callback and calculating delivered messages as a percentage of total sent. Rates below 90% usually indicate carrier filtering or phone number quality issues that need investigation before production traffic scales up.

How do I simulate carrier rejection in an ASP.NET test?

Configure your mock to return SmsDeliveryStatus.Undelivered with an error code matching carrier rejection (Twilio uses error code 30007). Verify your application logs the failure with enough detail to diagnose it in production, including the MessageId, destination, and error code.

Download the SMS Delivery Testing Starter Kit to get a ready-to-use ASP.NET test project with mock provider, sample xUnit tests, and webhook test helpers pre-configured. Subscribe to the my-asp.net newsletter to receive updates when SMS provider APIs change in ways that affect .NET integrations.

Theresa Dunn
Contact
Address

3815 Wayback Lane, Bohemia, NY 11716

Phone

(+1) 631-398-1086