Context

This is a 3 part series for unit testing the controller, service, and repository layers in a typical REST architecture. There are many approaches to doing this, I’m sharing a pattern that I find very flexible and hopefully, you can pick up some tips and tricks along the way.

Part 1 — Unit Testing the Controller | Git branch can be found here.

Part 2 — Unit Testing the service | Git branch can be found here.

Part 3 — Unit Testing the repository | Git branch can be found here.

Intro

A common pattern amongst projects contain:

  • Controllers that are the entry point for API calls.
  • Services to handle the business logic. 
  • Repositories to handle the DB calls. 

This architecture is very common and bodes well for separation of concerns, dependency injection, and in turn, easy testing. 

This post will test the controller. It shows a step-by-step process so it’s possible to follow along if you want to learn.

Set the Scene

Story: ‘The information about the space ship needs to be saved.’

  • A new API is required.
  • Another team is working on sending the data to this API.

Data contract: spaceShipId is a primary key in this case and it conforms strictly to the format below:

{
  "spaceShipId": "abc-123-ship",
  "spaceShipName": "Star Harvester",
  "spaceShipNumber": 42,
  "isFasterThanLight": true
}

Let’s go!

Unit Testing the Controller

NestJS will be used as it is a nice toolkit that saves a developer lots of time and is very feature-rich, let’s start. Run these 3 commands to create and start the project:

npm i -g @nestjs/cli
nest new tdd-nestjs-api-jest
npm install

This will create a starter project that conveniently has something similar to what we want, run the test:

npm test

The console should show the output of the test — so we know it’s running:

Output of test

Let’s create a new module and call it space-ship and then create a controller also with the same name:

nest g module space-ship
nest g controller space-ship

This creates a module and adds the app module references. Also creates the controller, conveniently with a test and its boilerplate, and adds it to the space-ship module. We can also run the tests again as before and should see it now has 2 tests that both pass.

2 tests, both passed

So Where Do We Start TDD?

This is usually the hardest part when starting a new task. It’s possible to work from the repo and back to the controller or vice-versa. On this occasion, start from the controller. 

Create a failing test for the controller:

Given — space ship data.

When — then the controller is called with the data.

Then — the space ship service should be called with the data.

This can be converted into code:

A failing test has now been achieved, but this is more than failing. It doesn’t have:

  • the function in the controller.
  • the services do not exist yet.

Sometimes you may want to create the skeleton first and then start wiring the tests but then you have lots of code that potentially could be forgotten about or never tested. Doing it this way allows you to create as you go. Also your IDE may offer assistance in creating the missing functions, creating new classes and offering hints.

Let’s fix the first problem and create the save function in the controller, then create the service with a save function as well and reference that in the test.

nest g service space-ship

Now we have fixed the errors and wired up the logic:

Now run the test and we have a failing test. The --watch field can be added to automatically run the tests on every change, which is very handy:

npx jest --watchAll

And of course, the test fails because we have not called the service in the controller:

Failed test

Wire up the controller — inject the service and call save:

So that’s it, it ‘should’ work but the test still fails. This is because the service needs to be mocked — the actual implementation of the service is being called here, not what is required for unit testing. Simply add this line to the import section of the controller test:

jest.mock('./space-ship.service');

Now we have those wonderful green lights we always like to see:

Wonderful green lights!

The test now gives a little more confidence that the controller will call the service. At this point, the bare minimum of unit testing has been achieved. However what it is calling with has been neglected, ie., the data being passed in.

Ensuring the Data Is Valid

As the data is known, create the interface and data model. Some good advice, seen time and time again — ‘Ensure your data is valid first’. This is key. If you know the data is correct from the start then there is less chance of incurring errors down the line. Also makes the app more secure as will be demonstrated further in the post.

We know what data to expect so let’s create an interface for this request in the controller and update the test with data.  

We know what data to expect but by no means does it mean it’s valid. 

It’s good practice to extract IDs out into their own class if they will be used elsewhere in the code such as DB identifiers, passed into functions, etc. This provides many benefits such as:

  • valid if exists.
  • adds context and meaning.
  • not referred to as an ID or string.
  • not easily mixed up when passing multiple IDs around.

To validate the data it’s tempting to stick a validation function in and assume that its fine, while this works it’s limited to this section of code. What if a null pointer occurs in the downstream code? Will a null check deep in the code be required? Yes. And then that is the start of code smells and problems. Let’s fix this

Tiny Types, Micro Types, Domain Primitives, to Name a Few

These names pretty much describe the same thing. They represent the smallest and meaningful entity in the domain. Let’s apply this to the spaceship ID. The ID should always follow the same pattern, ie., xxx-000-xxx. Anything else should be invalid. 

This can be converted into a SpaceShipId class that can be created from a string. Notice the private constructor; allowing it only to be created from the from method:

This only allows the return of a valid ID or throws an error.

We can also write some test to check that the input is valid:

The first test fails as expected as it allows an invalid ID. We can update the validation function to add all the validation requirements, eg., regex, smart checks plus many others. For simplicity we will just check that the length is correct but you can really ‘go to town’ on these check to ensure it’s valid and what is expected.

Now we can get a guaranteed valid (as valid as your checks are) spaceShipId that is not only valid as soon as it comes into the system but anytime, anywhere it is used in the code. It will provide a level of assurance that it is correct:

So the spaceship request now has a valid ID. What about the rest of the data. Of course this should be validated in the front end before it hits the API but to make make our API more secure, it should also have its own validation to prevent potential hacks. We have started to extract the body out and make it more robust:

Before going further, it’s now apparent that the save method is starting to violate the single responsibility principle — it’s also trying to validate the request. Let’s refactor this and extract it out. 

Instead of passing the service handler the raw JSON object, create a new class to hold the transformed validated data a DTO if you like. Update the service to accept this object, the next step then processes this object to pass into the service…

Pipes, Validation, and Transformation

NestJS provides the concept of Pipes that allow validation and transformation of object data. In most cases, data will need to be transformed in some way or another. There are many techniques to do this including class-validator and class-transformer but I feel implementing the PipeTransform offers most flexibility and benefit, but requires more code.

nest g pipe spaceShipSaveRequestToSpaceShip --flat

This will create a pipe and a test. 

joi will be used to validate the incoming request, install it:

npm install joi
npm install --save-dev @types/joi

Let’s add tests for the pipe — it should eventually take in a save request, validate it and transform the data to a new SpaceShip object. The first test should fail by throwing an error if the body is empty:

We can use joi to validate the object and throw an error if it isn’t valid. Also, we can update the class with the types — the request in and the new SpaceShip class out:

It’s worth noting as this stage Joi would not run in Jest because of the tsconfig.js had the missing config

"esModuleInterop": true

This had to be added in order to import CommonJS modules in compliance with ES6 — more info. But add this and it’ll work.

Next, add another test that tests the conversion is working as expected and a SpaceShip is returned from the incoming request.

Many more test cases and to cover edge conditions can be added at this point to ensure the conversion is done correctly. And best of all it’s totally separated from the controller and service.

Now we can be confident that the request coming in is secure to process. Now, let’s update the controller with the transformation and update the service to now take a SpaceShip object.

The controller is not much neater — with validation taken out of the process. The validation is now done by injecting the SpaceShipSaveRequestToSpaceShip into the @Body which will pipe the value back as a SpaceShip object

The test will now need to change to accept the SpaceShip object:

Also, update the service to take the secure and validated SpaceShip object 

Controller Test Summary

This almost seems like an eternity to reach here, lots of areas have been covered — project creation, controllers and service creation, validation and transformation, and all for what? 

It would be really easy to just pass the raw Json object straight into the service and neglect the validity and security of the data.  

We have managed to create a robust API endpoint that is going to be pretty solid and hold up against bad data. It’s with this discipline, a robust and clean architecture is starting to emerge. 

Part 1 — Unit Testing the Controller | Git branch can be found here.

Next —Part 2 — Unit Testing the service | Git branch can be found here.

Part 3 — Unit Testing the repository | Git branch can be found here.



Source link

Write A Comment