Automated Testing of Twilio C# Apps with Moq

January 10, 2017
Written by
Reviewed by
Paul Kamp
Twilion
Kat King
Twilion

automated-testing-csharp-moq

Automating testing of apps that rely on third-party API's like Twilio can be challenging. Let's look at how structure your C# app and write tests using Moq to achieve automated testing nirvana. All of the example code in this guide is available on GitHub.

Set Things Up

Create a Test Project

We'll use Visual Studio's built-in test projects for this guide, but the practices here translate readily to alternative testing frameworks such as NUnit and xUnit.

To add a new test project to your existing solution, right-click on your solution in the Solution Explorer and select Add... New Project:

Visual Studio - Add Test Project

Select a Unit Test project and name your new project:

Visual Studio - New Test Project

Don't forget to add project references to other projects in the solution that you want to test:

Visual Studio - Add References to Test Project

Visual Studio - Add References to Test Project

Install the Moq Mocking Framework

When testing our services in isolation we'll create mock objects. Mock objects are the crash-test dummies of software development.

Let's install the mocking framework Moq using Nuget:

Install-Package Moq

Make sure before you run that command the "Default project" is set to your test project:

Visual Studio - Install Moq Package

Improve Your App's Testability

Decouple Code That Makes External API Calls

Let's say you're building an OrderService class that needs to process orders for items. It needs to reserve inventory, ship the order, and notify the customer via SMS that their order has been shipped. You could call the Twilio API to send the SMS directly from your service. Your code might look like this:

public void ProcessOrder(IOrder order)
{
    TryReserveInventory(order);
    var trackingNum = ShipOrder(order);
    var message = $"Your order {order.Id} has shipped! Tracking: {trackingNum}";

    var accountSid = ConfigurationManager.AppSettings["TwilioAccountSid"];
    var authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
    var fromPhoneNumber = ConfigurationManager.AppSettings["TwilioFromNumber"];
    Twilio.Init(accountSid, authToken);
    MessageResource.Create(
        to: new PhoneNumber(order.Customer.MobileNumber),
        from: new PhoneNumber(fromPhoneNumber), 
        body: message);
}

The problem is that your OrderService is tightly coupled to the Twilio API. Tight coupling is bad for a number of reasons, but it's especially bad for testability. What we need is a simple NotificationService that can handle talking to the Twilio API. This will allow us to test our OrderService in isolation without actually sending a text message.

Now our code can look something like:

public void ProcessOrder(IOrder order)
{
    TryReserveInventory(order);
    var trackingNum = ShipOrder(order);
    _notificationService.SendText(order.Customer.MobileNumber,
        $"Your order {order.Id} has shipped! Tracking: {trackingNum}");
}

And here's the full code of our new NotificationService:

This is a migrated tutorial. Find the original code at https://github.com/TwilioDevEd/automated-testing-csharp/

using System;
using Twilio.Clients;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
using TwilioStore.Interfaces.Services;
using TwilioStore.Services.Exceptions;

namespace TwilioStore.Services
{
    public class NotificationService : INotificationService
    {
        private readonly TwilioRestClient _client;

        public NotificationService()
        {
            var accountSid = 
                ConfigurationManager.AppSettings["TwilioAccountSid"];
            var authToken = 
                ConfigurationManager.AppSettings["TwilioAuthToken"];
            _client = new TwilioRestClient(accountSid, authToken);
        }

        public void SendText(string to, string message)
        {
            var fromPhoneNumber = 
                ConfigurationManager.AppSettings["TwilioFromNumber"];
            try
            {
                MessageResource.Create(
                    to: new PhoneNumber(to),
                    from: new PhoneNumber(fromPhoneNumber),
                    body: message,
                    client: _client
                );
            }
            catch (Exception e)
            {
                throw new NotificationException(e.Message, e);
            }
        }
    }
}

Notice that it implements an INotificationService interface, which will come in handy for mocking later.

namespace TwilioStore.Interfaces.Services
{
    public interface INotificationService
    {
        void SendText(string to, string message);
    }
}

Decouple API Credential Configuration

