Skip to content

Commit

Permalink
DOCUMENTATIONS: Unit Testing API Controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
hassanhabib committed Oct 28, 2024
1 parent bfa743c commit db14f82
Showing 1 changed file with 135 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,141 @@ namespace OtripleS.Web.Api.Controllers
Home controllers are not required to have any security. They open a gate for heartbeat tests to ensure the system as an entity is running without checking any external dependencies. This practice is very important to help engineers know when the system is down and quickly act on it.

## 3.1.1.5 Tests
Controllers can be potentially unit tested to verify the mapping of exceptions to error codes is in place. But that's not a pattern I have been following myself so far. What is more important is Acceptance tests, which verify that all the system components are fully and successfully integrated.
There are three different types of tests that cover API controllers as well as any other exposure layer. These tests are: unit tests, acceptance tests and integration or end-to-end (E2E) tests. Integration tests can vary between smoke testing, availability testing, performance testing and many others. But for the purpose of this chapter, we will focus on unit and acceptance tests.

### 3.1.1.5.0 Unit Tests
Controllers have a similar type of logic that exists in Services, this logic is the mapping between exceptions coming from a dependency or internally and what these exceptions are being mapped to for the consumer of these APIs. For instance, a `StudentValidationException` can be mapped to a `BadRequest` status code. This logic is tested in unit tests. Let's take a look at an example:

Starting with the test before the implementation, let's assume we have a controller `StudentsController` that retrieves all students. When the call succeeds the controller should return `200 OK` status code. Let's write a test for that:

First, let's setup our `StudentsController` class as follows:

```csharp
namespace School.Core.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class StudentsController : RESTFulController
{
private readonly IStudentService studentService;

public StudentsController(IStudentService studentService) =>
this.studentService = studentService;

[HttpGet]
public async ValueTask<ActionResult<IQueryable<Student>>> GetAllStudentsAsync()
{
return NotImplemented(new NotImplementedException());
}
}
```
In the above code snippet, we initialized `StudentsController`, we inherited `RESTFulController` so we can have support for all possible status codes. But additionally, we created a `GetAllStudentsAsync` method that returns `NotImplemented` status code with a `NotImplementedException` exception. Notice the difference here between throwing the exception `NotImplementedException` at the Services layer compared to controllers.

Now, let's move on to writing a controller unit test. Let's setup our `StudentsControllerTest` class as follows:
```csharp
public partial class StudentsControllerTests : RESTFulController
{
private readonly Mock<IStudentService> studentServiceMock;
private readonly StudentsController studentsController;

public StudentsControllerTests()
{
this.studentServiceMock = new Mock<IStudentService>();

this.studentsController = new StudentsController(
studentService: this.studentServiceMock.Object);
}

....
}
```
In the above example, we did three important things:
1. We made sure the `SourcesControllerTests` class is partial so we can write other files that are still a part of this class but target particular areas and methods.
2. We inherited from `RESTFulController` which is a class that comes from `RESTFulSense` .NET library which we will use later to create the expected response such as `Ok(retrievedStudents)`.
3. We mocked the dependency so we don't actually call the `StudentService` but rather call a controlled mock so we can simulate responses and exceptions depends on the context of the unit test.

Now, let's write a unit test for `GetAllStudentsAsync` controller method as follows:

```csharp
[Fact]
public async Task ShouldReturnOkOnGetAllStudentsAsync()
{
// given
List<Student> randomStudents =
CreateRandomStudents();

List<Student> returnedStudents =
randomStudents;

List<Student> expectedStudents =
returnedStudents.DeepClone();

OkObjectResult expectedObjectResult =
Ok(expectedStudents);

var expectedActionResult =
new ActionResult<List<Student>>(
expectedObjectResult);

this.studentServiceMock.Setup(service =>
service.RetrieveAllStudentsAsync())
.ReturnsAsync(returnedStudents);

// when
ActionResult<List<Student>> actualActionResult =
await this.studentsController
.GetAllStudentsAsync();

// then
actualActionResult.ShouldBeEquivalentTo(
expectedActionResult);

this.studentServiceMock.Verify(service =>
service.RetrieveAllStudentsAsync(),
Times.Once);

this.studentServiceMock.VerifyNoOtherCalls();
}
```

In the above test, just like we did with Services unit tests we did the following:
1. We created a list of random students to simulate a response from the service.
2. We cloned the list of students to create an expected response.
3. We created an `OkObjectResult` object to simulate the expected response from the controller.
4. We setup the `studentServiceMock` to return the list of students when `RetrieveAllStudentsAsync` is called.
5. We called the `GetAllStudentsAsync` method on the controller.
6. We verified that response `expectedActionResult` is equivalent to the actual response `actualActionResult`.
7. We verified that the `RetrieveAllStudentsAsync` method was called once.
8. Lastly, we wanted to verify that the controller isn't making any additional unnecessary calls from the dependency.

The above test will fail with expected code being `200 OK` but instead the actual is `501 Not Implemented`. Now, let's make that test pass as following:

```csharp
namespace School.Core.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class StudentsController : RESTFulController
{
private readonly IStudentService studentService;

public StudentsController(IStudentService studentService) =>
this.studentService = studentService;

[HttpGet]
public async ValueTask<ActionResult<IQueryable<Student>>> GetAllStudentsAsync()
{
List<Student> students =
await this.studentService.RetrieveAllStudentsAsync();

return Ok(students);
}
}
```

In the above code, we implemented `GetAllStudentsAsync` method and now our unit test will successfully pass.

### 3.1.1.5.1 Acceptance Tests
Here's an example of an acceptance test:

```csharp
Expand Down Expand Up @@ -487,6 +620,7 @@ Acceptance tests are required to cover every available endpoint on a controller

Acceptance tests are also implemented after the fact, unlike unit tests. An endpoint has to be fully integrated and functional before a test is written to ensure implementation success is in place.

[*] [Controller Unit Tests] (https://www.youtube.com/watch?v=Fc4LgUR2174)
[*] [Acceptance Tests (Part 1)](https://www.youtube.com/watch?v=WWN-9ahbdIU)
Expand Down

0 comments on commit db14f82

Please sign in to comment.