Unit Tests in ASP.Net Core with NUnit and Moq

Unit Tests in ASP.Net Core with NUnit and Moq

Unit testing is a key practice that guarantees all basic software requirements are met when we develop are code and it also makes sure that any new modifications don't regress the existing functionalities.

Today, we will use Nunit and Moq frameworks to implement unit test cases in an Asp.Net core-based rest API. The test cases will include checks for basic validations and mocking for third-party API calls.

I assume you have basic knowledge of what NUnit and Moq are and have landed here to see a practical implementation of the same.

We won't be implementing the entire visual studio solution from scratch as that is typically the same for all web API-based projects, but we will focus on all major code fragments involved in implementing the unit test cases.

Full source code can be found in the following GitHub repository.

Understanding the API under Test

For this demo, I have created a simple CartController which has a Checkout API accepting an Order object. The purpose of the API is to validate the cart items and card details and then call the Payment and Shipment method for further processing.

[HttpPost]
public ActionResult<string> CheckOut(Order order)
{
    var result = _cartService.ValidateCart(order);
    return Ok(result);
}

The Order object contains details related to the Card, Address and also list of CartItems. We then have three different services. First, the CartService validates the Cart. We will be using two simple checks here to understand how we can write test cases for them. Once the validations are successful, PaymentService is called.

public class CartService : ICartService
    {
        IPaymentService _paymentService;
        public CartService(IPaymentService paymentService)
        {
            _paymentService = paymentService;
        }
        public string ValidateCart(Order order)
        {
            if (order.CartItems.Count < 0)
                return "Invalid Cart";
            if (order.CartItems.Any(x => x.Quantity < 0 || x.Quantity > 10))
                return "Invalid Product Quantity";

            return _paymentService.ChargeAndShip(order);

        }
    }

PaymentService contains the implementation for ChargeAndShip. Here, again there are some basic validations for the amount and card and then a dummy method named MakePayment.

For now, we directly return true from this method, but in the real world, this method would initiate a call to the payment gateway and since we don't include testing for third-party calls we will eventually use MOQ for the two possible responses i.e. either payment successful (true) or payment failed (false).

public class PaymentService : IPaymentService
    {
        IShipmentService _shipment;
        public PaymentService(IShipmentService shipment)
        {
            _shipment = shipment;
        }
        public string ChargeAndShip(Order order)
        {
            if(order.Card.amount <= 0)
            {
                return "Amount Not Valid";
            }
            if(order.Card != null)
            {
                if(order.Card.ValidTo < DateTime.Now)
                    return "Card Expired";
                if (order.Card.CardNumber.Length < 16)
                    return "CardNumber Not Valid";
            }

            bool paymentSuccess = MakePayment(order.Card);

            if (paymentSuccess)
            {
                var shipment = _shipment.Ship(order.Address);
                if (shipment != null)
                    return "Item Shipped";
                else
                    return "Something went wrong with the shipment!!!";
            }
            else
            {
                return "Payment Failed";
            }

        }

        public virtual bool MakePayment(Card card)
        {
            //Third party call to payment service provider.
            return true;
        }
    }

If the payment is successful, we then initiate a call to ShipmentService which has a Ship method. Again the implementation is ignored as it would be a third-party service.

public class ShipmentService : IShipmentService
    {

        public ShipmentDetails Ship(AddressInfo info)
        {
            //I am a third party service whose result cannot be predicted for now so I will always be mocked
            return new ShipmentDetails();
        }
    }

Writing Tests for our Checkout API

When we MOQ an implementation we can either use MOQ and mock the entire class and then setup every calls that are possible there. Another way is to use the actual implementation and then mock only the things which are dynamic and are not controlled within the code base like a call to Payment or Shipment service in our case.

For our use case, we will be using the actual implementation of the CartService, a Mock of the PaymentService which calls some of its actual implementations and mocked version of some uncontrolled one. ShipmentService is fully mocked as there is nothing controlled in that code.

Setting up the Service objects

I have created a class name CartTest in which we will be writing the multiple TestCases for our Checkout method.

public class CartTest
    {
        private ICartService cartService;
        private Mock<PaymentService> paymentServicePMoq;
        private Mock<IShipmentService> shipmentServiceMock;

        [SetUp]
        public void Setup()
        {
            // Shipment is considered to be a third party service which cannot be tested so it has been fully mocked 
            shipmentServiceMock = new Mock<IShipmentService>();

            // for partial moq, concreate implementation of the class needs to be mocked
            // PaymentService is partial mocked as MakePayment() is mocked and not the others
            paymentServicePMoq = new Mock<PaymentService>(shipmentServiceMock.Object);

            // Not mocked, actual service is used as we want to test the implementation
            cartService = new CartService(paymentServicePMoq.Object);
        }
}

