In the last module we were able to easily piggy back off the paging architecture and add in sorting. Now it should also be fairly easy to add in some sorting unit tests just as we did for paging once we completed that feature.
Unit Test Sorting in ApiResult
Open the ApiResultTests.cs file and modify it with the contents shown below in bold blue font.
FredsCarsAPI.Tests/Data/ApiResultTests.cs
using FredsCarsAPI.Data;
using MockQueryable.Moq;
namespace FredsCarsAPI.Tests.Data
{
public class TestPageData
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public class ApiResultTests
{
private List<TestPageData> _data = new List<TestPageData>()
{
new TestPageData { Id = 1, Name = "Sam", },
new TestPageData { Id = 2, Name = "George" },
new TestPageData { Id = 3, Name = "Scott" },
new TestPageData { Id =4, Name = "James" },
new TestPageData { Id =5, Name = "Melissa" },
new TestPageData { Id =6, Name = "Ferris" },
new TestPageData { Id =7, Name = "Debrah" },
new TestPageData { Id =8, Name = "John" },
new TestPageData { Id =9, Name = "Greg" },
new TestPageData { Id =10, Name = "Sarah" },
new TestPageData { Id = 11, Name = "Mike" },
new TestPageData { Id = 12, Name = "Larry" },
new TestPageData { Id = 13, Name = "Emily" },
new TestPageData { Id = 14, Name = "Jane" }
};
/*** existing code ***/
#region sorting tests
[Fact]
public async Task CanSortAscending()
{
// Arrange
// 1- Create the mock DbSet from the test data
var dataDbSet = _data.AsQueryable().BuildMockDbSet();
// 2- Convert the DbSet to an IQueryable interface
var dataIQueryable = dataDbSet.Object.AsQueryable();
// Act
var response = await ApiResult<TestPageData>.CreatAsync(
dataIQueryable, 2, 4, "Name", "asc");
// Assert
// 1st and last data ids of page 3 (pageIndex of 2 + 1)
// should be 9 and 12 from testPageData.
Assert.True(response.Data[0].Id == 12
&& response.Data[0].Name == "Larry"
&& response.Data[3].Id == 1
&& response.Data[3].Name == "Sam");
}
[Fact]
public async Task CanSortDescending()
{
// Arrange
// 1- Create the mock DbSet from the test data
var dataDbSet = _data.AsQueryable().BuildMockDbSet();
// 2- Convert the DbSet to an IQueryable interface
var dataIQueryable = dataDbSet.Object.AsQueryable();
// Act
var response = await ApiResult<TestPageData>.CreatAsync(
dataIQueryable, 2, 4, "Name", "desc");
// Assert
// 1st and last data ids of page 3 (pageIndex of 2 + 1)
// should be 9 and 12 from testPageData.
Assert.True(response.Data[0].Id == 4
&& response.Data[0].Name == "James"
&& response.Data[3].Id == 6
&& response.Data[3].Name == "Ferris");
}
#endregion
}
}
In the code above, I started by giving all the items in the test data more realistic names we can test sorting by.
Test sort ascending
The first test I created is called CanSortAscending()
to see if we can indeed sort in ascending order.
In the arrange section I created a mockDbSet from our test data and converted into a mock IQueryable just as we did or paging.
In the act section I called the ApiResult factory method and pass it the mocked IQueryable, paging info, and new sort parameters; “Name”, and “asc”.
In the assert section I assert that the Ids and Names are the expected result when looking at our test data.
The sorted list of names from our test data looks like the following:
George
Greg
James
Jane
John
Larry
Melissa
Mike
Sam
Sarah
Scott
Here we can see that the third page down of four records each should be Larry with an Id of 12 from the test data and three items down from that should be Sam with an Id of 1.
Test sort descending
The second test I created is called CanSortDescending()
.
This test is almost identicle to the first except that I pass “desc” to the ApiResult factory method’s sortOrder parameter and look for matches in the assert statement in the test data starting from the bottom of the results and working towards the top rather then top to bottom as in the fist test.
Unit Test Sorting in the Vehicles controller
Open the VehiclesControllerTests.cs file and modify it’s contents with the code shown below in bold blue font.
FredsCarsAPI.Tests/Controllers/VehiclesControllerTests.cs
using Moq;
using MockQueryable.Moq;
using FredsCarsAPI.Controllers;
using FredsCarsAPI.Repositories;
using FredsCarsAPI.Models;
using FredsCarsAPI.Models.DTOs;
using FredsCarsAPI.Data;
namespace FredsCarsAPI.Tests.Controllers
{
public class VehiclesControllerTests
{
private List<Vehicle> _testData =
new List<Vehicle>
{
new Vehicle { Id = 1,
Status = Status.New,
Year = "2022",
Make = "M1",
Model = "M1",
Color = "C1",
Price = 64000,
VIN = "123",
VehicleTypeId = 1,
VehicleType = new VehicleType { Id = 1, Name = "Car" }
},
new Vehicle { Id = 2,
Status = Status.New,
Year = "2022",
Make = "M2",
Model = "M2",
Color = "C2",
Price = 64000,
VIN = "456",
VehicleTypeId = 2,
VehicleType = new VehicleType { Id = 2, Name = "Truck" }
},
new Vehicle { Id = 3,
Status = Status.New,
Year = "2022",
Make = "M3",
Model = "M3",
Color = "C3",
Price = 64000,
VIN = "789",
VehicleTypeId = 3,
VehicleType = new VehicleType { Id = 3, Name = "Jeep" }
},
new Vehicle { Id = 4,
Status = Status.New,
Year = "2022",
Make = "M4",
Model = "M4",
Color = "C4",
Price = 64000,
VIN = "012",
VehicleTypeId = 1,
VehicleType = new VehicleType { Id = 1, Name = "Car" }
},
new Vehicle { Id = 5,
Status = Status.New,
Year = "2022",
Make = "M5",
Model = "M5",
Color = "C5",
Price = 64000,
VIN = "345",
VehicleTypeId = 2,
VehicleType = new VehicleType { Id = 2, Name = "Truck" }
},
new Vehicle { Id = 6,
Status = Status.New,
Year = "2022",
Make = "M6",
Model = "M6",
Color = "C6",
Price = 64000,
VIN = "678",
VehicleTypeId = 3,
VehicleType = new VehicleType { Id = 3, Name = "Jeep" }
},
new Vehicle { Id = 7,
Status = Status.New,
Year = "2022",
Make = "M7",
Model = "M7",
Color = "C7",
Price = 64000,
VIN = "901",
VehicleTypeId = 1,
VehicleType = new VehicleType { Id = 1, Name = "Car" }
},
new Vehicle { Id = 8,
Status = Status.New,
Year = "2022",
Make = "M8",
Model = "M8",
Color = "C8",
Price = 64000,
VIN = "234",
VehicleTypeId = 2,
VehicleType = new VehicleType { Id = 2, Name = "Truck" }
},
new Vehicle { Id = 9,
Status = Status.New,
Year = "2022",
Make = "M9",
Model = "M9",
Color = "C9",
Price = 64000,
VIN = "567",
VehicleTypeId = 3,
VehicleType = new VehicleType { Id = 3, Name = "Jeep" }
},
new Vehicle { Id = 10,
Status = Status.New,
Year = "2022",
Make = "M10",
Model = "M10",
Color = "C10",
Price = 64000,
VIN = "890",
VehicleTypeId = 1,
VehicleType = new VehicleType { Id = 1, Name = "Car" }
},
new Vehicle { Id = 11,
Status = Status.New,
Year = "2022",
Make = "M11",
Model = "M11",
Color = "C11",
Price = 64000,
VIN = "abc",
VehicleTypeId = 2,
VehicleType = new VehicleType { Id = 2, Name = "Truck" }
},
new Vehicle { Id = 12,
Status = Status.New,
Year = "2022",
Make = "M12",
Model = "M12",
Color = "C12",
Price = 64000,
VIN = "def",
VehicleTypeId = 3,
VehicleType = new VehicleType { Id = 3, Name = "Jeep" }
},
new Vehicle { Id = 13,
Status = Status.New,
Year = "2022",
Make = "M13",
Model = "M13",
Color = "C13",
Price = 64000,
VIN = "ghi",
VehicleTypeId = 1,
VehicleType = new VehicleType { Id = 1, Name = "Car" }
},
new Vehicle { Id = 14,
Status = Status.New,
Year = "2022",
Make = "M14",
Model = "M14",
Color = "C14",
Price = 64000,
VIN = "jkl",
VehicleTypeId = 2,
VehicleType = new VehicleType { Id = 2, Name = "Truck" }
},
};
/*** existing code ***/
#region Sorting Tests
[Fact]
public async Task CanSortAscending()
{
// Arrange
// Create the mock from the test data
Mock<IVehicleRepository> mockVehicleRepo =
new Mock<IVehicleRepository>();
var mockVehicleIQueryable =
_testData.AsQueryable().BuildMock();
mockVehicleRepo.Setup(m => m.Vehicles).Returns(mockVehicleIQueryable);
var controller = new VehiclesController(mockVehicleRepo.Object);
// Act
ApiResult<VehicleDTO> result =
await controller.GetVehicles(3, 3, "vehicleType", "asc");
// Assert
Assert.True(result.Data[0].Id == 2
&& result.Data[0].VehicleType == "Truck");
Assert.True(result.Data[2].Id == 8
&& result.Data[2].VehicleType == "Truck");
}
[Fact]
public async Task CanSortDescending()
{
// Arrange
// Create the mock from the test data
Mock<IVehicleRepository> mockVehicleRepo =
new Mock<IVehicleRepository>();
var mockVehicleIQueryable =
_testData.AsQueryable().BuildMock();
mockVehicleRepo.Setup(m => m.Vehicles).Returns(mockVehicleIQueryable);
var controller = new VehiclesController(mockVehicleRepo.Object);
// Act
ApiResult<VehicleDTO> result =
await controller.GetVehicles(3, 3, "vehicleType", "desc");
// Assert
// Should get back Ids 13 - 7
Assert.True(result.Data[0].VehicleType == "Car");
Assert.True(result.Data[2].VehicleType == "Car");
}
#endregion
}
}
Test sort ascending
The fist test we added for the Vehicles controller is also called CanSortAscending()
to see if the controller can indeed sort our test data in ascending order. Here we try to sort by vehicleType which is a good test because it will also test the extension method to convert Vehicles in the IQueryable to VehcileDTOs and then try to sort on the vehicleType string after the conversions.
This test is very similar to the CanSortAscending()
test for ApiResult but we have to use real Vehicle objects rather then TestPageData objects because the controller will throw an exception when trying to convert TestPageData to VehicleDTO objects without a VehicleType property.
Test sort descending
CanSortDescending()
is also very similar to CanSortAscending()
in the Vehicle controller except we pass “desc” instead of “asc” to the Vehicle controller’s HTTP GET method, GetVehicles()
.
Corrections to ApiReslt
I just want to note here that I made a few little corrections to the ApiResult class while creating the paging and sorting unit tests.
I realized that while we do indeed need the paging info returned in the JSON result to the client, we don’t really need to return the sorting info in the JSON result.
The reason why is because the client sends the pageIndex and pageSize parameters upstream to the controller, and the controller passes these to ApiReslt. Next ApiResult returns PageIndex and PageSize properties to the controller and the contoller sends pageIndex and pageSize properties in JSON format back downstream to the client. The client then uses these properties to set the current Page Index and Page Size properties for MatPaginator after each page request. In other words the paging information is needed to go upstream and downstream.
With sorting, on the other hand, while MatSort sends sorting information upstream to the Vehicle contoller and the controller then passes this information to ApiResult, we were not doing anything with the information once it was passed back to the client. The MatSort component keeps track of its own state and knows if it just sorted on “asc”, the next click should be “desc”, or the third “” empty click state which we correct in the Angular Vehicle component’s TypeScript in getVehicleData()
The following code is the complete ApiResult.cs file as it stands at the end of this module.
FredsCarsAPI/Data/ApiResult.cs
using Microsoft.EntityFrameworkCore;
using System.Reflection;
using System.Linq.Dynamic.Core;
namespace FredsCarsAPI.Data
{
public class ApiResult<T>
{
private ApiResult(
List<T> data,
int count,
int pageIndex,
int pageSize
//string? sortColumn,
//string? sortOrder
)
{
Data = data;
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = count;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
// SortColumn = sortColumn;
// SortOrder = sortOrder;
}
// factory method
public static async Task<ApiResult<T>> CreatAsync(
IQueryable<T> source,
int pageIndex,
int pageSize,
string? sortColumn = null,
string? sortOrder = null
)
{
var count = await source.CountAsync();
// sorting
if (!string.IsNullOrEmpty(sortColumn)
&& IsValidProperty(sortColumn))
{
sortOrder = !string.IsNullOrEmpty(sortOrder)
&& sortOrder.ToUpper() == "ASC"
? "ASC"
: "DESC";
// Dynamic LINQ Query
source = source.OrderBy(
string.Format(
"{0} {1}",
sortColumn,
sortOrder)
);
}
// paging
source = source
.Skip(pageIndex * pageSize)
.Take(pageSize);
var data = await source.ToListAsync();
return new ApiResult<T>(
data,
count,
pageIndex,
pageSize
// sortColumn,
// sortOrder
);
}
// SQL Injection Guard
public static bool IsValidProperty(
string propertyName,
bool throwExceptionIfNotFound = true)
{
var prop = typeof(T).GetProperty(
propertyName,
BindingFlags.IgnoreCase |
BindingFlags.Public |
BindingFlags.Instance);
if (prop == null && throwExceptionIfNotFound)
throw new NotSupportedException(
string.Format(
$"ERROR: Property '{propertyName}' does not exist.")
);
return prop != null;
}
public List<T> Data { get; private set; }
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
// public string? SortColumn { get; set; }
// public string? SortOrder { get; set; }
// total record count
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
}
}
This is yet another reason to unit test. I find that unit testing really helps me uncover bugs I wouldn’t otherwise find by just clicking through the application while it is running. Or, in this case not necessarily a bug, but just extraneous code we don’t need. This process can help us to understand even our own code more clearly and make it more concise. If we had left those two sorting properties in ApiResult, this could be very confusing to other developers coming behind us in the future or even other current programmers on our team. They might be screaming in their head, “What are these two properties for!!!?”. What was the intent of the previous programmer?
What’s Next
In this module we got our unit testing all caught up with our new sorting feature. In the next module we are going to implement a “Filter (quick search)” feature.