Chapter 12

Getting started with Jest

What is Jest

Jest, developed by Facebook, stands as a prominent testing framework within the JavaScript ecosystem. It finds extensive use across various JavaScript applications, fostering robust testing practices in environments built upon React, Vue, Angular, and Node.js.

The framework encompasses a myriad of features that aid in crafting thorough tests:

  1. Test Runner and Assertion APIs: Jest's integrated test runner efficiently executes tests, while its assertion APIs allow for meticulous verification of expected results, ensuring the reliability of tests.

  2. Organized Test Suites and Matchers: Jest enables structuring tests into suites for better organization, coupled with a diverse range of matchers for precise value assertions, contributing to clearer and more maintainable test code.

  3. Mock Functions and Spies: Its built-in mocking utilities support the creation of stand-in functions, modules, and dependencies. Spies are also available to monitor and verify the invocation of functions, an essential aspect of testing behavior and interactions.

  4. Snapshot Testing: This unique Jest feature facilitates capturing and comparing snapshots of component outputs or data structures, playing a pivotal role in identifying unintended alterations, a practice that greatly enhances change detection workflows.

  5. Coverage Analysis: The included code coverage tool quantifies test coverage, highlighting untested parts of a codebase, and prompts a thorough testing discipline that strives for comprehensive coverage.

  6. Asynchronous Code Testing: The framework caters to the complexity of asynchronous operations through dedicated utilities, streamlining the testing of promises, callbacks, and other asynchronous patterns.

Jest's straightforward setup, combined with its execution efficiency and expansive support documentation, positions it as a highly favored framework for JavaScript testing. Its seamless integration with widespread JavaScript tools further solidifies its status as a foundational element of modern JavaScript development workflows.

Step 1: Create Package.json

We are going to play around with Jest to learn the basics of the Jest framework. I want you to open the new directory in your vs code editor and create a package.json file

{

  "scripts": {

    "test": "jest",

    "test:watch": "jest --watchAll"

  },

  "devDependencies": {

    "jest": "^29.5.0",

    "@types/jest": "^29.5.2"

  }

}



Install the dependencies by running npm install

b

function sum(a, b) {

  return a + b;

}

module.exports = sum;



Step 3: Write a test case

  1. A new file named sum.test.js should be created. The Jest framework is designed to recognize files with .test.js as testing files automatically.

  2. This detection triggers the execution of tests within that file when the test suite is run. It is a best practice to name test files with this convention, as it ensures clarity and a standard across the project's test environment.

const sum = require("./sum");



test("add 1+2 to equal 3", () => {

  expect(sum(1, 2)).toBe(3);

  expect(sum(1, 2)).toBeGreaterThan(2);

});



  1. Writing the first test in Jest involves crafting test cases that are meant to assert the expected functionality of the application code. It is a good practice to explore various assertions that Jest provides to cover different scenarios and edge cases.

  2. Engaging with a variety of Jest assertions can ensure a comprehensive testing suite. Effective testing strategies involve utilizing matchers like .toBe(), .toEqual(), and others that can verify the accuracy of different data types and structures within the application.




Auto-Mocking



What is Auto Mocking

Auto Mocking in Jest is the process of automatically creating mock versions of modules or dependencies for unit testing. This feature in Jest, known as "automocking," simplifies test setup by negating the need for manual mocks, which is considered a best practice for maintaining test isolation and efficiency.

With automocking enabled, Jest preemptively generates mock facades for modules imported into the test environment. This substitute of genuine modules with their mock counterparts allows for precise control over their responses and the simulation of various test conditions.

Employing auto-mocking is advantageous for segregating the unit under test from its external dependencies. Mocking these dependent modules ensures that the unit's functionality can be assessed in isolation, a technique recommended for accurate unit testing.

Incorporating Auto Mocking can streamline testing of intricate systems, easing dependency management. It shifts the focus to the unit of code in question, while interactions with its dependencies are managed in a predictable manner, a strategy that aligns with advanced software testing methodologies.