There's one more change we should make to improve our testability. In our notification service, we're loading configuration values like so:

var accountSid = ConfigurationManager.AppSettings["TwilioAccountSid"];
var authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
...
var fromPhoneNumber = ConfigurationManager.AppSettings["TwilioFromNumber"];

When we test NotificationService, we don't want to be tightly coupled to the configuration system. In fact, we might even want to test our service with different values for different tests. So, let's extract a new class we'll call NotificationConfiguration:

using System.Configuration;
using TwilioStore.Interfaces.Services;

namespace TwilioStore.Services
{
    public class NotificationConfiguration : INotificationConfiguration
    {
        public NotificationConfiguration()
        {
            AccountSid = ConfigurationManager.AppSettings["TwilioAccountSid"];
            AuthToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
            DefaultFromPhoneNumber = 
                ConfigurationManager.AppSettings["TwilioFromNumber"];
        }

        public string AccountSid { get; }
        public string AuthToken { get; }
        public string DefaultFromPhoneNumber { get; }
    }
}

Again, we created an interface for this class:

namespace TwilioStore.Interfaces.Services
{
    public interface INotificationConfiguration
    {
        string AccountSid { get; }
        string AuthToken { get; }
        string DefaultFromPhoneNumber { get; }
    }
}

Now our NotificationService can make use of this configuration class to get the required variables. Notice below how we allow passing in a custom INotificationConfiguration. If none is passed in, it defaults to our NotificationConfiguration implmentation. For fun, let's also add MakePhoneCall() and BuyPhoneNumber() methods to our service.

Here's the completed code:

using System;
using Twilio.Clients;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
using TwilioStore.Interfaces.Services;
using TwilioStore.Services.Exceptions;

namespace TwilioStore.Services
{
    public class NotificationService : INotificationService
    {
        private readonly TwilioRestClient _client;
        private readonly INotificationConfiguration _config;

        public NotificationService() : this(new NotificationConfiguration())
        {
        }

        public NotificationService(INotificationConfiguration config)
        {
            _config = config;
            _client = new TwilioRestClient(_config.AccountSid, 
                _config.AuthToken);
        }

        public void SendText(string to, string message)
        {
            try
            {
                MessageResource.Create(
                    to: new PhoneNumber(to),
                    from: new PhoneNumber(_config.DefaultFromPhoneNumber),
                    body: message,
                    client: _client
                );
            }
            catch (Exception e)
            {
                throw new NotificationException(e.Message, e);
            }
        }

        public void MakePhoneCall(string to, string voiceUrl)
        {
            try
            {
                CallResource.Create(
                    to: new PhoneNumber(to),
                    from: new PhoneNumber(_config.DefaultFromPhoneNumber),
                    url: new Uri(voiceUrl),
                    client: _client
                );
            }
            catch (Exception e)
            {
                throw new NotificationException(e.Message, e);
            }
        }

        public void BuyPhoneNumber(string number)
        {
            try
            {
                IncomingPhoneNumberResource.Create(
                    phoneNumber: new PhoneNumber(number),
                    client: _client
                );
            }
            catch (Exception e)
            {
                throw new NotificationException(e.Message, e);
            }
        }
    }
}

And the final interface:

namespace TwilioStore.Interfaces.Services
{
    public interface INotificationService
    {
        void SendText(string to, string message);
        void MakePhoneCall(string to, string voiceUrl);
        void BuyPhoneNumber(string number);
    }
}

Test Your App's Logic with Moq

Now, let's test our ProcessOrder() method. We just want to make sure that it calls our NotificationService.SendText() method with the right values, but we don't need to actually talk to the Twilio API. Let's use Moq to create a mock INotificationService:

var notificationServiceMock = new Mock<INotificationService>();
string textMessage = null;
notificationServiceMock.Setup(
        x => x.SendText(ValidToNumber, It.IsAny<string>())
    )
    .Callback<string, string>((_, message) => textMessage = message);

The call to Setup() tells our mock to expect a call to the SendText() method with the first parameter a constant we defined to represent a valid customer's phone number and any string for the second, message parameter. We then provide a callback method that saves the message into a variable for later inspection.

