Skip to content

Latest commit

 

History

History
2277 lines (1777 loc) · 69.2 KB

File metadata and controls

2277 lines (1777 loc) · 69.2 KB

Chapter-8 Testing

Introducing to JEST

chapter 8 1
Introducing To JEST

Automated testing is considered an essential part of any serious software development effort. Automation makes it easier to repeat "individual - tests", or "test - suites" -quickly during development.

With NestJS we can use any testing framework we prefer. However it can be quite tedious to set everything up. Luckily for us Nest provides a built-in integration with the "Jest" - testing framework out of the box, so we don’t have to do anything to get started.

chapter 8 2
Introducing To JEST -2

"Jest" is a delightful JavaScript testing framework with a focus on simplicity. It allows you to write test with an approachable, familiar, and feature rich API that gives you result quickly.

"Jest" provides great "Error - messages" and built-in Mocking utilities, to make testing your applications much simpler. Also it reliably run test in "parallel".

To make tests run even faster, "Jest" runs previously failed test first, and then reorganizes test runs, based on how long "Test - file" take.

chapter 8 3
Introducing To JEST -3

In NestJS applications, we use "Jest" to run "unit - tests" and "end-to-end - tests"

There are several command you can use to run "specific test",

  • npm run test, for unit tests

  • npm run test:cov, for unit test and collecting testing coverage

  • npm run test:e2e, for end-to-end tests

With nest. We automatically have all of this power with no additional setup needed!.

So far in this course we haven’t paid too much attention to "testing". Thus, some of the existing, or automatically generated tests may fail.

In the next few lesson. We’ll introduce you to the "Jest" framework. Show you how to use NestJS - "testing - utilities", and fix some of the existing "test - file" - all step by step.

Getting Started With Test Suites

chapter 8 4
Getting Started With Test Suites

Testing by itself is quite the endless topic that could be fragmented into many separate lessons. We can’t really cover every aspect of testing itself. We’ll be focusing primarily on how testing works within NestJS.

We’ll be focusing mainly on tips and tricks, showcasing some best practices, and go over how you can add manage test in your "Nest - applications".

chapter 8 5
Getting Started With Test Suites

For unit tests in NestJS, it’s a common practice to keep the ".spec.ts" - files in the same folder as the application source code files that they test.

Each "Controller", "Provider", "Service", etc should have its own dedicated "test - file".

The test file extension must be ".spec.ts". This is so that integrated - "test tooling" can identify it as a "test - file" with "test - suites" or "end-to-end - test".

chapter 8 6
Getting Started With Test Suites

These files are typically located in a dedicated "/test/" - directory by default.

"End-to-end - test" are typically grouped into separate files by the "feature" or "functionality" that they test.

For "end-to-end - test". The file extension must be ".e2e-spec.ts".

While "unit - tests" focus on individual classes and functions. "End-to-end - test" are great for "high level validation" of the entire system.

"End-to-end - testing" covers the interaction "End-to-end - testing" covers the interaction of classes and models at more "aggregate level[1]" closer to the kid of interaction that "end-users" will have with the "production system".

As application grows, it becomes hard to manually test the "end-to-end - behavior" of each "API - endpoint".

Automated "end-to-end - test" help us ensure that the "overall behavior" of the system is correct, and it meets project requirement.

To better understand how tests look an how they work. Let’s open up "CoffeesService" - ".spec" - file, we haven’t looked at yet and walk through everything - step by step!.

// coffees.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { CoffeesService } from "./coffees.service";

describe("CoffeesService", () => {
    let service: CoffeesService;

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [CoffeesService],
        }).compile();

        service = module.get<CoffeesService>(CoffeesService);
    });

    it("should be defined", () => {
        expect(service).toBeDefined();
    });
});

First, we can see that the "describe()" - function that define a "block" which groups together several related tests.

In this case, the "describe()" - block is grouping all the "unit - test" that concern "CoffeesService" - class.

Inside this block. We can see that "beforeEach()" - function.

This function passed into the beforeEach() - hook, will be executed before EVERY test. This is typically referred to as the "setup phase".

Often while writing tests. We have some "setup work" that needs to happen BEFORE each test runs; and sometimes you need to do something "after" EACH test runs.

"Jest" provides several other "helper" - functions to handle situations like this.

We know beforeEach() now, but the other available options are: "beforeAll()", "afterEach()", and "afterAll()".

So looking at this test file. Let’s dive into the beforeEach() - function call here, to see what it’s doing.

// coffees.service.spec.ts
    ...
    ...
    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [CoffeesService],
        }).compile();

        service = module.get<CoffeesService>(CoffeesService);
        // service = wait module.resolve(CoffeesService)
        //                       ~~~~~~~
    });
    ...
    ...

At a very high level. We can see that we’re instantiating some pf Module by compiling a "Test - Module", and then utilizing this module to get a hold of the "CoffeesService".

Then it looks like we’re storing it in the "service" - variable, we use within our "individual - tests" inside of this describe() - block.

Let’s start by analyzing everything that’s happening here.

The "Test" - Class is, useful for providing an application "ExecutionContext" that essentially Mocks the full Nest "runtime". But gives you "hooks" that make it easy to manage "Class - instances", and do anything like Mocking and "overriding aspects" of your application.

The "Test" - Class, has a "createTestingModule({})" - method that takes a 'Module - Metadata" - Object as its argument the same object we pass into our @Module({ /* this object */ }) - decorators.

One important thing to note here is, the ".compile()" - method.

This method bootstrapped the Module with its dependencies, similar to the way we bootstrap our application in our main.ts - file: with "NestFactory.create(/AppModule/)".

The .compile() - method return a "TestingModule" - instance which in turn gives us access to a few helpful methods!.

Once our "TestingModule" is compiled. We can retrieve any static instance declared within the Module by using the ".get()" - method we see being used here!.

As we can see, we’re calling the get() - method here to retrieve our "CoffeesService" from within our compiled "TestingModule", and then storing it in the "service" - variable defined within our describe() - block.

This is standard practice for testing, that allows us to "store" and "use" this Service, within ALL of our tests that are a part of THIS describe() - block!.

Note
Just as a side note for future reference. If you need to retrieve "request-scoped" or "transient-scoped" - Providers. Use the ".resolve()" method here instead of "get()".

The next big area in our "test - file" is the "it()" - function.

// coffees.service.spec.ts
    ...
    ...
    it("should be defined", () => {
        expect(service).toBeDefined();
    });
    ...
    ...

An "it()" - function represents an "individual - test".

In this automatically generated test. There isn’t much going on. The test is currently just checking whether the "service" - variable is defined or not. If the service is not defined, the test fails.

All right. So let’s run this particular "test file", and see if our 1 (one) - test passes or fails.

Let’s open up the terminal and run the following command:

$ npm run test:watch -- coffees.service

The "test:watch" - script will run our "unit - tests" in "watch" mode, automatically re-running test on any file change.

We’re passing in "coffees.service" here for example sake, just to indicate that we want to -only- run test for this specific file. Typically we won’t need to do this.

After running the script in our terminal, it looks like the "TestingModule" couldn’t bootstrap properly.

$ npm run test:watch -- coffees.service
 FAIL  src/coffees/coffees.service.spec.ts
  CoffeesService
    ✕ should be defined (16 ms)

  ● CoffeesService › should be defined

    Nest can't resolve dependencies of the CoffeesService (?, FlavorRepository, Connection, ConfigService, CONFIGURATION(coffees)).
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Please make sure that the argument CoffeeRepository at index [0] is available in the RootTestModule context.

    Potential solutions:
    - If CoffeeRepository is a provider, is it part of the current RootTestModule?
    - If CoffeeRepository is exported from a separate @Module, is that module imported within RootTestModule?
      @Module({
        imports: [ /* the Module containing CoffeeRepository */ ]
      })
      ...
      ...

  ● CoffeesService › should be defined

    expect(received).toBeDefined()

    Received: undefined

      14 |
      15 |     it("should be defined", () => {
    > 16 |         expect(service).toBeDefined();
         |                         ^
      17 |     });
      18 | });
      19 |

      at Object.<anonymous> (coffees/coffees.service.spec.ts:16:25)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.468 s, estimated 8 s
Ran all test suites matching /coffees.service/i.

Active Filters: filename /coffees.service/
 › Press c to clear filters.

Watch Usage
 › Press a to run all tests.
 › Press f to quit "only failed tests" mode.
 › Press o to only run tests related to changed files.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.

After running the script in our terminal, it looks like the "TestingModule" couldn’t bootstrap properly and it’s throwing some Errors, but what happened?

Since the "CoffeesService" - class depends on many other database related providers like "Connection" or "Entity - Repositories" that are NOT registered in the "TestingModule". Nest throw the "Nest can’t resolve dependencies error of ..".

So how can we fix this?

Let’s had back to our testing - file and look at the "Modules - Metadata".

// coffees.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { CoffeesService } from "./coffees.service";

describe("CoffeesService", () => {
    let service: CoffeesService;

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [CoffeesService],
                    // ~~~~~~~~~~~~~~~~
        }).compile();

        service = module.get<CoffeesService>(CoffeesService);
    });

    it("should be defined", () => {
        expect(service).toBeDefined();
    });
});

Our "TestingModule" consist of only 1 - Provider which is "CoffeesService". In theory, to fix the error we could just add the required providers here into "providers:[]" Array. However this would "against best practice" and the general philosophy behind the "unit - test!".

"Unit - test" should be performed in "-isolation-" but that doesn’t necessarily mean "complete isolation". By isolation we mean that a test shouldn’t rely on "external - dependencies".

One philosophy for "unit - testing" is, to Mock everything in situations like this. But that often lead to "fragile - test" that are hard to maintain, and don’t bring any significant value.

Our "CoffeesService" depends on "database" related providers, but the last thing we want to do is, to instantiate a Connection to a "real - database", just to perform "unit - tests".

So what other options do we have?

Without needing to create complicated Mocks or connecting to a real database. All we really need to do is make sure that all "requested - providers" are available to the "TestingModule".

As a "temporary solution". Let’s provide all the "classes" our "CoffeesService" depends on, using the "custom providers" - syntax.

// coffees.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { CoffeesService } from "./coffees.service";

describe("CoffeesService", () => {
    let service: CoffeesService;

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [
                CoffeesService,
                { provide: Connection, useValue: {} },                  // <<<
                { provide: getRepositoryToken(Coffee), useValue: {} },  // <<<
                { provide: getRepositoryToken(Flavor), useValue: {} },  // <<<
            ],
        }).compile();

        service = module.get<CoffeesService>(CoffeesService);
    });

    it("should be defined", () => {
        expect(service).toBeDefined();
    });
});

We need to provide “Connection”, and our two Entities using the "getRepositoryToken()" - function, making sure to import it from @nestjs/typeorm - package.

"getRepositoryToken()" accepts an Entity. In our case "Flavor" and "Coffee" as an argument, and returns an "InjectionToken".

For now, let’s just give all of these Providers and "empty - Object" "{}" for value.

Once we start testing - "specific - methods". We will replace these "empty - Objects" with "Mocks".

Now, we get back to our "CoffeesService" - file, and make some temporary - changes to PASS the "suites - test"

// coffees.service.ts
...
...

@Injectable({ scope: Scope.DEFAULT })
export class CoffeesService {
    constructor(
        @InjectRepository(Coffee)
        private readonly coffeeRepository: Repository<Coffee>,
        @InjectRepository(Flavor)
        private readonly flavorRepository: Repository<Flavor>,
        private readonly connection: Connection,
        // XXX NOTE: unused variable make Jest - 'suite - test' fail XXX
        // private readonly configService: ConfigService,
        // @Inject(coffeesConfig.KEY)
        // private readonly coffeesConfiguration: ConfigType<typeof coffeesConfig>,
    ) {
        console.log("[!!] CoffeesService - instantiated");
    }
}

We put some comment into "ConfigService" and "coffeesConfiguration" since these two variable is unused or never called by the "CoffeesService" - "signature - methods", for testing purpose.

Let’s save everything, and open up the terminal again to see if these changes worked.

$ npm run test:watch -- coffees.service
 PASS  src/coffees/coffees.service.spec.ts
  CoffeesService
    ✓ should be defined (43 ms)

  console.log
    [!!] CoffeesService - instantiated

      at new CoffeesService (coffees/coffees.service.ts:28:17)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.488 s
Ran all test suites matching /coffees.service/i.

Watch Usage: Press w to show more.

As we can see, the "TestingModule" was properly bootstrapped now and our basic generated test doesn’t fail anymore.

Adding Unit Tests

There are many different strategies on how we can structure our "test - suites". When testing "services" or similar Classes that contain business logic. We prefer "grouping" the related tests by "methods". Using "method names" as our "describes()"- blocks.

In this lesson, let’s figure out how we can test the findOne() - method.

// coffees.service.ts
...
...

@Injectable({ scope: Scope.DEFAULT })
export class CoffeesService {
    constructor(
        ...
        ...
    ) {
        console.log("[!!] CoffeesService - instantiated");
    }
    ...
    ...
    async findOne(id: string) {
        // throw "A Random Error";
        const coffee = await this.coffeeRepository.findOne(id, {
            //                    ~~~~~~~~~~~~~~~~~~~~~~~~
            relations: ["flavors"],
        });

        if (!coffee) {
            throw new NotFoundException(`Coffee with 'id: #${id}' not found`);
        }

        return coffee;
    }
    ...
    ...
}