What are Mock Functions

Mock functions, also known as mocks or mock implementations, serve as stand-ins for real functions or dependencies during testing. Within NestJS, these are integral for unit testing to ensure that the code under test is isolated from external interactions.

  1. Control Behavior: Mock functions in NestJS are crafted to dictate precise behaviors and outcomes, crucial for simulating various scenarios to evaluate code branches. They are especially important when the real implementations involve unpredictability or non-deterministic behavior.

  2. Remove Dependencies: NestJS advocates for mocks to replace actual implementations, thus eliminating the need for external systems or services during testing. This approach is fundamental for creating tests that are both robust and self-contained.

  3. Track Function Calls: Mock functions in a NestJS context are designed to record their usage statistics, such as invocation count and argument details. Such information is pivotal for verifying that components interact as expected.

  4. Simplify Testing: In NestJS, mock functions streamline the testing process by emulating elaborate or resource-intensive functions. For instance, they can mimic database calls or external API interactions, facilitating more efficient and focused testing environments.

In NestJS, Jest is a favored testing framework where mock functions are either created via the Jest mocking APIs or generated through Jest's auto-mocking feature. Utilizing these mock functions in tandem with assertions is a best practice that confirms the correct behavior of components under test.

Step 1: Basic Mock Function

Now we are going to write examples on Mock functions. I want you to create a test folder and create a new file with mock-function.spec.js

//mock-function.spec.js

describe("Mock Function Examples", () => {

  it("should create a basic mock function", () => {

    const mockFn = jest.fn(); //1

    // mockFn.mockReturnValue('HELLO WORLD');

    mockFn.mockReturnValue(3); //2

    console.log(mockFn());

    expect(mockFn()).toBe(3);

    expect(mockFn.mock.calls.length).toBe(2); //3

    expect(mockFn).toHaveBeenCalled();

  });

});



  1. An empty mock function has been created. This serves as a placeholder in testing environments to simulate the behavior of real functions without implementing their logic.

  2. The function is configured to return a value of 3. Returning a static value is a common practice for simplifying tests, ensuring that the output is predictable and the function's behavior is consistent.

  3. Tracking the calls to mock functions is achievable. This allows for the verification of interactions within the code, ensuring that functions are invoked as expected, which aligns with the best practices of test-driven development.

Analogously, think of a mock function as a stunt double in a film: it performs the actions required for the scene (test case) without being the actual actor (real function), and its performance can be precisely controlled and reviewed.

Step 2: Basic Mock Function with Arguments

A basic mock function with arguments is designed to simulate the behavior of a real function, allowing for controlled responses and interactions. It takes predefined arguments and, when invoked within tests, returns a controlled output without executing any actual logic, akin to a rehearsal before a live performance.

Best practice dictates that such mock functions should be as close to the real implementation as possible to ensure reliable tests. Additionally, specifying clear return values or behaviors based on the arguments received allows for a comprehensive testing strategy that can catch a wide array of potential issues before deployment.

it("should create a mock function with argument", () => {

  const createSongMock = jest.fn((createSongDTO) => ({

    id: 1,

    title: createSongDTO.title,

  }));



  console.log(createSongMock({ title: "Lover" }));

  expect(createSongMock).toHaveBeenCalled();

  expect(createSongMock({ title: "Lover" })).toEqual({ id: 1, title: "Lover" });

});



You can also pass an argument to the mock function just like we did createSongDTO

Step 3: Create a Mock Function

it("create mock function using mockImplementation", () => {

  const mockFn = jest.fn();

  mockFn.mockImplementation(() => {

    //1

    console.log("Mock function called");

  });



  mockFn(); // Output: 'Mock function called'

  expect(mockFn).toHaveBeenCalled();

  expect(mockFn).toHaveBeenCalledTimes(1);

  expect(mockFn).toHaveBeenCalledWith();

});



  1. You can also create a mock function with mockImplementation method.

Step 4: Create Mock Function with a Promise