Using the mock object is as easy as passing it into our OrderService:

var orderService = new OrderService(notificationServiceMock.Object);
orderService.ProcessOrder(fakeOrder);

Finally, we can assert that the SendText() method was called exactly once:

notificationServiceMock.Verify(
        x => x.SendText(ValidToNumber, It.IsAny<string>()),
        Times.Once
    );

Here's the complete code for our test:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using TwilioStore.Domain;
using TwilioStore.Interfaces.Services;
using TwilioStore.Services;

namespace TwilioStore.Tests.Services
{
    [TestClass]
    public class OrderServiceTest
    {
        private const string ValidToNumber = "+18778894546";

        [TestMethod]
        public void ProcessOrder_Should_Notify_Customer()
        {
            // Arrange
            var fakeOrder = GetFakeOrder();

            var notificationServiceMock = new Mock<INotificationService>();
            string textMessage = null;
            notificationServiceMock.Setup(
                    x => x.SendText(ValidToNumber, It.IsAny<string>())
                )
                .Callback<string, string>((_, message) => textMessage = message);

            // Act
            var orderService = new OrderService(notificationServiceMock.Object);
            orderService.ProcessOrder(fakeOrder);

            // Assert
            notificationServiceMock.Verify(
                    x => x.SendText(ValidToNumber, It.IsAny<string>()),
                    Times.Once
                );
            var expectedMessageStart = $"Your order {fakeOrder.Id} has " +
                "shipped! Tracking: ";
            Assert.IsTrue(textMessage.StartsWith(expectedMessageStart));
            Assert.IsTrue(textMessage.Length > expectedMessageStart.Length);
        }

        private static Order GetFakeOrder()
        {
            var fakeOrder = new Order
            {
                Id = "1234",
                Customer = new Customer
                {
                    Name = "Joe Customer",
                    MobileNumber = ValidToNumber
                }
            };
            fakeOrder.Items.Add(new OrderDetail
            {
                ItemId = "5678",
                Description = "Widget",
                Quantity = 1
            });
            return fakeOrder;
        }
    }
}

Test Your External API Classes

Since your application logic will likely have many tests, it makes sense for the tests not to hit an external API every time. But what about the NotificationService? Where's the test coverage love for him?

Thankfully, Twilio provides test credentials that we can use to allow us to write a suite of tests to exercise our service. Our approach will be to create a mock of INotificationConfiguration that provides our test credentials instead of reading them from a configuration file:

private static INotificationConfiguration GetTestConfig()
{
    var configMock = new Mock<INotificationConfiguration>();
    configMock.SetupGet(x => x.AccountSid)
        .Returns(TestAccountSid);
    configMock.SetupGet(x => x.AuthToken)
        .Returns(TestAuthToken);
    configMock.SetupGet(x => x.DefaultFromPhoneNumber)
        .Returns(ValidFromNumber);
    return configMock.Object;
}

Test Sending an SMS

As documented in our API Reference, when you use your test credentials there are special phone numbers you can use for your testing. For SMS tests you can send a text to any valid phone number and you will receive a successful response.

Here's how we could test that, passing in the mock config object we created above:

[TestMethod]
public void SendText_Should_Send_a_Text()
{
    var config = GetTestConfig();
    var service = new NotificationService(config);
    service.SendText(ValidToNumber, "Test message");
    // Should complete w/o exception
}

To test an invalid phone number, use the magic number "+15005550001" (which we can assign to the constant InvalidNumber):

[TestMethod]
[ExpectedException(typeof(NotificationException))]
public void SendText_Should_Throw_Exception_If_Bad_Number()
{
    var config = GetTestConfig();
    var service = new NotificationService(config);
    service.SendText(InvalidNumber, "Test message");
}

Notice how this test uses the ExpectedException attribute to ensure that an exception is raised when trying to send a text to an invalid number.

Test Making a Phone Call

Testing phone calls can be done using the same phone numbers. Any valid number will return a success response and "+15005550001" will raise an exception.

