├── src ├── database │ ├── index.ts │ ├── database.module.ts │ ├── database.service.ts │ └── database-ormconfig.constant.ts ├── test │ ├── fixtures │ │ ├── _order.json │ │ ├── Teacher.json │ │ ├── TeacherUser.json │ │ └── User.json │ ├── testing.module.ts │ └── test.utils.ts └── user │ ├── teacher.entity.ts │ ├── user.entity.ts │ ├── user.repository.ts │ ├── teacher-user.entity.ts │ └── user.repsitory.spec.ts ├── .prettierrc ├── nest-cli.json ├── tsconfig.build.json ├── tsconfig.json ├── tslint.json ├── .gitignore ├── readme.md └── package.json /src/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database.module'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /src/test/fixtures/_order.json: -------------------------------------------------------------------------------- 1 | [ 2 | "User", 3 | "Teacher", 4 | "TeacherUser" 5 | ] 6 | -------------------------------------------------------------------------------- /src/test/fixtures/Teacher.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Björn", 4 | "id": 1 5 | } 6 | ] -------------------------------------------------------------------------------- /src/test/fixtures/TeacherUser.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "user": 1, 4 | "teacher": 1 5 | } 6 | ] -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/test/fixtures/User.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Peter", 5 | "description": "1" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /src/user/teacher.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, Unique, OneToMany } from "typeorm" 2 | 3 | @Entity() 4 | export class Teacher extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number 7 | 8 | @Column() 9 | name: string 10 | } 11 | -------------------------------------------------------------------------------- /src/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, Unique, OneToMany } from "typeorm" 2 | 3 | @Entity() 4 | export class User extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number 7 | 8 | @Column() 9 | name: string 10 | 11 | @Column() 12 | description: string 13 | } 14 | -------------------------------------------------------------------------------- /src/user/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Connection, EntityRepository, Repository } from "typeorm" 2 | import { User } from "./user.entity" 3 | 4 | @EntityRepository(User) 5 | export class UserRepository extends Repository { 6 | async findAll(): Promise<[User[], number]> { 7 | return await this.createQueryBuilder("user").getManyAndCount() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/user/teacher-user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | Unique, 7 | OneToMany, 8 | TableForeignKey 9 | } from "typeorm" 10 | 11 | @Entity() 12 | export class TeacherUser extends BaseEntity { 13 | @PrimaryGeneratedColumn() 14 | id: number 15 | 16 | @Column() 17 | user: number 18 | 19 | @Column() 20 | teacher: number 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /src/test/testing.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common" 2 | import { TypeOrmModule } from "@nestjs/typeorm" 3 | import { Connection } from "typeorm" 4 | import { DatabaseModule } from "../database/database.module" 5 | 6 | @Module({ 7 | imports: [DatabaseModule] 8 | }) 9 | /** 10 | * The Testing Module provides 11 | * Utility functions for easier testing 12 | */ 13 | export class TestingModule {} 14 | -------------------------------------------------------------------------------- /src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { getOrmConfig } from './database-ormconfig.constant'; 5 | import { DatabaseService } from './database.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forRoot(getOrmConfig()) 10 | ], 11 | providers: [ 12 | DatabaseService, 13 | ], 14 | exports: [ 15 | DatabaseService 16 | ] 17 | }) 18 | export class DatabaseModule { } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | /.history/ 36 | /db/ 37 | -------------------------------------------------------------------------------- /src/database/database.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { Connection, Repository } from 'typeorm'; 3 | 4 | @Injectable() 5 | export class DatabaseService { 6 | /** 7 | * Initializes the database service 8 | * @param connection The connection, which gets injected 9 | */ 10 | constructor(@Inject('Connection') public connection: Connection) { } 11 | 12 | /** 13 | * Returns the repository of the given entity 14 | * @param entity The database entity to get the repository from 15 | */ 16 | async getRepository(entity): Promise> { 17 | return this.connection.getRepository(entity); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Example for NestJS Database Integration Testing 2 | 3 | This repository is a quick example, on how to 4 | setup a database for each test scenario. 5 | 6 | ## Fixtures 7 | 8 | In the folder `src/test/fixtures` are JSON files which will be used as test data. 9 | The file `src/test/fixtures/_order.json` defines in which order the entites should be inserted. 10 | This is made, to create relations between entities. 11 | 12 | To clarify: The TeacherUser has the foreign key `user` and `teacher`. In order to insert 13 | the TeacherUser, the `test.utils.ts`-Component needs to know the order of the entities. Because 14 | TeacherUser can not be inserted, without an User or Teacher entity. 15 | 16 | ## Install 17 | 18 | ``` 19 | npm install 20 | ``` 21 | 22 | ## Run 23 | 24 | ``` 25 | npm test 26 | ``` 27 | -------------------------------------------------------------------------------- /src/database/database-ormconfig.constant.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user/user.entity'; 2 | import { Teacher } from '../user/teacher.entity'; 3 | import { TeacherUser } from '../user/teacher-user.entity'; 4 | 5 | export function getOrmConfig() { 6 | let OrmConfig; 7 | const settings = { 8 | host: process.env.POSTGRES_HOST, 9 | port: parseInt(process.env.POSTGRES_PORT, 10), 10 | username: process.env.POSTGRES_USER, 11 | password: process.env.POSTGRES_PASSWORD, 12 | database: process.env.POSTGRES_DATABASE, 13 | }; 14 | 15 | if (process.env.NODE_ENV !== 'test') { 16 | OrmConfig = { 17 | type: 'postgres', 18 | host: settings.host, 19 | port: settings.port, 20 | username: settings.username, 21 | password: settings.username, 22 | database: settings.database, 23 | entities: [ User, Teacher, TeacherUser], 24 | synchronize: true 25 | }; 26 | } else { 27 | OrmConfig = { 28 | type: 'sqlite', 29 | database: './db/test-db.sql', 30 | entities: [ User, Teacher, TeacherUser], 31 | synchronize: true, 32 | dropSchema: true 33 | }; 34 | } 35 | return OrmConfig; 36 | } 37 | -------------------------------------------------------------------------------- /src/user/user.repsitory.spec.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from "typeorm" 2 | 3 | import { TestUtils } from "../test/test.utils" 4 | import { Test } from "@nestjs/testing" 5 | 6 | import * as Fs from "fs" 7 | import * as Path from "path" 8 | import { TypeOrmModule } from "@nestjs/typeorm" 9 | import { DatabaseService } from "../database/database.service" 10 | import { DatabaseModule } from "../database/database.module" 11 | import { UserRepository } from "./user.repository" 12 | 13 | describe("UserRepository", () => { 14 | let userRepository: UserRepository 15 | let testUtils: TestUtils 16 | beforeEach(async done => { 17 | const module = await Test.createTestingModule({ 18 | imports: [DatabaseModule], 19 | providers: [DatabaseService, TestUtils] 20 | }).compile() 21 | testUtils = module.get(TestUtils) 22 | await testUtils.reloadFixtures() 23 | userRepository = testUtils.databaseService.connection.getCustomRepository(UserRepository) 24 | 25 | done() 26 | }) 27 | 28 | afterEach(async done => { 29 | await testUtils.closeDbConnection() 30 | done() 31 | }) 32 | 33 | describe("findAll", () => { 34 | it("should return all users", async done => { 35 | const [data, total] = await userRepository.findAll() 36 | expect(total).toBe(1) 37 | expect(data.length).toEqual(1) 38 | expect(data[0].name).toBe("Peter") 39 | done() 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-integration-test-db-example", 3 | "version": "0.0.1", 4 | "description": "This repo demonstrates how you can test a NestJS application against a real database", 5 | "author": "Livio Brunner, Torsten Uhlmann", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "tslint -p tsconfig.json -c tslint.json", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^6.7.2", 24 | "@nestjs/core": "^6.7.2", 25 | "@nestjs/platform-express": "^6.7.2", 26 | "@nestjs/typeorm": "^6.2.0", 27 | "reflect-metadata": "^0.1.13", 28 | "rimraf": "^3.0.0", 29 | "rxjs": "^6.5.3", 30 | "sqlite": "^3.0.3", 31 | "typeorm": "^0.2.19" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^6.9.0", 35 | "@nestjs/schematics": "^6.7.0", 36 | "@nestjs/testing": "^6.7.1", 37 | "@types/express": "^4.17.1", 38 | "@types/jest": "^24.0.18", 39 | "@types/node": "^12.7.5", 40 | "@types/supertest": "^2.0.8", 41 | "@typescript-eslint/eslint-plugin": "^2.3.2", 42 | "@typescript-eslint/parser": "^2.3.2", 43 | "eslint": "^6.5.1", 44 | "eslint-config-prettier": "^6.3.0", 45 | "eslint-config-react": "^1.1.7", 46 | "eslint-plugin-prettier": "^3.1.1", 47 | "eslint-plugin-react": "^7.15.1", 48 | "jest": "^24.9.0", 49 | "prettier": "^1.18.2", 50 | "supertest": "^4.0.2", 51 | "ts-jest": "^24.1.0", 52 | "ts-loader": "^6.1.1", 53 | "ts-node": "^8.4.1", 54 | "tsconfig-paths": "^3.9.0", 55 | "tslint": "^5.20.0", 56 | "typescript": "^3.6.3" 57 | }, 58 | "jest": { 59 | "moduleFileExtensions": [ 60 | "js", 61 | "json", 62 | "ts" 63 | ], 64 | "rootDir": "src", 65 | "testRegex": ".spec.ts$", 66 | "transform": { 67 | "^.+\\.(t|j)s$": "ts-jest" 68 | }, 69 | "coverageDirectory": "./coverage", 70 | "testEnvironment": "node" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/test.utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as Path from "path" 3 | 4 | import { DatabaseService } from "../database/database.service" 5 | import { Injectable } from "@nestjs/common" 6 | 7 | /** 8 | * This class is used to support database 9 | * tests with unit tests in NestJS. 10 | * 11 | * This class is inspired by https://github.com/jgordor 12 | * https://github.com/nestjs/nest/issues/409#issuecomment-364639051 13 | */ 14 | @Injectable() 15 | export class TestUtils { 16 | databaseService: DatabaseService 17 | 18 | /** 19 | * Creates an instance of TestUtils 20 | */ 21 | constructor(databaseService: DatabaseService) { 22 | if (process.env.NODE_ENV !== "test") { 23 | throw new Error("ERROR-TEST-UTILS-ONLY-FOR-TESTS") 24 | } 25 | this.databaseService = databaseService 26 | } 27 | 28 | /** 29 | * Shutdown the http server 30 | * and close database connections 31 | */ 32 | async shutdownServer(server) { 33 | await server.httpServer.close() 34 | await this.closeDbConnection() 35 | } 36 | 37 | /** 38 | * Closes the database connections 39 | */ 40 | async closeDbConnection() { 41 | const connection = await this.databaseService.connection 42 | if (connection.isConnected) { 43 | await (await this.databaseService.connection).close() 44 | } 45 | } 46 | 47 | /** 48 | * Returns the order id 49 | * @param entityName The entity name of which you want to have the order from 50 | */ 51 | getOrder(entityName) { 52 | const order: string[] = JSON.parse( 53 | fs.readFileSync(Path.join(__dirname, "../test/fixtures/_order.json"), "utf8") 54 | ) 55 | return order.indexOf(entityName) 56 | } 57 | 58 | /** 59 | * Returns the entites of the database 60 | */ 61 | async getEntities() { 62 | const entities = [] 63 | ;(await (await this.databaseService.connection).entityMetadatas).forEach(x => 64 | entities.push({ name: x.name, tableName: x.tableName, order: this.getOrder(x.name) }) 65 | ) 66 | return entities 67 | } 68 | 69 | /** 70 | * Cleans the database and reloads the entries 71 | */ 72 | async reloadFixtures() { 73 | const entities = await this.getEntities() 74 | await this.cleanAll(entities) 75 | await this.loadAll(entities) 76 | } 77 | 78 | /** 79 | * Cleans all the entities 80 | */ 81 | async cleanAll(entities) { 82 | try { 83 | for (const entity of entities.sort((a, b) => b.order - a.order)) { 84 | const repository = await this.databaseService.getRepository(entity.name) 85 | await repository.query(`DELETE FROM ${entity.tableName};`) 86 | // Reset IDs 87 | await repository.query(`DELETE FROM sqlite_sequence WHERE name='${entity.tableName}'`) 88 | } 89 | } catch (error) { 90 | throw new Error(`ERROR: Cleaning test db: ${error}`) 91 | } 92 | } 93 | 94 | /** 95 | * Insert the data from the src/test/fixtures folder 96 | */ 97 | async loadAll(entities: any[]) { 98 | try { 99 | for (const entity of entities.sort((a, b) => a.order - b.order)) { 100 | const repository = await this.databaseService.getRepository(entity.name) 101 | const fixtureFile = Path.join(__dirname, `../test/fixtures/${entity.name}.json`) 102 | if (fs.existsSync(fixtureFile)) { 103 | const items = JSON.parse(fs.readFileSync(fixtureFile, "utf8")) 104 | const result = await repository 105 | .createQueryBuilder(entity.name) 106 | .insert() 107 | .values(items) 108 | .execute() 109 | } 110 | } 111 | } catch (error) { 112 | throw new Error(`ERROR [TestUtils.loadAll()]: Loading fixtures on test db: ${error}`) 113 | } 114 | } 115 | } 116 | --------------------------------------------------------------------------------