DEV Community

Cover image for How YOU can Learn Mock testing in .NET Core and C# with Moq
Chris Noring for .NET

Posted on • Edited on • Originally published at softchris.github.io

How YOU can Learn Mock testing in .NET Core and C# with Moq

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

Subscribe

When we test we just want to test one thing - the business logic of the method. Often our method needs the help of dependencies to be able to carry out its job properly. Depending on what these dependencies answer - there might be several paths through a method. So what is Mock testing? It's about testing only one thing, in isolation, by mocking how your dependencies should behave.

In this article we will cover the following:

  • Why test, it's important to understand why we test our code. Is it to ensure our code works? Or maybe we are adding tests for defensive reasons so that future refactors don't mess up the business logic?
  • What to test, normally this question has many answers. We want to ensure that our method does what it says it does, e.g 1+1 equals 2. We might also want to ensure that we test all the different paths through the method, the happy path as well as alternate/erroneous paths. Lastly, we might want to assert that a certain behavior takes place.
  • Demo, let's write some code that has more than one execution path and introduce the Mocking library Moq and see how it can help us fulfill the above.

References

Why test

As we mentioned already there are many answers to this question. So how do we know? Well, I usually see the following reasons:

  • Ensuring Quality, because I'm not an all-knowing being I will make mistakes. Writing tests ensures that at least the worst mistakes are avoided.
  • Is my code testable, before I've written tests for my code it might be hard to tell whether it lends itself to be tested. Of course, I need to ask myself at this point whether this code should be tested. My advice here if it's not obvious what running the method will produce or if there is more than one execution path - it should be tested.
  • Being defensive, you have a tendency to maintain software over several years. The people doing the maintaining might be you or someone else. One way to communicate what code is important is to write tests that absolutely should work regardless of what refactorings you, or anyone else, attempts to carry out.
  • Documentation, documentation sounds like a good idea at first but we all know that out of sync documentation is worse than no documentation. For that reason, we tend to not write it in the first place, or maybe feel ok with high-level documentation only or rely on tools like Swagger for example. Believe it or not but tests are usually really good documentation. It's one developer to another saying, this is how I think the code should be used. So for the sake of that future maintainer, communicate what your intentions were/are.

What to test

So what should we test? Well, my first response here is all the paths through the method. The happy path as well as alternate paths.

My second response is to understand whether we are testing a function to produce a certain result like 1+1 equals 2 or whether it's more a behavior like - we should have been paid before we can ship the items in the cart.

Demo - let's test it

What are we doing? Well, we have talked repeatedly about that Shopping Cart in an e-commerce application so let's use that as an example for our demo.

This is clearly a case of behavior testing. We want the Cart items to be shipped to a customer providing we got paid. That means we need to verify that the payment is carried out correctly and we also need a way to assert what happens if the payment fails.

We will need the following:

  • A CartController, will contain logic such as trying to get paid for a cart's content. If we are successfully paid then ship the items in the cart to a specified address.
  • Helper services, we need a few helper services to figure this out like:
    • ICartService, this should help us calculate how much the items in cart costs but also tell us exactly what the content is so we can send this out to a customer once we have gotten paid.
    • IPaymentService, this should charge a card with a specified sum
    • IShipmentService, this should be able to ship the cart content to a specific address

Creating the code

We will need two different .NET Core projects for this:

  • a webapi project, this should contain our production code and carry out the business logic as stated by the CartController and its helper services.
  • a test project, this project will contain all the tests and a reference to the above project.

The API project

For this project, this could be either an app using the template mvc, webapp or webapi

First, let's create a solution. Create a directory like so:

mkdir <new directory name>
cd <new directory name>
Enter fullscreen mode Exit fullscreen mode

Thereafter create a new solution like so:

dotnet new sln
Enter fullscreen mode Exit fullscreen mode

To create our API project we just need to instantiate it like so:

dotnet new webapi -o api
Enter fullscreen mode Exit fullscreen mode

and lastly add it to the solution like so:

dotnet sln add api/api.csproj
Enter fullscreen mode Exit fullscreen mode

Controllers/CartController.cs

Add the file CartController.cs under the directory Controllers and give it the following content:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Services;

namespace api.Controllers
{
  [ApiController]
  [Route("[controller]")]
  public class CartController 
  {
    private readonly ICartService _cartService;
    private readonly IPaymentService _paymentService;
    private readonly IShipmentService _shipmentService;

