DEV Community

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

Posted on β€’ Edited 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.


Want to write readable and maintainable unit tests in C#? Join my course Mastering C# Unit Testing with Real-world Examples on Udemy and learn unit testing best practices while refactoring real unit tests from my past projects. No more tests for a Calculator class.

Happy testing!

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More