Skip to content
  • iImagine
  • Register
  • Log In

Web Development School

Learning made easy.

  • Books
    • Beginning Web Development with ASP.Net Core & Client-Side Technologies
      • TOC
      • Part 1
        • Chapter 1: Static HTML – Designing the landing page
      • Part 2
        • Chapter 2: ASP.Net Core – Let’s talk Dynamic
        • Chapter 3: Introduction to ASP.Net Core MVC
          [ASP.Net Core v9]
      • Part 4
        • Chapter 7: Using Server Side & Client Side technologies together
          [ASP.Net Core v7 & Angular 15]
  • Environment Setup
    • Installing Angular
    • Installing Visual Studio 2022
    • Installing SQL Server 2022 Express
    • Installing Postman
    • Installing Git for Windows
  • Blog
  • iImagine WebSolutions
  • Events
  • Learning Videos
  • Toggle search form

Unit Tests: Sorting

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.

Note: The pathway I am using for development in this module is: C:\Development\FredsCars\FullStack\Module28.
Table Of Contents
  1. Unit Test Sorting in ApiResult
    • Test sort ascending
    • Test sort descending
  2. Unit Test Sorting in the Vehicles controller
    • Test sort ascending
    • Test sort descending
  3. Corrections to ApiReslt
  4. What's Next

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.

< Prev
Next >

Leave a ReplyCancel reply

Chapter 1: Static HTML – Designing the landing page.

  • Static HTML – Designing the landing page.
  • Let’s get started!
  • Mock your site with HTML
  • Make CSS easy with Bootstrap
  • Mock your content
  • Introducing JavaScript
  • JavaScript Code Improvements
  • Results Data
  • Images and the HTML Image Element.
  • Revisiting Reusability for CSS and JavaScript
  • Reuse for HTML: PART 1
  • Reuse for HTML: PART 2
  • Details Page – Using a Bootstrap Component
  • Creating Links
  • Chapter One Conclusion

Chapter 2: ASP.Net Core – Let’s talk Dynamic

  • Introduction to ASP.Net Core
  • What is .Net?
  • What is ASP.Net
  • Introduction to Entity Framework Core

Chapter 3: ASP.Net MVC Core – Models, Views, and Controllers [ASP.Net Core v9]

  • Introduction to ASP.Net Core MVC
  • Create the project: ASP.Net Core MVC
  • Explore the ASP.Net Core Empty Web Project Template
  • Configure the Application for MVC
  • Create a Controller: Home Controller
  • Create a View: Index View for the Home Controller
  • Install Bootstrap using Libman
  • Create the Layout template
  • Create the Model
  • Install EF Core & Create the Database
  • Seed the Database: Loading test data
  • DI (Dependency Injection): Display a List of Vehicles
  • Repository Pattern: The Vehicles Repo
  • Unit Test 1: Home Controller Can Use Vehicle Repository
  • Unit Test 2: Vehicle Repository Can Return List
  • Add the ImagePath Migration and Thumbnail images to results
  • Pagination: Create a Custom Tag Helper
  • Sorting
  • Category Filter
  • Partial View: Break out the vehicle results
  • View Component: Create dynamic category buttons
  • Create the Details page
  • Create the Create Page

Chapter 7: Using Server Side & Client Side technologies together. [ASP.Net Core v7 & Angular v15]

  • Intro to Full Stack Development
  • Fred’s Cars – Full Stack Development
  • Prepare the environment
  • Create the Visual Studio Solution
  • Add the ASP.Net Core Web API project
  • Add the Angular Project
  • Wire it up!
  • WeatherForecast: Understanding the basics
  • Vehicles API Controller: Mock Data
  • Vehicles Angular Component: Consuming Data
  • Routing and Navigation
  • Using a Component Library: Angular Material
  • Our first Angular Material Component: MatToolbar
  • Configuring for Saas: CSS with superpowers
  • Create the Header & Footer components
  • Displaying Results with MatTable
  • Loading: Using a Progress Spinner
  • MatTable: Client-Side Paging and Sorting
  • MatSidenav: Create a Search Sidebar
  • MatCheckbox: Category Search UI
  • Adding an image to the welcome page
  • Create the database with Entity Framework Core migrations
  • MatPaginator & PageEvent: Custom Server-Side Paging
  • Unit Testing: Custom Server-Side Paging
  • Repository Pattern: VehicleRepository
  • Unit Test: Paging in the Vehicles controller
  • Server-Side Sorting
  • Unit Tests: Sorting
  • Filter (Quick Search)
  • Unit Tests: Filter feature
  • Advanced Search: Categories
  • Unit Tests: Search by category
  • Progress Spinner: Final Fix

TOC

  • What were WebForms?
  • Enter MVC
    • Understanding MVC
    • Advantages of MVC
  • ASP.Net Core MVC – A total rewrite
  • ASP.Net Core 2 MVC – Here come Razor Pages
    • Understanding Razor Pages
  • ASP.Net Core 3 – Dropping the MVC reference
    • Understanding Blazor
  • Dropping the MVC reference
  • Hello .Net 5!
  • What’s Next? – Here comes .Net 6.

Recent Posts

  • Angular Commands Cheat Sheet
  • Installing Git for Windows
  • Installing Postman
  • Installing SQL Server 2022 Express
  • Installing Visual Studio 2022

Recent Comments

No comments to show.

Archives

  • November 2023
  • October 2023
  • June 2023
  • October 2021

Categories

  • Angular
  • ASP.Net
  • Environment Setup
  • See All
  • SQL Server
  • Visual Studio
  • Web API & Rest Services

WordPress Theme Editor

Copyright © 2025 Web Development School.

Powered by PressBook Blog WordPress theme