One problem we’ll notice is, that the findOne() - method use "CoffeeRepository.findOne() inside of the it. We’ll have to make sure to "mock" this "Repository - method" for our tests to run properly!.

Looking at the code above. It seems depending on whether a Coffee "exists". There are two different scenarios we must cover with a "unit - test".

Let’s head back to the .spec.ts - file and start by defining a new "describe()" - block.

// coffees.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { CoffeesService } from "./coffees.service";

describe("CoffeesService", () => {
    let service: CoffeesService;

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            ....
            ....
        }).compile();

        service = module.get<CoffeesService>(CoffeesService);
    });

    it("should be defined", () => {
        expect(service).toBeDefined();
    });

    describe("findOne", () => {
        describe("when Coffee with ID exists", () => {
            it("should return the Coffee - Object", async () => {
                // ...
                // ...
            });
        });

        describe("otherwise", () => {
            it("should throw 'NotFoundException'", async () => {
                //...
                //...
            });
        });
    });

});

Inside this block. Let’s first define the "success" - path. This part is pretty self-explanatory, but we want to name our describe() and it() - functions with actions.

As you can see we have describe: "when coffee with ID exists", and it:"should return the Coffee - Object". These are very descriptive, easy to read, and it gets right to the point of what exactly this test is covering.

All of these names get output into the terminal when our tests run. So it’s always helpful to make these names and descriptions very specific.

But back to our test at hand, in our findOne() - method. When a "coffee" with a specific "ID" exists we want to test if the method return the "Coffee - Object", or in our case the "Coffee - Entity".

Let’s leave the implementation blank for now.

Next let’s define a test for the "failure" path here. We’re describing a test for when a "Coffee doesn’t exist". Explaining in "it()" - function, that we expect to findOne() - method to throw an Exception.

All right, with this in place, we can now start writing the test logic itself.

Inside our "success" - path test. Let’s add the following code.

// coffees.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { CoffeesService } from "./coffees.service";

describe("CoffeesService", () => {
    ...
    ...

    describe("findOne", () => {
        describe("when Coffee with ID exists", () => {
            it("should return the Coffee - Object", async () => {
                const coffeeId = "1";
                const expectedCoffee = {};

                const coffee = await service.findOne(coffeeId);
                expect(coffee).toEqual(expectedCoffee);
            });
        });

        describe("otherwise", () => {
            it("should throw 'NotFoundException'", async () => {
                //...
                //...
            });
        });
    });

});

In this test, we’re hard-coding a random "id", and calling to findOne() - method of our "CoffeesService" with this "id" passed in as an argument.

The important part here is, the actual test itself, which is that we expect(coffee).toEqual(expectedCoffee). There are many many different ways to assert these tests. After you declare your expect(), pusth dot " . ", and check your intellisence, to see all of the other great options for test assertions.

All right. So let’s save our change so far, open up our terminal, and see what "Jest" shows us now.

$ npm run test:watch -- coffees.service
 FAIL  src/coffees/coffees.service.spec.ts (6.495 s)
  CoffeesService
    ✓ should be defined (43 ms)
    findOne
      when Coffee with ID exists
        ✕ should return the Coffee - Object (14 ms)
      otherwise
        ✓ should throw 'NotFoundException' (5 ms)

  ● CoffeesService › findOne › when Coffee with ID exists › should return the Coffee - Object

    TypeError: this.coffeeRepository.findOne is not a function

      40 |         // throw "A Random Error";
    > 41 |         const coffee = await this.coffeeRepository.findOne(id, {
         |                                                    ^
      42 |             relations: ["flavors"],
      43 |         });
      44 |

      at CoffeesService.findOne (coffees/coffees.service.ts:41:52)
      at Object.<anonymous> (coffees/coffees.service.spec.ts:35:46)

  console.log
    [!!] CoffeesService - instantiated

      at new CoffeesService (coffees/coffees.service.ts:24:17)

  console.log
    [!!] CoffeesService - instantiated

      at new CoffeesService (coffees/coffees.service.ts:24:17)

  console.log
    [!!] CoffeesService - instantiated

      at new CoffeesService (coffees/coffees.service.ts:24:17)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 2 passed, 3 total
Snapshots:   0 total
Time:        6.942 s
Ran all test suites matching /coffees.service/i.

It’s look like we’re getting the Type - error: "this.coffeeRepository.findOne() is not a function" Error.

In the previous lesson we used empty JavaScript - Object "{}" as our "Entity - Repositories".

// coffees.service.spec.ts
...
...
    { provide: Connection, useValue: {} },
    { provide: getRepositoryToken(Coffee), useValue: {} },
    { provide: getRepositoryToken(Flavor), useValue: {} },
...
...

Obviously these JavaScript - Objects don’t have any methods defined, so it makes sense were seeing

So how can we fix it?

There are many different ways of how we can "Mock - Repositories" or any other Classes we need to Mock. For example, we could use the "Object.create()" - function and pass in the Repositories - "class prototype" to initialize the" Repository - Object" without any dependencies.

But in the long run, this isn’t the best idea.

In this case "TypeORM" plans to remove the "Repository - Class" entirely at some point in the future. They might use "literal - objects" instead one day.

Although that is specific to this situation. It shows that we need to be careful with" 3rd-party libraries" and potential changes they make, and how those changes can even impact our tests!.

Another way would be, to create a "generic function" that simply returns a "Mocked - Object "{}" " with all of the same methods that repository class provided.

And then "stub[2]" on these methods to manipulate their behavior based on certain conditions. It could potentially look something like above code.

Note
this "generic - function" in Mock just for illustration purposes. There are many other methods that we didn’t cover here in this Mock.
// coffees.service.spec.ts
import { Connection, Repository } from "typeorm";
...

type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
const createMockRepository = <T = any>(): MockRepository<T> => ({
    findOne: jest.fn(),
    create: jest.fn(),
})

describe("CoffeesService", () => {
    ...
    ...
})

So let’s analyze this code real quick.

First, we have a new "MockRepository" - Type, which represents on object that consist of some of the properties that the Repository - Type" contains as well.

However all of these "values" are of Type "jest.Mock" which is a "mock - function" provided by "Jest".

Then we have a "createMockRepository()"- generic function that returns newly defined "MockRepository" - Class. When the Type argument is not provided it falls back to "any".

Note
Just keep in mind, that we’re not implementing every method for Repository. We could obviously add more here if we needed them for out tests.
// coffees.service.spec.ts
import { Connection, Repository } from "typeorm";
...
...