    public CartController(
      ICartService cartService,
      IPaymentService paymentService,
      IShipmentService shipmentService
    ) 
    {
      _cartService = cartService;
      _paymentService = paymentService;
      _shipmentService = shipmentService;
    }

    [HttpPost]
    public string CheckOut(ICard card, IAddressInfo addressInfo) 
    {
        var result = _paymentService.Charge(_cartService.Total(), card);
        if (result)
        {
            _shipmentService.Ship(addressInfo, _cartService.Items());
            return "charged";
        }
        else {
            return "not charged";
        }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok, our controller is created but it has quite a few dependencies in place that we need to create namely ICartService, IPaymentService and IShipmentService.

Note how we will not create any concrete implementations of our services at this point. We are more interested in establishing and testing the behavior of our code. That means that concrete service implementations can come later.

Services/ICartService.cs

Create the file ICartService.cs under the directory Services and give it the following content:

namespace Services 
{
  public interface ICartService 
  {
    double Total();
    IEnumerable<CartItem> Items();
  }
}
Enter fullscreen mode Exit fullscreen mode

This interface is just a representation of a shopping cart and is able to tell us what is in the cart through the method Items() and how to calculate its total value through the method Total().

Services/IPaymentService.cs

Let's create the file IPaymentService.cs in the directory Services and give it the following content:

namespace Services 
{
  public interface IPaymentService 
  {
    bool Charge(double total, ICard card);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we have a payment service that is able to take total for the amount to be charged and card which is debit/credit card that contains all the needed information to be charged.

Services/IShipmentService.cs

For our last service let's create the file IShipmentService.cs under the directory Services with the following content:

using System;
using System.Generic;

namespace Services
{
  public interface IShipmentService
  {
    void Ship(IAddressInfo info, IEnumerable<CartItem> items);
  }
}
Enter fullscreen mode Exit fullscreen mode

This contains a method Ship() that will allow us to ship a cart's content to the customer.

Services/Models.cs

Create the file Models.cs in the directory Services with the following content:

namespace Services 
{
  public interface IAddressInfo 
  {
    public string Street { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
    public string PhoneNumber { get; set; }
  }

  public interface ICard 
  {
    public string CardNumber { get; set; }
    public string Name { get; set; }
    public DateTime ValidTo { get; set; }
  }

  public interface CartItem 
  {
    public string ProductId { get; set; }
    public int Quantity { get; set; }
    public double Price{ get; set; }
  }
}
Enter fullscreen mode Exit fullscreen mode

This contains some supporting interfaces that we need for our services.

Creating a test project

Our test project is interested in testing the behavior of CartController. First off we will need a test project. There are quite a few test templates supported in .NET Core like nunit, xunit and mstest. We'll go with nunit.

To create our test project we type:

dotnet new nunit -o api.test
Enter fullscreen mode Exit fullscreen mode

Let's add it to the solution like so:

dotnet sln add test/test.csproj
Enter fullscreen mode Exit fullscreen mode

Thereafter add a reference of the API project to the test project, so we are able to test the API project:

dotnet add test/test.csproj reference api/api.csproj
Enter fullscreen mode Exit fullscreen mode

Finally, we need to install our mocking library moq, with the following command:

dotnet add package moq
Enter fullscreen mode Exit fullscreen mode

Moq, how it works

Let's talk quickly about our Mock library moq. The idea is to create a concrete implementation of an interface and control how certain methods on that interface responds when called. This will allow us to essentially test all of the paths through code.

Creating our first Mock

Let's create our first Mock with the following code:

var paymentServiceMock = new Mock<IPaymentService>();
Enter fullscreen mode Exit fullscreen mode

The above is not a concrete implementation but a Mock object. A Mock can be:

  • Instructed, you can tell a mock that if a certain method is called then it can answer with a certain response
  • Verified, verification is something you carry out after your production code has been called. You carry this out to verify that a certain method has been called with specific arguments

Instruct our Mock

Now we have a Mock object that we can instruct. To instruct it we use the method Setup() like so:

paymentServiceMock.Setup(p => p.Charge()).Returns(true)
Enter fullscreen mode Exit fullscreen mode

Of course, the above won't compile, we need to give the Charge() method the arguments it needs. There are two ways we can give the Charge() method the arguments it needs:

  1. Exact arguments, this is when we give it some concrete values like so:
var card = new Card("owner", "number", "CVV number");

paymentServiceMock.Setup(p => p.Charge(114,card)).Returns(true)
Enter fullscreen mode Exit fullscreen mode
  1. General arguments, here we can use the helper It, which will allow us to instruct the method Charge() that any values of a certain data type can be passed through:
paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(),card)).Returns(true)
Enter fullscreen mode Exit fullscreen mode

Accessing our implementation

We will need to pass an implementation of our Mock when we call the actual production code. So how do we do that? There's an Object property on the Mock that represents the concrete implementation. Below we are using just that. We first construct cardMock and then we pass cardMock.Object to the Charge() method.

var cardMock = new Mock<ICard>();

paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(),cardMock.Object)).Returns(true)
Enter fullscreen mode Exit fullscreen mode

Add unit tests

Let's rename the default test file we got to CartControllerTest.cs. Next, let's discuss our approach. We want to:

  • Test all the execution paths, there are currently two different paths through our CartController depending on whether _paymentService.Charge() answers with true or false
  • Write two tests, we need at least two different tests, one for each execution path
  • Assert, we need to ensure that the correct thing happens. In our case, that means if we successfully get paid then we should ship, so that means asserting that the shipmentService is being called.

Let's write our first test:

// CartControllerTest.cs

[Test]
public void ShouldReturnCharged()
{
  // arrange
  paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(), cardMock.Object)).Returns(true);

  // act
  var result = controller.CheckOut(cardMock.Object, addressInfoMock.Object);

  // assert
  shipmentServiceMock.Verify(s => s.Ship(addressInfoMock.Object, items.AsEnumerable()), Times.Once());

  Assert.AreEqual("charged", result);
}
Enter fullscreen mode Exit fullscreen mode

We have three phases above.

Arrange

Let's have a look at the code:

paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(), cardMock.Object)).Returns(true);
Enter fullscreen mode Exit fullscreen mode

here we are setting things up and saying that if our paymentService.Charge() method is called with any value It.IsAny<double>() and with a card object cardMock.Object then we should return true, aka .Returns(true). This means we have set up a happy path and are ready to go to the next phase Act.

Act

Here we call the actual code:

var result = controller.CheckOut(cardMock.Object, addressInfoMock.Object);
Enter fullscreen mode Exit fullscreen mode

As we can see above we get the answer assigned to the variable result. This takes us to our next phase, Assert.

Assert

Let's have a look at the code:

shipmentServiceMock.Verify(s => s.Ship(addressInfoMock.Object, items.AsEnumerable()), Times.Once());

Assert.AreEqual("charged", result);
Enter fullscreen mode Exit fullscreen mode

Now, there are two pieces of assertions that take place here. First, we have a Mock assertion. We see that as we are calling the method Verify() that essentially says: I expect the Ship() method to have been called with an addressInfo object and a cartItem list and that it was called only once. That all seems reasonable, our paymentService says it was paid, we set it up to respond true.

Next, we have a more normal-looking assertion namely this code:

Assert.AreEqual("charged", result);
Enter fullscreen mode Exit fullscreen mode

It says our result variable should contain the value charged.

A second test

So far we tested the happy path. As we stated earlier, there are two paths through this code. The paymentService could decline our payment and then we shouldn't ship any cart content. Let's see what the code looks like for that:


[Test]
public void ShouldReturnNotCharged() 
{
    // arrange
    paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(), cardMock.Object)).Returns(false);

    // act
    var result = controller.CheckOut(cardMock.Object, addressInfoMock.Object);

    // assert
    shipmentServiceMock.Verify(s => s.Ship(addressInfoMock.Object, items.AsEnumerable()), Times.Never());
    Assert.AreEqual("not charged", result);
}
Enter fullscreen mode Exit fullscreen mode

Above we see that we have again the three phases Arrange, Act and Assert.

Arrange

This time around we are ensuring that our paymentService mock is returning false, aka payment bounced.

paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(), cardMock.Object)).Returns(false);
Enter fullscreen mode Exit fullscreen mode

Act

This part looks exactly the same:

var result = controller.CheckOut(cardMock.Object, addressInfoMock.Object);
Enter fullscreen mode Exit fullscreen mode

Assert

We are still testing two pieces of assertions - behavior and value assertion:

shipmentServiceMock.Verify(s => s.Ship(addressInfoMock.Object, items.AsEnumerable()), Times.Never());
Assert.AreEqual("not charged", result);
Enter fullscreen mode Exit fullscreen mode