Figuring out the Test Cases

We have implemented multiple validations at every stage of our ValidateCart method so let us first list down the TestCases that we need to cover to make sure our code is working as expected

  • Fail when the amount is less than 0

  • Fail when the card number is invalid

  • Fail when card number expiry date is not valid

  • Fail when quantity is more than 10

  • Fail when payment failed on the third party

  • Fail when shipment failed

  • Pass when all checks and flow go through correctly

So we can see that there are 6 cases in which one or other validations or flow will break in our implementation failing the call and only when everything goes right do we get desired output.

Now either we can write multiple tests for each of these test cases but the better way would be to have a single ValidateCart test with multiple TestCases covering all scenarios. Sometimes based on the complexity of the logic it would make sense to distribute TestCases across multiple tests.

TestCases in NUnit

NUnit allows each Test to have multiple TestCases with parameters to decide the flow. These parameters are passed on two the actual TestMethod so that it can be accessed inside the Test.

Let us understand our TestCases and their params

[Test]
        [TestCase(-1,"4041000011114567",true,1,true, true, "Amount Not Valid")] // fail as amount in less than 0
        [TestCase(10, "404100001111456", true,2, true, true, "CardNumber Not Valid")] // fail as card number is invalid
        [TestCase(12, "4041000011114561", false,3, true, true, "Card Expired")] // fail as card number expiry date  is not valid
        [TestCase(11, "40410000111145610", true, 11, true, true, "Invalid Product Quantity")] // fail as quantity is more than 10
        [TestCase(5, "40410000111145610", true, 9,false, true, "Payment Failed")] // fail as payment failed on third party
        [TestCase(8, "40410000111145610", true, 9, true, false, "Something went wrong with the shipment!!!")] // fail as shipment failed
        [TestCase(4, "40410000111145610", true,9,true, true, "Item Shipped")] // pass
        public void CartService_Validated_ShipsProduct(double amount, string cNumber, bool validDate, int quantity, 
            bool paymentSuccess, bool shipmentSuccess, string expectedResult)
        {
        }

We will be using these parameters to create or Order object in such a way that it meets the criteria of the test case and then we compare the expectedResult and the actual result returned from calling the method to check if our code is behaving as implemented.

For example, our first test case is that our method should fail when we pass on an amount that is less than 0. So the amount variable is passed as -1. All other parameters are correct so that we can test the exact flow we want. As per our actual implementation when the Amount is less than 0 then our method returns an "Amount Not Valid" response which is passed as a parameter to the expectedResult variable.

Let us implement the actual Test to see how we use these parameters and then we can come back to another TestCase and go through it.

Preparing the Order object

var cardObj = new Card()
            {
                CardNumber = cNumber,
                ValidTo = validDate ? DateTime.Now.AddDays(10): DateTime.Now.AddDays(-10),
                Name = "Random User",
                amount = amount
            };

            var address = new AddressInfo();

            var cartItems = new List<CartItem>();
            cartItems.Add(new CartItem()
            {
                ProductId = "1001",
                Quantity = quantity,
                Price = 100
            });

            var order = new Order()
            {
                Address = address,
                CartItems = cartItems,
                Card = cardObj
            };

In the above code fragment, we prepare our Order object based upon the param supplied to the Test method. cNumber, validDate, and quantity are the three dynamic values that will differ in each TestCase run thus preparing our Order object to meet the criteria of the test case.

For example, in the second TestCase "Fail when the card number is invalid" we pass a 15-digit value to the cNumber parameter as we know that the check is 16 so it will fail to allow us to check that flow.

Now, we can create objects using C# or we can read them from JSON files etc to simulate them coming as a response from a third-party source. Let us now look at how we can do that.

Our ShipmentService returns a ShipmentDetails object which could have either some success values or some failures. We can have JSON files created and we can read them as responses.

var shipmentAck = new ShipmentDetails();
            using (StreamReader r = new StreamReader("C:\\Users\\rasrivas\\source\\repos\\Moq_POC\\test_moq_poc\\Mocked_Response\\MockedShipmentAcknowledgement.json"))
            {
                string json = r.ReadToEnd();
                shipmentAck = JsonSerializer.Deserialize<ShipmentDetails>(json);
            }

            //creating response of the mocked ShipmentService
            shipmentServiceMock.Setup(x => x.Ship(It.IsAny<AddressInfo>())).Returns(shipmentSuccess ? shipmentAck : null);

