This article will demonstrate how to write Unit Test Cases for CRUD operations in ASP.NET Core Web API with the xUnit project. In this demonstration, we will write the Unit Test Cases for CRUD (CREATE, READ, UPDATE and DELETE) operations. We will write at least 3 different Unit Test Cases for 3 different scenarios. In this demonstration, we will not implement CRUD operation in ASP.NET Core Web API because we have already written one article on this previously. We have used the code which we have written for the previous article. If you are willing to learn how to perform CRUD operations in ASP.NET Core Web API using Entity Framework Core, you can visit here.
If you are willing to download the code for the above article, you can download it from GitHub here.
We are writing this article because we haven't got much information about how to write a Unit Test Case for CRUD Operations on the internet with step-by-step information. So, we have decided to write on this topic so that it can help others. Without wasting much time on an introduction, let's move to the practical demonstration of how to write CRUD operations Unit Test Cases for the ASP.NET Core Web API project.
First, let's download the project from GitHub as provided in the link above and open it in Visual Studio (We are using Visual Studio 2017 for this demonstration). Once the project opens, move to the Models folder and open the Post Model and let's modify the existing project "CoreServies" Post entity make the Title column a required field, and define the length of the field as follows. We are modifying this Post entity because we will use it while writing the Unit Test Cases for "Create" and "Update" operations.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CoreServices.Models
{
public partial class Post
{
public int PostId { get; set; }
[Column(TypeName = "varchar(20)")]
[Required]
public string Title { get; set; }
public string Description { get; set; }
public int? CategoryId { get; set; }
public DateTime? CreatedDate { get; set; }
public Category Category { get; set; }
}
}
Let's build the CoreServices project and see if everything is fine or not. Once all are green then we can move ahead.
Now, we will create one Test project where we will write the Unit Test Cases. So, right-click on the solution of the "CoreServices" project choose Add, and then choose "New Project".
The next screen will provide lots of options to add a new project, but you have to move ".Net Core" inside the Installed template and then choose "xUnit Test Project (.Net Core)" as you can see in the following image. Just provide the suitable name as per standard, it should be "Project name with Test" (CoreServices.Test) and click to OK.
Now, we have the Test Project ready. Let's move and add the references of the Main project (CoreServices) where we have written actual Controller, Repository, and Model Classes. While writing the Unit Test Cases, we will use these existing Controller, Repository, Model, and DbContext instances.
To add the project reference, right-click on the "Dependencies" of the "CoreServices.Test" project choose "Add References" and then choose "CoreServices" project from the Project > Solution as shown in the following image and click OK.
Now, we have added the reference of the "CoreServices" project inside the "CoreServices.Test" project. After this point, we can access the components that are defined in the "CoreServices" project. Actually, we are only willing to use Controller and Repository for writing Unit Test Cases, but we will not use the Actual DB.
For testing purposes, we will create one separate Test DB (BlogDB) with the same name on a different server or with some other name on the same server. For this demonstration, we are using the same database name as some other server.
Once the database is ready, we have to seed some data into a database before performing testing. So, for that, we will create one class which will be responsible for creating some dummy data which we will use further while running Test Cases.
So, let's create a class as "DummyDataDBInitializer" with a "Seed" method, which will first delete your all database tables every time regenerate tables based on your Model configurations and add some dummy data. You can get help with the following code snippets. Here you can see we are adding some data for the "Category" and "Post" tables and then committing it using the "context.SaveChanges()" method.
using CoreServices.Models;
using System;
namespace CoreServices.Test
{
public class DummyDataDBInitializer
{
public DummyDataDBInitializer()
{
}
public void Seed(BlogDBContext context)
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Category.AddRange(
new Category() { Name = "CSHARP", Slug = "csharp" },
new Category() { Name = "VISUAL STUDIO", Slug = "visualstudio" },
new Category() { Name = "ASP.NET CORE", Slug = "aspnetcore" },
new Category() { Name = "SQL SERVER", Slug = "sqlserver" }
);
context.Post.AddRange(
new Post() { Title = "Test Title 1", Description = "Test Description 1", CategoryId = 2, CreatedDate = DateTime.Now },
new Post() { Title = "Test Title 2", Description = "Test Description 2", CategoryId = 3, CreatedDate = DateTime.Now }
);
context.SaveChanges();
}
}
}
In this demonstration, we will use "Fluent Assertions" for writing a beautiful and user-friendly Unit Test Case.
Fluent Assertions is a very extensive set of extension methods that allows you to more naturally specify the expected outcome of a TDD or BDD-style unit test. Targets .NET Framework 4.5 and 4.7, as well as .NET Core 2.0, .NET Standard 1.3, 1.6 and 2.0.
So, let's open the NuGet Package Manager for the "CoreServies.Test" project browse for "FluentAssertions" and install it.
Now, it's time to create a class where we will write the actual Unit Test Cases. So, let's create one class as "PostUnitTestController.cs" in "CoreServices.Test" as follows.
Once the PostUnitTestController class is ready, first we will try to access our database where at the runtime, we will seed some dummy data and access the dummy data for testing. So, let's prepare the connection string and based on that get the instance of "BlogDBContext".
Note. Here connection string is defined inside the class, that is not a good approach but we are using it only for this demonstration, but you can use some configuration files to keep this information and access this connection string from that configuration file.
public class PostUnitTestController
{
private PostRepository repository;
public static DbContextOptions<BlogDBContext> dbContextOptions { get; }
public static string connectionString = "Server=ABCD;Database=BlogDB;UID=sa;PWD=xxxxxxxxxx;";
static PostUnitTestController()
{
dbContextOptions = new DbContextOptionsBuilder<BlogDBContext>()
.UseSqlServer(connectionString)
.Options;
}
}
Once we have available the instance of the "BlogDBContext" then we will go to get the instance of the actual repository "PostRepository" based on the instance of "BlogDBContext" as follows inside the "PostUnitTestControler" constructor.
public PostUnitTestController()
{
var context = new BlogDBContext(dbContextOptions);
DummyDataDBInitializer db = new DummyDataDBInitializer();
db.Seed(context);
repository = new PostRepository(context);
}
Now, we have everything like a connection string, an instance of BlogDBContext, an instance of PostRepository, and all. So, we can move next and write Unit Test Cases for all EndPoints which are defined inside the PostController in the "CoreServices" project.
We will write the Unit Test Cases one by one for all the EndPoints. If you are new in Unit Testing and willing to write it then you can go with my article "Getting started with Unit Testing using C# and xUnit" where you will learn more about Unit Testing's Pros and Cons and the best way to write Unit Test Cases.
We need to keep three things while writing the Unit Test Cases and these are Arranging the data, Performing the action, and Matching the output (Arrange, Act, Assert).
So, let's first write the Unit Test Case for the "Get By Id " method as follows. We have multiple Unit Test Cases to test a single method. Each Unit Test Case has its own responsibilities like matching the OK Result, checking for the Not Found Result, checking for Bad Requests, etc.
When we talk about Fluent Assertions, we are implementing "Task_GetPostById_MatchResult" to get the actual data.
#region Get By Id
[Fact]
public async Task GetPostById_Return_OkResult()
{
// Arrange
var controller = new PostController(repository);
var postId = 2;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<OkObjectResult>(data);
}
[Fact]
public async Task GetPostById_Return_NotFoundResult()
{
// Arrange
var controller = new PostController(repository);
var postId = 3;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<NotFoundResult>(data);
}
[Fact]
public async Task GetPostById_Return_BadRequestResult()
{
// Arrange
var controller = new PostController(repository);
int? postId = null;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<BadRequestResult>(data);
}
[Fact]
public async Task GetPostById_MatchResult()
{
// Arrange
var controller = new PostController(repository);
int? postId = 1;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<OkObjectResult>(data);
var okResult = data.Should().BeOfType<OkObjectResult>().Subject;
var post = okResult.Value.Should().BeAssignableTo<PostViewModel>().Subject;
Assert.Equal("Test Title 1", post.Title);
Assert.Equal("Test Description 1", post.Description);
}
#endregion
Following are the Unit Test Cases for the "Get All" methods. Here we have written 3 Unit Test Cases one for Ok Result, another for Bad Request Result when the wrong result is passed, and a last one for matching the Ok Result Output with our data.
#region Get By Id
[Fact]
public async Task Task_GetPostById_Return_OkResult()
{
// Arrange
var controller = new PostController(repository);
var postId = 2;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<OkObjectResult>(data);
}
[Fact]
public async Task Task_GetPostById_Return_NotFoundResult()
{
// Arrange
var controller = new PostController(repository);
var postId = 3;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<NotFoundResult>(data);
}
[Fact]
public async Task Task_GetPostById_Return_BadRequestResult()
{
// Arrange
var controller = new PostController(repository);
int? postId = null;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<BadRequestResult>(data);
}
[Fact]
public async Task Task_GetPostById_MatchResult()
{
// Arrange
var controller = new PostController(repository);
int? postId = 1;
// Act
var data = await controller.GetPost(postId);
// Assert
Assert.IsType<OkObjectResult>(data);
var okResult = data.Should().BeOfType<OkObjectResult>().Subject;
var post = okResult.Value.Should().BeAssignableTo<PostViewModel>().Subject;
Assert.Equal("Test Title 1", post.Title);
Assert.Equal("Test Description 1", post.Description);
}
#endregion
The following Unit Test Cases are for CREATE operation, let me confirm one thing here we will prepare the data for adding in the Arrange section. One thing you should notice here is that in the "Task_Add_InvalidData_Return_BadRequest" Unit Test Cases, we are passing more than 20 characters for the Title, which is not correct because, in the Post model, we have defined the size of the Title as 20 characters.
#region Add New Blog
[Fact]
public async void Task_Add_ValidData_Return_OkResult()
{
//Arrange
var controller = new PostController(repository);
var post = new Post()
{
Title = "Test Title 3",
Description = "Test Description 3",
CategoryId = 2,
CreatedDate = DateTime.Now
};
//Act
var data = await controller.AddPost(post);
//Assert
Assert.IsType<OkObjectResult>(data);
}
[Fact]
public async void Task_Add_InvalidData_Return_BadRequest()
{
//Arrange
var controller = new PostController(repository);
Post post = new Post()
{
Title = "Test Title More Than 20 Characters",
Description = "Test Description 3",
CategoryId = 3,
CreatedDate = DateTime.Now
};
//Act
var data = await controller.AddPost(post);
//Assert
Assert.IsType<BadRequestResult>(data);
}
[Fact]
public async void Task_Add_ValidData_MatchResult()
{
//Arrange
var controller = new PostController(repository);
var post = new Post()
{
Title = "Test Title 4",
Description = "Test Description 4",
CategoryId = 2,
CreatedDate = DateTime.Now
};
//Act
var data = await controller.AddPost(post);
//Assert
Assert.IsType<OkObjectResult>(data);
var okResult = data.Should().BeOfType<OkObjectResult>().Subject;
// var result = okResult.Value.Should().BeAssignableTo<PostViewModel>().Subject;
Assert.Equal(3, okResult.Value);
}
#endregion
Here as follows are the Unit Test Cases for the UPDATE operation. In Unit Test Cases for an Update operation, we first get the Post details based on the Post Id and then modify the data and send it for updating.
#region Update Existing Blog
[Fact]
public async Task Task_Update_ValidData_Return_OkResult()
{
//Arrange
var controller = new PostController(repository);
var postId = 2;
//Act
var existingPost = await controller.GetPost(postId);
var okResult = existingPost.Should().BeOfType<OkObjectResult>().Subject;
var result = okResult.Value.Should().BeAssignableTo<PostViewModel>().Subject;
var post = new Post
{
Title = "Test Title 2 Updated",
Description = result.Description,
CategoryId = result.CategoryId,
CreatedDate = result.CreatedDate
};
var updatedData = await controller.UpdatePost(post);
//Assert
Assert.IsType<OkResult>(updatedData);
}
[Fact]
public async Task Task_Update_InvalidData_Return_BadRequest()
{
//Arrange
var controller = new PostController(repository);
var postId = 2;
//Act
var existingPost = await controller.GetPost(postId);
var okResult = existingPost.Should().BeOfType<OkObjectResult>().Subject;
var result = okResult.Value.Should().BeAssignableTo<PostViewModel>().Subject;
var post = new Post
{
Title = "Test Title More Than 20 Characteres",
Description = result.Description,
CategoryId = result.CategoryId,
CreatedDate = result.CreatedDate
};
var data = await controller.UpdatePost(post);
//Assert
Assert.IsType<BadRequestResult>(data);
}
[Fact]
public async Task Task_Update_InvalidData_Return_NotFound()
{
//Arrange
var controller = new PostController(repository);
var postId = 2;
//Act
var existingPost = await controller.GetPost(postId);
var okResult = existingPost.Should().BeOfType<OkObjectResult>().Subject;
var result = okResult.Value.Should().BeAssignableTo<PostViewModel>().Subject;
var post = new Post
{
PostId = 5,
Title = "Test Title More Than 20 Characteres",
Description = result.Description,
CategoryId = result.CategoryId,
CreatedDate = result.CreatedDate
};
var data = await controller.UpdatePost(post);
//Assert
Assert.IsType<NotFoundResult>(data);
}
#endregion
Here are the last Unit Test Cases for the DELETE operation as follows.
#region Delete Post
[Fact]
public async void Task_Delete_Post_Return_OkResult()
{
// Arrange
var controller = new PostController(repository);
var postId = 2;
// Act
var data = await controller.DeletePost(postId);
// Assert
Assert.IsType<OkResult>(data);
}
[Fact]
public async void Task_Delete_Post_Return_NotFoundResult()
{
// Arrange
var controller = new PostController(repository);
var postId = 5;
// Act
var data = await controller.DeletePost(postId);
// Assert
Assert.IsType<NotFoundResult>(data);
}
[Fact]
public async void Task_Delete_Return_BadRequestResult()
{
// Arrange
var controller = new PostController(repository);
int? postId = null;
// Act
var data = await controller.DeletePost(postId);
// Assert
Assert.IsType<BadRequestResult>(data);
}
#endregion
So, we have written a total of 16 Unit Test Cases for CRUD Operations along with the "Get All" method. Now it's time to run the Unit Test Cases and see the output. So, running the Unit Test Cases, let's move to Test Explorer and click on Run All links. It will start executing all your Unit Test Cases one by one and the final output will be as below.
Conclusion
So, today, we have learned with a practical demonstration how to write Unit Test Cases for Asp.Net Core Web API CRUD operations.
I hope this post will help you. Please leave your feedback using comments which helps me to improve myself for the next post. If you have any doubts please ask your doubts or queries in the comment section and if you like this post, please share it with your friends. Thanks.