In any software life-cycle, Testing is an important step for ensuring a good quality software application. Typically, in case of web applications, testing plays an important role. In the software applications, we have the following major testing types:
- Unit Testing is the most popular testing methodology. The unit test helps to test every small unit of the source code. Unit testing is used to mock the dependency of the code so that the individual unit of the code is tested separately without including the dependency in the test code. Typically, in a software application, the unit is a class or a method in a class. If the class has any dependency, then to unit test the code, we mock the dependency to attain loose coupling. Unit Testing can be done by a developer and the developer can test the quality of the code.
- Integration Testing uses an approach where tests includes individual units and all these units are tested as a group. The advantage of the integration testing is to detect and expose faults during an interaction between these integrated units of the software application. Since, the integration testing uses all units in the test, writing integration test is a costlier approach.
- End-to-End Testing is an approach of testing the entire system. In this approach, the testing is implemented by considering every component of the software application right from the UI to the data persistence layer. This guarantees the expected workflow of the software application. The entire application is tested for the critical functionalities such as communication with third party components, databases, networks, etc.
If we consider the number of tests required for software applications, then we can say that we must write several unit tests. How many? As a thumb rule, the overall number of methods written by developers in software applications, should be the number of unit tests you write. Also, if we have conditional statements in these methods, then we have to write one unit test per conditional statement. Cumbersome, but totally worth the cost and time put into it.
This is also the reason why unit tests are the responsibility of the developer or so to say if Test-Driven-Development approach (or user stories and test cases) is understood by the developer, then the code can be bug-free. Figure 1 shows the summary of the testing methodologies.
Figure 1: Testing methodologies summary
In this article, we will be implementing Unit Testing of an ASP.NET Core 3.1 application. We will implement the Unit Test MVC Controller, API Controller by mocking their dependencies.
(Note: Since the article focuses on Unit Testing, the steps for creating the ASP.NET Core application are omitted. You can download the code of an ASP.NET Core application or from the link provided at the end of the article.).
The application has repository service classes which implement the IService<TEntity, in TPk> interface. Using this interface, the repository services classes are registered in dependency container of the ASP.NET Core application and these classes are constructor injected in the MVC and API controllers. Listing 1 shows the repository service interface
public interface IService where TEntity : class
{
Task> GetAsync();
Task GetAsync(TPk id);
Task CreateAsync(TEntity entity);
Task UpdateAsync(TPk id, TEntity entity);
Task DeleteAsync(TPk id);
}
Listing 1: The IService interface
The above interface defines asynchronous methods for performing CRUD operations. The interface is implemented by CategoryService class. The IService<TEntity, in TPk> uses the Category class as type for TEntity type. The Category class is shown in listing 2
public class Category
{
[Key] // primary identity key
public int CategoryRowId { get; set; }
[Required(ErrorMessage ="Category Id is must")]
[StringLength(20)]
public string CategoryId { get; set; }
[Required(ErrorMessage = "Category Name is must")]
[StringLength(200)]
public string CategoryName { get; set; }
[Required(ErrorMessage = "Base Price is must")]
public int BasePrice { get; set; }
public ICollection Products { get; set; } // expected one-to-many relationship
}
Listing 2: The Category class
The CategoryService class is constructor injected with AppJune2020DbContext class.This class is the base class for EntityFrameworkCode (EFCore). The CategoryService class uses AppJune2020DbContext class to perform CRUD operations. All these operations are asynchronous operations. Listing 3 shows code for CategoryService class:
public class CategoryService : IService<Category, int>
{
private readonly IService<Category,int> ctx;
///
/// Inject the AppJune2020DbContext in the Service class using ctor injection
///
public CategoryService(IService<Category,int> ctx)
{
this.ctx = ctx;
}
public async Task CreateAsync(Category entity)
{
var res = await ctx.Categories.AddAsync(entity);
await ctx.SaveChangesAsync();
return res.Entity;
}
public async Task DeleteAsync(int id)
{
var c = await ctx.Categories.FindAsync(id);
if (c != null)
{
ctx.Categories.Remove(c);
await ctx.SaveChangesAsync();
return true;
}
return false;
}
public async Task> GetAsync()
{
return await ctx.Categories.ToListAsync();
}
public async Task GetAsync(int id)
{
var cat = await ctx.Categories.FindAsync(
id);
return cat;
}
public async Task UpdateAsync(int id, Category entity)
{
var c = await ctx.Categories.FindAsync(id);
if (c != null)
{
c.CategoryId = entity.CategoryId;
c.CategoryName = entity.CategoryName;
c.BasePrice = entity.BasePrice;
await ctx.SaveChangesAsync();
}
return entity;
}
}
Listing 3: CategoryService class
The CategoryService class and AppJune2020DbContext classes are registered in dependency container in ConfigureServices() method of the Startup class as shown in listing 4
.......
services.AddDbContext<AppJune2020DbContext>(options => {
options.UseSqlServer(Configuration.GetConnectionString("DbAppConnection"));
});
services.AddScoped<IRepository<Category, int>, CategoryRepository>();
.......
Listing 4: Dependency registration of classes
Since we will be unit testing the ASP.NET Core MVC controller and API Controller, the code of these two classes are shown in listing 5 and listing 6
public class CategoryController : Controller
{
private readonly IService<Category, int> catService;
///
/// Inject the service using Ctor injection
///
public CategoryController(IService<Category, int> catService)
{
this.catService = catService;
}
public async Task Index()
{
var cats = await catService.GetAsync();
return View(cats);
}
public IActionResult Create()
{
return View(new Category());
}
[HttpPost]
public async Task Create(Category category)
{
// validate the model
if (ModelState.IsValid)
{
if (category.BasePrice < 0) throw new Exception("Base Price cannot be -ve");
category = await catService.CreateAsync(category);
return RedirectToAction("Index");
}
return View(category); // stey on Same View with validation error messages
}
public async Task Edit(int id)
{
var cat = await catService.GetAsync(id);
return View(cat);
}
[HttpPost]
public async Task Edit(int id, Category category)
{
try
{
// validate the model
if (ModelState.IsValid)
{
category = await catService.UpdateAsync(id,category);
return RedirectToAction("Index");
}
return View(category); // stey on Same View with validation error messages
}
catch (Exception ex)
{
// redirect to error view
return View("Error");
}
}
}
Listing 5: MVC CategoryController [Route("api/[controller]")]
[ApiController]
public class CategoryAPIController : ControllerBase
{
private readonly IService<Category, int> catService;
public CategoryAPIController(IService<Category, int> catService)
{
this.catService = catService;
}
[HttpGet]
public async Task GetAsync()
{
var cats = await catService.GetAsync();
return Ok(cats);
}
[HttpGet("id")]
public async Task GetAsync(int id)
{
var cat = await catService.GetAsync(id);
if (cat == null) return NotFound($"Category based on Category Row Id {id} is removed");
return Ok(cat);
}
[HttpPost]
public async Task PostAsync(Category category)
{
if (ModelState.IsValid)
{
if (category.BasePrice < 0) throw new Exception("Base Price is wrong");
var cat = await catService.CreateAsync(category);
return Ok(cat);
}
return BadRequest(ModelState);
}
[HttpPut("id")]
public async Task PutAsync(int id, Category category)
{
if (ModelState.IsValid)
{
var cat = await catService.UpdateAsync(id,category);
return Ok(cat);
}
return BadRequest(ModelState);
}
[HttpDelete("id")]
public async Task DeleteAsync(int id)
{
var res = await catService.DeleteAsync(id);
return Ok(res);
}
}
Listing 6: The CategoryAPIController
To write unit tests, we will use xUnit and the Moq frameworks. xUnit is a free, open source unit testing tool for .NET Framework applications. More information about xUnit can be read from this link. Moq is a mocking framework. More information about Moq can be found from this link.
To add a Unit Test project in the current solution, right-click on the solution and select Add > New Project, from the Add a new Project window select xUnit Test Project (.NET Core) as shown in Figure 2
Figure 2: Adding New xUnit Test Project
Name this project as CoreNetAppTest. Expand the Project dependencies, it will show the packages used in the test project as shown in figure 3.
Figure 3: List of packages referred in the project
The project referrers xunit package, this provides the access of the xunit object model to test ASP.NET Core applications. The Microsoft.Net.Test.Sdk package is the MSBuild targets and properties for building .NET Test projects. The xunit.runner.visualstudio package is a Test Explorer for xUnit.net framework. This is integrated with Visual Studio 2017 15.9+ update and Visual Studio 2019 version. This test runner shows the status of the tests.
As shown in figure 3, we have the UnitTest1.cs file. This is a class file containing the Test class and a test method. The test method is applied with the Fact attribute. This attribute represent the parameterless unit test.
To use xUnit to test the CategoryController, we need to add a reference of the ASP.NET Core Project in the test project. Right-Click on the Dependencies of the Test project and select Add project reference as shown in figure 4
Figure 4: Adding reference of the ASP.NET Core project
This will show the Reference Manager window from where we can select the project of which reference is to be added. Select the project as shown in the figure 5
Figure 5: Adding Project Reference
Since the Controllers (MVC and API) are constructor injected using IService<TEntity, in TPk> type, to instantiate the Controller class, we need to mock the IService interface. To perform object mocking we need to install the Moq package in the project as shown in the figure 6
Figure 6: Installing the Moq package
As seen in Figure 3, rename UnitTest1.cs to CategoryControllerTest.cs. We will use this class file to write test for action methods in CategoryController and CategoryAPIController classes.
Let's test Index() action method from the CategoryController class to test if this method returns ViewResult with List<Category> class. In the CategoryControllerTest class, add a private method that contains the test data as shown in Listing 7
private IEnumerable GetTestCategories()
{
return new List()
{
new Category(){CategoryRowId=1, CategoryId="Cat0001",CategoryName="Electronics",BasePrice=12000 },
new Category(){CategoryRowId=2, CategoryId="Cat0002",CategoryName="Electrical",BasePrice=20 }
};
}
Listing 7: The Test data method
Let's add a new method of name Index_ReturnsViewResult_WithAListOfCategories(). Here we have to keep in mind that the Test method name should represent the actual test purpose. Since we will be testing Index() method returning ViewResult with List of Categories, the test method name is Index_ReturnsViewResult_WithAListOfCategories(). Write the code in this method as shown in listing 8
[Fact]
public void Index_ReturnsViewResult_WithAListOfCategories()
{
// Arrange
var mockRepo = new Mock<IService<Category, int>>();
// define the setup on the mocked type
mockRepo.Setup(repo => repo.GetAsync()).ReturnsAsync(GetTestCategories());
var controller = new CategoryController(mockRepo.Object);
// Act
// call the Index() method from the controller
var result = controller.Index().Result;
//Asert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<List<Category>>(
viewResult.ViewData.Model
);
// Assert the model count
Assert.Equal(2, model.Count());
}
Listing 8: The Index() method test from the CategoryController class
The code in listing 8 performs following operations:
The Mock instance is created using the IService<Category, int> interface. This will create a fake object using the Iservice interface. Using the Setup() method of the mock object the GetAsync() method form the fake object is called. The GetAsync() method executes asynchronously that returns Task<IEnumerable<Category,int>. The ResturnsAsync() method specifies the value to return from the asynchronous method.
The code further creates an instance of the CategoryController class by passing an instance of the Fake object. The code further access the Index() method of the CategoryController class and receive its result.
The last part of the code is the Test Assertion, here the assertion check the result assertion type using Assert.IsType<T>() method, this accepts the result returned from the controller's action method to verify that the result is ViewResult. Then the Assert.IsAssignableForm<T> () method verifies that the result from the action method is of the type of model that is passed to view. In the case of Index() action method the model data passed to view is List<Category>. So the important part of the assertion is Assert.IsType<T>() method and Assertion.IsAssignableForm<T>() method. These two methods will define the fate of the test case if it will execute successfully or not. Finally, test method asserts the model data count for the view with count of List<Category> records. If they match, then the test will be successfully executed else it will fail.
Apply breakpoint on this test method and right-click inside this method and select option Debug Test(s) as shown in figure 7
Apply breakpoint on this test method and right-click inside this method and select option Debug Test(s) as shown in figure 7
Figure 7: test debugging
This will start the test debugging, we can see the mock object created using IService<Category,int> as shown in figure 8.
Figure 8: The Debug with mock object
Once debugging is done, the Test Explorer will show the test result as shown in figure 9.
Figure 9: The Test Execution
Likewise, we can also test the action method that redirects to other action methods. The CategoryController contains Create() action method with HttpPost request. We will add the private method in the class that will define a test data for the create method as shown in listing 9.
private Category GetTestCategory() { return new Category() { CategoryId = "Cat001", CategoryName = "ECT", BasePrice =2000 }; }
Listing 9: A method for Test data to test the create method
We can Assert the test using Assert.IsType<RedirectToActionResult>. Listing 10 shows the code for testing Create() action method from the CategoryController class.
[Fact]
public void Create_CategoryAndReturnsARedirect_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IService<Category,int>>();
mockRepo.Setup(repo => repo.CreateAsync(It.IsAny()))
.Verifiable();
var controller = new CategoryController(mockRepo.Object);
var newEmployee = GetTestCategory();
// Act
var result = controller.Create(newEmployee).Result;
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
Listing 10: The Create action method
The code in listing 10, creates an instance of the mock object to define mock for IService interface. Furthermore, the CreateAsync() method is called. The Create() action method is called from the CategoryController using its instance and the test data method is passed to this method.
The assertion part of the method detects the assertion type for RedirectToActionResult from the action method. The assertion verifies if the controller name from the result is Null. This is null because the Create() action method does not result Controller Name. The Assert.Equal() checks if the ActionName from result is Index. If the action name is matched, then the test will be successful.
The assertion part of the method detects the assertion type for RedirectToActionResult from the action method. The assertion verifies if the controller name from the result is Null. This is null because the Create() action method does not result Controller Name. The Assert.Equal() checks if the ActionName from result is Index. If the action name is matched, then the test will be successful.
So, the important learning here is the in ASP.NET Core applications, it is important to test Controller action methods. This makes sure that the controllers which is the face of MVC apps, is working as per the expectations.
Now, let's test the API Controller.
The CategoryAPIController, contains the PostAsync() method. This method accepts the Category object. If the category object fails validation, then the method returns BadRequestObjectResult. So let's test this method in the test class as shown in listing 11
The CategoryAPIController, contains the PostAsync() method. This method accepts the Category object. If the category object fails validation, then the method returns BadRequestObjectResult. So let's test this method in the test class as shown in listing 11
[Fact]
public void Create_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IService<Category,int>>();
var controller = new CategoryAPIController(mockRepo.Object);
controller.ModelState.AddModelError("CategoryName", "Required");
var newCategory = GetTestCategory();
// Act
var result = controller.PostAsync(newCategory).Result;
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
Listing 11: The PostAsync() method test
The important part in the code of listing 11 is the the Model error on the CategoryName property of the Category class. The Act part of the method makes call to PostAsync() method of the CategoryAPIController class. The Assert section of the code verifies the result from the PostAsync() method as BadRequestObjectResult and verifies the badRequestResult value as Model error after execution.
So we have added 3 test methods in the Test class. Let's run the test project using F5 key. The test explorer will show the test results as shown in figure 10
Figure 9: The Test explorer will all running tests
Conclusion: Testing is the most important process for any software application. xUnit is an important framework for testing ASP.NET Core applications - for testing Action methods, MVC controllers and API Controllers. The Moq framework provides an easy mechanism to mock the dependencies which makes it easier to test classes having constructor injection.
Download the source code.
Download the source code.
No comments:
Post a Comment