├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── 3rd-party-licenses.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jest.config.js ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── index.ts ├── lib │ ├── FakeConnection.ts │ ├── FakeConnectionManager.ts │ ├── FakeDBDriver.ts │ ├── FakeDriverFactory.ts │ ├── FakePostgresQueryRunner.ts │ ├── createFakeConnection.ts │ ├── index.ts │ ├── typeorm-test-core.module.ts │ └── typeorm-test.module.ts └── tests │ ├── custom-repository.spec.ts │ ├── repository-provided.spec.ts │ ├── shared │ ├── User.ts │ ├── UserCustomRepository.ts │ ├── UsersService.ts │ └── UsersServiceWithCustomRepository.ts │ └── testing-module.spec.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.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 | 36 | # NPM audit html 37 | npm-audit.html -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest All", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "args": ["--runInBand"], 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "windows": { 14 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 15 | } 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Jest Current File", 21 | "program": "${workspaceFolder}/node_modules/.bin/jest", 22 | "args": [ 23 | "${fileBasenameNoExtension}", 24 | "--config", 25 | "jest.config.js" 26 | ], 27 | "console": "integratedTerminal", 28 | "internalConsoleOptions": "neverOpen", 29 | "disableOptimisticBPs": true, 30 | "windows": { 31 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 32 | } 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /3rd-party-licenses.md: -------------------------------------------------------------------------------- 1 | Thanks to: 2 | 3 | ### [@nestjs/typeorm](https://github.com/nestjs/typeorm) 4 | License: MIT 5 | https://github.com/nestjs/nest/blob/master/LICENSE 6 | 7 | ### [typeorm](https://github.com/typeorm/typeorm) 8 | License: MIT 9 | https://github.com/typeorm/typeorm/blob/master/LICENSE 10 | 11 | Other sources: 12 | 13 | ### [joseym](https://github.com/joseym) 14 | https://github.com/typeorm/typeorm/issues/1267#issuecomment-456200490 15 | 16 | Thanks you all. 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | __0.1.5-alpha__ 13-FEBRUARY-2022 2 | - Support for `@nestjs/typeorm` v8. 3 | 4 | __0.1.4-alpha__ 12-FEBRUARY-2022 5 | - Support for typeorm ^0.2.41. 6 | 7 | __0.1.1-alpha__ 21-DECEMBER-2020 8 | - Updated to typeorm ^0.2.29. 9 | - Updated to jest ^26.6.3. 10 | - Added fake `on` and `removeListener` to `fakeConnection`. 11 | 12 | __0.1.0-alpha__ 12-SEPTEMBER-2020 13 | - Fixed problem about not restoring `typeorm.createConnection` stub (removed `sinon`). 14 | - Internal `TypeOrmTestCoreModule.forRoot` now is async `TypeOrmTestCoreModule.forRootAsync` to wait for any pending async operation in the compilation. 15 | - Added chore(deps) updates from dependabot. 16 | - Updated dependencies (jest, typescript). 17 | - Updated tests. 18 | 19 | __0.0.9-alpha__ 11-APRIL-2020 20 | - Removed unneccesary log. 21 | 22 | __0.0.8-alpha__ 11-APRIL-2020 23 | - Added `FakeConnection` disconnection on `module.close()` or `connection.close()`. 24 | 25 | __0.0.7-alpha__ 11-APRIL-2020 26 | - Added tests. 27 | - Added option to add custom connection name. 28 | - TIL: `@InjectRepository` looks for entities on the `default` connection, you should provide the connection name as a second argument, check the `testing-module.spec.ts` tests. 29 | - Updated `default` connection to `DEFAULT_CONNECTION_NAME`. 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. [Fork it](https://help.github.com/articles/fork-a-repo/) 4 | 2. Install dependencies (`npm install`) 5 | 3. Create your feature branch (`git checkout -b my-new-feature`) 6 | 4. Commit your changes (`git commit -am 'Added some feature'`) 7 | 5. Test your changes (`npm test`) 8 | 6. Push to the branch (`git push origin my-new-feature`) 9 | 7. [Create new Pull Request](https://help.github.com/articles/creating-a-pull-request/) 10 | 11 | ## Testing 12 | 13 | We use [Jest](https://github.com/facebook/jest) to write tests. Run our test suite with this command: 14 | 15 | ``` 16 | npm test 17 | ``` 18 | 19 | ## Code Style 20 | 21 | We use [Prettier](https://prettier.io/) and tslint to maintain code style and best practices. 22 | Please make sure your PR adheres to the guides by running: 23 | 24 | ``` 25 | npm run format 26 | ``` 27 | 28 | and 29 | ``` 30 | npm run lint 31 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniel Flores 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Typeorm Testing module for NestJS, with this module you don't need an access to DB to test the BeforeInsert hook of typeorm entities. 2 | 3 | Based on https://github.com/typeorm/typeorm/issues/1267#issuecomment-456200490 and some debugging/reading of the typeorm and @nestjs/typeorm flow. 4 | 5 | Created with https://github.com/nestjsplus/nestjs-package-starter. 6 | 7 | ## Install: 8 | ```shell 9 | npm install --save-dev @devniel/nestjs-typeorm-testing 10 | ``` 11 | 12 | ## Example of use: 13 | 14 | Invoke `TypeOrmTestModule.forTest` and pass as an argument an array with the entities used in the app. 15 | 16 | By now it will create fake queries against a fake postgresql database connection. 17 | 18 | If your entity (e.g. `User`) has hooks like `@BeforeInsert()`, the testing module will invoke it just like a regular typeorm module when using the injected repository in the proper services. 19 | 20 | ```ts 21 | import { TypeOrmTestModule } from '@devniel/nestjs-typeorm-testing'; 22 | 23 | const module: TestingModule = await Test.createTestingModule({ 24 | controllers: [AuthResolver], 25 | imports: [TypeOrmTestModule.forTest([User])], 26 | providers: [ 27 | AuthService, 28 | UsersService, 29 | { 30 | provide: JwtService, 31 | useValue: jwtServiceMock, 32 | }, 33 | { 34 | provide: RedisService, 35 | useValue: redisServiceMock, 36 | }, 37 | ], 38 | }).compile(); 39 | ``` 40 | 41 | ### Custom connection name 42 | 43 | To provide a custom connection name, then set the first argument of `forTest` as an options object. 44 | 45 | ```ts 46 | import { TypeOrmTestModule } from '@devniel/nestjs-typeorm-testing'; 47 | 48 | const module: TestingModule = await Test.createTestingModule({ 49 | controllers: [AuthResolver], 50 | imports: [TypeOrmTestModule.forTest({ 51 | entities: [User], 52 | name: 'default2' 53 | })], 54 | providers: [ 55 | AuthService, 56 | UsersService, 57 | { 58 | provide: JwtService, 59 | useValue: jwtServiceMock, 60 | }, 61 | { 62 | provide: RedisService, 63 | useValue: redisServiceMock, 64 | }, 65 | ], 66 | }).compile(); 67 | ``` 68 | 69 | ### Getting the fake connection 70 | You can get the created fake connection and check its properties or close it. 71 | ```ts 72 | import { TypeOrmTestModule } from '@devniel/nestjs-typeorm-testing'; 73 | import { getConnectionToken } from '@nestjs/typeorm'; 74 | 75 | const module: TestingModule = await Test.createTestingModule({ 76 | controllers: [AuthResolver], 77 | imports: [TypeOrmTestModule.forTest({ 78 | entities: [User], 79 | name: 'default2' 80 | })] 81 | }).compile(); 82 | const connection = module.get(getConnectionToken('default2')); 83 | connection.close(); // module.close() also closes the connection 84 | ``` 85 | 86 | ## Todo: 87 | 88 | - Capture queries. 89 | - Create utils to override the repositories with ease. 90 | 91 | ## License 92 | 93 | Licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 94 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": "src", 8 | "testRegex": ".spec.ts$", 9 | "transform": { 10 | "^.+\\.(t|j)s$": "ts-jest" 11 | }, 12 | "coverageDirectory": "../coverage", 13 | "testEnvironment": "node" 14 | }; -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devniel/nestjs-typeorm-testing", 3 | "version": "0.1.5-alpha", 4 | "description": "Typeorm testing module, initially for testing @BeforeInsert hook.", 5 | "author": "devniel", 6 | "license": "MIT", 7 | "readmeFilename": "README.md", 8 | "main": "dist/index", 9 | "files": [ 10 | "dist/**/*", 11 | "*.md" 12 | ], 13 | "scripts": { 14 | "start:dev": "tsc -w", 15 | "build": "tsc", 16 | "prepare": "npm run build", 17 | "format": "prettier --write \"src/**/*.ts\"", 18 | "lint": "tslint -p tsconfig.json -c tslint.json", 19 | "test": "jest", 20 | "test:watch": "jest --watch", 21 | "test:cov": "jest --coverage", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "keywords": [ 25 | "nestjs", 26 | "typeorm", 27 | "testing" 28 | ], 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/devniel/nestjs-typeorm-testing" 35 | }, 36 | "bugs": "https://github.com/devniel/nestjs-typeorm-testing", 37 | "peerDependencies": { 38 | "@nestjs/common": "^7.6.18 || ^8.0.0", 39 | "@nestjs/core": "^7.6.18 || ^8.0.0", 40 | "@nestjs/typeorm": "^7.1.5 || ^8.0.1", 41 | "rxjs": "^6.5.4" 42 | }, 43 | "dependencies": { 44 | "pg": "^7.18.2", 45 | "typeorm": "^0.2.41" 46 | }, 47 | "devDependencies": { 48 | "@nestjs/common": "^8.2.6", 49 | "@nestjs/core": "^8.2.6", 50 | "@nestjs/platform-express": "^8.2.6", 51 | "@nestjs/testing": "^8.2.6", 52 | "@nestjs/typeorm": "^8.0.3", 53 | "@types/jest": "24.0.11", 54 | "@types/node": "16.0.0", 55 | "@types/supertest": "2.0.7", 56 | "jest": "^26.6.3", 57 | "prettier": "1.17.0", 58 | "rxjs": "^6.5.4", 59 | "supertest": "4.0.2", 60 | "ts-jest": "^26.3.0", 61 | "ts-node": "8.1.0", 62 | "tsc-watch": "^4.6.0", 63 | "tsconfig-paths": "3.8.0", 64 | "tslint": "5.16.0", 65 | "typescript": "^4.0.2" 66 | }, 67 | "resolutions": { 68 | "**/**/minimist": "^1.2.5", 69 | "**/**/acorn": "^6.4.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /src/lib/FakeConnection.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Driver } from 'typeorm'; 2 | import { FakeDriverFactory } from './FakeDriverFactory'; 3 | 4 | export class FakeConnection extends Connection { 5 | _driver: Driver; 6 | // @ts-ignore 7 | get driver(): Driver { 8 | return this._driver; 9 | } 10 | set driver(options) { 11 | this._driver = new FakeDriverFactory().create(this); 12 | } 13 | async close() { 14 | (this as any)['isConnected'] = false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/FakeConnectionManager.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions, ConnectionManager } from 'typeorm'; 2 | import { FakeConnection } from './FakeConnection'; 3 | import { AlreadyHasActiveConnectionError } from 'typeorm/error/AlreadyHasActiveConnectionError'; 4 | import { DEFAULT_CONNECTION_NAME } from '@nestjs/typeorm/dist/typeorm.constants'; 5 | 6 | export class FakeConnectionManager extends ConnectionManager { 7 | // Complete copy from ConnectionManager.connect, but now the Connection class uses the fake driver. 8 | create(options: ConnectionOptions): FakeConnection { 9 | // check if such connection is already registered 10 | const existConnection = this.connections.find( 11 | connection => 12 | connection.name === (options.name || DEFAULT_CONNECTION_NAME), 13 | ); 14 | if (existConnection) { 15 | // if connection is registered and its not closed then throw an error 16 | if (existConnection.isConnected) { 17 | throw new AlreadyHasActiveConnectionError( 18 | options.name || DEFAULT_CONNECTION_NAME, 19 | ); 20 | } 21 | // if its registered but closed then simply remove it from the manager 22 | this.connections.splice(this.connections.indexOf(existConnection), 1); 23 | } 24 | // create a new connection 25 | const connection = new FakeConnection(options); 26 | this.connections.push(connection); 27 | return connection; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/FakeDBDriver.ts: -------------------------------------------------------------------------------- 1 | import { PostgresDriver } from 'typeorm/driver/postgres/PostgresDriver'; 2 | import { fakeConnection } from './createFakeConnection'; 3 | import { PostgresQueryRunner } from 'typeorm/driver/postgres/PostgresQueryRunner'; 4 | import { FakePostgresQueryRunner } from './FakePostgresQueryRunner'; 5 | 6 | export class FakeDBDriver extends PostgresDriver { 7 | // Returning a fake connection to the master node. 8 | async connect(): Promise { 9 | this.master = { 10 | connect: cb => { 11 | cb(null, fakeConnection, () => {}); 12 | }, 13 | }; 14 | this.database = this.options.database; 15 | } 16 | createQueryRunner(mode): PostgresQueryRunner { 17 | return new FakePostgresQueryRunner(this, 'master'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/FakeDriverFactory.ts: -------------------------------------------------------------------------------- 1 | import { FakeConnection } from './FakeConnection'; 2 | import { DriverFactory } from 'typeorm/driver/DriverFactory'; 3 | import { FakeDBDriver } from './FakeDBDriver'; 4 | import { Driver } from 'typeorm'; 5 | 6 | export class FakeDriverFactory extends DriverFactory { 7 | create(connection: FakeConnection): Driver { 8 | return new FakeDBDriver(connection); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/FakePostgresQueryRunner.ts: -------------------------------------------------------------------------------- 1 | import { PostgresQueryRunner } from 'typeorm/driver/postgres/PostgresQueryRunner'; 2 | 3 | export class FakePostgresQueryRunner extends PostgresQueryRunner { 4 | release(): Promise { 5 | return Promise.resolve(); 6 | } 7 | query(_query: string, _parameters?: any[], _useStructuredResult?: boolean): Promise { 8 | return Promise.resolve({ 9 | records: [] 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/createFakeConnection.ts: -------------------------------------------------------------------------------- 1 | import * as typeorm from 'typeorm'; 2 | import { FakeConnectionManager } from './FakeConnectionManager'; 3 | import { DEFAULT_CONNECTION_NAME } from '@nestjs/typeorm/dist/typeorm.constants'; 4 | 5 | export interface FakeConnectionOptions { 6 | type: 'postgres'; 7 | name: string; 8 | entities: (string | Function | typeorm.EntitySchema)[]; 9 | } 10 | 11 | export const defaultFakeConnectionOptions: FakeConnectionOptions = { 12 | type: 'postgres', 13 | name: DEFAULT_CONNECTION_NAME, 14 | entities: [], 15 | }; 16 | 17 | // The fake connection attached to the driver and used 18 | // from the master connection to startup queries, no parameters provided; 19 | // The regular one with parameters is the one run by PostgresQueryRunner. 20 | export const fakeConnection = { 21 | query: (_query, cb) => { 22 | cb(null, { 23 | rows: [ 24 | { server_version: "12.0" } 25 | ] 26 | }); 27 | }, 28 | // Ignore any event listener action 29 | on: () => {}, 30 | removeListener: () => {}, 31 | }; 32 | 33 | const fakeConnectionManager = new FakeConnectionManager(); 34 | 35 | export const getFakeConnectionManager = () => { 36 | return fakeConnectionManager; 37 | }; 38 | 39 | export const createFakeConnection = (options: FakeConnectionOptions) => 40 | getFakeConnectionManager() 41 | .create(options as typeorm.ConnectionOptions) 42 | .connect(); 43 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmTestModule } from './typeorm-test.module'; 2 | import { 3 | createFakeConnection, 4 | fakeConnection, 5 | getFakeConnectionManager, 6 | } from './createFakeConnection'; 7 | import { FakeConnection } from './FakeConnection'; 8 | import { FakeConnectionManager } from './FakeConnectionManager'; 9 | import { FakeDBDriver } from './FakeDBDriver'; 10 | import { FakeDriverFactory } from './FakeDriverFactory'; 11 | import { FakePostgresQueryRunner } from './FakePostgresQueryRunner'; 12 | 13 | export { 14 | TypeOrmTestModule, 15 | createFakeConnection, 16 | fakeConnection, 17 | getFakeConnectionManager, 18 | FakeConnection, 19 | FakeConnectionManager, 20 | FakeDBDriver, 21 | FakeDriverFactory, 22 | FakePostgresQueryRunner, 23 | }; 24 | -------------------------------------------------------------------------------- /src/lib/typeorm-test-core.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | Global, 4 | Inject, 5 | Module, 6 | Provider, 7 | } from '@nestjs/common'; 8 | import { ModuleRef } from '@nestjs/core'; 9 | import { 10 | generateString, 11 | getConnectionToken, 12 | getEntityManagerToken, 13 | } from '@nestjs/typeorm/dist/common/typeorm.utils'; 14 | import { EntitiesMetadataStorage } from '@nestjs/typeorm/dist/entities-metadata.storage'; 15 | import { TypeOrmModuleOptions } from '@nestjs/typeorm/dist/interfaces'; 16 | import { 17 | DEFAULT_CONNECTION_NAME, 18 | TYPEORM_MODULE_ID, 19 | TYPEORM_MODULE_OPTIONS, 20 | } from '@nestjs/typeorm/dist/typeorm.constants'; 21 | import { Connection, ConnectionOptions } from 'typeorm'; 22 | 23 | import { 24 | createFakeConnection, 25 | FakeConnectionOptions, 26 | } from './createFakeConnection'; 27 | 28 | /** 29 | * A fake TypeOrmMockModule as a copy because it's not available to inherit from @nestjs/typeorm. 30 | * The only change is in `createConnectionFactory` where `createConnection` is replaced by 31 | * `createFakeConnection`. 32 | */ 33 | @Global() 34 | @Module({}) 35 | export class TypeOrmTestCoreModule { 36 | constructor( 37 | @Inject(TYPEORM_MODULE_OPTIONS) 38 | private readonly options: TypeOrmModuleOptions, 39 | private readonly moduleRef: ModuleRef, 40 | ) {} 41 | 42 | static forRoot( 43 | options: FakeConnectionOptions = { 44 | type: 'postgres', 45 | name: DEFAULT_CONNECTION_NAME, 46 | entities: [], 47 | }, 48 | ): DynamicModule { 49 | const typeOrmModuleOptions = { 50 | provide: TYPEORM_MODULE_OPTIONS, 51 | useValue: options, 52 | }; 53 | const connectionProvider = { 54 | provide: getConnectionToken(options as ConnectionOptions) as string, 55 | useFactory: async () => await this.createConnectionFactory(options), 56 | }; 57 | const entityManagerProvider = this.createEntityManagerProvider( 58 | options as ConnectionOptions, 59 | ); 60 | return { 61 | module: TypeOrmTestCoreModule, 62 | providers: [ 63 | entityManagerProvider, 64 | connectionProvider, 65 | typeOrmModuleOptions, 66 | ], 67 | exports: [entityManagerProvider, connectionProvider], 68 | }; 69 | } 70 | 71 | static forRootAsync(options: FakeConnectionOptions): DynamicModule { 72 | const connectionProvider = { 73 | provide: getConnectionToken(options as ConnectionOptions) as string, 74 | useFactory: async () => { 75 | return await this.createConnectionFactory(options); 76 | }, 77 | inject: [TYPEORM_MODULE_OPTIONS], 78 | }; 79 | const entityManagerProvider = { 80 | provide: getEntityManagerToken(options as ConnectionOptions) as string, 81 | useFactory: (connection: Connection) => connection.manager, 82 | inject: [getConnectionToken(options as ConnectionOptions)], 83 | }; 84 | 85 | return { 86 | module: TypeOrmTestCoreModule, 87 | providers: [ 88 | entityManagerProvider, 89 | connectionProvider, 90 | { 91 | provide: TYPEORM_MODULE_ID, 92 | useValue: generateString(), 93 | }, 94 | { 95 | provide: TYPEORM_MODULE_OPTIONS, 96 | useFactory: () => options, 97 | }, 98 | ], 99 | exports: [entityManagerProvider, connectionProvider], 100 | }; 101 | } 102 | 103 | private static createEntityManagerProvider( 104 | options: ConnectionOptions, 105 | ): Provider { 106 | return { 107 | provide: getEntityManagerToken(options) as string, 108 | useFactory: (connection: Connection) => connection.manager, 109 | inject: [getConnectionToken(options)], 110 | }; 111 | } 112 | 113 | private static async createConnectionFactory( 114 | options: FakeConnectionOptions, 115 | ): Promise { 116 | const connectionToken = options.name || DEFAULT_CONNECTION_NAME; 117 | let entities = options.entities; 118 | if (entities) { 119 | entities = entities.concat( 120 | EntitiesMetadataStorage.getEntitiesByConnection(connectionToken), 121 | ); 122 | } else { 123 | entities = EntitiesMetadataStorage.getEntitiesByConnection( 124 | connectionToken, 125 | ); 126 | } 127 | return createFakeConnection({ 128 | ...options, 129 | entities, 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/lib/typeorm-test.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | Module, 4 | OnApplicationShutdown, 5 | Inject, 6 | Type, 7 | } from '@nestjs/common'; 8 | import { 9 | TypeOrmModule, 10 | TypeOrmModuleOptions, 11 | getConnectionToken, 12 | } from '@nestjs/typeorm'; 13 | import { ConnectionOptions, Connection } from 'typeorm'; 14 | import { TypeOrmTestCoreModule } from './typeorm-test-core.module'; 15 | import { 16 | DEFAULT_CONNECTION_NAME, 17 | TYPEORM_MODULE_OPTIONS, 18 | } from '@nestjs/typeorm/dist/typeorm.constants'; 19 | import { ModuleRef } from '@nestjs/core'; 20 | 21 | interface TypeOrmTestModuleOptions { 22 | entities: Function[]; 23 | type?: string; 24 | name?: string; 25 | } 26 | 27 | const TYPE = 'postgres'; 28 | 29 | @Module({}) 30 | export class TypeOrmTestModule implements OnApplicationShutdown { 31 | constructor( 32 | @Inject(TYPEORM_MODULE_OPTIONS) 33 | private readonly options: TypeOrmModuleOptions, 34 | private readonly moduleRef: ModuleRef, 35 | ) {} 36 | 37 | static forTest( 38 | entitiesOrOptions: TypeOrmTestModuleOptions | Function[], 39 | ): DynamicModule { 40 | let options; 41 | 42 | if (!(entitiesOrOptions instanceof Array)) { 43 | const _options = entitiesOrOptions as TypeOrmTestModuleOptions; 44 | options = { 45 | type: _options.type || TYPE, 46 | name: _options.name || DEFAULT_CONNECTION_NAME, 47 | entities: _options.entities || [], 48 | }; 49 | } else { 50 | entitiesOrOptions = entitiesOrOptions || []; 51 | options = { 52 | type: TYPE, 53 | name: DEFAULT_CONNECTION_NAME, 54 | entities: entitiesOrOptions, 55 | }; 56 | } 57 | 58 | // The type serves as the handler to create the queries, it should be one of the 59 | // supported types of typeorm. 60 | const root = TypeOrmTestCoreModule.forRootAsync(options); 61 | const feature = TypeOrmModule.forFeature(options.entities, options.name); 62 | 63 | const result = { 64 | module: TypeOrmTestModule, 65 | providers: [...root.providers, ...feature.providers], 66 | exports: [...root.exports, ...feature.exports], 67 | }; 68 | 69 | return result; 70 | } 71 | 72 | async onApplicationShutdown() { 73 | const connection = this.moduleRef.get(getConnectionToken(this 74 | .options as ConnectionOptions) as Type); 75 | connection && (await connection.close()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/tests/custom-repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TypeOrmTestModule } from '../lib'; 3 | import { UserRepository } from './shared/UserCustomRepository'; 4 | import { UsersServiceWithCustomRepository } from './shared/UsersServiceWithCustomRepository'; 5 | 6 | describe('Custom repository provided', () => { 7 | let usersCustomRepository: UserRepository; 8 | let usersService: UsersServiceWithCustomRepository; 9 | 10 | beforeAll(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [TypeOrmTestModule.forTest([UserRepository])], 13 | providers: [UsersServiceWithCustomRepository], 14 | }).compile(); 15 | usersService = module.get(UsersServiceWithCustomRepository); 16 | usersCustomRepository = module.get(UserRepository); 17 | }); 18 | 19 | it('repository for access entity should be defined', () => { 20 | expect(usersService).toBeDefined(); 21 | expect(usersCustomRepository).toBeDefined(); 22 | }); 23 | 24 | it('should call the parent class method findOne() when calling instance findByEmail()', async () => { 25 | const spyfindOne = jest.spyOn(usersCustomRepository, 'findOne'); 26 | await usersCustomRepository.findByEmail('test'); 27 | expect(spyfindOne).toHaveBeenCalledTimes(1); 28 | }); 29 | 30 | it('should call the repository findByEmail() method when calling the service', async () => { 31 | const spyRepository = jest.spyOn(usersCustomRepository, 'findByEmail'); 32 | await usersService.findByEmail('test'); 33 | expect(spyRepository).toHaveBeenCalledTimes(1); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/tests/repository-provided.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TypeOrmTestModule } from '../lib'; 3 | import { UsersService } from './shared/UsersService'; 4 | import { User } from './shared/User'; 5 | import { Repository } from 'typeorm'; 6 | import { getRepositoryToken } from '@nestjs/typeorm'; 7 | 8 | describe('Entity repository provided in services', () => { 9 | let usersService: UsersService; 10 | let usersRepository: Repository; 11 | 12 | beforeAll(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | imports: [TypeOrmTestModule.forTest([User])], 15 | providers: [UsersService], 16 | }).compile(); 17 | usersService = module.get(UsersService); 18 | usersRepository = module.get(getRepositoryToken(User)); 19 | }); 20 | 21 | it('service for access entity should be defined', () => { 22 | expect(usersService).toBeDefined(); 23 | expect(usersRepository).toBeDefined(); 24 | }); 25 | 26 | it('should call the repository save() method when creating a user', async () => { 27 | const spyRepository = jest.spyOn(usersRepository, 'save'); 28 | const user = new User(); 29 | user.email = 'test@test.com'; 30 | user.name = 'test'; 31 | user.password = 'test'; 32 | const createdUser = await usersService.create(user); 33 | expect(spyRepository).toHaveBeenCalledTimes(1); 34 | // It seems that the repository returns the sent entity 35 | expect(createdUser).toBeTruthy(); 36 | expect(createdUser).toHaveProperty('email', user.email); 37 | expect(createdUser).toHaveProperty('name', user.name); 38 | expect(createdUser).toHaveProperty('password', user.password); 39 | }); 40 | 41 | it('should call the repository find() method when calling the service', async () => { 42 | const spyRepository = jest.spyOn(usersRepository, 'find'); 43 | await usersService.find({}); 44 | expect(spyRepository).toHaveBeenCalledTimes(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/tests/shared/User.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } 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('text', { unique: true }) 12 | username: string; 13 | 14 | @Column('text', { unique: true }) 15 | email: string; 16 | 17 | @Column() 18 | password: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/tests/shared/UserCustomRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from "typeorm"; 2 | import { User } from "./User"; 3 | 4 | @EntityRepository(User) 5 | export class UserRepository extends Repository { 6 | findByEmail(email: string): Promise { 7 | return this.findOne({ email: email }); 8 | } 9 | } -------------------------------------------------------------------------------- /src/tests/shared/UsersService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from './User'; 4 | import { Repository } from 'typeorm'; 5 | 6 | @Injectable() 7 | export class UsersService { 8 | private readonly users: User[]; 9 | 10 | constructor( 11 | @InjectRepository(User) private readonly userRepository: Repository, 12 | ) { 13 | this.users = []; 14 | } 15 | 16 | async findAll(): Promise { 17 | return this.userRepository.find(); 18 | } 19 | 20 | async find(query): Promise { 21 | return this.userRepository.find(query); 22 | } 23 | 24 | async findOne(query): Promise { 25 | return this.userRepository.findOne(query); 26 | } 27 | 28 | async create(user: User): Promise { 29 | return this.userRepository.save(user); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/tests/shared/UsersServiceWithCustomRepository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from './User'; 4 | import { UserRepository } from './UserCustomRepository'; 5 | 6 | @Injectable() 7 | export class UsersServiceWithCustomRepository { 8 | private readonly users: User[]; 9 | 10 | constructor( 11 | private readonly userRepository: UserRepository, 12 | ) { 13 | this.users = []; 14 | } 15 | 16 | async findByEmail(email: string): Promise { 17 | return this.userRepository.findByEmail(email); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/tests/testing-module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TypeOrmTestModule, FakeConnection } from '../lib'; 3 | import { UsersService } from './shared/UsersService'; 4 | import { User } from './shared/User'; 5 | import { Connection, Repository } from 'typeorm'; 6 | import { getConnectionToken, InjectRepository } from '@nestjs/typeorm'; 7 | import { Injectable } from '@nestjs/common'; 8 | import * as typeorm from 'typeorm'; 9 | 10 | const CONNECTION_1_NAME = 'default1'; 11 | const CONNECTION_2_NAME = 'default2'; 12 | const CONNECTION_3_NAME = 'default3'; 13 | 14 | @Injectable() 15 | export class UsersServiceConnection1 { 16 | private readonly users: User[]; 17 | 18 | constructor( 19 | @InjectRepository(User, CONNECTION_1_NAME) 20 | private readonly userRepository: Repository, 21 | ) { 22 | this.users = []; 23 | } 24 | 25 | async findAll(): Promise { 26 | return this.userRepository.find(); 27 | } 28 | 29 | async find(query): Promise { 30 | return this.userRepository.find(query); 31 | } 32 | 33 | async findOne(query): Promise { 34 | return this.userRepository.findOne(query); 35 | } 36 | 37 | async create(user: User): Promise { 38 | return this.userRepository.save(user); 39 | } 40 | } 41 | 42 | @Injectable() 43 | export class UsersServiceConnection2 { 44 | private readonly users: User[]; 45 | 46 | constructor( 47 | @InjectRepository(User, CONNECTION_2_NAME) 48 | private readonly userRepository: Repository, 49 | ) { 50 | this.users = []; 51 | } 52 | 53 | async findAll(): Promise { 54 | return this.userRepository.find(); 55 | } 56 | 57 | async find(query): Promise { 58 | return this.userRepository.find(query); 59 | } 60 | 61 | async findOne(query): Promise { 62 | return this.userRepository.findOne(query); 63 | } 64 | 65 | async create(user: User): Promise { 66 | return this.userRepository.save(user); 67 | } 68 | } 69 | 70 | describe('Create multiple testing modules', () => { 71 | it('should create multiple testing modules when using different names', async () => { 72 | const module: TestingModule = await Test.createTestingModule({ 73 | imports: [ 74 | TypeOrmTestModule.forTest({ 75 | entities: [User], 76 | name: CONNECTION_1_NAME, 77 | }), 78 | ], 79 | providers: [UsersServiceConnection1], 80 | }).compile(); 81 | const connection: FakeConnection = module.get( 82 | getConnectionToken(CONNECTION_1_NAME), 83 | ); 84 | expect(connection).toBeTruthy(); 85 | expect(connection.name).toBeTruthy(); 86 | const module2: TestingModule = await Test.createTestingModule({ 87 | imports: [ 88 | TypeOrmTestModule.forTest({ 89 | entities: [User], 90 | name: CONNECTION_2_NAME, 91 | }), 92 | ], 93 | providers: [UsersServiceConnection2], 94 | }).compile(); 95 | const connection2: FakeConnection = module2.get( 96 | getConnectionToken(CONNECTION_2_NAME), 97 | ); 98 | expect(connection2).toBeTruthy(); 99 | expect(connection2.name).toBeTruthy(); 100 | expect(connection.name).not.toEqual(connection2.name); 101 | await module.close(); 102 | await module2.close(); 103 | }); 104 | it('should return error while creating the same connection based on its name', async () => { 105 | const module = await Test.createTestingModule({ 106 | imports: [TypeOrmTestModule.forTest([User])], 107 | providers: [UsersService], 108 | }).compile(); 109 | try { 110 | await Test.createTestingModule({ 111 | imports: [TypeOrmTestModule.forTest([User])], 112 | providers: [UsersService], 113 | }).compile(); 114 | } catch (e) { 115 | expect(e).toBeTruthy(); 116 | } 117 | await module.close(); 118 | }); 119 | it('should disconnect and then use a connection of the same name', async () => { 120 | const module: TestingModule = await Test.createTestingModule({ 121 | imports: [ 122 | TypeOrmTestModule.forTest({ 123 | entities: [User], 124 | name: CONNECTION_3_NAME, 125 | }), 126 | ], 127 | }).compile(); 128 | const connection = module.get( 129 | getConnectionToken(CONNECTION_3_NAME), 130 | ); 131 | await module.close(); 132 | const module2: TestingModule = await Test.createTestingModule({ 133 | imports: [ 134 | TypeOrmTestModule.forTest({ 135 | entities: [User], 136 | name: CONNECTION_3_NAME, 137 | }), 138 | ], 139 | }).compile(); 140 | expect(module2).toBeTruthy(); 141 | await module2.close(); 142 | }); 143 | it('should restore stub of typeorm when closing the testing module', async () => { 144 | const module: TestingModule = await Test.createTestingModule({ 145 | imports: [ 146 | TypeOrmTestModule.forTest({ 147 | entities: [User], 148 | name: CONNECTION_3_NAME, 149 | }), 150 | ], 151 | }).compile(); 152 | const connection = module.get( 153 | getConnectionToken(CONNECTION_3_NAME), 154 | ); 155 | expect(typeorm.createConnection).not.toHaveProperty('restore'); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "sourceMap": false, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "noLib": false 13 | }, 14 | "include": ["src/**/*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /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 | } 19 | --------------------------------------------------------------------------------