Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeOrm: clear database when testing #409

Closed
jselesan opened this issue Feb 9, 2018 · 20 comments
Closed

TypeOrm: clear database when testing #409

jselesan opened this issue Feb 9, 2018 · 20 comments

Comments

@jselesan
Copy link

jselesan commented Feb 9, 2018

Hi!. Nice work with the TypeOrmModule, I really enjoy it.

I'm wondering how can I clear the database (or even drop it) before each test, in order to isolate tests from pevious tests and previous runs

@jgordor
Copy link

jgordor commented Feb 10, 2018

Hi @jselesan this is the class I use, calling await reloadFixtures() before each jest test (has a simple json fixture loading system):

import { Component } from '@nestjs/common';
import { Connection, Repository } from 'typeorm';
import { DatabaseService } from './../database/database.service';

import * as fs from 'fs';

@Component()
export class TestUtils {
  databaseService: DatabaseService;

  constructor(databaseService: DatabaseService) {
    if (process.env.NODE_ENV !== 'test') {
      throw new Error('ERROR-TEST-UTILS-ONLY-FOR-TESTS');
    }
    this.databaseService = databaseService;
  }

  async shutdownServer(server) {
    await server.httpServer.close();
    await this.closeDbConnection();
  }

  async closeDbConnection() {
    const connection = (await this.databaseService.connection);
    if (connection.isConnected) {
      await (await this.databaseService.connection).close();
    }
  }

  async getEntities() {
    const entities = [];
    (await (await this.databaseService.connection).entityMetadatas).forEach(
      x => entities.push({name: x.name, tableName: x.tableName})
    );
    return entities;
  }

  async reloadFixtures() {
    const entities = await this.getEntities();
    await this.cleanAll(entities);
    await this.loadAll(entities);
  }

  async cleanAll(entities) {
    try {
      for (const entity of entities) {
        const repository = await this.databaseService.getRepository(entity.name);
        await repository.query(`TRUNCATE TABLE \`${entity.tableName}\`;`);
      }
    } catch (error) {
      throw new Error(`ERROR: Cleaning test db: ${error}`);
    }
  }

  async loadAll(entities) {
    try {
      for (const entity of entities) {
        const repository = await this.databaseService.getRepository(entity.name);
        const fixtureFile = `src/test/fixtures/${entity.name}.json`;
        if (fs.existsSync(fixtureFile)) {
          const items = JSON.parse(fs.readFileSync(fixtureFile, 'utf8'));
          await repository
            .createQueryBuilder(entity.name)
            .insert()
            .values(items)
            .execute();
        }
      }
    } catch (error) {
      throw new Error(`ERROR [TestUtils.loadAll()]: Loading fixtures on test db: ${error}`);
    }
  }

}

Best

@kamilmysliwiec
Copy link
Member

Hi @jselesan,
It depends on what actually test framework you use to run your tests. For those kind of questions you should rather use stackoverflow instead of github 🙂 Here's a nestjs tag https://stackoverflow.com/questions/tagged/nestjs

@otroboe
Copy link

otroboe commented Feb 19, 2018

@jgordor I'm curious what your DatabaseService looks like :-P

@BrunnerLivio
Copy link
Member

BrunnerLivio commented Mar 17, 2018

Nice approach @jgordor !

@otroboe after hours and hours of googling, I finally found a solution.

The class src/test/test.utils.ts:

import { Component } from '@nestjs/common';
import { Connection, Repository } from 'typeorm';
import { DatabaseService } from '../database/database.service';

import * as Path from 'path';
import * as fs from 'fs';

/**
 * This class is used to support database
 * tests with unit tests in NestJS.
 * 
 * This class is inspired by https://github.com/jgordor
 * https://github.com/nestjs/nest/issues/409#issuecomment-364639051
 */
@Component()
export class TestUtils {
  databaseService: DatabaseService;

  /**
   * Creates an instance of TestUtils
   */
  constructor(databaseService: DatabaseService) {
    if (process.env.NODE_ENV !== 'test') {
      throw new Error('ERROR-TEST-UTILS-ONLY-FOR-TESTS');
    }
    this.databaseService = databaseService;
  }

  /**
   * Shutdown the http server
   * and close database connections
   */
  async shutdownServer(server) {
    await server.httpServer.close();
    await this.closeDbConnection();
  }

