Exploring the functionalities of the Container/ContainerModule in InversifyJs

Seeking advice on how to effectively test IoC wiring using InversifyJS with Typescript and Jest. Utilizing ContainerModules to divide my application, I construct the container by loading these modules into it.

Possible testing approaches:

  1. Create the production container, then verify dependencies are returned correctly using get/resolve for matching interfaces or instances.

  2. Inject a mock container to confirm correct calls are made for each dependency, potentially simplifying testing external dependencies.

Challenges include difficulties in mocking due to Inversify's fluent interface chaining, and finding a method to test the process of loading a container module without additional wrapping functions.

The current setup for my container module is as follows:

export const FooModule = new ContainerModule((bind) => {
    bind<Controller>(TYPES.Controller).to(ConcreteController).inSingletonScope().whenTargetNamed(NAMES.FooController)
    bind<Controller>(TYPES.Controller).to(ConcreteController).inSingletonScope().whenTargetNamed(NAMES.BarController)
})

Main concern lies in ensuring precise and accurate wiring, especially for singletons, to prevent unintentional changes that may go unnoticed. Incorporating comprehensive test coverage becomes crucial.

Testing may prove challenging due to constant/dynamic dependencies requiring mocks for construction, resulting in nested layers of IoC. Centralizing dependencies can aid in minimizing potential extensive modifications.

Answer №1

Through some experimentation, I was able to figure out a solution for a partial test case I had been working on.

I encapsulated the solution in a class and created a Jest custom matcher to ensure the expectations are smooth and aligned with regular Jest expectations.

This exploration offered insights into mocking fluent interfaces, a topic not widely discussed except for Builder patterns that always return the same object. Inversify differs slightly as each chained call narrows down the options to avoid duplication and conflicts.

The actual implementation of the solution is outlined below, but first, let's look at the limitations and an example:

Limitations

  • This is a partial implementation; the provided mock for bind is inadequate for many possible chained calls on bind.
  • It only functions with ContainerModule, which requires a list of functions to modify the container being loaded into. You can still use it with a Container by dividing your container into at least one ContainerModule and loading that. This offers a simple way to inject the mock into your tests. Organizing containers into modules seems like a practical approach with Inversify.
  • This does not cover implementations other than bind, merely offering mocks for unbind, isBound, etc. Expanding this will be up to the user. If there are use cases warranting expansion, I may revisit and update here in the future. However, creating a full implementation might require a separate package. This goes beyond my immediate needs or capabilities currently.
  • Return values for dynamic inputs are not validated. A Jest Asymmetric Matcher could be employed if necessary for your scenario.
  • To support other registry callbacks, the expectation handling would need restructuring to accommodate them. Currently, it is simplistic since it only supports bind in a limited manner.

Usage & Example

A class called InversifyMock can be initialized in your tests to provide mocks for usage with the ContainerModule under testing while tracking their usage.

Upon loading the ContainerModule, you can verify if the correct chain of calls was made. The code handles multiple calls with the same serviceIdentifier (symbol, string, or class as per Inversify - the first argument to bind()), and the custom matcher looks for at least one match.

Note that using Jest Asymmetric Matchers when specifying call arguments can be helpful, especially with functions like toDynamicValue that expect a function. This enables validation that a function is used even if the return value may be incorrect.

Important: This solution avoids calling dynamic functions to prevent invoking external dependencies, which we aim to avoid dealing with. If needed, you could create your own Asymmetric Matcher specifically for this purpose.

Let's begin with an example of a ContainerModule:

export const MyContainerModule = new ContainerModule((bind) => {
    bind<MySimpleService>(TYPES.MySimpleService).to(MySimpleServiceImpl)
    bind<MySingletonService>(TYPES.MySingletonService).to(MySingletonServiceImpl)
    bind<MyDynamicService>(TYPES.MyDynamicService).toDynamicValue(() => new MyDynamicService()).whenTargetNamed("dynamo")
})

Now, onto some tests:

import { MyContainerModule } from 'my-container-module'