describe("CoffeesService", () => {
    let service: CoffeesService;
    let coffeeRepository: MockRepository;   // [2] <<<

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [
                CoffeesService,
                { provide: Connection, useValue: {} },
                { provide: getRepositoryToken(Coffee), useValue: createMockRepository() },  // [1] <<<
                { provide: getRepositoryToken(Flavor), useValue: createMockRepository() },  // [1] <<<
            ],
        }).compile();

        service = module.get<CoffeesService>(CoffeesService);
        CoffeeRepository = module.get<MockRepository>(getRepositoryToken(Coffee));  // [2] <<<
    });
    ...
    ...
})

All right, with our Mock function created. Let’s replace those previously used "empty - Objects" with this "createMockRepository()" function call.

Great, now for the second step, we need to make the "coffeeRepository" - variable available in our test functions.

To do this, let’s define a new variable under where we have "service: CoffeesService", and name it `"CoffeeRepository: MockRepository".

Now in our "beforeEach()" - hook, let’s retrieve the "value" and set it to "MockRepository" variable.

Now, we can move onto our test again and Mock the findOne()- method.

// coffees.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { CoffeesService } from "./coffees.service";
...
...

describe("CoffeesService", () => {
    ...
    ...

    describe("findOne", () => {
        describe("when Coffee with ID exists", () => {
            it("should return the Coffee - Object", async () => {
                const coffeeId = "1";
                const expectedCoffee = {};

                coffeeRepository.findOne.mockReturnValue(expectedCoffee);   // <<<
                const coffee = await service.findOne(coffeeId);
                expect(coffee).toEqual(expectedCoffee);
            });
        });

        describe("otherwise", () => {
            it("should throw 'NotFoundException'", async () => {
                //...
                //...
            });
        });
    });

});

If you type "coffeeRepository.findOne()" and type dot (.) after, you’ll see various method available to us now, thanks to "Jest!".

Since we simply want to mock that this findOne() - method return back our expected value variable. Let’s just use: "mockReturnValue()" - method and return "expectedCoffee".

Let’s save our changes and open up terminal again.

$ npm run test:watch -- coffees.service
 PASS  src/coffees/coffees.service.spec.ts (6.459 s)
  CoffeesService
    ✓ should be defined (42 ms)
    findOne
      when Coffee with ID exists
        ✓ should return the Coffee - Object (6 ms)
      otherwise
        ✓ should throw 'NotFoundException' (4 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        6.884 s
Ran all test suites matching /coffees.service/i.

Watch Usage: Press w to show more.

As we can see our test succeeds now!. Great!.

Lastly, we need to finish the test logic for the "failure" path.

Let’s head over to our "text - editor" again and add the logic in the "it()" - block.

// coffees.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { CoffeesService } from "./coffees.service";
...
...

describe("CoffeesService", () => {
    ...
    ...

    describe("findOne", () => {
        describe("when Coffee with ID exists", () => {
	...
	...
        });

        describe("otherwise", () => {
            it("should throw 'NotFoundException'", async () => {
                const coffeeId = "1";
                coffeeRepository.findOne.mockReturnValue(undefined);

                try {
                    await service.findOne(coffeeId);
                } catch (err) {
                    expect(err).toBeInstanceOf(NotFoundException);
                    expect(err.message).toEqual(`Coffee with 'id: #${coffeeId}' not found`);
                }
            });
        });
    });

});

In this code, we can see that we’re also using "mockReturnValue()", but passing in "undefined" this time.

This mimics is a Response that our Entity does not exist in the database. If everything works as it should here, the "service" should throw a "NotFoundException" with expected error message which is "Coffee with 'id: #${coffeeId}' not found".

We’re wrapping all the code here in "try-catch" - block, just to make sure we capture any exceptions that that will obviously occur here, since we Mocked the "findOne()" return value to be "undefined".

Let’s once again save our changes, open up the terminal again, and see if everything works properly.

$ npm run test:watch -- coffees.service
 PASS  src/coffees/coffees.service.spec.ts (6.459 s)
  CoffeesService
    ✓ should be defined (42 ms)
    findOne
      when Coffee with ID exists
        ✓ should return the Coffee - Object (6 ms)
      otherwise
        ✓ should throw 'NotFoundException' (4 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        6.884 s
Ran all test suites matching /coffees.service/i.

Watch Usage: Press w to show more.

Perfect!. As we can see all of our tests passed!.

We could see how nicely structured "Jest" outputs everything is well.

All of our describe()" - blocks, "nested - describe() " - blocks, and "it()" - functions are all nicely indented here with their descriptions all output for us, and extremely easy to read!.

Although we just scratched the surface of "unit - testing" in general, the principles and concepts we learned here, can be applied to almost any situation you may run into, while applying "unit - tests" to your NestJS - applications.

Diving into E2E Test

"End-to-end - testing" covers the interaction of Classes and Modules at a more aggregate level. Closer to the kind of the interaction that our end-user will have with the production team.

In previous lessons, we learned how to manage "unit - tests" in our previous lesson. Now let’s shift our focus to how we can create and structure "end-to-end - testing" for our system.

Let’s start by opening up the existing app. "e2e-spec.ts" - file located in /test/ - directory, to better understand how we can create our own "end-to-end - testing" in the future.

// e2e-spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication } from "@nestjs/common";
import * as request from "supertest";
import { AppModule } from "./../src/app.module";

describe("AppController (e2e)", () => {
    let app: INestApplication;

    beforeEach(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [AppModule],
        }).compile();

        app = moduleFixture.createNestApplication();
        await app.init();
    });

    it("/ (GET)", () => {
        return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
    });
});

As we can see, "end-to-end" configuration here looks very similar to what we saw in our "unit - test suites". However there are a few key differences that we should pay attention to here.

We’re using the ".compile()" - method again, we used in our "unit - tests", but for "end-to-end - tests" we need to use the "createNestApplication()" - method to instantiate an actual Nest - "runtime environment".

Instead of saving a reference to a "service" like we did in our "unit -tests". Here in "end-to-end - test"`. We’re saving a reference to the "running application" in the "app" - variable, so we can use it to simulate "HTTP - Requests".

Lastly, we need to manually called the "app.int()" - method to initialize our application, which will mount all of our "Routes", trigger "lifecycle - hooks", etc..

So that’s it for the initial setup!. Let’s look at how an "end-to-end - testing" is actually structured.

If we look at "it()" - block here, we can see that we’re simulating an HTTP - Request" with help from the library `"supertest", and calling the "request()" - function provided by it.

"supertest" is an "npm" - package that provides a high-level abstraction, for testing "HTTP - applications". We need these "HTTP - Request" to route to our running Nest application.

This is why the request() - method needs to be passed a reference to the HTTP a listener, our "Nest - app" is running on - which maybe ExpressJS or Fastify - depending on how we set up our app. This is what "app.getHttpServer()" is trying to do.

The call to request() now hands us a wrapped "HTTP - server", connected to our Nest application!. We now have the ability to "chain methods" here to simulate an actual "HTTP - Request".