  /**
   * Closes the database connections
   */
  async closeDbConnection() {
    const connection = (await this.databaseService.connection);
    if (connection.isConnected) {
      await (await this.databaseService.connection).close();
    }
  }

  /**
   * Returns the entites of the database
   */
  async getEntities() {
    const entities = [];
    (await (await this.databaseService.connection).entityMetadatas).forEach(
      x => entities.push({name: x.name, tableName: x.tableName})
    );
    return entities;
  }

  /**
   * Cleans the database and reloads the entries
   */
  async reloadFixtures() {
    const entities = await this.getEntities();
    await this.cleanAll(entities);
    await this.loadAll(entities);
  }

  /**
   * Cleans all the entities
   */
  async cleanAll(entities) {
    try {
      for (const entity of entities) {
        const repository = await this.databaseService.getRepository(entity.name);
        await repository.query(`DELETE FROM ${entity.tableName};`);
      }
    } catch (error) {
      throw new Error(`ERROR: Cleaning test db: ${error}`);
    }
  }

  /**
   * Insert the data from the src/test/fixtures folder
   */
  async loadAll(entities) {
    try {
      for (const entity of entities) {
        const repository = await this.databaseService.getRepository(entity.name);
        const fixtureFile = Path.join(__dirname, `../test/fixtures/${entity.name}.json`);
        if (fs.existsSync(fixtureFile)) {
          const items = JSON.parse(fs.readFileSync(fixtureFile, 'utf8'));
          await repository
            .createQueryBuilder(entity.name)
            .insert()
            .values(items)
            .execute();
        }
      }
    } catch (error) {
      throw new Error(`ERROR [TestUtils.loadAll()]: Loading fixtures on test db: ${error}`);
    }
  }
}

My src/test/testing.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { DatabaseModule } from '../database/database.module';

@Module({
  imports: [
    DatabaseModule
  ],
})
export class TestingModule {
  constructor() {
  }
}

My src/database/database.module

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { getOrmConfig } from './database-ormconfig.constant';
import { DatabaseService } from './database.service';

@Module({
  imports: [
    TypeOrmModule.forRoot(getOrmConfig())
  ],
  components: [
    DatabaseService,
  ]
})
export class DatabaseModule {
  constructor() {
  }
}

My src/database/database.service.ts

import { Component, Inject } from '@nestjs/common';
import { Connection, Repository } from 'typeorm';

@Component()
export class DatabaseService {
    constructor(@Inject('Connection') public connection: Connection) { }

    async getRepository<T>(entity): Promise<Repository<T>> {
        return this.connection.getRepository(entity);
    }
}

A test case:

describe('ImageRepository', () => {
    let imageRepository: ImageRepository;
    let connection: Connection;
    let testUtils: TestUtils;
    beforeEach(async (done) => {
        const module = await Test.createTestingModule({
            imports : [
                DatabaseModule
            ],
            components: [
                DatabaseService,
                ImageRepositoryProvider,
                TestUtils
            ]
        }).compile();
        testUtils = module.get<TestUtils>(TestUtils);
        await testUtils.reloadFixtures();
        imageRepository = testUtils.databaseService.connection.getCustomRepository(ImageRepository);
        done();
    });
    
    afterEach(async done => {
        await testUtils.closeDbConnection();
        done();
    });

Sorry for the huge dump. I probably should create a repo for that, but don't have time at the moment for that.

@kamilmysliwiec can NestJs maybe provide something like the test.utils-class from @jgordor ?

@otroboe
Copy link

otroboe commented Mar 19, 2018

@BrunnerLivio Thank you for sharing your solution. I would have use that If I had this back then ;-)

But I'm sure it will help a bunch of people 👍

@krivochenko
Copy link

Hi everyone! Thanks for great solution. I have one problem. Typeorm doesn't include primary key in INSERT statement. So after every reloading of database I have new IDs. How I can resolve it?

@otroboe
Copy link

otroboe commented Mar 27, 2018

@krivochenko Why do you need sames IDs for your tests ? I think it's not a good approach for e2e tests.

If you want an automatic ID in your table/document, use @PrimaryGeneratedColumn().

@krivochenko
Copy link

@otroboe I already have automatic ID. I wanna define it by my self for some entity, which ID used as foreign key in another table. For now I have null-value in columns restricted by foreign key.

@otroboe
Copy link

otroboe commented Mar 27, 2018

@krivochenko You should open an issue on stack-overflow or come on Gitter to ask ;-)