Creating a mock function with a Promise in NestJS facilitates the simulation of database calls or external service responses during testing. By returning a resolved or rejected promise, the mock replicates the asynchronous behavior of real-world scenarios, ensuring comprehensive test coverage.

It is considered a best practice to isolate and mock external dependencies in tests to verify that the system's reaction to various potential responses is handled correctly. Analogous to a flight simulator for pilots, this technique allows developers to safely test the behavior of the application in a controlled environment before deployment.

it("it should create a mock function with promise", () => {

  const mockFetchSongs = jest.fn();

  mockFetchSongs.mockResolvedValue({ id: 1, title: "Dancing Feat" }); //1



  mockFetchSongs().then((result) => {

    console.log(result);

  });



  expect(mockFetchSongs).toHaveBeenCalled();

  expect(mockFetchSongs()).resolves.toEqual({ id: 1, title: "Dancing Feat" }); //2

});



  1. The operation will resolve to a promise that yields the object { id: 1, title: 'Dancing Feat' }. This approach is aligned with best practices, encapsulating the asynchronous nature of operations which may involve I/O processes, such as database interactions.

  2. To verify the resolution of a promise, one can utilize the resolves matcher in testing frameworks. This facilitates the writing of clean, robust tests that ensure promises are fulfilled as expected, which is an essential aspect of reliable software delivery.




SpyOn Functionality



Spies play a crucial role in testing scenarios where there is a need to monitor and manipulate the behavior of function calls. They enable the validation of how functions are used without altering their actual implementation, aligning with best practices for maintaining clean and reliable test suites.

The jest.spyOn() function in Jest is employed to construct a spy for any method of an existing object, allowing for a granular control and assertion of that method's interactions. This utility provides the advantage of asserting calls and responses, facilitating a mock implementation when necessary, which is particularly useful when needing to simulate real-world scenarios in a controlled test environment.

Step 1: SpyOn existing Object

Creating a new file named spyon-demo.spec.js in the test directory initiates the process for implementing test spies. In NestJS, utilizing jest.spyOn() allows monitoring and asserting the behavior of functions within existing objects, ensuring that best practices for test coverage and quality assurance are adhered to.

Incorporating spies into unit tests by using jest.spyOn() is analogous to deploying surveillance equipment; it discreetly observes the interactions without obstructing the

const songRepository = {

  create: (createSongDTO) => {

    // Original method implementation

  },

};



describe("spyOn Demo", () => {

  it("should spyon the existing object", () => {

    const spy = jest.spyOn(songRepository, "create"); //1



    // Call the method

    songRepository.create({ id: 1, title: "Lover" }); //2



    // Assertions

    expect(spy).toHaveBeenCalled(); //3

    expect(spy).toHaveBeenCalledWith({ id: 1, title: "Lover" });

    expect(spy).toHaveBeenCalled();

    expect(spy).toHaveBeenCalledTimes(1);



    // Restore the original method

    spy.mockRestore();

  });

});



  1. A mock function has been implemented for the create property in the SongRepository to simulate functionality without invoking the actual implementation. Utilizing spyOn provides the additional benefit of creating a mock function while also monitoring the call activities, which aligns with best practices for testing where monitoring interactions with certain components is essential.

  2. Upon invoking songRepository.create, the underlying mechanism defers to the spy function equipped with the bespoke fake implementation. This approach ensures that tests can verify that the repository's create function is being called as expected, a technique considered a best practice to validate the integration point without relying on the actual database operation, enhancing test reliability and execution speed.

By employing these techniques, one mimics the delicate workings of a timepiece, ensuring each gear (or function) interacts correctly with the others, while still allowing for inspection of each individual movement (or function call).

Step 2: Spy on the class Method

Simulating the behavior of a class method requires employing a spy. In NestJS, spies are commonly utilized in testing scenarios where the actual execution of a method should be observed without triggering its real effects, akin to a surveillance camera monitoring activity discreetly.