In the example shown here, we’re using "request.get()" with the string " / " inside of it. This will initiate a Request to our Nest app that is identical to an actual "HTTP - Request" coming in over the network.

Since we still have that demo "AppController" on the root-level of our application, which in turn replies with the "Hello world!" message. This test should succeed!.

To test it real quick. Let’s open up our terminal and enter in:

$ npm run test:e2e
FAIL  test/app.e2e-spec.ts
  AppController (e2e)
    ✕ / (GET) (435 ms)

  ● AppController (e2e) › / (GET)

    expected 200 "OK", got 403 "Forbidden"

      17 |
      18 |     it("/ (GET)", () => {
    > 19 |         return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
         |                                                      ^
      20 |     });
      21 | });
      22 |

      at Object.<anonymous> (app.e2e-spec.ts:19:54)
      ----
      at Test.Object.<anonymous>.Test._assertStatus (../node_modules/supertest/lib/test.js:296:12)
      at ../node_modules/supertest/lib/test.js:80:15
      at Test.Object.<anonymous>.Test._assertFunction (../node_modules/supertest/lib/test.js:311:11)
      at Test.Object.<anonymous>.Test.assert (../node_modules/supertest/lib/test.js:201:21)
      at Server.localAssert (../node_modules/supertest/lib/test.js:159:12)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        3.132 s, estimated 9 s
Ran all test suites.
  console.log
    [!!] Hi from Middleware!

      at LoggingMiddleware.use (../src/common/middleware/logging.middleware.ts:7:17)

  console.time
    [!!] Request-response time: 6 ms

      at ServerResponse.<anonymous> (../src/common/middleware/logging.middleware.ts:9:40)

Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests.
Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.

As we can see the test is actually failing, and the error messages "expected 200 'OK', got 403 'Forbidden'". This is caused by our "APIKeyGuard" - mounted globally that we created in a previous lesson, on "Other building blocks by example".

Note
Just to remind ourselves the "APIKeyGuard" requires us to pass a hard-coded "API_KEY" within a "header".

Since the "root - Route" we’re trying to hit here is not decorated with the "@Public()" - decorators we’ve previously setup. We NEED to pass this "header" in the "request", for the test to pass.

We can fix this real quick by opening up the text-editor again and chaining a ".set()" - method to our "HTTP - Request"

// e2e-spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication } from "@nestjs/common";
import * as request from "supertest";
import { AppModule } from "./../src/app.module";

describe("AppController (e2e)", () => {
    let app: INestApplication;

    beforeEach(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [AppModule],
        }).compile();

        app = moduleFixture.createNestApplication();
        await app.init();
    });

    it("/ (GET)", () => {
        return request(app.getHttpServer())
        .get("/")
        .set("Authorization:, process.env.API_KEY)
    //  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        .expect(200)
        .expect("Hello World!");
    });
});

This ".set()" - method lets us set any "headers" we might need.

Though here, we’re just going to pass in that "Authorization" and "key" we previously set in our ".env" - file.

Let’s save our changes, open up the terminal, and see what "Jest" gives us now.

 PASS  src/coffees/coffees.service.spec.ts (7.303 s)
  CoffeesService
    ✓ should be defined (17 ms)
    findOne
      when Coffee with ID exists
        ✓ should return the Coffee - Object (5 ms)
      otherwise
        ✓ should throw 'NotFoundException' (5 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        7.647 s
Ran all test suites matching /coffees.service/i.

Watch Usage: Press w to show more.
[daun@arch-daun iluvcoffe]$ npm run test:e2e

> [email protected] test:e2e
> jest --config ./test/jest-e2e.json

  console.log
    [!] DatabaseModule - instantiated

      at Function.register (../src/database/database.module.ts:30:17)

  console.log
    [!!] CoffeesModule - instantiated

      at new CoffeesModule (../src/coffees/coffees.module.ts:20:17)
          at async Promise.all (index 0)

 PASS  test/app.e2e-spec.ts
  AppController (e2e)
    ✓ / (GET) (465 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.216 s, estimated 11 s
Ran all test suites.
  console.log
    [!!] Hi from Middleware!

      at LoggingMiddleware.use (../src/common/middleware/logging.middleware.ts:7:17)

  console.time
    [!!] Request-response time: 6 ms

      at ServerResponse.<anonymous> (../src/common/middleware/logging.middleware.ts:9:40)

Jest did not exit one second after the test run has completed.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This usually means that there are asynchronous operations that weren't stopped in your tests.
 Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.

Perfect. It seems that our "end-to-end - test" succeeded now!.

However, we can also see that we got a "-warning-" - "Jest did not exit one second after the test run has completed".

This warning means, that there are some "asynchronous - operations" that weren’t terminated in our tests. In this particular case, it’s an "open database connection".

Back to our text-editor again. Let’s add the "afterAll() - hook below all of our tests.

// e2e-spec.ts
...
...

describe("AppController (e2e)", () => {
    let app: INestApplication;

    beforeEach(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [AppModule],
        }).compile();
        ...
        ...
    });

    it("/ (GET)", () => {
        ...
        ...
    });

    afterAll(async () => {  // <<<
        await app.close();
    });
});

This "afterAll()" - hook will be called by the "Jest" - framework after ALL tests are completed.

Here we called "app.close()" which lets Nest know that it should trigger the "OnModuleDestroy" and "OnApplicationShutdowm" - "lifecycle hooks" which will terminate "all - connections" in our application.

For the sake of consistency. Let’s also change our "beforeEach()" - here, to "beforeAll()".

// e2e-spec.ts
...
...

describe("AppController (e2e)", () => {
    let app: INestApplication;

    beforeAll(async () => {     // <<<
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [AppModule],
        }).compile();
        ...
        ...
    });

    it("/ (GET)", () => {
        ...
        ...
    });

    afterAll(async () => {
        await app.close();
    });
});

Since typically we don’t want to recreate the application for each "end-to-end - test".

