Practical .NET

Testing Only the Code You Write: Isolating Components with Moq

When you're testing an ASP.NET MVC controller (or, really, any class at all) you want to make sure the code that fails is the code you're testing. Moq provides a simple way to isolate the code you're testing and lets you generate test cases.

Let's assume you have an ASP.NET MVC application with a controller and action method that look like this:

Public Class CustomerManagementController
  Inherits System.Web.MVC.Controller

  Public Function UpdateCreditStatus(
    CustId As String, Orders As List<SalesOrder>) As Action Result
  Dim cust As Customer 
  cust = CustomerRepository.GetCustomerById(CustId)
  '...more code

This action method accepts two parameters: An Id value for a customer and a list of purchases for that customer. Inside the method, the code retrieves the relevant Customer object and then, based on the customer's purchases, goes on to figure out the customer's current credit status. Because that credit status determines how much the customer can buy from me without actually having to pay for it, it's important to ensure that this code is working correctly. As I build out this method, I'll want to use test-driven development (TDD) to ensure that, at each step in development, my code is doing what the business wants.

To do TDD, I need to first add a Test Project to my Solution and then use NuGet to add ASP.NET MVC to that project. I'll also add a reference to my Test Project that points to my ASP.NET MVC project. With all that done, I'm ready to create a TestClass and TestMethod to prove that my action method is doing the right thing. A first cut at my test code would look like something in Listing 1.

Listing 1: A Test for an Action Method
<TestClass()> 
Public Class CustomerManagementTests

  <TestMethod()>
  Public Sub UpdateCreditStatusTest()
    Dim cmc As New CustomerManagementController

    Dim res As ViewResult
    res = CType(hc.UpdateCreditStatusTest("A123", OrdersList), ViewResult)
    Assert.IsInstanceOfType(res, GetType(ViewResult))

    Dim cust As Customer
    cust = CType(res.Model, Customer)
    Assert.AreEqual("A", cust.CreditStatus)
  End Sub

What To Test? What Not To Test?
But, in even when working in a TDD kind of way, there are some things in my action method I want to test and some things I don't want to test.

I don't, for example, want to test that the the GetCustomerById method is retrieving the right Customer object or that it's creating/configuring the Customer object correctly -- that's a separate set of tests organized around the CustomerRepository class. And, if I felt that I needed a set of tests to ensure that my CustomerManagementController and my CustomerRepository class could work well together, I'd create a separate set of integration tests to prove that. As I've said in another article, with tests (as with classes), I'm a big fan of the Single Responsibility Principle: One test class should contain the tests for one thing.

But, in this test method, I do want to test that my code works with a variety of different customers: Does my action method work well when no Customer object is found? Does my code do the right thing with customers in specific credit statuses? It may be that, in the database, there are customers that haven't been assigned a credit status -- does my action method work well with those Customer objects? I also want to be sure that, should my action method fail a test, it's because of the code in my action method and not because of some weird bug in Customer Repository.

I could seed my database with relevant cases but, quite frankly, that's going to require me to maintain tighter control over my database than for which I have time. After all, as part of enhancing the action method, I could pull these tests out a year from now and I still want them to be viable -- it's probably not reasonable to assume my test data will still be there.

There are a couple of solutions to this problem. I could, for example add a TestInitialize method to my test suite to insert some sample data into the database and then use a TestCleanup method to remove the data afterward. That solution has some risks: My inserted test data might conflict with existing data ("Customer A123 already exists"), for example. And, by adding database access code, this solution will definitely slow down my tests. The problem here is that, when my tests slow me down, I stop testing as often as I should. Finally, by inserting test data into the database, I'm still dependent on CustomerRepository (and whatever classes it uses) to do the right thing. If my test fails, I can't be absolutely sure that it's my action method's fault.

The right answer is to take CustomerRepository out of the testing path -- to isolate my action method from CustomerRepository.

Isolating Your Action Methods
There are a couple of ways I can do that. One solution is to use a mole, which rewrites my compiled code to bypass the "real" CustomerRepository and redirects my action method to some replacement code. I've discussed Microsoft's own Fakes, which lets you use moles to isolate test code, but that's only available in more expensive versions of Visual Studio. If you're interested in a third-party product that you'll have to pay for, there's also TypeMock Isolator, which I've reviewed.

Assuming you'd prefer a more cost-effective (read: "free") solution, then you're going to be looking at one of the mocking solutions (for example, Moq or Rhino Mocks, among others). However, to use those solutions, you'll need to structure your controller to support the mock objects they produce. That could take up to 15 minutes.

Isolating Your Code with Moq
Before I look at strategies for setting up your controller to accept a mock object, here's how to create a mock object using Moq. The first step is to create a mock object based on a class or an interface. This example uses the CustomerRepository class itself:

Dim custRepMock As New Mock(Of CustomerRepository)