describe('MyContainerModule', () => {

  const inversifyMock = new InversifyMock()

  beforeEach(() => {
    MyContainerModule.registry(...inversifyMock.registryHandlers)
  })

  afterEach(() => {
    inversifyMock.clear()    // reset for next test
    jest.clearAllMocks()     // clear other mocks if present
  })

  it('should bind MySimpleService', () => {

    expect(inversifyMock).toHaveBeenBoundTo(TYPES.MySimpleService, [
      { to: [ MySimpleService ] }
    ])

  })

  it('should bind MySingletonService', () => {

    expect(inversifyMock).toHaveBeenBoundTo(TYPES.MySingletonService, [
      { to: [ MySingletonService ] },
      { inSingletonScope: [] },
    ])

  })

  it('should bind MyDynamicService', () => {

    expect(inversifyMock).toHaveBeenBoundTo(TYPES.MyDynamicService, [
      { toDynamicValue: [ expect.any(Function) ] },
      { whenTargetNamed: [ "dynamo" ] },
    ])

  })
})

Clearly, with toHaveBeenBoundTo, we pass the serviceIdentifier as the first argument, followed by an array of objects where each represents a call in the chain. It's essential to maintain the order of calls accurate. For each chained call, we receive the name of the chained function and its arguments within an array. Note how an Asymmetric Matcher is utilized in the toDynamicValue instance just to ensure that a function is received as the dynamic value.

It's plausible that all the calls could be unified within the same object, considering Inversify possibly doesn't support multiple calls to the same chained function. However, further investigation is required in this area. This method seemed reliable albeit a bit lengthy.

The Solution

import { MatcherFunction } from "expect"
import { interfaces } from "inversify"

export interface ExpectedCalls {
    type: interfaces.ServiceIdentifier<any>
    calls: Record<string, any[]>[]
}

type InversifyRegistryHandlers = [ interfaces.Bind, interfaces.Unbind, interfaces.IsBound, interfaces.Rebind, interfaces.UnbindAsync, interfaces.Container['onActivation'], interfaces.Container['onDeactivation'] ]

/**
 * Interface for mocking Inversify ContainerModules
 */
export class InversifyMock {
    private bindCalls = new Map<interfaces.ServiceIdentifier<any>, Record<string, any>[][]>()
    private bind: jest.Mock = jest.fn(this.handleBind.bind(this))
    private unbind: jest.Mock = jest.fn()
    private isBound: jest.Mock = jest.fn()
    private rebind: jest.Mock = jest.fn()
    private unbindAsync: jest.Mock = jest.fn()
    private onActivation: jest.Mock = jest.fn()
    private onDeactivation: jest.Mock = jest.fn()

    get registryHandlers(): InversifyRegistryHandlers {
        return [ this.bind, this.unbind, this.isBound, this.rebind, this.unbindAsync, this.onActivation, this.onDeactivation ]
    }

    expect(expected: ExpectedCalls): void {
        const actual = this.bindCalls.get(expected.type)
        expect(actual).toContainEqual(expected.calls)
    }