Just to make sure that everything’s working as expected. Let’s open up the terminal again and run:

 PASS  src/coffees/coffees.service.spec.ts (7.303 s)
  CoffeesService
    ✓ should be defined (17 ms)
    findOne
      when Coffee with ID exists
        ✓ should return the Coffee - Object (5 ms)
      otherwise
        ✓ should throw 'NotFoundException' (5 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        7.647 s
Ran all test suites matching /coffees.service/i.

Watch Usage: Press w to show more.
[daun@arch-daun iluvcoffe]$ npm run test:e2e

> [email protected] test:e2e
> jest --config ./test/jest-e2e.json

  console.log
    [!] DatabaseModule - instantiated

      at Function.register (../src/database/database.module.ts:30:17)

  console.log
    [!!] CoffeesModule - instantiated

      at new CoffeesModule (../src/coffees/coffees.module.ts:20:17)
          at async Promise.all (index 0)

 PASS  test/app.e2e-spec.ts
  AppController (e2e)
    ✓ / (GET) (465 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.216 s, estimated 11 s
Ran all test suites.
  console.log
    [!!] Hi from Middleware!

      at LoggingMiddleware.use (../src/common/middleware/logging.middleware.ts:7:17)

  console.time
    [!!] Request-response time: 6 ms

      at ServerResponse.<anonymous> (../src/common/middleware/logging.middleware.ts:9:40)

Great as we can see, everything’s passed, and we have no more "errors" or "warnings".

Although we fixed this end-to-end test and it’s working properly now, it’s still far from perfect.

We importing the "AppModule" here, which means that in fact we’re instantiating the entire applications "-including-" all Controllers, Providers, Enhancers, Configurations, "establishing Database - Connection", and so on and so forth..

"Nest - Modules" typically represent features. So ideally we should be able to test these features in "isolation independently".

In the next lesson. We’ll look at how we can "test Modules" in isolation without worrying about other Modules that might have been imported into the applications.

Creating our First e2e test

chapter 8 7
Creating our First e2e Test

Grouping our application’s functionality into Modules is strongly recommended as an effective way to organize our components.

For most applications. The resulting architecture will employ "multiple - Modules", each encapsulating a related set of capabilities.

Because of this "encapsulation - organization". This allows us to test each feature independently by importing a specific Module that we want to test, into our "TestingModule".

In this lesson, we’ll be testing the "Coffee’s" - feature we worked on throughout this course, and test some of CRUD - endpoint we’ve provided in it so far.

To get started, let’s create a new folder our "/test/" - directory, named "/coffee/". Within this folder. Let’s create a single "end-to-end test" - file named: "coffees.e2e-spec.ts".

Let’s open up "coffees.e2e-spec.ts" - file. Let’s start by creating the basic structure for our test.

// coffees.e2e-spec.ts
import { INestApplication } from "@nestjs/common";
import { TestingModule, Test } from "@nestjs/testing";

import { CoffeesModule } from "../../src/coffees/coffees.module";

describe("[Feaute] Coffees - /coffees", () => {
    let app: INestApplication;

    beforeAll(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [CoffeesModule],
        }).compile();

        app = moduleFixture.createNestApplication();
        await app.init();
    });

    afterAll(async () => {
        await app.close();
    });
});

As we can see. We have a top-level "describe()" - block again, which represent our "test - suite".

Within this block. We have the "beforeAll()" and "afterAll()" - hooks again, represnting the setup and tear-down phases of our tests.

Lastly within our "TestingModule’s" - "imports:[]" Array. We’re passing in "CoffeesModule" this time.

We haven’t implemented any test yet. So let’s add a few placeholders and we’ll work our way through these in a few moments.

// coffees.e2e-spec.ts
import { INestApplication } from "@nestjs/common";
import { TestingModule, Test } from "@nestjs/testing";

import { CoffeesModule } from "../../src/coffees/coffees.module";

describe("[Feaute] Coffees - /coffees", () => {
    let app: INestApplication;

    beforeAll(async () => {
        ...
        ...
    });

    it.todo("Create [Post /]");
    it.todo("Get all [Get /]");
    it.todo("Get one [Get /:id]");
    it.todo("Update one [PATCH /:id]");
    it.todo("Delete on [DELETE /:id]");

    afterAll(async () => {
        await app.close();
    });
});

With these "it.todo()" - reminders in place we won’t forget to cover all the important scenarios later.

"todo()" provided by "Jest", is a great way to track test that need to be completed in the future, as they even show up in the "Jest" report to help remind you.

All right, so let’s run this particular rest and see if any Eros pop up.

Open up the terminal and let’s enter in the "npm run test:e2e" command again, but passing in the "-- coffees" after it.

$ npm run test:e2e -- coffees
[daun@arch-daun iluvcoffe]$ npm run test:e2e -- coffees

> [email protected] test:e2e
> jest --config ./test/jest-e2e.json "coffees"

 PASS  test/coffee/coffees.e2e-spec.ts (6.917 s)
  [Feaute] Coffees - /coffees
    ✎ todo Create [Post /]
    ✎ todo Get all [Get /]
    ✎ todo Get one [Get /:id]
    ✎ todo Update one [PATCH /:id]
    ✎ todo Delete on [DELETE /:id]

  ● [Feaute] Coffees - /coffees › Create [Post /]

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

    Potential solutions:
    - If Connection is a provider, is it part of the current TypeOrmModule?
    - If Connection is exported from a separate @Module, is that module imported within TypeOrmModule?
      @Module({
        imports: [ /* the Module containing Connection */ ]
      })

	...
	...

  ● [Feaute] Coffees - /coffees › Get all [Get /]

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

    Potential solutions:
    - If Connection is a provider, is it part of the current TypeOrmModule?
    - If Connection is exported from a separate @Module, is that module imported within TypeOrmModule?
      @Module({
        imports: [ /* the Module containing Connection */ ]
      })

	...
	...

  ● [Feaute] Coffees - /coffees › Get one [Get /:id]

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

    Potential solutions:
    - If Connection is a provider, is it part of the current TypeOrmModule?
    - If Connection is exported from a separate @Module, is that module imported within TypeOrmModule?
      @Module({
        imports: [ /* the Module containing Connection */ ]
      })

	...
	...

  ● [Feaute] Coffees - /coffees › Update one [PATCH /:id]

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

    Potential solutions:
    - If Connection is a provider, is it part of the current TypeOrmModule?
    - If Connection is exported from a separate @Module, is that module imported within TypeOrmModule?
      @Module({
        imports: [ /* the Module containing Connection */ ]
      })
	...
	...


  ● [Feaute] Coffees - /coffees › Delete on [DELETE /:id]

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

    Potential solutions:
    - If Connection is a provider, is it part of the current TypeOrmModule?
    - If Connection is exported from a separate @Module, is that module imported within TypeOrmModule?
      @Module({
        imports: [ /* the Module containing Connection */ ]
      })
	...
	...

  console.log
    [!!] CoffeesModule - instantiated

      at new CoffeesModule (../src/coffees/coffees.module.ts:23:17)

Test Suites: 1 passed, 1 total
Tests:       5 todo, 5 total
Snapshots:   0 total
Time:        6.962 s
Ran all test suites matching /coffees/i.

It looks like we’re getting an Error, but we’ve seen this one a few times now!. It’s the "Nest cannot result dependencies error".

If we look at the Error description, we can see hat the "Connection Provider" is missing. Since our application uses a database as a data source, and we leverage "Repository - classes" heavily within the "CoffeesModule" - scope. The "TestingModule" cannot properly bootstrapped.

How can we fix this?

