diff --git a/3. Exposers/3.1 Communication Protocols/3.1.1 RESTful APIs/3.1.1 RESTful APIs.md b/3. Exposers/3.1 Communication Protocols/3.1.1 RESTful APIs/3.1.1 RESTful APIs.md index a42dfb6..d1b9a61 100644 --- a/3. Exposers/3.1 Communication Protocols/3.1.1 RESTful APIs/3.1.1 RESTful APIs.md +++ b/3. Exposers/3.1 Communication Protocols/3.1.1 RESTful APIs/3.1.1 RESTful APIs.md @@ -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>> 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 studentServiceMock; + private readonly StudentsController studentsController; + + public StudentsControllerTests() + { + this.studentServiceMock = new Mock(); + + 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 randomStudents = + CreateRandomStudents(); + + List returnedStudents = + randomStudents; + + List expectedStudents = + returnedStudents.DeepClone(); + + OkObjectResult expectedObjectResult = + Ok(expectedStudents); + + var expectedActionResult = + new ActionResult>( + expectedObjectResult); + + this.studentServiceMock.Setup(service => + service.RetrieveAllStudentsAsync()) + .ReturnsAsync(returnedStudents); + + // when + ActionResult> 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>> GetAllStudentsAsync() + { + List 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 @@ -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)