Unit testing for Web Api Controller using xUnit

Unit Testing is important in any Software development life cycle as it helps us to identify bugs in the software at the very early stage and below are the benefits of Unit Testing:

  • Help facilitates change as each unit is tested individually.
  • It reduces cost because defects are detected in the early stage so cost is significantly reduced compared to defects detected in the later stage in SDLC.
  • It improves Code Coverage which implies quality software and business stakeholders will trust your software more compare to the software which has less code coverage percentage.
There are many Unit Testing Frameworks that can be used for .Net Framework like NUnit, xUnit, MSTest, etc...
In this article, we will use xUnit as a Unit Testing Framework for unit testing ASP.NET Web API Controller as xUnit has many advantages compared to other Unit Test frameworks

Advantages of xUnit


  • xUnit is far more extensible and flexible as it allows you to create new attributes to control your tests. [Theory] attribute in xUnit allows you to execute the method multiple times.
  • It isolates the test methods which means you can each test method run independently and in any order as it eliminates the risk of test method dependencies.
  • xUnit uses Assert.Throws construct compared to NUnit which uses specific exception types like AuthenticationException which fails the unit tests if a different exception type is thrown.

Let's check the code now and suppose we have API Controller as below

StudentContoller

[Route("api/[controller]")]
[ApiController]
public class StudentController : ControllerBase
{
    private readonly ILogger<StudentController> _logger;
    public StudentController(ILogger<StudentController> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }


    [HttpGet("{classId}")]
    public async Task<IActionResult> GetAllStudent(int classId, [FromServices] IStudentService studentService)
    {
        if (studentService == null)
        {
            return StatusCode(StatusCodes.Status500InternalServerError);
        }


        if (!await studentService.ValidateClass(classId)
                                     .ConfigureAwait(false)) return NotFound("Invalid Class Id");


        var res = await studentService.GetAllStudents(classId).ConfigureAwait(false);
        return Ok(res);
    }
}

Suppose we want to write a Unit test for GetAllStudent endpoint and let's start with negative unit test cases like if classId is invalid or the dependency StudentService is not resolved.

Invalid Class ID for Student


[Fact]
public async Task GetAllStudents_Returns_404_IfClassIdNotFound()
{
    // Arrange
    var studentService = new Mock<IStudentService>();

    studentService.Setup(x => x.ValidateClass(It.IsAny<int>()))
        .Returns(Task.FromResult(false));

    // Act
    var result = await CreateObjectUnderTest().GetAllStudent(
        400,
        studentService.Object);

    // Assert

    Assert.NotNull(result);
    Assert.IsAssignableFrom<IStatusCodeActionResult>(result);
    Assert.Equal(404, (result as IStatusCodeActionResult).StatusCode);
}

private StudentController CreateObjectUnderTest() =>
           new StudentController(
               logger: _logger.Object
               );
private readonly Mock<ILogger<StudentController>> _logger = new Mock<ILogger<StudentController>>();


As you can see above we have a unit test where we are asserting 404 status code if an invalid classid is provided.
Please note CreateObjectUnderTest() method and _logger field are common for all the Unit test cases so I will not repeat it everywhere.

Service Not Available 


[Fact]
public async Task GetAllStudents_Returns_500_IfServiceNotAvailable()
{
    var result = await CreateObjectUnderTest().GetAllStudent(
        400,
        studentService: null
       );

    Assert.NotNull(result);
    Assert.IsAssignableFrom<IStatusCodeActionResult>(result);
    Assert.Equal(500, (result as IStatusCodeActionResult).StatusCode);
}

Successful Data Returning 

[Fact]
public async Task GetAllStudents_Returns_SuccessResult()
{
    // Arrange
    var studentService = new Mock<IStudentService>();

    studentService
        .Setup(sS => sS.GetAllStudents(It.IsAny<int>()))
        .Returns(Task.FromResult((new List<Student> { new Student { } }).AsEnumerable()));

    studentService.Setup(x => x.ValidateClass(It.IsAny<int>()))
        .Returns(Task.FromResult(true));

    // Act
    var result = await CreateObjectUnderTest().GetAllStudent(
        3,
        studentService.Object);

    // Assert
    Assert.NotNull(result);
    Assert.IsAssignableFrom<IStatusCodeActionResult>(result);
    Assert.Equal(200, (result as IStatusCodeActionResult).StatusCode);
}


Similarly, if there is an AddStudent method in StudentController where we want to assert 201 created status code then we can do as below.

AddStudent method


[HttpPost("{classId}")]
public async Task<IActionResult> AddStudent([FromBody] Student student,
    int classId, [FromServices] IStudentService studentService)
{
    if (studentService == null)
    {
        return StatusCode(StatusCodes.Status500InternalServerError);
    }


    if (!await studentService.ValidateClass(classId)
                             .ConfigureAwait(false)) return NotFound("Invalid Class Id");


    await studentService.AddStudent(student, classId).ConfigureAwait(false);
    return Created("Student Addition Successful", student);
}


Successful Student Addition


[Fact]
public async Task AddBranch_Returns_SuccessResult()
{
    // Arrange
    var studentService = new Mock<IStudentService>();

    studentService
        .Setup(sS => sS.AddStudent(It.IsAny<Student>(),It.IsAny<int>()))
        .Returns(Task.CompletedTask);

    studentService.Setup(x => x.ValidateClass(It.IsAny<int>()))
        .Returns(Task.FromResult(true));

    // Act
    var result = await CreateObjectUnderTest().AddStudent(
        new Student { },
        3,
        studentService.Object);

    // Assert

    Assert.NotNull(result);
    Assert.IsAssignableFrom<IStatusCodeActionResult>(result);
    Assert.Equal(201, (result as IStatusCodeActionResult).StatusCode);
}


Similarly, if we have DeleteStudent method which asserts 202 Accepted status code then we can use like below


Delete Student method


[HttpDelete("{studentId}")]
public async Task<IActionResult> DeleteStudent(int studentId,
    [FromServices] IStudentService studentService)
{
    if (studentService == null)
    {
        return StatusCode(StatusCodes.Status500InternalServerError);
    }

    // Use student object to delete the student in DB
    await studentService.DeleteStudent(studentId).ConfigureAwait(false);
    return Accepted("Deleted the Student");
}


Successful Deletion Unit test

[Fact]
public async Task DeleteStudent_Returns_202_SuccessResult()
{
    // Arrange
    var studentService = new Mock<IStudentService>();

    studentService
        .Setup(sS => sS.DeleteStudent(It.IsAny<int>()))
        .Returns(Task.CompletedTask);

    // Act
    var result = await CreateObjectUnderTest().DeleteStudent(
        3,
        studentService.Object);

    // Assert

    Assert.NotNull(result);
    Assert.IsAssignableFrom<IStatusCodeActionResult>(result);
    Assert.Equal(202, (result as IStatusCodeActionResult).StatusCode);
}


Conclusion

Unit Testing is important and is part and parcel of any developer and since Unit Test is important for Code Coverage, therefore, it's crucial for producing quality software.


Share This Post

Linkedin
Fb Share
Twitter Share
Reddit Share

Support Me

Buy Me A Coffee