Looking at the code above we, however, are asserting that shipmentService is not called Times.Never(). That's important to verify as that otherwise would lose us money.

The second assertion just tests that the result variable now says not charged.

Full code

Let's have a look at the full code so you are able to test this out for yourself:

// CartControllerTest.cs

using System;
using Services;
using Moq;
using NUnit.Framework;
using api.Controllers;
using System.Linq;
using System.Collections.Generic;

namespace test
{
  public class Tests
  {
      private CartController controller;
      private Mock<IPaymentService> paymentServiceMock;
      private Mock<ICartService> cartServiceMock;

      private Mock<IShipmentService> shipmentServiceMock;
      private Mock<ICard> cardMock;
      private Mock<IAddressInfo> addressInfoMock;
      private List<CartItem> items;

      [SetUp]
      public void Setup()
      {

          cartServiceMock = new Mock<ICartService>();
          paymentServiceMock = new Mock<IPaymentService>();
          shipmentServiceMock = new Mock<IShipmentService>();

          // arrange
          cardMock = new Mock<ICard>();
          addressInfoMock = new Mock<IAddressInfo>();

          // 
          var cartItemMock = new Mock<CartItem>();
          cartItemMock.Setup(item => item.Price).Returns(10);

          items = new List<CartItem>()
          {
              cartItemMock.Object
          };

          cartServiceMock.Setup(c => c.Items()).Returns(items.AsEnumerable());

          controller = new CartController(cartServiceMock.Object, paymentServiceMock.Object, shipmentServiceMock.Object);
      }

      [Test]
      public void ShouldReturnCharged()
      {
          paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(), cardMock.Object)).Returns(true);

          // act
          var result = controller.CheckOut(cardMock.Object, addressInfoMock.Object);

          // assert
          // myInterfaceMock.Verify((m => m.DoesSomething()), Times.Once());
          shipmentServiceMock.Verify(s => s.Ship(addressInfoMock.Object, items.AsEnumerable()), Times.Once());

          Assert.AreEqual("charged", result);
      }

      [Test]
      public void ShouldReturnNotCharged() 
      {
          paymentServiceMock.Setup(p => p.Charge(It.IsAny<double>(), cardMock.Object)).Returns(false);

          // act
          var result = controller.CheckOut(cardMock.Object, addressInfoMock.Object);

          // assert
          shipmentServiceMock.Verify(s => s.Ship(addressInfoMock.Object, items.AsEnumerable()), Times.Never());
          Assert.AreEqual("not charged", result);
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

Final thoughts

So we have managed to test out the two major paths through our code but there are more tests, more assertions we could be doing. For example, we could ensure that the value of the Cart corresponds to what the customer is actually being charged. As well all know in the real world things are more complicated. We might need to update the API code to consider timeouts or errors being thrown from the Shipment service as well as the payment service.

Summary

I've hopefully been able to convey some good reasons for why you should test your code. Additionally, I hope you think the library moq looks like a good candidate to help you with the more behavioral aspects of your code.

Top comments (2)

Collapse
 
quooston profile image
Quooston

Respect! I sadly feel that many people these days have been lead astray, and think that unit/integration/acceptance testing is for some reason not worth the trouble. It's terrible how the industry does this, and that there are not enough thought leaders out there that push things back in the right direction. I sincerely appreciate that you are doing your bit.

Myself and my teams have been exclusively TDDing since about 2008. Most of what we do is green-fields regulated medical software, and the tests are the only way to get the confidence and evidence required to prove that we aren't going to present a hazard to a patient. If you write the correct tests, you have objective evidence that what you have created works exactly as expected.

But, in order to do this, planning needs to line up. Planning needs to produce the structured inputs that tests require. Or at least the inspiration for what the tests require. If you plan correctly, it's easy to identify high value tests and drive implementation with those. But most environments can't even do any kind of agile process with any real success, and therefore things come undone. I believe that this is one big reason why testing has suffered. Having acceptance criteria for core features as a result of sprint planning makes writing the associated tests incredibly easy, and produces more robust software.

I have empirical evidence of this, there is no discussion here! Opinions to the contrary are uninformed and a symptom of environments finding it too difficult to put the pieces together (process and testing standards) to make this happen with success.

Anyway, enough rambling. Great post, thank you.

Collapse
 
enriqueedelberto profile image
Edelberto Enrique Reyes

Thank you for sharing. It's really interesting.