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

[BUG] DITest cannot handle multi-instance injection #2709

Closed
supercoffee opened this issue May 17, 2024 · 2 comments
Closed

[BUG] DITest cannot handle multi-instance injection #2709

supercoffee opened this issue May 17, 2024 · 2 comments
Assignees

Comments

@supercoffee
Copy link
Contributor

Describe the bug

When using DiTest / PlatformTest, classes registered using a "type" instead of "provide" key in order to allow multi-instance injection. This functionality seems to work when running the actual server instance, but when writing tests using injection, these instances don't seem to be resolved.

Inside a test, I'm unable to create an instance of the ApiClient using DiTest.invoke or PlatformTest.invoke. Either one fails the same way, either some crypticTypeError ReflectMetadata error or the result being undefined.

Scenario

I have several instances of an API client that I want to inject into a service. I've decorated them all with a decorator like so

const ApiClient = Symbol("ApiClient");

interface ApiClient {}

@Injectable({
  type: ApiClient
})
export class MyApiClientA implements ApiClient {}

@Injectable({
  type: ApiClient
})
export class MyApiClientB implements ApiClient {}


@Injectable()
class MyService {

  constructor(@Inject(SomeInjectionToken) private clients: ApiClient[]) {}
}

To Reproduce

Run the following code snippet (using Jest framework).

import { DITest } from "@tsed/common";
import { Inject, Injectable } from "@tsed/di";

describe("FakeCrmClient", () => {
    beforeEach(async () => await DITest.create());
    afterEach(async () => await DITest.reset());

    it("Should return a client", async () => {
        const token = Symbol("token_provide");
        @Injectable({
            provide: token,
        })
        class FakeCrmClient {}

        const it = await DITest.invoke(token);
        expect(it).toBeInstanceOf(FakeCrmClient);
    });

    it("Should inject a client", async () => {
        const token = Symbol("token_type");

        interface Foo {}

        @Injectable({
            type: token,
        })
        class FakeCrmClient implements Foo {}

        @Injectable({
            type: token,
        })
        class FakeCrmClient2 implements Foo {}

        @Injectable()
        class FakeService {
            constructor(@Inject(token) public instances: Foo[]) {}
        }

        const service = await DITest.invoke(FakeService);
        expect(service).toBeInstanceOf(FakeService);
        expect(service.instances[0]).toBeInstanceOf(FakeCrmClient);
    });

    it("Should return a client", async () => {
        const token = Symbol("token_type");

        @Injectable({
            type: token,
        })
        class FakeCrmClient {}

        const it = await DITest.invoke<FakeCrmClient[]>(token);
        expect(it).toBeInstanceOf(FakeCrmClient);
    });
});

The last two test fail in a similar manner. Something to the effect of

Error: expect(received).toBeInstanceOf(expected)

Expected constructor: FakeCrmClient

Received value has no prototype
Received value: undefined

    at Object.<anonymous> (/Users/me/Development/myproject/api/src/packages/crm/clients/FakeCrmClient.spec.ts:41:38)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)

I have also seen the following error but I can't figure out exactly what circumstances produce this vs the previous error


TypeError    at Reflect.getMetadata (/Users/me/Development/myproject/api/node_modules/reflect-metadata/Reflect.js:354:23)
    at Function.getParamTypes (/Users/me/Development/myproject/api/node_modules/@tsed/core/src/domain/Metadata.ts:488:20)
    at InjectorService.mapInvokeOptions (/Users/me/Development/myproject/api/node_modules/@tsed/di/src/services/InjectorService.ts:704:31)
    at InjectorService.resolve (/Users/me/Development/myproject/api/node_modules/@tsed/di/src/services/InjectorService.ts:601:31)
    at InjectorService.invoke (/Users/me/Development/myproject/api/node_modules/@tsed/di/src/services/InjectorService.ts:202:23)
    at Function.invoke (/Users/me/Development/myproject/api/node_modules/@tsed/di/src/services/DITest.ts:99:46)
    at Object.<anonymous> (/Users/me/Development/myproject/api/src/packages/crm/clients/FakeCrmClient.spec.ts:52:33)
    at Promise.then.completed (/Users/me/Development/myproject/api/node_modules/jest-circus/build/utils.js:298:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (/Users/me/Development/myproject/api/node_modules/jest-circus/build/utils.js:231:10)
    at _callCircusTest (/Users/me/Development/myproject/api/node_modules/jest-circus/build/run.js:316:40)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async _runTest (/Users/me/Development/myproject/api/node_modules/jest-circus/build/run.js:252:3)
    at async _runTestsForDescribeBlock (/Users/me/Development/myproject/api/node_modules/jest-circus/build/run.js:126:9)
    at async _runTestsForDescribeBlock (/Users/me/Development/myproject/api/node_modules/jest-circus/build/run.js:121:9)
    at async run (/Users/me/Development/myproject/api/node_modules/jest-circus/build/run.js:71:3)
    at async runAndTransformResultsToJestFormat (/Users/me/Development/myproject/api/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
    at async jestAdapter (/Users/me/Development/myproject/api/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
    at async runTestInternal (/Users/me/Development/myproject/api/node_modules/jest-runner/build/runTest.js:367:16)
    at async runTest (/Users/me/Development/myproject/api/node_modules/jest-runner/build/runTest.js:444:34)

Expected behavior

DiTest.invoke / PlatformTest.invoke should produce an instance or instances of a provider registered using the type property.

Code snippets

No response

Repository URL example

No response

OS

macOS

Node version

Node 18.13.1

Library version

7.69.4

Additional context

No response

@Romakita
Copy link
Collaborator

Romakita commented May 21, 2024

Hello @supercoffee

this is because Symbol("token_type"); is not considered by the DI as a Token, so you cannot use this symbol with .invoke method. If you want to retrieve all providers related to a group type, you have to use the special method getMany():

const providers = DITest.injector.getMany(Symbol("token_type"))
// or  use the real token provider FakeCrmClient
const provider = DITest.invoke(FakeCrmClient, [])

See you

Copy link

🎉 Are you happy?

If you appreciated the support, know that it is free and is carried out on personal time ;)

A support, even a little bit makes a difference for me and continues to bring you answers!

github opencollective

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

2 participants