    clear(): void {
        this.bindCalls.clear()
        this.bind.mockClear()
…

(expect continues...)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Encountering an issue when utilizing inversifyJS inject with the error message "Reading '_userService' cannot be done as it is undefined."

I'm currently working on implementing a DI system, but it seems like I may be missing some key concepts of Inversify. Do I need to create a "get" method for the "user.controller" and then "bind" it to the routes function? Link to complete code reposi ...

Checking if a route path is present in an array using Typescript and React

Here is a sample of my array data (I have simplified it here, but there are approximately 100 elements with about 20 values each): 0: odata.type: "SP.Data.ProductListItem" Title: "This is Product 1" Id: 1 1: odata.type: "SP.Data.ProductListItem" Title: ...

Error: The StsConfigLoader provider is not found! MSAL angular

I am currently using Auth0 to manage users in my Angular application, but I want to switch to Azure Identity by utilizing @azure/msal-angular. To make this change, I removed the AuthModule from my app.module and replaced it with MsalModule. However, I enco ...

What is the reason behind TypeScript's lack of reporting an incorrect function return type?

It's surprising to see that TypeScript 4.4.3 does not identify an invalid type for the callback function. It makes sense when the function returns a "non-promise" type, but when it returns a Promise, one would expect TypeScript to recognize that we ne ...

Approach to streamlining and making services more flexible

Currently, I am immersed in a complex Angular 2 endeavor that requires fetching various types of objects from a noSQL database. These objects are represented using straightforward model classes, and I have set up services to retrieve the data from the DB a ...

Guide to implement editable columns in Angular 4 with a click functionality

I have a table displaying records using ngFor, and I am looking to enable editing of a column upon clicking it. <tr *ngFor="let cd of descriptionCodes; let i = index"> <td><input type="checkbox"></td> <td> {{cd.code}} ...

An easy guide to using validators to update the border color of form control names in Angular

I'm working on a form control and attempting to change the color when the field is invalid. I've experimented with various methods, but haven't had success so far. Here's what I've tried: <input formControlName="pe ...

Creating an object efficiently by defining a pattern

As a newcomer to Typescript (and Javascript), I've been experimenting with classes. My goal is to create an object that can be filled with similar entries while maintaining type safety in a concise manner. Here is the code snippet I came up with: le ...

Aligning the React Navigation header component's bottom shadow/border width with the bottom tabs border-top width

Currently, I am working on achieving a uniform width for the top border of the React Navigation bottom tabs to match that of the top header. Despite my efforts, I am unable to achieve the exact width and I am uncertain whether it is due to the width or sha ...

Unable to locate the namespace for the installed library

Looking for a solution in my ExpressJS setup with Pino logger. I am trying to create a class that can be initialized with a Pino logger. Here is the code snippet: import express, { NextFunction, Request, Response } from 'express'; import pino fr ...

Converting lengthy timestamp for year extraction in TypeScript

I am facing a challenge with extracting the year from a date of birth value stored as a long in an object retrieved from the backend. I am using Angular 4 (TypeScript) for the frontend and I would like to convert this long value into a Date object in order ...

Utilizing TypeScript with Msal-React for Prop Type Validation

I'm currently implementing authentication in my app using msal-React. Within my app, I have a component that utilizes msalcontext and is wrapped by withMsal: function App(props: any) { return ( <> <AuthenticatedTemplate> ...

The Battle of Identifiers: Named Functions against Anonymous Functions in TypeScript

When it comes to performance and performance alone, which option is superior? 1) function GameLoop() { // Performing complex calculations requestAnimationFrame(GameLoop); } requestAnimationFrame(GameLoop); 2) function GameLoop() { // ...

I was able to use the formGroup without any issues before, but now I'm encountering an error

<div class="col-md-4"> <form [formGroup]="uploadForm" (ngSubmit)="onSubmit(uploadForm.organization)"> <fieldset class="form-group"> <label class="control-label" for="email"> <h6 class="text-s ...

Observe the task while returning - Firebase Functions

I am working on a Firebase Cloud Functions project where I need to run a scraping task within one of the functions. While the scraping is in progress, I also need to provide progress updates to the consumer. For example, informing them when the task is at ...

Is it possible to dynamically incorporate methods into a class while specifying their types?

Can the def method be enhanced for type safety without needing to manually define interfaces? // Can we add a proper `Cat.meow(message: string)` declaration on `Cat`? const Cat = function() {} def(Cat, { meow: function(message: string) { console.log(&apo ...

Buffer Overflow - Security Audit - Node JS TypeScript Microservice Vulnerability Scan Report

Person Data Schema: import JoiBase from '@hapi/joi'; import JoiDate from '@hapi/joi-date'; const Joi = JoiBase.extend(JoiDate); const personDataSchema = Joi.object().keys({ person: Joi.object().keys({ personId: Joi.string().max( ...

Generate a versatile Union type featuring a mapped property

I am currently working with different types of data enum DataTypes { Email = 'email', Checkbox = 'checkbox', } type DataTypeValues = { [DataTypes.Email]: string; [DataTypes.Checkbox]: boolean; }; type Type1<T extends DataTy ...

What is the method for implementing a custom layout for the items within a Select component?

I want to customize the options of a Select component by adding HTML elements. Here is an example: <mat-select [(ngModel)]="items"> <mat-option *ngFor="let item of ($items | async)" [value]="item.id"> <span>{{item.name}}</span&g ...

Implementing Batch File Uploads using Typescript

Is there a way to upload multiple files in TypeScript without using React or Angular, but by utilizing an interface and getter and setter in a class? So far I have this for single file upload: <input name="myfile" type="file" multi ...