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

Sample to run Jest tests using the data-source #9109

Open
Cry0nicS opened this issue Jun 14, 2022 · 6 comments
Open

Sample to run Jest tests using the data-source #9109

Cry0nicS opened this issue Jun 14, 2022 · 6 comments

Comments

@Cry0nicS
Copy link

Cry0nicS commented Jun 14, 2022

Documentation Issue

What was unclear or otherwise insufficient?

According to this post, as well as the many others on the internet, testing with Jest is based on creating connections with createConnection and using the ormconfig.js. While I did not test this myself, it does seem to work.

However, recently, the createConnection was deprecated and replaced by data-source. I haven't been able to find any documentation about how this works with Jest, nor was I able to make it work after spending some time on it.

I've starting with the indications in the documentation and went with something like this

import { DataSource } from "typeorm"

const AppDataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "app",
})

const TestDataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "app.test",
})

AppDataSource.initialize()
    .then(() => {
        console.log("Data Source has been initialized!")
    })
    .catch((err) => {
        console.error("Error during Data Source initialization", err)
    })

export {AppDataSource, TestDataSource};

The issue I am facing is that I use the repositories pattern and everything related to the DB works like this (with AppDataSource)

class UserRepository extends Repository<User> {
    private readonly userRepository = AppDataSource.getRepository(User);

    public constructor() {
        super(User, AppDataSource.manager);
    }

    public async findAll(): Promise<User[]> {
        return this.userRepository.find();
    }
}

Now, I am using Jest to initialize and drop my test database

beforeAll(async () => {
    await TestDataSource.initialize();
});

afterAll(async () => {
    await TestDataSource.dropDatabase();
});

However, the tests do not work. I guess I need to find out a way to make use of the correct data-source in the main repositories and services. As in, find a way to inject whichever data-source I need from a central place.

I'm pretty sure this is not a bug, but either an error in my understanding, or in my approach. I believe an update to the documentation with a basic test would help a lot.

Recommended Fix

Having an example of running a basic test with Jest using a test DB would help a lot.

Additional Context

Are you willing to resolve this issue by submitting a Pull Request?

  • ✖️ Yes, I have the time, and I know how to start.
  • ✅Yes, I have the time, but I don't know how to start. I would need guidance.
  • ✖️ No, I don’t have the time, but I can support (using donations) development.
  • ✖️ No, I don’t have the time and I’m okay to wait for the community / maintainers to resolve this issue.
@Cry0nicS
Copy link
Author

I've manage to find a way to make it work. It's probably not ideal, but does the trick for now.

The main issue above is making use of the data-source. The only way I could do that was by relying on a dependency-injection lib. I've used typeDi.

I have now 2 places of "absolute truth" when it comes to the data-source. One that's created when the server starts (in the index.ts) and one that's created by the test-case. Since the two never overlap, I just inject it into the TypeDI container and make use of it everywhere I need.

To show a little bit of code: the above changes to
(tests/config.ts)

const initializeDataSource = async (): Promise<DataSource> => {
    const dataSource = testDataSource();  // <--- made it a function, because of other reason.

    await dataSource
        .initialize()
        .then(() => Container.set(DataSource, dataSource)); // <--- this is the important line

    return dataSource;
};

And now that we have a function that initializes a connection to the testing database, we can use it when writing a test.

(tests/user.test.ts)

let dataSource: DataSource;

beforeAll(async (): Promise<DataSource> => {
    dataSource = await initializeDataSource();

    return dataSource;
});

afterAll(async () => dataSource.destroy());

const createUser= `
    mutation CreateUser($name: String!) {
      createUser(name: $name) {
        id
        name
      }
    }
`;

