InversifyJS icon indicating copy to clipboard operation
InversifyJS copied to clipboard

decorate(injectable(), EventEmitter)) fails with Jest

Open jrhite opened this issue 7 years ago • 10 comments

decorate(injectable(), EventEmitter)) fails in Jest test environment, but works fine when running typescript code using ts-node.

Expected Behavior

When injecting a component that extends EventEmitter (thus requiring decorate(injectable(), EventEmitter))), the component should be injected correctly.

Current Behavior

This works fine when running with ts-node src/event-emitter-inversify/run.ts. It fails when running ts-node ./node_modules/.bin/jest src/event-emitter-inversify/__tests__/event-emitter-inversify.test.ts.

The error is: TypeError: Cannot read property 'constructor' of null

at

at getClassPropsAsTargets (node_modules/inversify/lib/planning/reflection_utils.js:82:75)

Possible Solution

When running the code outside of a test environment I see inversify stepping into relection_utils.getClassPropsAsTargets().

It does this recursively from the child class extending EventEmitter, then into EventEmitter. At the EventEmitter level when we call Object.getPrototypeOf(constructorFunc.prototype).constructor and function Object() is returned and the recursion stops. Indeed the constructor is an Object.

However when running the code in Jest, the same thing happens and at the EventEmitter level, Object.getPrototypeOf(constructorFunc.prototype).constructor returns function Object() as well. However the if (baseConstructor !== Object) test passes and we continue recursing until we bottom out and get the error TypeError: Cannot read property 'constructor' of null.

Steps to Reproduce (for bugs)

run.ts

import { Container } from 'inversify';
import { EventEmitterChild, EventEmitterModule } from './event-emitter-inversify';
import { EventEmitterDependent } from './event-emitter-inversify-dependent';

const referenceContainer = new Container();
referenceContainer.load(EventEmitterModule);

const eventEmitterChild = referenceContainer.get<EventEmitterChild>('EventEmitterChild');
eventEmitterChild.greet();

const eventEmitterDependent = referenceContainer.get<EventEmitterDependent>('EventEmitterDependent');

eventEmitterDependent.run();

event-emitter-inversify.ts

import 'reflect-metadata';
import { EventEmitter } from 'events';
import { ContainerModule, decorate, injectable, interfaces } from 'inversify';
import { EventEmitterDependent } from './event-emitter-inversify-dependent';

decorate(injectable(), EventEmitter);

@injectable()
export class EventEmitterChild extends EventEmitter {
  constructor() {
    super();
  }

  greet() {
    console.log('hello world.');
  }
}

export const EventEmitterModule = new ContainerModule(
  (bind: interfaces.Bind) => {
    bind<EventEmitterChild>('EventEmitterChild').to(EventEmitterChild).inSingletonScope();
    bind<EventEmitterDependent>('EventEmitterDependent').to(EventEmitterDependent).inSingletonScope();
  }
);

event-emitter-inversify-depdendent.ts

import { inject, injectable } from 'inversify';
import { EventEmitterChild } from './event-emitter-inversify';

@injectable()
export class EventEmitterDependent {
  constructor(@inject('EventEmitterChild') private readonly _eventEmitterChild: EventEmitterChild) {
  }

  run() {
    this._eventEmitterChild.greet();
  }
}

tests/event-emitter-inversify.test.ts

import { Container } from 'inversify';
import { EventEmitterChild, EventEmitterModule } from '../event-emitter-inversify';

describe('EventEmitter test', () => {
  const container: Container = new Container();

  beforeAll(async () => {
    container.load(EventEmitterModule);
  });

  test('get EventEmitterChild from container', () => {
    const eventEmitterChild = container.get<EventEmitterChild>('EventEmitterChild');

    expect(eventEmitterChild).toBeDefined();
  });
});

Your Environment

Mac OSX 10.14 ts-node v6.1.1 node v10.0.0 typescript v2.9.1

Stack trace

    TypeError: Cannot read property 'constructor' of null

      at getClassPropsAsTargets (node_modules/inversify/lib/planning/reflection_utils.js:82:75)
      at getClassPropsAsTargets (node_modules/inversify/lib/planning/reflection_utils.js:84:27)
      at getClassPropsAsTargets (node_modules/inversify/lib/planning/reflection_utils.js:84:27)
      at getTargets (node_modules/inversify/lib/planning/reflection_utils.js:28:27)
      at Object.getDependencies (node_modules/inversify/lib/planning/reflection_utils.js:12:19)
      at node_modules/inversify/lib/planning/planner.js:106:51
          at Array.forEach (<anonymous>)
      at _createSubRequests (node_modules/inversify/lib/planning/planner.js:94:20)
      at Object.plan (node_modules/inversify/lib/planning/planner.js:136:9)
      at node_modules/inversify/lib/container/container.js:318:37

jrhite avatar Oct 19 '18 12:10 jrhite

I am also having the same issue with a class that extends EventEmitter

blocka avatar Nov 02 '18 14:11 blocka

As a workaround, I'm no longer extending EventEmitter, but including an event emitter as an instance property, and manually creating proxy methods to call that event emitter.

blocka avatar Nov 03 '18 17:11 blocka

Yeah, that seems to be the best workaround right now. Dislike changing design just to make tests work due to a bug, but it's effective. :wink:

jrhite avatar Nov 14 '18 04:11 jrhite

I am having the same problem in this environment:

  • Mac OSX 10.13.6
  • node 10.15.0
  • typescript 3.2.4
  • jest 23.6.0 and 24.0.0

^ do you have an idea when this may be fixed?

stephandrab avatar Feb 01 '19 08:02 stephandrab

The same issue:

  • Windows 10
  • TypeScript 3.3
  • jest 20.0.0

ghost avatar Feb 13 '19 00:02 ghost

Does feel a bit weird that have to decorate these 3rd party classes at all, no? Can't it just assume the defaults of whatever decorate(injectable(), ThirdPartyClass) does ? What's the reason for this?

sublimator avatar Sep 07 '19 05:09 sublimator

Any ideas why this happens? I'm running into the same issue, but instead of it occurring in tests it happens in a Nuxt.js app. Why is it that there are 2 versions of Object() that are incompatible?

EDIT: Never mind, this helped https://github.com/nuxt/nuxt.js/issues/6076

fabis94 avatar Jul 09 '21 12:07 fabis94

Is there any solution for this ? I am facing same issue in jest

akshay-rvce avatar Oct 13 '22 19:10 akshay-rvce

Doing this, at the top of a test file, "fixes" the problem:

const { EventEmitter } = require('events');
Object.getPrototypeOf(EventEmitter.prototype).constructor = Object;

But It feels a bit hacky 😅

Nikoms avatar Oct 24 '22 14:10 Nikoms

I think this could be fixed by the following change in reflection_utils:

baseConstructor !== Object.prototype.constructor

Jameskmonger avatar Oct 25 '22 12:10 Jameskmonger