nestjs / mongoose

Mongoose module for Nest framework (node.js) 🍸
https://nestjs.com
MIT License
528 stars 118 forks source link

Jest: Connection not closing after test even with testingModule.close() #167

Closed michaelvbe closed 4 years ago

michaelvbe commented 5 years ago

I'm submitting a...


[ ] Regression 
[x] Bug report
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

When using a database (mongodb-memory-server) in a test case, the test passes but runs indefinitely with Jest throwing the warnings:

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.

--detectOpenHandles supresses the warning but doesn't show any info.

Expected behavior

The connection should be closed after all tests and the test case should complete after all tests pass, and there should be no async warning.

Minimal reproduction of the problem with instructions

test-database.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';

@Module({
  imports: [
    MongooseModule.forRootAsync({
      useFactory: async () => {
        const mongod = new MongoMemoryServer();
        return {
          uri: await mongod.getConnectionString(),
          useNewUrlParser: true,
          useUnifiedTopology: true,
          useCreateIndex: true,
        };
      },
    }),
  ],
})
export class TestDatabaseModule {}

test.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

@Injectable()
export class TestService {
  constructor(
    @InjectModel('test') private readonly test: Model<any>,
  ) {}
}

test.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { MongooseModule } from '@nestjs/mongoose';
import { TestDatabaseModule } from '../database/test-database.module';
import * as mongoose from 'mongoose';
import { TestService } from './test.service';

const testSchema = new mongoose.Schema({
  test: String,
});

describe('TestService', () => {
  let testingModule: TestingModule;
  let service: TestService;

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      imports: [
        TestDatabaseModule,
        MongooseModule.forFeature([{ name: 'test', schema: testSchema }]),
      ],
      providers: [TestService],
    }).compile();

    service = testingModule.get<TestService>(TestService);
  });

  afterEach(async () => {
    await testingModule.close();
  });

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

jest.config.js

// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
/** @type {jest.DefaultOptions} */
module.exports = {
  // All imported modules in your tests should be mocked automatically
  // automock: false,

  // Stop running tests after `n` failures
  // bail: 0,

  // Respect "browser" field in package.json when resolving modules
  // browser: false,

  // The directory where Jest should store its cached dependency information
  // cacheDirectory: "C:\\Users\\mn\\AppData\\Local\\Temp\\jest",

  // Automatically clear mock calls and instances between every test
  clearMocks: true,

  // Indicates whether the coverage information should be collected while executing the test
  // collectCoverage: false,

  // An array of glob patterns indicating a set of files for which coverage information should be collected
  // collectCoverageFrom: null,

  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",

  // An array of regexp pattern strings used to skip coverage collection
  // coveragePathIgnorePatterns: [
  //   "\\\\node_modules\\\\"
  // ],

  // A list of reporter names that Jest uses when writing coverage reports
  // coverageReporters: [
  //   "json",
  //   "text",
  //   "lcov",
  //   "clover"
  // ],

  // An object that configures minimum threshold enforcement for coverage results
  // coverageThreshold: null,

  // A path to a custom dependency extractor
  // dependencyExtractor: null,

  // Make calling deprecated APIs throw helpful error messages
  // errorOnDeprecated: false,

  // Force coverage collection from ignored files using an array of glob patterns
  // forceCoverageMatch: [],

  // A path to a module which exports an async function that is triggered once before all test suites
  // globalSetup: null,

  // A path to a module which exports an async function that is triggered once after all test suites
  // globalTeardown: null,

  // A set of global variables that need to be available in all test environments
  // globals: {},

  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
  // maxWorkers: "50%",

  // An array of directory names to be searched recursively up from the requiring module's location
  // moduleDirectories: [
  //   "node_modules"
  // ],

  // An array of file extensions your modules use
  moduleFileExtensions: [
    "js",
    "json",
    "ts",
  ],

  // A map from regular expressions to module names that allow to stub out resources with a single module
  // moduleNameMapper: {},

  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
  // modulePathIgnorePatterns: [],

  // Activates notifications for test results
  // notify: false,

  // An enum that specifies notification mode. Requires { notify: true }
  // notifyMode: "failure-change",

  // A preset that is used as a base for Jest's configuration
  // preset: null,

  // Run tests from one or more projects
  // projects: null,

  // Use this configuration option to add custom reporters to Jest
  // reporters: undefined,

  // Automatically reset mock state between every test
  // resetMocks: false,

  // Reset the module registry before running each individual test
  // resetModules: false,

  // A path to a custom resolver
  // resolver: null,

  // Automatically restore mock state between every test
  // restoreMocks: false,

  // The root directory that Jest should scan for tests and modules within
  rootDir: 'src',

  // A list of paths to directories that Jest should use to search for files in
  // roots: [
  //   "<rootDir>"
  // ],

  // Allows you to use a custom runner instead of Jest's default test runner
  // runner: "jest-runner",

  // The paths to modules that run some code to configure or set up the testing environment before each test
  // setupFiles: [],

  // A list of paths to modules that run some code to configure or set up the testing framework before each test
  // setupFilesAfterEnv: [],

  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
  // snapshotSerializers: [],

  // The test environment that will be used for testing
  testEnvironment: "node",

  // Options that will be passed to the testEnvironment
  // testEnvironmentOptions: {},

  // Adds a location field to test results
  // testLocationInResults: false,

  // The glob patterns Jest uses to detect test files
  // testMatch: [
  //   "**/__tests__/**/*.[jt]s?(x)",
  //   "**/?(*.)+(spec|test).[tj]s?(x)"
  // ],

  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
  // testPathIgnorePatterns: [
  //   "\\\\node_modules\\\\"
  // ],

  // The regexp pattern or array of patterns that Jest uses to detect test files
  testRegex: '.spec.ts$',

  // This option allows the use of a custom results processor
  // testResultsProcessor: null,

  // This option allows use of a custom test runner
  // testRunner: "jasmine2",

  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
  // testURL: "http://localhost",

  // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
  // timers: "real",

  // A map from regular expressions to paths to transformers
  transform: {
    "^.+\\.(t|j)s$": "ts-jest"
  },

  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
  // transformIgnorePatterns: [
  //   "\\\\node_modules\\\\"
  // ],

  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
  // unmockedModulePathPatterns: undefined,

  // Indicates whether each individual test should be reported during the run
  // verbose: null,

  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
  // watchPathIgnorePatterns: [],

  // Whether to use watchman for file crawling
  // watchman: true,
};

What is the motivation / use case for changing the behavior?

N/A

Environment


Nest version: 6.7.2
@nestjs/mongoose version: 6.1.2


For Tooling issues:
- Node version: v12.13.0  
- Platform:  Windows

Others:

kamilmysliwiec commented 5 years ago

Please, provide a minimal repository which reproduces your issue.

michaelvbe commented 4 years ago

Hi,

I'm sorry for the delayed reply.

I have provided a working (well actually broken, but replicable) example here: https://github.com/michaelvbe/nest-mongoose-issue

A few more things to note:

Edit: running jest now also breaks after removing the controller spec.

michaelvbe commented 4 years ago

I've been under the impression that the mongodb memory server closes when the client disconnects. Thinking about it again made me realise that that's most likely not the case.

This would mean that the problem here is not caused by this library

uncledent commented 4 years ago

@michaelvbe did you manage to solve this problem? even when stopping mongo memory server there seems to be some interval running that prevents the test to stop, which also causes massive memory problems

NikolajDL commented 4 years ago

I have the same issue, but using just a regular local MongoDB instnace instead of the in-memory mongo server.

VictorAssis commented 4 years ago

I found a solution that works in my case. To Jest exits properly its necessary disconnect Mongoose and MongoMemoryServer.

db-test-module.ts

import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';

