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.
"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.
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.
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".
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".
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.
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.
"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.
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,
-
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"
-
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.
-
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.
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.