The second step is to pass your Mock object's Setup method a lambda expression that specifies a method on the repository along with the values you want to pass to the method as part of your test. Once you've identified the method and its parameters, you next use Moq's Returns method to specify the value you want returned to use in your test. Here's all the code you need to create a mock for the CustomerRepository's GetCustomerById method that returns a null value when passed the value A123:

custRepMock.Setup(Function(cr) cr.GetCustomerById("A123")).Returns(Nothing)

In C#, the equivalent code would look like this:

custRepMock.Setup(cr => cr.GetCustomerById("A123")).Returns(null);

However, using Moq places some restriction on the method you're mocking: First, the method must be declared as virtual (C#) or Overridable (Visual Basic) and, second, must not be declared as a static (C#) or Shared (Visual Basic) method. If I have access to the CustomerRepository class, slapping the virtual or Overridable keyword on the method probably won't create any problems for clients using that code. However, removing the Shared/static keyword will be a breaking change for every client using the repository. If, for example, you look back in my original controller method, you can see that I didn't bother instantiating my CustomerRepository class in my action method, indicating that GetCustomerById is a Shared/static method.

Fortunately, there is a workaround for both of these problems: Create a wrapper class that makes the CustomerRepository class acceptable to Moq. That class would look something like this:

Public Class CustomerRepositoryAdapter
  Public Overridable Function GetCustomerById(custId As String) As Customer
    Return CustomerRepository.GetCustomerById(custId)
  End Function
End Class

You can now rewrite your controller class to use your adapter:

Dim cra As New CustomerRepositoryAdapter
cust = cra.GetCustomerById(CustId)

Other code that's outside of your control can still continue to use the static/Shared method. You'll also need to rewrite the first line of your Moq code to use the adapter:

Dim custRepMock As New Mock(Of CustomerRepositoryAdapter)

As I said, I feel I need more complicated test methods than just that one that returns null. I want, for example, to test with Customer objects with a variety of credit status codes. To support that, I add an additional method to my test class:

Private Function ReturnCustomerStatusX() As Customer
  Dim cust As New Customer
  cust.Id = "A123"
  cust.FirstName = "Peter"
  cust.CustCreditStatus = "X"
  Return cust
End Function

I can then write calls to my mock's Setup method to use this method (in C#, the AddressOf keyword is unnecessary):

custRepMock.Setup(Function(cr) cr.GetCustomerById("A123")).Returns(
  AddressOf ReturnCustomerStatusX)

Integrating the Mock Object
Of course, it's no help to set up a mock if your code won't use it -- and, as it's currently written, my action method is irretrievably wired to my CustomerRepository class. If my code was using a dependency injection manager (and I think Microsoft's Managed Extensibility Framework would be a good choice here) I might be able to convince it to select my mock instead of my real object.

However, it's probably just as easy to add constructors to my controller class that let me specify the class to be used when I need the CustomerRepository. To do that, I add two constructors to my controller: one constructor accepts no parameters and instantiates the CustomerRepositoryAdapter already in my production code; the other constructor accepts a replacement adapter … including the mock ones that I want to use for testing. Both constructors store the adapter in a field that I'll use in my action method. The start of that controller with the rewritten action method would look something like the code in Listing 2.

Listing 2: An ASP.NET MVC Controller Set Up for Unit Testing
Public Class CustomerManagementController
  Inherits System.Web.MVC.Controller

  Private cra As CustomerRepositoryAdapter
  Public Sub New(cr As CustomerRepositoryAdapter)
    Me.cra = cr
  End Sub
  Public Sub New()
    Me.cra = New CustomerRepositoryAdapter
  End Sub

  Public Function UpdateCreditStatus(CustId As String, 
    Orders As List<SalesOrder>) As Action Result
  Dim cust As Customer 
  cust = cra.GetCustomerById(CustId)

The start of my test code will now have to pass my mock object to the controller's constructor when instantiating the controller. That mock object is available through the Object property of my Mock variable. As a result, the start of my test method now looks like this:

<TestMethod()>
Public Sub UpdateCreditStatusTest()
  Dim custRepMock As New Mock(Of CustomerRepositoryAdapter)
  custRepMock.Setup(Function(cr) cr.GetCustomerById("A123")).Returns(
    AddressOf ReturnCustomerStatusX)
  Dim cmc As New CustomerManagementController(custRepMock.Object)

As I build out my test class, I can create more methods like ReturnCustomerStatusX to supply inputs for all of test cases. Eventually, I build up a library of test methods that I can use in other tests.

But I won't try to claim that putting your ASP.NET MVC controllers under test is painless: As you've seen, you'll probably have to mark methods in the classes your action methods call with the virtual or Overridable keywords; if you've been using static/Shared methods, you'll have to create an adapter class and integrate that into your action methods. Finally, you'll need to set up your controller so that you can use your mock methods. That's 10 to 15 minutes of your life you'll never get back again (though, if you're willing to upgrade your copy of Visual Studio to use Fakes or pay for TypeMock Isolator, you can avoid that).

But, on the other hand, you'll be able to prove line-by-line, step-by-step, that your code does what it's supposed to do. And, I suspect, that's worth something to you.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube