26
loading...
This website collects cookies to deliver better user experience
> Install-Package Moq
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();
}
}
}
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);
}
}
}
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.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.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. 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. 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));
}
}
}
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. 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.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);
}
}
}
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. 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.