typescriptdependency-injectionmockingioc-containerdependency-inversion

Do we need IoC containers in typescript if ts-mock-imports exists


So as kind of a foreword I am coming from the world of compiled languages like C# and C++. There when importing types defined in other namespaces or even different assemblies with using or #include statements in C# and C++ respectively we are basically creating a hard dependency. For this reason depending on interfaces and IoC containers are very valuable to make code more testable and maintainable.

I am now developing my first project in typescript. It is an express backend. Based on my experience with other languages I used before I was immediately scared of extensive use of imports thinking they are also hard dependencies and therefor making my code not testable. So I started designing a code architecture that would allow for easily swapping the dependencies of my modules by importing only types/interfaces and resolving them with tsyringe(I used tsyringe but the question is agnostic of the actual container used I think). This resulted in much boilerplate code but effectively allowed for easy mocking of my internal dependencies.

I now stumbled upon this NDC talk by Rob Richardson about mocking in typescript tests. He presented two typescript libraries ts-auto-mock and ts-mock-imports. ts-auto-mock is nothing really special. Actual mocking of an interface is not the problem. Making the code undertest using the mock seemed tricky. With ts-mock-imports we can make any imports made by different modules use our mock instead. This seems like everything one would need to test any code. So now I am starting to question if IoC containers are really even useful in typescript with tools like ts-mock-imports.

Now there is still the issue of code maintainability where an IoC container may still help but I am not actually sure this is the case with typescript. In case of my project I used the IoC container primarily for making my code testable.

Are there use cases in typescript where an IoC container can make life easier if it is not needed for making the code testable? Is dependency injection even needed if we can mock the imports at will?


Solution

  • ts-mock-imports actually relies on JavaScript test library Sinon.JS:

    ts-mock-imports is built on top of sinon.

    In Node.js and browser, all testing libraries provide stubbing feature to mock imported modules. They essentially swap the objects (modules) properties/methods, thanks to the dynamic nature of objects in JS. Easy for classes, but for variables and functions exported at the root of a module, it should not be feasible as per ES6 import spec (modules should be readonly); however most bundling tools are loose about this point, so stubbing root variables and functions is usually still possible.

    When using such build tools, most also offer path alias options, which can be used to retarget the module specifiers per build configuration.

    It is now also possible to directly hook into Node require mechanism.

    It might be different in the case of Deno, though.


    As for Dependency Injection itself, it is still interesting in JavaScript/TypeScript at the framework level: this enables instantiating services with different configurations, possibly simultaneously.

    While you can implement your own mechanism, you may be interested in frameworks that already provide this feature, e.g. NestJS for web backend. It is inspired by Angular (frontend), where each Component receives its requested Services, which are defined at Module level.