let mongod:MongoMemoryServer

export default (customOpts: MongooseModuleOptions = {}) => MongooseModule.forRootAsync({
  useFactory: async () => {
    mongod = new MongoMemoryServer();
    const uri = await mongod.getUri()
    return {
      uri,
      ...customOpts
    }
  }
})

export const closeMongoConnection = async () => {
  if (mongod) await mongod.stop()
}

test.service.spect.ts

import { Test, TestingModule } from '@nestjs/testing';
import { TestService } from './test.service';
import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
import { TestSchema } from '../models/test.model';
import DbModule, { closeMongoConnection } from '../../../test/utils/db-test.module';

describe('TestService', () => {
  let service: TestService;
  let connection: Connection;

  beforeEach(async () => {
    const module:TestingModule = await Test.createTestingModule({
      imports: [
        DbModule({
          connectionName: (new Date().getTime() * Math.random()).toString(16)
        }),
        MongooseModule.forFeature([
          { name: 'Test', schema: TestSchema }
        ])
      ],
      providers: [TestService]
    }).compile();

    service = module.get<TestService>(TestService);
    connection = await module.get(getConnectionToken());
  });

  afterEach(async () => {
    await connection.close()
    await closeMongoConnection()
  })

  it('should be defined', async () => {
    expect(service).toBeDefined();
  });
});
jsdevtom commented 3 years ago

Force closing worked for me with the regular MongoDB Server:

import { Test, TestingModule } from '@nestjs/testing';
import { TestService } from './test.service';
import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
import { TestSchema } from '../models/test.model';

describe('TestService', () => {
  let connection: Connection;

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

    connection = await module.get(getConnectionToken());
  });

  afterEach(async () => {
    await connection.close(/*force:*/ true); // <-- important
  });
});
tschannik commented 2 years ago

Hi, i have the same issue. I'm using the in-memory mongodb.

I've tried both using the force close method referred by you @jsdevtom and specifying a close function @VictorAssis

After my tests run i'm getting the following message:

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.

When using --detectOpenHandlers nothing really happens

My entry.service.spec.ts

import { getModelToken, MongooseModule, getConnectionToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { EntryService } from '../entry/entry.service';
import {
  TestDocumentDatabaseModule,
  closeInMongodConnection,
} from './../common/database/test/test-mongo-database.module';
import { Entry, EntrySchema } from './entry.schema';
import { Model, Connection } from 'mongoose';

describe('EntryService', () => {
  let service: EntryService;
  let connection: Connection;
  let entryModel: Model<Entry>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        TestDocumentDatabaseModule,
        MongooseModule.forFeature([{ name: Entry.name, schema: EntrySchema }]),
      ],
      providers: [
        EntryService,
        {
          provide: getModelToken(Entry.name),
          useValue: entryModel,
        },
      ],
    }).compile();

    service = module.get<EntryService>(EntryService);
    entryModel = module.get<Model<Entry>>(getModelToken(Entry.name));
    connection = await module.get(getConnectionToken());
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
  describe('create', () => {
    it('should create Entry', async () => {
      const user = await service.create('test');
      expect(user).not.toBeNull();
      expect(user.name).toEqual('test');
    });
  });

  afterAll(async () => {
    await connection.close(true);
    await closeInMongodConnection();
  });
});

test-mongo-database.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';

let mongo;

@Module({
  imports: [
    MongooseModule.forRootAsync({
      imports: [],
      useFactory: async () => {
        mongo = await MongoMemoryServer.create();
        const uri = await mongo.getUri();
        return {
          uri: uri,
        };
      },
    }),
  ],
})
export class TestDocumentDatabaseModule {}

export const closeInMongodConnection = async () => {
  if (mongo) await mongo.stop();
};

Any ideas?

kamilmysliwiec commented 2 years ago

Please, use our Discord channel (support) for such questions. We are using GitHub to track bugs, feature requests, and potential improvements.