Introduction
An important aspect of unit testing is being able to test how your code handles responses from services. While dependency injection allows you to inject your services into your classes, you don't want your unit tests directly calling those services, especially those making HTTP requests or database calls.
To circumvent this, you want to be able to mock your services so you can simulate them returning responses without actually calling them. Fortunately, the Moq framework allows you to be able to do such a thing, and it's extremely easy to setup. In this lesson, we shall cover the Moq framework and how you can use it to simulate your services while unit testing.
Installation and Sample Code
To install Moq, you can install it within your unit test project from the NuGet Package Manager or use the following command from the Package Manager Console:
> Install-Package Moq
Once installed, you are ready to start using Moq! Let's take an example of some code and how we can use Moq to our advantage:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MyProject
{
/// <summary>
/// A class to denote students in a system
/// </summary>
public class Student
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public double GradeAverage { get; set; }
}
/// <summary>
/// Enumeration to represent a grade a student may obtain
/// </summary>
public enum Grade
{
A,
B,
C,
D,
F
}
/// <summary>
/// A repository containing all the students in a system
/// </summary>
public interface IStudentRepository
{
/// <summary>
/// Returns a student by an ID value
/// </summary>
/// <param name="id">Numeric ID value of student to pull</param>
/// <returns>If student with ID supplied exists in the repository, it return such a Student with Id. If no student in repository has such Id, it returns null.</returns>
public Student GetStudentById(int id);
/// <summary>
/// Asynchronously returns all the students in the repository
/// </summary>
/// <returns>A list of students in the repository</returns>
public Task<List<Student>> GetStudentsAsync();
}
public class StudentRepository : IStudentRepository
{
public Student GetStudentById(int id)
{
// Make a DB call to get a student by ID
throw new System.NotImplementedException();
}
public Task<List<Student>> GetStudentsAsync()
{
// Make a DB call to get all student by ID
throw new System.NotImplementedException();
}
}
public class StudentService
{
private readonly IStudentRepository _repository;
public StudentService(IStudentRepository repository)
{
_repository = repository;
}
/// <summary>
/// Determines the grade value of a student based on their GradeAverage score.
/// </summary>
/// <param name="id">An Id value of a student in the system</param>
/// <returns>Returns a Grade enumeration value of the student if student exists. If no student in the repository contains Id value provided, an Argument exception is thrown.</returns>
public Grade GetStudentGrade(int id)
{
var student = _repository.GetStudentById(id);
if (student == null)
throw new ArgumentException("Could not find student with that ID.", nameof(id));
if(student.GradeAverage >= 90)
{
return Grade.A;
} else if (student.GradeAverage >= 80)
{
return Grade.B;
} else if (student.GradeAverage >= 70)
{
return Grade.C;
} else if (student.GradeAverage >= 60)
{
return Grade.D;
} else
{
return Grade.F;
}
}
/// <summary>
/// Asynchronously returns a list of students in order by their last name.
/// </summary>
/// <returns>A list of students organized by their last name, ascending.</returns>
public async Task<List<Student>> GetStudentsInAlphabeticalOrderAsync()
{
var students = await _repository.GetStudentsAsync();
return students.OrderBy(p => p.LastName).ToList();
}
}
}
Testing the Synchronous Methods
To verify the code of our StudentService class, we want to be able to write a unit test against it, but we don't want to depend on the StudentRepository class doing any work. Of course, in this example, none of the StudentRepository methods have been implemented, but assuming they have, we don't want our unit tests making any actual database calls. Therefore, we must mock responses for the IStudentRepository classes in our unit tests. But, with Moq, this isn't a problem.
Consider the following code:
namespace MyProject.Tests
{
public class Tests
{
private StudentService _service;
[SetUp]
public void Setup()
{
}
[Test]
public void ShouldCalculateStudentGradeCorrectly()
{
var student = new Student
{
Id = 1,
FirstName = "John",
LastName = "Doe",
GradeAverage = 81.5
};
var mockStudentService = new Mock<IStudentRepository>();
mockStudentService.Setup(p => p.GetStudentById(It.IsAny<int>())).Returns(student);
_service = new StudentService(mockStudentService.Object);
var grade = _service.GetStudentGrade(123);
Assert.AreEqual(Grade.B, grade);
}
}
}
Within the ShouldCalculateStudentGradeCorrectly
method, we've created a Student
object to simulate our repository returning, and then in the next line we instantiate a Mock version of the IStudentRepsitory
interface called mockStudentService
, and then on mockStudentService
we setup the GetStudentById
method, with any integer provided, to return our Student
variable.
With the It.IsAny<T>
declared within the variable, the Moq framework tells our method that any value of type T
will return the Student
value. Therefore, we could provide the value 123 to the GetStudentGrade
method, and, no matter what, our IStudentRepository
interface would return the Student
value, whose Id is 1.
With this configuration, we know the code will always return Student
with Id = 1 and GradeAverage = 81.5. Because 81.5 is less than 90.0, but greater than or equal to 80.0, we should expect our code to return a Grade enum of B. And, based on our unit test passing, this seems to be the case.
We can be more specific with our inputs that using the generic It.IsAny<int>
with Moq. If we want to specify that, for only the integer 1, we want student with Id of 1 to come back from the service (as it would in an actual instance), we can do so. This will also allow our unit test to explore instances in which an Id that doesn't exist in our repository to be called, and we can therefore test edge cases.
Let's consider the following piece of code:
namespace MyProject.Tests
public class Tests
{
private StudentService _service;
[Test]
public void ShouldThrowExceptionIfStudentIdNotExist()
{
var student = new Student
{
Id = 1,
FirstName = "John",
LastName = "Doe",
GradeAverage = 81.5
};
var mockStudentService = new Mock<IStudentRepository>();
mockStudentService.Setup(p => p.GetStudentById(1)).Returns(student);
_service = new StudentService(mockStudentService.Object);
// Confirm that if we pass 1, we get our expected value
var grade = _service.GetStudentGrade(1);
Assert.AreEqual(Grade.B, grade);
// If we pass in a value not defined to Moq, its result will be treated as null.
Assert.Throws<ArgumentException>(() => _service.GetStudentGrade(123));
}
}
}
In this scenario, we specify that if our GetStudentById
method on the mockkStudentService
service receives a parameter value of 1
, it will return the Student
object whose Id = 1. And, we confirm that when we do pass the value of 1 to that method that we still get our Grade.B
result as expected.
So, what happens if we try to pass in the 123
value now as our parameter? Well, to our Moq framework, this value isn't yet defined. Therefore, when the GetStudentById
method is called with this input, a value of null
is returned. Thus, our unit test is able to explore the edge case scenario and hit the null-checking portion of the GetStudentGrade
method in the StudentService class, and we can verify that an ArgumentException is thrown when no student with such Id exists.
Testing the Asynchronous Functions
So far, we've looked at how we can test the synchronous functions within the Moq testing framework, but what about testing the asynchronous functions? Fortunately, we are still able to do so.
Let's consider the following code in a unit test project:
namespace MyProject.Tests
{
public class Tests
{
private StudentService _service;
[Test]
public async Task ShouldSortStudentsInAlphabeticalOrder()
{
var students = new List<Student>()
{
new Student()
{
Id = 1,
FirstName = "Jacob",
LastName = "Jingleheimerschmidt"
},
new Student()
{
Id = 2,
FirstName = "John",
LastName = "Appleseed"
},
new Student()
{
Id = 3,
FirstName = "Zig",
LastName = "Zag"
}
};
var mockStudentService = new Mock<IStudentRepository>();
mockStudentService.Setup(p => p.GetStudentsAsync()).Returns(Task.FromResult(students));
_service = new StudentService(mockStudentService.Object);
var result = await _service.GetStudentsInAlphabeticalOrderAsync();
// Verify all 3 Student values are turned from the function
Assert.AreEqual(result.Count, 3);
// Verify the order of the last names is as expected (item 0 = Appleseed (Id = 2), item 1 = Jingleheimerschmidt (Id = 1), item 2 = Zag (Id = 3))
Assert.AreEqual(result[0].Id, 2);
Assert.AreEqual(result[1].Id, 1);
Assert.AreEqual(result[2].Id, 3);
}
}
}
If our asynchronous method GetStudentsAsync
required any parameters, we could still use the It.IsAny<T>
code and still supply it as generic parameters, but in this case no parameters are needed. Truly, the only important item to note is the Task.FromResult()
within the Returns
method of the GetStudentsAsync
method. For asynchronous functions, you can declare that your mocked values are the result of a task and achieve the same result you would for mocking synchronous methods.
If we run our unit test code, we see that the GetStudentsInAlphabeticalOrderAsync
does fetch all 3 Student
values available in the repository and does order the results according by the Student
last name, as we can tell by the ordering of the Id
values.
Conclusion
In this lesson, we can see how easy it is to setup a mock interface for a unit test to work with using the Moq framework, and we have explored how to mock up responses for both synchronous and asynchronous functions. With Moq, we can easily be able to simulate how our code can handle responses from dependencies without actually needing to call upon their concrete implementations.
This lesson only provides a simple overview of Moq, but there's a lot more to Moq than just what we've covered. For example, it's good to take a look at the callback functionality of Moq or just the documentation from Moq's project page.
Thanks for reading!
Top comments (0)