DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Understanding the Fan-Out/Fan-In API Integration Pattern
  • OWASP TOP 10 API Security Part 2 (Broken Object Level Authorization)
  • How to Use State Inside of an Effect Component With ngrx
  • Enhanced API Security: Fine-Grained Access Control Using OPA and Kong Gateway

Trending

  • Customer 360: Fraud Detection in Fintech With PySpark and ML
  • AI-Driven Root Cause Analysis in SRE: Enhancing Incident Resolution
  • Vibe Coding With GitHub Copilot: Optimizing API Performance in Fintech Microservices
  • Mastering Advanced Aggregations in Spark SQL
  1. DZone
  2. Coding
  3. Languages
  4. TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test

TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test

This is a 3 part series for unit testing the controller, service, and repository layers in a typical REST architecture. This part will test the Controller Unit.

By 
Dale Waterworth user avatar
Dale Waterworth
·
Feb. 26, 21 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
21.5K Views

Join the DZone community and get the full member experience.

Join For Free

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 hence the length of the article.

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 the 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 it 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.

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:

TypeScript
 




xxxxxxxxxx
1
12


 
1
//class
2
@Controller('space-ship')
3
export class SpaceShipController {}
4
 
          
5
//test
6
it('should call the service', () => {
7
  const spaceShip = {};
8
 
          
9
  controller.save(spaceShip);
10
 
          
11
  expect(spaceShipService.save).toHaveBeenCalled();
12
});



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 and 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:

TypeScript
 




x


 
1
@Controller('space-ship')
2
export class SpaceShipController {
3
 
          
4
  public save(spaceShip: any) { }
5
}
6
 
          
7
@Injectable()
8
export class SpaceShipService {
9
    save(save: any) {
10
        throw new Error("Method not implemented.");
11
    }
12
}
13
 
          
14
//test
15
describe('SpaceShipController', () => {
16
  let controller: SpaceShipController;
17
  let service: SpaceShipService;
18
 
          
19
  beforeEach(async () => {
20
    const module: TestingModule = await Test.createTestingModule({
21
      controllers: [SpaceShipController],
22
      providers: [SpaceShipService]
23
    }).compile();
24
 
          
25
    controller = module.get<SpaceShipController>(SpaceShipController);
26
    service = module.get<SpaceShipService>(SpaceShipService);
27
  });
28
  
29
  it('should call the service', () => {
30
    const spaceShip = {};
31
 
          
32
    controller.save(spaceShip);
33
 
          
34
    expect(service.save).toHaveBeenCalled();
35
  });
36
});



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:

TypeScript
 




xxxxxxxxxx
1
10


 
1
@Controller('space-ship')
2
export class SpaceShipController {
3
 
          
4
  constructor(private service: SpaceShipService) {
5
  }
6
  @Post()
7
  public save(spaceShip: any) {
8
    this.service.save(spaceShip);
9
  }
10
}


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.  

TypeScript
 




xxxxxxxxxx
1
31


 
1
export interface SaveSpaceShipRequest {
2
  'spaceShipId': string;
3
  'spaceShipName': string;
4
  'spaceShipNumber': number;
5
  'isFasterThanLight': boolean
6
}
7
 
          
8
@Controller('space-ship')
9
export class SpaceShipController {
10
 
          
11
  constructor(private service: SpaceShipService) {
12
  }
13
 
          
14
  @Post()
15
  public save(saveSpaceShipRequest: SaveSpaceShipRequest) {
16
    this.service.save(saveSpaceShipRequest);
17
  }
18
}
19
 
          
20
// test
21
  it('should call the service', () => {
22
    const spaceShip: SaveSpaceShipRequest = {
23
      'spaceShipId': 'abc-123-ship',
24
      'spaceShipName': 'Star Harvester',
25
      'spaceShipNumber': 42,
26
      'isFasterThanLight': true,
27
    };
28
 
          
29
    controller.save(spaceShip);
30
 
          
31
    expect(service.save).toHaveBeenCalled();
32
  });



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

It's good practise 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 space ship 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.

TypeScript
 




xxxxxxxxxx
1
24


 
1
export class SpaceShipId {
2
  public static from(id: string): SpaceShipId {
3
    return new SpaceShipId(id);
4
  }
5
 
          
6
  private static validate(id: string) {
7
    const isValid = true;
8
 
          
9
    if (!isValid) {
10
      throw new Error('Invalid Space ship ID');
11
    }
12
  }
13
 
          
14
  private readonly id: string;
15
 
          
16
  private constructor(code: string) {
17
    SpaceShipId.validate(code);
18
    this.id = code;
19
  }
20
 
          
21
  public value(): string {
22
    return this.id;
23
  }
24
}



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:

TypeScript
 




x
14


 
1
describe('SpaceShipId', () => {
2
  it('should throw an error', () =>{
3
    const id = 'should-fail';
4
 
          
5
    const spaceShipId = () => SpaceShipId.from(id);
6
 
          
7
    expect(spaceShipId).toThrow(Error);
8
  });
9
  
10
  it('should return back a valid id', () =>{
11
    const id = 'abc-0000-xyx';
12
 
          
13
    expect(SpaceShipId.from(id)).toBeTruthy();
14
  });
15
});



The first test fails as expected as it allows an invalid id. We can update the validation function to add all the validation require 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.

TypeScript
 




xxxxxxxxxx
1


 
1
  private static validate(id: string) {
2
    const isValid = id
3
    && id.length == 12;
4
      // regex, other checks etc
5
    
6
    if (!isValid) {
7
      throw new Error('Invalid Space ship ID');
8
    }
9
  }




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.

TypeScript
 




xxxxxxxxxx
1


 
1
const spaceShipId = SpaceShipId.from(saveSpaceShipRequest.spaceShipId);


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:

TypeScript
 




xxxxxxxxxx
1


 
1
@Post()
2
public save(saveSpaceShipRequest: SaveSpaceShipRequest) {
3
  const spaceShipId = SpaceShipId.from(saveSpaceShipRequest.spaceShipId);
4
  this.service.save(saveSpaceShipRequest);
5
}



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 other. 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. 

TypeScript
 




x


 
1
//
2
@Injectable()
3
export class SpaceShipSaveRequestToSpaceShip implements PipeTransform {
4
  transform(value: any, metadata: ArgumentMetadata) {
5
    return value;
6
  }
7
}



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:

TypeScript
 




x


 
1
export class SpaceShip {
2
  spaceShipId: SpaceShipId;
3
  spaceShipName: string;
4
  spaceShipNumber: number;
5
  isFasterThanLight: boolean
6
}
7
// validator and transformer 
8
describe('SpaceShipRequestValidatorPipe', () => {
9
  let transformer;
10
  beforeEach(()=>{
11
    transformer  = new SpaceShipSaveRequestToSpaceShip();
12
  })
13
 
          
14
  it('should be defined', () => {
15
    expect(transformer).toBeDefined();
16
  });
17
 
          
18
  it('should throw error if no body', () => {
19
    const response = () => transformer.transform({}, {});
20
    expect(response).toThrow(BadRequestException)
21
  });
22
});
23
 
          
24
 
          
29
    return ;



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:

TypeScript
 




xxxxxxxxxx
1
28


 
1
@Injectable()
2
export class SpaceShipSaveRequestToSpaceShip implements PipeTransform<SaveSpaceShipRequest, SpaceShip> {
3
 
          
4
  transform(value: SaveSpaceShipRequest, metadata: ArgumentMetadata): SpaceShip {
5
 
          
6
    const schema = Joi.object({
7
      spaceShipId: Joi.string().min(12).max(12).required(),
8
      spaceShipName: Joi.string().max(20).required(),
9
      spaceShipNumber: Joi.number().required(),
10
      isFasterThanLight: Joi.boolean().required(),
11
    });
12
 
          
13
    const {error} = schema.validate(value);
14
 
          
15
    if(error){
16
      throw new BadRequestException('Validation failed');
17
    }
18
 
          
19
    const spaceShip: SpaceShip = {
20
      spaceShipId: SpaceShipId.from(value.spaceShipId),
21
      spaceShipName: value.spaceShipName,
22
      spaceShipNumber: value.spaceShipNumber,
23
      isFasterThanLight: value.isFasterThanLight,
24
    };
25
 
          
26
    return spaceShip;
27
  }
28
}


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 which tests the conversion is working as expected and a SpaceShip is returned from the incoming request.

TypeScript
 




xxxxxxxxxx
1
19


 
1
  it('should convert to valid Space Ship', () => {
2
    const spaceShipRequest: SaveSpaceShipRequest = {
3
      'spaceShipId': 'abc-123-ship',
4
      'spaceShipName': 'Star Harvester',
5
      'spaceShipNumber': 42,
6
      'isFasterThanLight': true,
7
    };
8
    const spaceShip: SpaceShip = {
9
      'spaceShipId': SpaceShipId.from(spaceShipRequest.spaceShipId),
10
      'spaceShipName': 'Star Harvester',
11
      'spaceShipNumber': 42,
12
      'isFasterThanLight': true,
13
    };
14
 
          
15
    // @ts-ignore
16
    const parsedSpaceShip = transformer.transform(spaceShipRequest, {});
17
 
          
18
    expect(parsedSpaceShip).toEqual(spaceShip);
19
  });


Many more tests 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 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:

TypeScript
 




x


 
1
@Post()
2
public save(@Body(new SpaceShipSaveRequestToSpaceShip()) spaceShip: SpaceShip) {
3
  this.service.save(spaceShip);
4
}



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

TypeScript
 




x


 
1
it('should call the service', () => {
2
  const spaceShip: SpaceShip = {
3
    'spaceShipId': SpaceShipId.from('abc-123-ship'),
4
    'spaceShipName': 'Star Harvester',
5
    'spaceShipNumber': 42,
6
    'isFasterThanLight': true,
7
  };
8
 
          
9
  controller.save(spaceShip);
10
 
          
11
  expect(service.save).toHaveBeenCalledWith(spaceShip);
12
});


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

TypeScript
 




xxxxxxxxxx
1


 
1
@Injectable()
2
export class SpaceShipService {
3
    save(save: SpaceShip) {
4
        throw new Error("Method not implemented.");
5
    }
6
}


Controller Test Summary

This almost seems like an eternity to reach here, lot's 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 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.

unit test API TypeScript Data (computing) IT Object (computer science) Requests Branch (computer science)

Opinions expressed by DZone contributors are their own.

Related

  • Understanding the Fan-Out/Fan-In API Integration Pattern
  • OWASP TOP 10 API Security Part 2 (Broken Object Level Authorization)
  • How to Use State Inside of an Effect Component With ngrx
  • Enhanced API Security: Fine-Grained Access Control Using OPA and Kong Gateway

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!