DEV Community

Cover image for Let's refactor a test: PaymentProxy
Cesar Aguirre
Cesar Aguirre

Posted on

Let's refactor a test: PaymentProxy

I originally posted this post on my blog. It's part of the Unit Testing 101 series. I updated it for accuracy and comprehensiveness.

Let's continue refactoring a test to make it more readable. In this post, let's learn a qick tip to write readable tests: using custom verification methods for mocks.

This time, let's say we have a payment processing system, and we want to provide our users a user-friendly client to call our endpoints.

We want to test that our PaymentProxy calls the right endpoints. For example, if we change the version number, our client uses that version number in the URLs.

These are the tests. Take a moment to see what you would refactor.

[TestClass]
public class PaymentProxyTests
{
    [TestMethod]
    public async Task PayAsync_ByDefault_CallsLatestVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object);

        await proxy.PayAsync(APaymentRequest);

        fakeClient
            .Verify(x => x.PostAsync<PaymentRequest, ApiResult>(
                It.Is<Uri>(t => t.AbsoluteUri.Contains("/v2/pay", StringComparison.InvariantCultureIgnoreCase)), It.IsAny<PaymentRequest>()),
                Times.Once);
    }

    [TestMethod]
    public async Task PayAsync_VersionNumber_CallsEndpointWithVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object, Version.V1);

        await proxy.PayAsync(APaymentRequest);

        fakeClient
            .Verify(x => x.PostAsync<PaymentRequest, ApiResult>(
                It.Is<Uri>(t => t.AbsoluteUri.Contains("/v1/pay", StringComparison.InvariantCultureIgnoreCase)), It.IsAny<PaymentRequest>()),
                Times.Once);
    }

    private static PaymentRequest APaymentRequest
        => new PaymentRequest
        {
            // All initializations here...
            Amount = new Amount
            {
                CurrencyCode = "USD",
                Value = 1_000
            }
        };
}
Enter fullscreen mode Exit fullscreen mode

These tests use Moq to write fake objects and object mothers to create test data.

Did you notice the Verify() methods in the two tests? And did you notice how buried inside that boilerplate is the URL we want to check? That's what we're interested in. All that noise!

Create a custom Verify method

Let's create an extension method for our fakeClient: VerifyUriContains(). Something like this,

using Moq;
using System;

namespace MyCoolExtensions;

public static class MockApiClientExtensions
{
    public static void VerifyUriContains(this Mock<IApiClient> fakeClient, string relativeUri)
    //                 πŸ‘†πŸ‘†πŸ‘†
    {
        fakeClient
            .Verify(x => x.PostAsync<PaymentRequest, ApiResult>(
                        It.Is<Uri>(t => t.AbsoluteUri.Contains(relativeUri, StringComparison.InvariantCultureIgnoreCase)),
                        It.IsAny<PaymentRequest>()),
                    Times.Once);
    }
}
Enter fullscreen mode Exit fullscreen mode

With our VerifyUriContains() in place, let's refactor our tests to use it,

[TestClass]
public class PaymentProxyTests
{
    [TestMethod]
    public async Task PayAsync_ByDefault_CallsLatestVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object);

        await proxy.PayAsync(AnyPaymentRequest);

        fakeClient.VerifyUriContains("/v2/pay");
        //         πŸ‘†πŸ‘†πŸ‘†
        // Now, it's way more readable
    }

    [TestMethod]
    public async Task PayAsync_VersionNumber_CallsEndpointWithVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object, Version.V1);

        await proxy.PayAsync(AnyPaymentRequest);

        fakeClient.VerifyUriContains("/v1/pay");
        //         πŸ‘†πŸ‘†πŸ‘†
        // No more boilerplate code to check things
    }

    private static PaymentRequest APaymentRequest
        => new PaymentRequest
        {
            // All initializations here...
            Amount = new Amount
            {
                CurrencyCode = "USD",
                Value = 1_000
            }
        };
}
Enter fullscreen mode Exit fullscreen mode

After this refactoring, our tests are more readable. And we wrote the tests in the same terms as our domain language. No more ceremony to check we called the right URL endpoints.

VoilΓ ! A quick refactoring tip I use often. Let's write assertions using the same language as our domain model. We could write custom assertions with private methods and share them in a base class or extension methods on "result" objects.


Hey, there! I'm Cesar, a software engineer and lifelong learner. To support my work and upgrade your unit testing skills, check my course: Mastering C# Unit Testing with Real-world Examples on my Gumroad page. Practice with hands-on exercises and learn best practices by refactoring real-world unit tests.

πŸš€ Master C# Unit Testing with Real-world Examples

Happy testing!

Top comments (0)