Test Buying a Phone Number

When buying a phone number, you can try to purchase "+15005550006" for a successful response and "+15005550000" to simulate trying to purchase an unavailable number.

Here's the full code for all our tests:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using TwilioStore.Interfaces.Services;
using TwilioStore.Services;
using TwilioStore.Services.Exceptions;

namespace TwilioStore.Tests.Services
{
    [TestClass]
    public class NotificationServiceTest
    {
        // TODO: Get your test credentials from 
        //   https://www.twilio.com/console/account/settings
        private static string _testAccountSid =
            "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        private static string _testAuthToken =
            "0123456789abcdef0123456789abcdef";

        // Magic numbers: https://www.twilio.com/docs/api/rest/test-credentials
        private const string ValidFromNumber = "+15005550006";
        private const string ValidToNumber = "+18778894546";
        private const string InvalidNumber = "+15005550001";
        private const string AvailableNumber = "+15005550006";
        private const string UnavailableNumber = "+15005550000";

        private static INotificationConfiguration GetTestConfig()
        {
            _testAccountSid =
                Environment.GetEnvironmentVariable("TWILIO_TEST_ACCOUNT_SID")
                ?? _testAccountSid;

            _testAuthToken =
                Environment.GetEnvironmentVariable("TWILIO_TEST_AUTH_TOKEN")
                ?? _testAuthToken;

            if (_testAccountSid == "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ||
                _testAuthToken == "0123456789abcdef0123456789abcdef")
            {
                throw new Exception("You forgot to set your TestAccountSid " +
                                    "and/or TestAuthToken");
            }

            var configMock = new Mock<INotificationConfiguration>();
            configMock.SetupGet(x => x.AccountSid)
                .Returns(_testAccountSid);
            configMock.SetupGet(x => x.AuthToken)
                .Returns(_testAuthToken);
            configMock.SetupGet(x => x.DefaultFromPhoneNumber)
                .Returns(ValidFromNumber);

            return configMock.Object;
        }

        [TestMethod]
        public void SendText_Should_Send_a_Text()
        {
            var config = GetTestConfig();
            var service = new NotificationService(config);
            service.SendText(ValidToNumber, "Test message");
            // Should complete w/o exception
        }

        [TestMethod]
        [ExpectedException(typeof(NotificationException))]
        public void SendText_Should_Throw_Exception_If_Bad_Number()
        {
            var config = GetTestConfig();
            var service = new NotificationService(config);
            service.SendText(InvalidNumber, "Test message");
        }

        [TestMethod]
        public void MakePhoneCall_Should_Initiate_Phone_Call()
        {
            var config = GetTestConfig();
            var service = new NotificationService(config);
            service.MakePhoneCall(ValidToNumber, 
                "http://demo.twilio.com/docs/voice.xml");
            // Should complete w/o exception
        }

        [TestMethod]
        [ExpectedException(typeof(NotificationException))]
        public void MakePhoneCall_Should_Throw_Exception_If_Bad_Number()
        {
            var config = GetTestConfig();
            var service = new NotificationService(config);
            service.MakePhoneCall(InvalidNumber, 
                "http://demo.twilio.com/docs/voice.xml");
        }

        [TestMethod]
        public void BuyPhoneNumber_Should_Purchase_Number()
        {
            var config = GetTestConfig();
            var service = new NotificationService(config);
            service.BuyPhoneNumber(AvailableNumber);
            // Should complete w/o exception
        }

        [TestMethod]
        [ExpectedException(typeof(NotificationException))]
        public void BuyPhoneNumber_Should_Throw_Exception_If_Unavailable()
        {
            var config = GetTestConfig();
            var service = new NotificationService(config);
            service.BuyPhoneNumber(UnavailableNumber);
        }
    }
}

To download all of the code in a complete, working Visual Studio solution, head over to GitHub.

What's Next

There's a lot more you can do with test credentials, so do read up on them in our API Reference. Also, check out our C# TutorialsC# Tutorials. Most have tests written for them, like the Conference & Broadcast tutorial.

Until next time, happy testing.