Here, we are mocking the ShipmentService to always return the ShipmentDetails object read from the JSON file whenever the Ship method is called. We have not implemented any further logic based on the Shipment response else we could have had this JSON dynamic as well.

paymentServicePMoq.CallBase = true;
paymentServicePMoq.Setup(z => z.MakePayment(It.IsAny<Card>())).Returns(paymentSuccess);

For the PaymentService we want to test the actual implementation so we set the Callbase parameter as true and only Moq the MakePayment method as we are assuming it to be a call to a third-party payment gateway. The response from the MakePayment method is also updated based on the value of the paymentSuccess parameter passed from the TestCase.

We are done with the actual setup and ready to call the method we want to test.

Test the implementation

var result = cartService.ValidateCart(order);
paymentServicePMoq.Verify();
shipmentServiceMock.Verify();

Assert.AreEqual(expectedResult, result);
Assert.Pass();

The "result" variable will contain the actual response from the ValidateCart method and the expectedResult will have our hard-coded response based on the TestCase which is being executed. If both of them match, our Test would pass else it would fail. Below is the complete code for the Test method.

[Test]
        [TestCase(-1,"4041000011114567",true,1,true, true, "Amount Not Valid")] // fail as amount in less than 0
        [TestCase(10, "404100001111456", true,2, true, true, "CardNumber Not Valid")] // fail as card number is invalid
        [TestCase(12, "4041000011114561", false,3, true, true, "Card Expired")] // fail as card number expiry date  is not valid
        [TestCase(11, "40410000111145610", true, 11, true, true, "Invalid Product Quantity")] // fail as quantity is more than 10
        [TestCase(5, "40410000111145610", true, 9,false, true, "Payment Failed")] // fail as payment failed on third party
        [TestCase(8, "40410000111145610", true, 9, true, false, "Something went wrong with the shipment!!!")] // fail as shipment failed
        [TestCase(4, "40410000111145610", true,9,true, true, "Item Shipped")] // pass
        public void CartService_Validated_ShipsProduct(double amount, string cNumber, bool validDate, int quantity, 
            bool paymentSuccess, bool shipmentSuccess, string expectedResult)
        {


            var cardObj = new Card()
            {
                CardNumber = cNumber,
                ValidTo = validDate ? DateTime.Now.AddDays(10): DateTime.Now.AddDays(-10),
                Name = "Random User",
                amount = amount
            };

            var address = new AddressInfo();

            var cartItems = new List<CartItem>();
            cartItems.Add(new CartItem()
            {
                ProductId = "1001",
                Quantity = quantity,
                Price = 100
            });

            var order = new Order()
            {
                Address = address,
                CartItems = cartItems,
                Card = cardObj
            };

            var shipmentAck = new ShipmentDetails();
            using (StreamReader r = new StreamReader("C:\\Users\\rasrivas\\source\\repos\\Moq_POC\\test_moq_poc\\Mocked_Response\\MockedShipmentAcknowledgement.json"))
            {
                string json = r.ReadToEnd();
                shipmentAck = JsonSerializer.Deserialize<ShipmentDetails>(json);
            }

            //creating response of the mocked ShipmentService
            shipmentServiceMock.Setup(x => x.Ship(It.IsAny<AddressInfo>())).Returns(shipmentSuccess ? shipmentAck : null);

            paymentServicePMoq.CallBase = true;
            paymentServicePMoq.Setup(z => z.MakePayment(It.IsAny<Card>())).Returns(paymentSuccess);


            var result = cartService.ValidateCart(order);
            paymentServicePMoq.Verify();
            shipmentServiceMock.Verify();

            Assert.AreEqual(expectedResult, result);

            Assert.Pass();
        }

If we run our Test now, we should see seven execution for the Test, one each for the seven TestCases we have written. Now in the future, if say we need to write a new validation we can come back and add execute the Test again to make sure the existing implementation is not affected.

Conclusion

I hope this article helps you in some way to figure out how MOQ framework can be used to mock certain implementations of our code and write a test for it. In the same GitHub repo, there is another Controller named ProductController which has the implementation to call a third-party H&M API to get a list of products. We also have a test written for that using the JSON file concept we used for Shipment.

Did you find this article valuable?

Support Rajat Srivastava by becoming a sponsor. Any amount is appreciated!