@BrunnerLivio
Copy link
Member

BrunnerLivio commented Mar 27, 2018

@krivochenko

Simply use `.query``

async cleanAll(entities) {
    try {
      for (const entity of entities.sort((a, b) => b.order - a.order)) {
        const repository = await this.databaseService.getRepository(entity.name);
        await repository.query(`DELETE FROM ${entity.tableName};`);
        // Reset IDs
        await repository.query(`DELETE FROM sqlite_sequence WHERE name='${entity.tableName}'`);
      }
    } catch (error) {
      throw new Error(`ERROR: Cleaning test db: ${error}`);
    }
  }

EDIT:

Created a repository, which shows the approach in my recent project.

https://github.com/BrunnerLivio/nestjs-unit-test-db-example

@krivochenko
Copy link

I resolved it by following approach:

  1. I hard-coded set of entities. Because I need load data with foreign keys in defined order.
  2. I changed loadAll():
  async loadAll(entities) {
    try {
      for (const entity of entities) {
        const repository = await this.databaseService.getRepository(entity.name);
        const fixtureFile = Path.join(__dirname, `../test/fixtures/${entity.name}.json`);
        if (fs.existsSync(fixtureFile)) {
          const items = JSON.parse(fs.readFileSync(fixtureFile, 'utf8'));
          const columns = Object.keys(items[0]);
          await repository
            .createQueryBuilder(entity.name)
            .insert()
            .into(entity.name, columns)
            .values(items)
            .execute();
        }
      }
    } catch (error) {
      throw new Error(`ERROR [TestUtils.loadAll()]: Loading fixtures on test db: ${error}`);
    }
  }

In last release of typeorm there is a bug, which I fixed by PR: typeorm/typeorm#1836

@0xHexE
Copy link

0xHexE commented May 21, 2018

Hey I used this method for the clearing the database.
When you are starting the server just add.

// You can get the connection from the app or you can use the `getConnection` from `typeorm`
await getConnection().synchronize(true); // Here the true is for `dropBeforeSync`

@biels
Copy link

biels commented May 23, 2018

TypeORM needs better documentation. I may have missed it, but await getConnection().synchronize(true); can't be found there AFAIK.

@BrunnerLivio
Copy link
Member

BrunnerLivio commented May 24, 2018

@biels The problem is, synchronize is part of typeorm, not NestJS. You can look it up here.

@lukeautry
Copy link

lukeautry commented Jul 21, 2018

If anyone (like me) is looking for a really simple way to do this in the future, here you go:

const clearDb = async () => {
  const entities = connection.entityMetadatas;

  for (const entity of entities) {
    const repository = await connection.getRepository(entity.name);
    await repository.query(`DELETE FROM ${entity.tableName};`);
  }
};

@0xHexE
Copy link

0xHexE commented Jul 21, 2018

Hey @lukeautry

If we skip synchronize like this

@Entity({ skipSync: true  })
export class Entity {  }

then this can cause problem because this table is not get created so need to check table is exists or not.

@obotor
Copy link

obotor commented Oct 19, 2018

Hey!
Working with tests, you do not really need to perform any kind of persistence. You should use a fresh database file. So why not use simplicity?:
if (fs.existsSync(fixtureFile)) {
fs.unlinkSync(fixtureFile)
}
That way, no need to perform queries on all database entities and sequences...
Cheers.

@rhlsthrm
Copy link

rhlsthrm commented Jul 3, 2019

Just FYI, I tried one of the solutions mentioned above:

const clearDb = async () => {
  const entities = connection.entityMetadatas;

  for (const entity of entities) {
    const repository = await connection.getRepository(entity.name);
    await repository.query(`DELETE FROM ${entity.tableName};`);
  }
};

This worked until it ran into an issue where it tried to delete over a foreign key constraint, and it won't work unless you cascade, which I don't want to modify the schema to do.

The other fix mentioned worked great though:

await connection.synchronize(true)

@Nargonath
Copy link

Based on @jgordor example you can TRUNCATE TABLE ... CASCADE

@lock
Copy link

lock bot commented Oct 31, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Oct 31, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests