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

How to test BeforeInsert with a mocked Repository #5593

Closed
devniel opened this issue Mar 1, 2020 · 9 comments
Closed

How to test BeforeInsert with a mocked Repository #5593

devniel opened this issue Mar 1, 2020 · 9 comments

Comments

@devniel
Copy link

devniel commented Mar 1, 2020

Issue type:

[x] question
[ ] bug report
[ ] feature request
[ ] documentation issue

Database system/driver:

[ ] cordova
[ ] mongodb
[ ] mssql
[ ] mysql / mariadb
[ ] oracle
[ ] postgres
[ ] cockroachdb
[x] sqlite
[ ] sqljs
[ ] react-native
[ ] expo

TypeORM version:

[x] latest
[ ] @next
[ ] 0.x.x (or put your version here)

Hi, I'm trying to check that the method decorated with @BeforeInsert in my User entity is called, but with a mocked repository it's not working because it seems that @BeforeInsert it's called when doing the actual insertion in the database, so maybe I need to mock one lower layer. What do you think ? (actually I'm trying to do this with nestjs).

@ChristianMurphy
Copy link

see #1267 for the general discussion on testing

@devniel
Copy link
Author

devniel commented Mar 1, 2020

I think it's kind of tricky, because I will need to play with the listeners used by the Broadcaster and the metadata, is there a function to get the metadata directly from the class without a connection to a DB ?

@artoodeeto
Copy link
Contributor

@devniel we have the same problem. My work around is query after the hooks and check the result. I know its more work. 😢

@devniel
Copy link
Author

devniel commented Mar 8, 2020

@artoodeeto I was able to get it 😁, I have created a testing module to make it easy to integrate in new tests https://github.com/devniel/nestjs-typeorm-testing, don't hesitate to contribute if you want.

@artoodeeto
Copy link
Contributor

@devniel thanks man. This works even though im not using nestjs?

@devniel
Copy link
Author

devniel commented Mar 15, 2020

@artoodeeto I have updated the module so it can be used in non-nestjs projects. there is an example here https://github.com/devniel/example-typeorm-testing.

@artoodeeto
Copy link
Contributor

@devniel you are my hero. much love man! ❤️

@devniel devniel closed this as completed Mar 18, 2020
@bhishp
Copy link

bhishp commented Jun 30, 2020

The way I approached this is to just call the beforeInsert function on an instance of the entity directly in a test file and then test the object shape after calling the function. E.g.

The entity

export class Meta {
  @Column('varchar', { name: 'META_QUALITY_CD', nullable: true, length: 50 })
  metaQualityCd!: string | null;
  
  @Column
   ...

  @BeforeInsert()
  generateInsertMeta() {
    const now = new Date(new Date().toUTCString());
    this.metaQualityCd = null;
    this.metaActionCd = MetaActionCD.Insert;
    this.metaCreatedDttm = now;
    this.metaCreatorNm = WEBAPP_USER_NAME;
    this.metaChangedDttm = now;
    this.metaChangedByNm = WEBAPP_USER_NAME;
  }
}

The test

describe('MetaEntity', () => {
  describe('beforeInsert', () => {
    it('should set meta fields as expected', () => {
      const meta = new Meta();
      MockDate.set('2020-01-22');
      const now = new Date();

      const expected: Partial<Meta> = {
        metaQualityCd: null,
        metaActionCd: MetaActionCD.Insert,
        metaCreatedDttm: now,
        metaCreatorNm: WEBAPP_USER_NAME,
        metaChangedDttm: now,
        metaChangedByNm: WEBAPP_USER_NAME,
      };

      // just call the BeforeInsert function directly (it is simply an instance function) and then assert the instance shape
      meta.generateInsertMeta();

      expect(meta).toEqual(expected);
    });
  });
});

Not sure if this will cover all cases but IMO it should be sufficient just to test the method like this rather than needing to involve the rest of TypeORM at all (by mocking typeorm connection all you are doing really is testing typeORMs behaviour for ensuring that the decorators are called as expected?)

EDIT

Though, thinking about it, this could lead to issues where you are not calling the correct function in our test (i.e. if the BeforeInsert decorator is actually over another instance method). Maybe we could mock the BeforeInsert decorator in the test somehow so that it picks up the correct method and then we use that one in our tests.

@bhishp
Copy link

bhishp commented Jul 1, 2020

I had a go with playing with mocks to see if something like that could be achieved and came up with the below:

// inside test.ts

// create a map to track the listener method names that have Decorators set
interface Listeners<E> {
  BeforeInsert?: keyof E;
  BeforeUpdate?: keyof E;
}
const listenerMap: Listeners<MyEntity> = {};

import * as typeorm from 'typeorm';

// use jest.spyOn to mock only the listeners
// mock the implementation of the decorators to capture the decorated method names and set them in the map
const mockBeforeInsert = jest.spyOn(typeorm, 'BeforeInsert').mockImplementation(() => (_: any, fnName: string) => {
  listenerMap.BeforeInsert = fnName as keyof MyEntity;
});
const mockBeforeUpdate = jest.spyOn(typeorm, 'BeforeUpdate').mockImplementation(() => (_: any, fnName: string) => {
  listenerMap.BeforeUpdate = fnName as keyof MyEntity;
});

// The mocks must be created prior to the import of the entity
import { MyEntity } from './MyEntity';
...
it('should call the correct BeforeInsert method', () => {
  const entity = new MyEntity();
  const modifiedEntity = {...};

  // find the BeforeInsert method name
  const beforeInsertMethodNm = listenerMap.BeforeInsert!;

  // call this method on your instance
  (meta[beforeInsertMethodNm as keyof Entity] as Function)();

  expect(mockBeforeInsert).toHaveBeenCalled();
  expect(entity).toEqual(modifiedEntity);
});

This will use mock implementations of BeforeInsert and BeforeUpdate decorators to extract the method names of functions that have been decorated, then these can be called on your instance of your entity.

e.g.

@BeforeInsert
doBeforeUpdate() { ... }

// then the mock will set listeners.BeforeUpdate as 'doBeforeUpdate'

There can be some extra cleanup here to types probably. Also Listeners.BeforeInsert/BeforeUpdate should be arrays in reality as several functions could be decorated

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

No branches or pull requests

4 participants