A best practice is to use spies from robust testing libraries like Jest, which can keep track of calls to the method, arguments passed, and even allow specifying return values. This strategy ensures that unit tests remain isolated and are not affected by external factors, much like using a test dummy in car crash simulations to understand impacts without risking actual harm.

An additional tip is to always restore or clean up spies after each test to prevent unexpected behavior in subsequent tests. This is similar to resetting a chessboard after a game; it ensures that the starting conditions are consistent every time.

class ArtistRepository {

  save(createArtistDTO) {

    // Original method implementation

  }

}

it("should spy on the class method", () => {

  const artist = new ArtistRepository();

  const spy = jest

    .spyOn(artist, "save")

    .mockImplementation((createArtistDTO) => createArtistDTO);



  // Call the method

  artist.save({ name: "Martin Garrix" }); //1



  console.log(spy({ name: "Martin Garrix" }));

  // Assertions

  expect(spy).toHaveBeenCalled();

  expect(spy).toHaveBeenCalledWith({ name: "Martin Garrix" });



  // Restore the original method

  spy.mockRestore();

});



  1. Invoking artist.save engages the implementation of the spy function, allowing for monitoring of function calls. The spy function accepts createArtistDTO as an argument and returns the identical createArtistDTO, enabling the confirmation of data integrity and flow through the function.

Best practice dictates the use of such spy functions in test cases to ensure that methods are called with the correct arguments, effectively simulating the behavior of complex dependencies. Analogous to a checkpoint in a relay race, it verifies the baton (in this case, createArtistDTO) is passed correctly, ensuring the next runner (function or method in the application) can proceed without issues.

Step 3: restore All mocks usuing beforeEach hook




describe('spyOn Demo', () => {

afterEach(() => jest.resetAllMocks());

}



Removing the manual mockRestore calls from each test case and utilizing jest.resetAllMocks within the afterEach hook streamlines the testing process. This approach ensures that all mocks are reset automatically after each test, promoting cleaner test architecture and reducing the chance of state leakage between tests, much like a blackboard is wiped clean after each lesson to ensure a fresh start for new information.

Incorporating this method is considered a best practice, as it adheres to the principle of test isolation, akin to how surgeons sterilize their tools after each procedure to prevent cross-contamination. Additionally, this tactic enhances test reliability and maintainability, simplifying future updates to the test suite.



Unit Test Controller



Unit Testing

Unit testing represents a pivotal software testing methodology where the smallest testable parts of an application are evaluated independently. It is designed to affirm that each code segment, be it functions, methods, or classes, operates as intended, delivering the correct outcomes for distinct inputs.

Insights into the practice of unit testing include:

  1. Unit tests facilitate the early discovery of issues, typically during the development phase. This preemptive measure allows for the rectification of problems prior to their expansion throughout the software, thereby streamlining the development process.

  2. The application of unit tests enhances code integrity and fosters the development of code that is both modular and maintainable. Such tests instill confidence in the functionality of code segments and serve as a bulwark against future regressions when the codebase evolves.

  3. When a unit test signals a failure, it precisely pinpoints the troubled segment, significantly expediting the debugging process. This targeted approach to problem-solving is integral to efficient software maintenance.

  4. As code undergoes refactoring, unit tests provide a framework that assures the consistency of unit functionality. They are the custodians of code behavior, ensuring that modifications do not inadvertently affect existing features.

Step 1: Run the Test

Initiating the test process requires executing npm run test:watch, which activates the test watcher. Upon initiation, pressing p allows for pattern matching where entering song.controller.spec.ts focuses the test watcher on this specific file.

As a best practice, targeting specific test files by pattern can significantly streamline the debugging process, akin to using a surgical strike to identify and resolve issues. This focused approach is more efficient than a broader testing strategy, which could be compared to casting a wide net and sifting through irrelevant information.

In addition, it’s advisable to keep the test files named consistently with their corresponding modules. This convention simplifies the identification and execution of related tests, much like arranging books by genre simplifies the search in a library.

import { Test, TestingModule } from "@nestjs/testing";

import { SongController } from "./song.controller";



describe("SongController", () => {

  let controller: SongController;



  beforeEach(async () => {

    const module: TestingModule = await Test.createTestingModule({

      controllers: [SongController],

    }).compile(); //1



    controller = module.get<SongController>(SongController); //2

  });



  it("should be defined", () => {

    expect(controller).toBeDefined();

  });

});



  1. Nest.js automatically generates a fake testing module. Upon execution of the compile function, it instantiates all dependencies required for the testing module, ensuring a controlled environment akin to a laboratory where experiments can be conducted in isolation.

  2. Obtaining an instance of the controller dependency is a straightforward process. It involves using the testing module’s get method, which serves as a direct line to the desired dependencies, much like retrieving a specific book from a well-organized shelf using a catalog.

When the test is executed, the error message is revealed. This error message acts as an early indicator, a lighthouse warning ships of potential danger, enabling developers to pinpoint the fault and implement corrective measures. As a best practice, thorough inspection and understanding of the error message are crucial; it should be seen as a map leading to the bug that needs to be fixed. It's advisable to treat tests as first-class citizens, crafting them with the same care as production code, and reviewing error messages from tests with a keen, diagnostic eye to maintain robustness in the application.

SongController



✕ should be defined (41 ms)



● SongController › should be defined



Nest can't resolve dependencies of the SongController (?). Please make sure that the argument SongService at index [0] is available in the RootTestModule context.



Potential solutions:

- Is RootTestModule a valid NestJS module?

- If SongService is a provider, is it part of the current RootTestModule?

- If SongService is exported from a separate @Module, is that module imported within RootTestModule?

@Module({

imports: [ /* the Module containing SongService */ ]

})





When you check the SongController it is dependent on the SongService. We have to unit test the controller functions individually. That's why we need to mock the SongService to individually test the controller functions

Step 2: Creating a Mock SongService

let service: SongService;



const module: TestingModule = await Test.createTestingModule({

  controllers: [SongController],

  providers: [

    SongService,

    {

      provide: SongService,

      useValue: {

        getSongs: jest

          .fn()

          .mockResolvedValue([{ id: "123131", title: "Dancing" }]),

        getSong: jest.fn().mockImplementation((id: string) => {

          return Promise.resolve({ id: id, title: "Dancing" });

        }),

        createSong: jest

          .fn()

          .mockImplementation((createSongDTO: CreateSongDTO) => {

            return Promise.resolve({ id: "a uuid", ...createSongDTO });

          }),

        updateSong: jest

          .fn()

          .mockImplementation((updateSongDTO: UpdateSongDTO) => {

            return Promise.resolve({ affected: 1 });

          }),



        deleteSong: jest.fn().mockImplementation((id: string) => {

          return Promise.resolve({ affected: 1 });

        }),

      },

    },

  ],

}).compile();



controller = module.get<SongController>(SongController);

service = module.get<SongService>(SongService);



The useValue syntax in NestJS's dependency injection system allows for substituting real implementations with fake ones, useful for testing purposes. For instance, when configuring a provider in a module, setting useValue to an object with methods mirroring those of SongService can simulate database interactions, aligning with the best practice of using fakes or mocks during unit testing to ensure tests run quickly and reliably without real database dependencies.

As an analogy, using useValue is like using a stunt double in a movie; it stands in for the real actor (the SongService), performing necessary actions during testing scenes, which allows the real service to remain unaffected and free from the potential side effects of rigorous testing procedures. This approach is an elegant solution to maintaining a controlled test environment.

Step 3: Test Controller functions

describe("getSongs", () => {

  it("should give me the array of songs", async () => {

    const songs = await controller.getSongs();

    expect(songs).toEqual([{ id: "123131", title: "Dancing" }]);

  });

});



describe("getSong by id", () => {

  it("should give me the song by id", async () => {

    const song = await controller.getSong("123131");

    expect(song.id).toBe("123131");

  });

});



describe("createSong", () => {

  it("should create a new song", async () => {

    const newSongDTO: CreateSongDTO = {

      title: "Runaway",

    };

    const song = await controller.createSong(newSongDTO);

    expect(song.title).toBe("Runaway");

  });

});



describe("updateSong", () => {

  it("should update the song DTO", async () => {

    const updatesongDTO: UpdateSongDTO = {

      title: "Animals",

    };

    const updateResults = await controller.updateSong("a uuid", updatesongDTO);

    expect(updateResults).toBeDefined();

    expect(updateResults.affected).toBe(1);

  });

});



describe("deleteSong", () => {

  it("should delete the song", async () => {

    const deleteResult = await controller.deleteSong("a uuid");

    expect(deleteResult.affected).toBe(1);

  });

});





Unit Test Service



Step 1: Run the test for SongService

Initially run the test for Song Service.

import { Test, TestingModule } from "@nestjs/testing";

import { SongService } from "./song.service";



describe("SongService", () => {

  let service: SongService;



  beforeEach(async () => {

    const module: TestingModule = await Test.createTestingModule({

      providers: [SongService],

    }).compile();



    service = module.get<SongService>(SongService);

  });



  it("should be defined", () => {

    expect(service).toBeDefined();

  });

});



npm run test:watch



Now there's a pattern that's established for testing. SongService needs similar testing help as the controller.

FAIL  src/song/song.service.spec.ts (34.922 s)

  SongService

    ✕ should be defined (42 ms)



  ● SongService › should be defined



    Nest can't resolve dependencies of the SongService (?). Please make sure that the argument SongRepository at index [0] is available in the RootTestModule context.



    Potential solutions:

    - Is RootTestModule a valid NestJS module?

    - If SongRepository is a provider, is it part of the current RootTestModule?

    - If SongRepository is exported from a separate @Module, is that module imported within RootTestModule?

      @Module({

        imports: [ /* the Module containing SongRepository */ ]

      })



       6 |

       7 |   beforeEach(async () => {

    >  8 |     const module: TestingModule = await Test.createTestingModule({

         |                                   ^

       9 |       providers: [SongService],

      10 |     }).compile();

      11 |




Now there's the same problem as the conroller. To fix this, create the mock SongRepository to run the test for SongService.

Step 2: Create the Mock Repository

let service: SongService;

let repo: Repository<Song>;



const oneSong = { id: "a uuid", title: "Lover" };

const songArray = [{ id: "a uuid", title: "Lover" }];



const module: TestingModule = await Test.createTestingModule({

      providers: [

        SongService,

        {

          provide: getRepositoryToken(Song),

          useValue: {

            find: jest

              .fn()

              .mockImplementation(() => Promise.resolve(songArray)),

            findOneOrFail: jest

              .fn()

              .mockImplementation((options: FindOneOptions<Song>) => {

                return Promise.resolve(oneSong);

              }),

            create: jest

              .fn()

              .mockImplementation((createSongDTO: CreateSongDTO) => {

                return Promise.resolve(oneSong);

              }),

            save: jest.fn(),

            update: jest

              .fn()

              .mockImplementation(

                (id: string, updateSongDTO: UpdateSongDTO) => {

                  return Promise.resolve(oneSong);

                },

              ),

            delete: jest

              .fn()

              .mockImplementation((id: string) =>

                Promise.resolve({ affected: 1 }),

              ),

          },

        },

      ],



service = module.get<SongService>(SongService);

repo = module.get<Repository<Song>>(getRepositoryToken(Song));



Step 3: Test SongService

it("should give me the song by id", async () => {

  const song = await service.getSong("a uudi");

  const repoSpy = jest.spyOn(repo, "findOneOrFail");

  expect(song).toEqual(oneSong);

  expect(repoSpy).toBeCalledWith({ where: { id: "a uudi" } });

});



it("should create the song", async () => {

  const song = await service.createSong({ title: "Lover" });

  expect(song).toEqual(oneSong);

  expect(repo.create).toBeCalledTimes(1);

  expect(repo.create).toBeCalledWith({ title: "Lover" });

});



it("should update the song", async () => {

  const song = await service.updateSong("a uuid", { title: "Lover" });

  expect(repo.update).toBeCalledTimes(1);

  expect(song).toEqual(oneSong);

});



it("should delete the song", async () => {

  const song = await service.deleteSong("a uuid");

  const repoSpyOn = jest.spyOn(repo, "delete");

  expect(repo.delete).toBeCalledTimes(1);

  expect(song.affected).toBe(1);

  expect(repoSpyOn).toBeCalledWith("a uuid");

});




End To End Testing



What is E2E Testing?

End-to-end testing (E2E testing) encompasses a comprehensive software testing approach, simulating real user scenarios to ensure the application operates effectively from start to finish. It scrutinizes the system's behavior and functionality through the lens of user interactions, extending across various components and systems.

In-depth analysis of end-to-end testing reveals:

  1. Integration issues become apparent through E2E testing, as it uncovers problems that surface during the interaction between disparate application segments. Adopting this testing strategy is a best practice for identifying data inconsistencies, communication breakdowns, and API compatibility issues, which are often obscured within the confines of unit and integration testing stages.

  2. E2E testing is instrumental in validating user journeys, ensuring that the application reliably navigates the critical pathways a user might traverse. It acts as a safeguard, confirming that the software responds with the correct outputs across a multitude of user interactions, which is a testament to meticulous software craftsmanship.

  3. The user experience is enhanced when E2E testing is employed, as it encompasses a holistic view of the application's operation. This approach is akin to a conductor overseeing an orchestra, ensuring every section performs harmoniously, resulting in a seamless performance—mirrored in the software by uniform navigation, fluid data transactions, accurate UI representations, and responsive user engagements.

  4. The deployment process is fortified with E2E testing, functioning as the final rehearsal before the live performance of the application in the production environment. It instills a level of assurance that all systems operate as expected, analogous to a dress rehearsal in theater, ensuring that every act and scene transitions flawlessly before the curtain rises.

Step 1: Add E2E script

"test:e2e:watch": "jest --watch --detectOpenHandles --config ./test/jest-e2e.json"



In Nest.js, a watch script for end-to-end (E2E) testing is not part of the default setup. For continuous feedback during development, it is recommended to implement a watch mechanism, such as using Jest's --watch flag, to automatically re-run E2E tests when changes are detected.

Step 2: Add TypeORM Module

The TypeORM module must be registered while creating the TestingModule.



imports: [

        TypeOrmModule.forRoot({

          type: 'postgres',

          url: 'postgres://postgres:root@localhost:5432/test-dev',

          synchronize: true,

          entities: [Song],

          dropSchema: true,

        }),

        SongModule,

      ],

    }).compile();



Ensuring the creation of a separate database for application testing is essential. Setting the dropSchema option to true within the TypeORM configuration ensures the schema is automatically dropped post-testing, maintaining a clean state.

Step 3: Clear SongRepository

afterEach(async () => {

  // Fetch all the entities

  const songRepository = app.get("SongRepository");

  await songRepository.clear();

});



The SongRepository instance can be obtained by invoking the app.get method.

Step 4: Test Get Songs endpoints

Registering the TypeORM module is a necessary step in the configuration of the TestingModule in NestJS, ensuring that the data layer is appropriately integrated for testing environments. This process aligns with established best practices, enabling the simulation of database interactions and the assessment of data persistence within the service under test.

When testing Get Songs endpoints, the approach includes simulating client requests and asserting the expected responses, which should reflect the retrieval of song data. It's a common practice to mock the service layer to return predefined data, thereby isolating the controller's response logic for accurate and efficient validation.

import * as request from "supertest";



const createSong = (createSongDTO: CreateSongDTO): Promise<Song> => {

  const song = new Song();

  song.title = createSongDTO.title;

  const songRepo = app.get("SongRepository");

  return songRepo.save(song);

};



it(`/GET songs`, async () => {

  const newSong = await createSong({ title: "Animals" });

  const results = await request(app.getHttpServer()).get("/songs");

  expect(results.statusCode).toBe(200);

  expect(results.body).toHaveLength(1);

  expect(results.body).toEqual([newSong]);

});



The supertest package, pre-installed in NestJS, facilitates the testing of HTTP APIs by simulating server behavior and handling HTTP requests and responses. This allows for the creation of robust test suites that ensure APIs behave as expected under various conditions, adhering to best practices for maintaining high-quality software standards.

Step 5: Test GET Song Endpoint

When conducting tests for the GET Song endpoint, it is essential to ensure that the request retrieves the correct song data and handles potential errors gracefully. The test should mimic a client's request for a song, verifying that the endpoint returns the expected song data with the correct HTTP status code.

it("/GET songs/:id", async () => {

  const newSong = await createSong({ title: "Animals" });

  const results = await request(app.getHttpServer()).get(

    `/songs/${newSong.id}`

  );

  expect(results.statusCode).toBe(200);

  expect(results.body).toEqual(newSong);

});



Step 6: Test PUT Song Endpoint

In testing the PUT Song endpoint, the focus is on the endpoint's ability to update an existing song's data accurately and to validate any changes made. The test should simulate a client updating song details, confirming that the endpoint processes the update correctly and returns a success response or appropriate error message.

it("/PUT songs/:id", async () => {

  const newSong = await createSong({ title: "Animals" });

  const updateSongDTO: UpdateSongDTO = { title: "Wonderful" };

  const results = await request(app.getHttpServer())

    .put(`/songs/${newSong.id}`)

    .send(updateSongDTO as UpdateSongDTO);

  expect(results.statusCode).toBe(200);

  expect(results.body.affected).toEqual(1);

});



Step 7: Test Create Song Endpoint

To effectively test the 'Create Song' endpoint, the TestingModule should instantiate with the TypeORM module integrated, ensuring database interactions are part of the test environment. This integration is crucial for replicating the application's behavior in a controlled testing scenario, allowing for accurate verification of the endpoint's functionality.

A best practice in this context is to utilize mock repositories or in-memory databases, which provide isolation for tests, thus preventing side effects on the actual database and ensuring that each test case runs in a consistent state. This technique not only accelerates testing procedures but also maintains the integrity of test scenarios.

it("/POST songs", async () => {

  const createSongDTO = { title: "Animals" };

  const results = await request(app.getHttpServer())

    .post(`/songs`)

    .send(createSongDTO as CreateSongDTO);

  expect(results.status).toBe(201);

  expect(results.body.title).toBe("Animals");

});



Step 8: Test Delete Song Endpoint

For testing the Delete Song endpoint within a NestJS application, the TestingModule should be configured to replicate the application's behavior in a controlled environment. The endpoint's robustness is ensured by creating test cases that cover all possible scenarios, including valid deletions, attempts to delete non-existent songs, and handling of unauthorized requests.

Best practices dictate the utilization of TypeORM's transactional operations during testing to maintain database integrity, which allows for each test to run in isolation without impacting the database state. Additionally, incorporating service mocks within the test suite is recommended to simulate interactions with the database or external services, ensuring tests are fast and reliable.

it("/DELETE songs", async () => {

  const createSongDTO: CreateSongDTO = { title: "Animals" };

  const newSong = await createSong(createSongDTO);

  const results = await request(app.getHttpServer()).delete(

    `/songs/${newSong.id}`

  );

  expect(results.statusCode).toBe(200);

  expect(results.body.affected).toBe(1);

});

$7 bundle of my best NestJS + backend developer Courses.
More details coming soon.


Get the latest insights from the marketing world.

A blog that focuses on providing practical tips and strategies for businesses to improve their marketing and sales efforts.

Solutions

Helping you help your customers.

Sell smarter, better, and faster.

The insights you need to make smarter business decisions.