Well there are many different approaches to tackle this issue,

chapter 8 8
Creating our First e2e test -2
  • First options, we have to Mock "interactions" with the database.

We could override all the Providers and replace them with "Mocked - Object", "stubbing Methods" and "manipulating the return values".

This gives us full flexibility in isolation, but as for "cons" approach:

  • It’s time consuming,

  • Very Error - prone,

  • Hard to maintain,

  • We can’t test any interaction with a real database, or test any plain "SQL

  • queries"

chapter 8 9
Creating our First e2e test -3
  • Second option is, to use "disk based databases", for example: "SQLite"

With this approach we don’t have to Mock anything or stub on any methods. It’s easier to maintain, an doesn’t add any complexity or overhead.

It’s fast and easy to setup, however it works for simple scenarios when we don’t use any database management system (DBMS) - "specific feature", such as "Postgres-related" - features in this case.

Some queries might fail for "Postgres" but not for "SQLite", and we don’t really test the entire flow.

chapter 8 10
Creating our First e2e test -4
  • Third option is, to "add an additional testing database".

With this option, we don’t have to Mock anything or stub on any methods. It’s easier to maintain, and we use the same database, for example "Postgres" in our case.

So our "end-to-end test" will have the same flow, as our "-real-" application does!.

Some "cons" to this approach. It does add more complexity and overhead. Since we need to start and close the database for "end-to-end test" both locally and in the CI/CD - pipeline, if we have one.

Note
Deciding which approach to use, heavily depends on your application’s requirements!.

In this lesson, we’ll be going with the "3rd" approach, as it’s the most reliable way to do a real world test of our features using the same type of database.

The additional complexity and overhead is not a big issue, since we’re already using Docker in our application!.

All right, so let’s get this setup by opening up our "docker-compose" - file and add another database in there.

// docker-compose.yml
version: "3"

services:
  db:
    image: postgres
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: pass123
      POSTGRES_DB: postgres1      # comment this if your db-name is still: 'postgres' in AppModule - class

  test-db:
    image: postgres
    restart: always
    ports:
      - "5433:5432"     # map port 5433
    environment:
      POSTGRES_PASSWORD: pass123
      POSTGRES_DB: postgres1      # comment this if your db-name is still: 'postgres' in AppModule - class

We’re going to use the same configuration options as for the original database, with one minor difference. We need to make sure that our "PORT’s" aren’t in conflict. So we are mapping "port: 5432" to port "port: 5433" here instead.

With this in place. Let’s open up our "package.json" - file and define "pre" and "post" - hooks for ur "test:e2e" - script to make our lives easier, and somewhat automate this process.

// package.json
{
  "name": "iluvcoffe",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
      "pretest:e2e": "docker-compose up -d test-db",
      "posttest:e2e": "docker-compose stop -d test-db && docker-compose rm  -f test-db",
  },
  "dependencies": {
  },
  "devDependencies": {
  },
  "jest": {
  }
}

"npm" - script allow us to add "lifecycle - hooks" to our scripts simply by prepending "pre" or "post" to a similarly named script.

With these "npm - lifecycle" - hook setup, our database will be automatically started before running "end-to-end test" and closed right afterwards.

Lastly, let’s headback to our "coffees.e2e-spec.ts" - file and import the "TypeOrmModule.forRoot()" to initialize a connection to this new testing database we just created.

// coffees.e2e-spec.ts
...
...

describe("[Feaute] Coffees - /coffees", () => {
    let app: INestApplication;

    beforeAll(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [
                CoffeesModule,
                TypeOrmModule.forRoot({
                    type: "postgres",
                    host: "localhost",
                    port: 5433,
                    username: "postgres",
                    password: "pass123",
                    database: "postgres1",
                    autoLoadEntities: true,
                    synchronize: true,
                }),
            ],
        }).compile();

        app = moduleFixture.createNestApplication();
        await app.init();
    });

    ...
    ...
});
Note
The different port "5433" being used here.

Now if we open up our terminal and run:

$ npm run test:e2e -- coffees

> [email protected] pretest:e2e
> docker-compose up -d test-db

Creating iluvcoffe_test-db_1 ... done

> [email protected] test:e2e
> jest --config ./test/jest-e2e.json "coffees"

  console.log
    [!!] CoffeesModule - instantiated

      at new CoffeesModule (../src/coffees/coffees.module.ts:23:17)
          at async Promise.all (index 0)

 PASS  test/coffee/coffees.e2e-spec.ts
  [Feaute] Coffees - /coffees
    ✎ todo Create [Post /]
    ✎ todo Get all [Get /]
    ✎ todo Get one [Get /:id]
    ✎ todo Update one [PATCH /:id]
    ✎ todo Delete on [DELETE /:id]

Test Suites: 1 passed, 1 total
Tests:       5 todo, 5 total
Snapshots:   0 total
Time:        3.495 s, estimated 8 s
Ran all test suites matching /coffees/i.

> [email protected] posttest:e2e
> docker-compose stop test-db && docker-compose rm -f test-db

Stopping iluvcoffe_test-db_1 ... done
Going to remove iluvcoffe_test-db_1
Removing iluvcoffe_test-db_1 ... done

As we can see, this time there are no errors!.

You can also see that the database container was created before running the test and was closed immediately after they completed.

Great. With this setup we’re ready to start implementing our actual "test - logic" which we’ll do in the next lesson.

We did a lot in this lesson. To summarize, we created a new test for just our "CoffeesModule". Created a new "test database" in our "docker-compose" - file, and even added some cool "npm" - hooks to help start and close our database whenever our tests run.

With a lot of this in place. We’re fully ready to handle "end-to-end tests" for our application, as it continues to grow in the future.

Implementing e2e Test Logic

In the previous lesson, we created a boilerplate for out test, and even setup a "test database" for all our future "end-to-end tests".

In this lesson, let’s focus on adding "real logic" to all of our "todo()" "end-to-end tests", and ensure that our CRUD /coffees/ - Route works as intended.

But before we get started let’s focus on the "beforeAll()" - hook here. As we know already. The "createNestApplication()" - method is used to instantiate a full "Nest runtime environment". You could think of it as an instance of the application that we have in our "main.ts" - file.

This means that any configuration that was added -outside- of Modules themselves - so basically anything was done in our "main.ts" - file " MUST be added and applied *here as well, unless it’s not needed for this particular "end-to-end test".

For this reason, we need to make sure we bind the "ValidationPipe()" we have setup here, so let’s copy "app.useGlobalPipes()" from "main.ts" - file and paste it over in our `"e2e-spec.ts" file.

// coffees.e2e-spec.ts
import { INestApplication, ValidationPipe } from "@nestjs/common";
...
...

describe("[Feaute] Coffees - /coffees", () => {
    let app: INestApplication;

    beforeAll(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [
                CoffeesModule,
                TypeOrmModule.forRoot({
                    type: "postgres",
                    host: "localhost",
                    port: 5433,
                    username: "postgres",
                    password: "pass123",
                    database: "postgres1",
                    autoLoadEntities: true,
                    synchronize: true,
                }),
            ],
        }).compile();

        app = moduleFixture.createNestApplication();
        app.useGlobalPipes(
            new ValidationPipe({
                whitelist: true,
                transform: true,
                forbidNonWhitelisted: true,
                transformOptions: {
                    enableImplicitConversion: true,
                },
            }),
        );
        await app.init();
    });

    ...
    ...
});

Perfect. With that in place, we should be ready to get started testing our endpoints.

Let’s start off with create() - POST endpoint.

// coffees.e2e-spec.ts
import { INestApplication, ValidationPipe } from "@nestjs/common";
...
...

describe("[Feaute] Coffees - /coffees", () => {
    let app: INestApplication;
    const coffee = {            // [2] <<<
        name: "Salemba Roast#1",
        brand: "Salemba Brew",
        flavors: ["chocolate", "vanilla"],
    };

    beforeAll(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            ...
            ...
        }).compile();

        app = moduleFixture.createNestApplication();
        app.useGlobalPipes(
            new ValidationPipe({
                ...
                ...
            }),
        );
        await app.init();
    });

    it("Create [Post /]", () => {   // [1] <<<
        ...
        ...
    })

    ...
    ...
});

First "[1]", let’s remove the ".todo()" suffix, and add a missing callback function instead.

Now, we can use the "supertest" - library which w have used already, to perform in an "HTTP - Request" to our application.

Before we proceed though. Let’s first define a sample - Coffee, that we can use for all of our other endpoint tests as well.

Let’s scroll at the beginning of the "describe()" - block and "[2]" add a "coffee" -variable, with some "key/value" that belong to our Entity, like: "name", "brand", and "flavors".

With the "coffee" - variable in place. Let’s head back to the "Create" - endpoint test and add the following logic.

// coffees.e2e-spec.ts
import { INestApplication, ValidationPipe, HttpStatus } from "@nestjs/common";
...
...

describe("[Feaute] Coffees - /coffees", () => {
    let app: INestApplication;
    const coffee = {
        name: "Salemba Roast#1",
        brand: "Salemba Brew",
        flavors: ["chocolate", "vanilla"],
    };

    beforeAll(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            ...
            ...
        }).compile();

        app = moduleFixture.createNestApplication();
        app.useGlobalPipes(
            new ValidationPipe({
                ...
                ...
            }),
        );
        await app.init();
    });

    it("Create [Post /]", () => {
        return request(app.getHttpServer())         // <<<
        .post("/coffees")                           // <<<
        .send(coffee as CreateCoffeeDto)            // <<<
        .expect(HttpStatus.CREATED);                // <<<
    })

    ...
    ...
});

In the body of the "request()". We must send an object that fulfills the "CreateCoffeeDto" - class.

Note
We’ve added "as CreateCoffeeDto" for better "Type - safety" here as well.

The test here is currently simple, and only expects an "HTTP - status" of Created from the API call.

But if we needed to test the return value more specifically, we could test it by looking at the "value" returned back from the "Promise", like so.

it("Create [Post /]", () => {
    return request(app.getHttpServer())
        .post("/coffees")
        .send(coffee as CreateCoffeeDto)
        .expect(HttpStatus.CREATED)
        .then(({ body }) => {
            const expectedCoffee = jasmine.objectContaining({
                ...coffee,
                flavors: jasmine.arrayContaining(coffee.flavors.map((name) => jasmine.objectContaining({ name }))),
            });
            expect(body).toEqual(expectedCoffee);
        });
});

If you’re not familiar with "Jasmine". It’s another testing framework that "Jest" is actually based on.

We can’t dive deep into "Jasmine" here, but just know that there are a lot of other great helper and utility methods available to you from "Jasmine", when creating your tests.

In the code shown above. We’re using "Jasmine" - helpers for partial matching. This is useful when an expectation only cares about certain "key/value" - pairs when performing the actual test.

In addition, we needed to transform "flavors" which are an Array of Strings to Objects. Since each "flavors" is an Entity in our applications.

Alternatively, we could just create a separate file with expected "static responses" and DTO’s to avoid putting too much repetitive logic like above in our "end-to-end tests".

With all this in place, let’s fire up our terminal and run our "end-to-end tests".

$ npm run test:e2e -- coffees
[daun@arch-daun iluvcoffe]$ npm run test:e2e -- coffees;

> [email protected] pretest:e2e
> docker-compose up -d test-db

iluvcoffe_test-db_1 is up-to-date

> [email protected] test:e2e
> jest --config ./test/jest-e2e.json "coffees"

  console.log
    [!!] CoffeesModule - instantiated

      at new CoffeesModule (../src/coffees/coffees.module.ts:23:17)
          at async Promise.all (index 0)

 PASS  test/coffee/coffees.e2e-spec.ts (7.5 s)
  [Feaute] Coffees - /coffees
    ✓ Create [Post /] (101 ms)
    ✎ todo Get all [Get /]
    ✎ todo Get one [Get /:id]
    ✎ todo Update one [PATCH /:id]
    ✎ todo Delete on [DELETE /:id]

Test Suites: 1 passed, 1 total
Tests:       4 todo, 1 passed, 5 total
Snapshots:   0 total
Time:        7.548 s, estimated 9 s
Ran all test suites matching /coffees/i.
  console.log
    [!!] CoffeesController created

      at new CoffeesController (../src/coffees/coffees.controller.ts:23:17)

  console.log
    [!!] CoffeesController REQUEST {
      name: 'Salemba Roast#1',
      brand: 'Salemba Brew',
      flavors: [ 'chocolate', 'vanilla' ]
    }

      at new CoffeesController (../src/coffees/coffees.controller.ts:24:17)


> [email protected] posttest:e2e
> docker-compose stop test-db && docker-compose rm -f test-db

Stopping iluvcoffe_test-db_1 ... done
Going to remove iluvcoffe_test-db_1
Removing iluvcoffe_test-db_1 ... done

Now, as we can see, the test was successful!.

As an exercise, try to implement all of the other “.todo()” - tests in this file, to get a better feel for "end-to-end testing" yourself. If you’re stuck in any point, and unsure of how to continue. Just scroll down and view all the completed test code above.

So, as we’ve seen in this testing chapter. NestJS - "Module System" allows us to test each feature independently.

Isolating specific features, makes it easier to develop and maintain tests "simply", "effectively", and "quickly".

With the testing fundamentals we went through here. You’re now ready to tackle any testing scenario you might run into, in your future Nest applications.


1. total, accumulative
2. piece, snippet