describe("Test the user module", () => {
    it("should create a user", async () => {
        const user: Partial<User> = {
            name: faker.name.jobType()
        };

        const response = await gqlHelper({   // <--- this is a helper function to call directly the GQL resolver, as a client would.
            source: createUser,
            variableValues: user
        });

        expect(response).toMatchObject({
            data: {
                createUser: user
            }
        });
    });

And now it works because the actual resolver invoked in the test is injecting the user repository, that internally injects the data-source we stored in the TypeDI Container.

The repository class changed to this:

class UserRepository extends Repository<User> {
   private readonly userRepository: Repository<User>; 

    public constructor(dataSource: DataSource) {   // <-- TypeDI knows to inject the DataSource because of its type.
        super(User, dataSource.manager);

        this.userRepository = dataSource.getRepository(User);
    }

    public async create(data: Partial<User>): Promise<User> {
        const user = Object.assign(new User(), data);
.
        return this.userRepository.save(user);  // <--- this will store the user to either the testing or the real DB, based on which data-source was initialized and injected in this class.
    }
}

If anyone is looking for a concrete working example, here's a commit I made in a test project that showcases exactly what I described above. It includes CRUD tests for a specific entity.

Now, while this worked for me, I am not really sure how this could make it to the documentation. If anyone has any pointers, please share.

@vanlinh30061995
Copy link

vanlinh30061995 commented Jul 28, 2022

I've manage to find a way to make it work. It's probably not ideal, but does the trick for now.

The main issue above is making use of the data-source. The only way I could do that was by relying on a dependency-injection lib. I've used typeDi.

I have now 2 places of "absolute truth" when it comes to the data-source. One that's created when the server starts (in the index.ts) and one that's created by the test-case. Since the two never overlap, I just inject it into the TypeDI container and make use of it everywhere I need.

To show a little bit of code: the above changes to (tests/config.ts)

const initializeDataSource = async (): Promise<DataSource> => {
    const dataSource = testDataSource();  // <--- made it a function, because of other reason.

    await dataSource
        .initialize()
        .then(() => Container.set(DataSource, dataSource)); // <--- this is the important line

    return dataSource;
};

And now that we have a function that initializes a connection to the testing database, we can use it when writing a test.

(tests/user.test.ts)

let dataSource: DataSource;

beforeAll(async (): Promise<DataSource> => {
    dataSource = await initializeDataSource();

    return dataSource;
});

afterAll(async () => dataSource.destroy());

const createUser= `
    mutation CreateUser($name: String!) {
      createUser(name: $name) {
        id
        name
      }
    }
`;

describe("Test the user module", () => {
    it("should create a user", async () => {
        const user: Partial<User> = {
            name: faker.name.jobType()
        };

        const response = await gqlHelper({   // <--- this is a helper function to call directly the GQL resolver, as a client would.
            source: createUser,
            variableValues: user
        });

        expect(response).toMatchObject({
            data: {
                createUser: user
            }
        });
    });

And now it works because the actual resolver invoked in the test is injecting the user repository, that internally injects the data-source we stored in the TypeDI Container.

The repository class changed to this:

class UserRepository extends Repository<User> {
   private readonly userRepository: Repository<User>; 

    public constructor(dataSource: DataSource) {   // <-- TypeDI knows to inject the DataSource because of its type.
        super(User, dataSource.manager);

        this.userRepository = dataSource.getRepository(User);
    }

    public async create(data: Partial<User>): Promise<User> {
        const user = Object.assign(new User(), data);
.
        return this.userRepository.save(user);  // <--- this will store the user to either the testing or the real DB, based on which data-source was initialized and injected in this class.
    }
}

If anyone is looking for a concrete working example, here's a commit I made in a test project that showcases exactly what I described above. It includes CRUD tests for a specific entity.

Now, while this worked for me, I am not really sure how this could make it to the documentation. If anyone has any pointers, please share.

Thanks

@francas
Copy link
Contributor

francas commented Nov 11, 2022

Also spent quite an amount of time on this, here's how we did it:

Repo file user.repository.ts:

export const UserRepository = Container.get(DataSource)
    .getRepository(User)
    .extend({
        async doSomething(): Promise<void> {
        },
        async doSomethingElse(): Promise<void> {
        },
     });

tests/setup.ts file

import Container from "typedi";
import { DataSource } from "typeorm";

import { getRepoMockImplementation } from "./mocks";

Container.set(
    DataSource,
    new (class DataSource {
        public getRepository(module) {
            return getRepoMockImplementation(module);
        }
    })()
);

tests/mocks.ts file

class RepositoryMock {
    public extend() {
        return this;
    }
}

export class UserRepositoryMock extends RepositoryMock {
    public doSomething(): Promise<any> {
        throw new Error("Method not implemented.");
    }
    public doSomethingElse(): Promise<any> {
        throw new Error("Method not implemented.");
    }
}

export const getRepoMockImplementation = (module) => {
    switch (module) {
        case User:
            return new UserRepositoryMock();
        default:
            throw new Error("Mock not implemented.");
    }
};


@maryamaljanabi
Copy link

maryamaljanabi commented Dec 30, 2022

Is there anyway to do this without a DI library? I need to mock my actual datasource for unit testing purposes. Despite typeDI library being really beneficial, it's not being fully maintained and it may cause vulnerabilities in the project I'm working on so I have to avoid it.

@landersonalmeida
Copy link

landersonalmeida commented Dec 30, 2022

maryamaljanabi

@maryamaljanabi

Try this:

Create a file called connection.ts somewhere in your project:

import { IMemoryDb, newDb } from 'pg-mem'
import { DataSource } from 'typeorm'

type FakeDbResponse = {
  db: IMemoryDb
  dataSource: DataSource
}

export const makeFakeDb = async (entities?: any[]): Promise<FakeDbResponse> => {
  const db = newDb({
    autoCreateForeignKeyIndices: true
  })

  db.public.registerFunction({
    implementation: () => 'test',
    name: 'current_database'
  })

  const dataSource = await db.adapters.createTypeormDataSource({
    type: 'postgres',
    entities: entities ?? ['........'] // Your entities folder here
  })
  await dataSource.initialize()
  await dataSource.synchronize()

  return { db, dataSource }
}

Then in the tests:

import { YourEntity } from 'YOUR_ENTITIES_FOLDER' // [Replace here] 
import { makeFakeDb } from 'connection.ts'

import { IBackup } from 'pg-mem'
import { DataSource, Repository } from 'typeorm'

describe('PgUserAccountRepository', () => {
  let pgUserRepo: Repository<YourEntity>
  let backup: IBackup
  let dataSource: DataSource

  beforeAll(async () => {
    const fakeDb = await makeFakeDb([YourEntity])

    dataSource = fakeDb.dataSource
    backup = fakeDb.db.backup()
    pgUserRepo = dataSource.getRepository(YourEntity)
  })

  afterAll(async () => {
    await dataSource.destroy()
  })

  beforeEach(() => {
    backup.restore()
  })

  it('should ... if email exists', async () => {
    await pgUserRepo.save({ email: 'any_email' })

    const account = await = someService.load({ email: 'any_email' })

    expect(account).toEqual({ id: '1' })
  })
})

I really hope that answer helps you ☺️.

@greatertomi
Copy link

greatertomi commented Jun 30, 2023

@maryamaljanabi

The simpler way around it is to create a util that would handle switching the data source between testing and development. Something like this.

import { EntityTarget, Repository } from 'typeorm';
import dataSource from '../data-source';

const handleGetRepository = <T>(entity: EntityTarget<T>): Repository<T> => {
  const environment = process.env.NODE_ENV || 'development';
  return environment === 'test'
    ? dataSource.TestDataSource.manager.getRepository(entity)
    : dataSource.AppDataSource.manager.getRepository(entity);
};

export default handleGetRepository;

Then you can just do const userRepository = handleGetRepository(User); in your repository.

You can check out this repo for more info.
https://github.com/greatertomi/nodejs-typeorm-template

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants