Dependency sandboxing in node.js with Jpex
Tags: typescript, node, di, news
Published: 2021-07-06 21:56:20

## React recap
Okay so I've written about [jpex](https://www.npmjs.com/package/jpex) a few times, particularly [in relation to react](https://dev.to/jackmellis/dependency-injection-in-react-with-jpex-178l)


Essentially it allows you to do something like this:

```tsx
import { useResolve } from 'react-jpex';

const useMyDep = () => {
  const dep = useResolve<IDep>();
  return dep.someValue;
};
```
and this:
```tsx
import { encase } from 'react-jpex';

const useMyDep = encase((dep: IDep) => () => {
  return dep.someValue;
})
```
depending on your preferred flavour.

Jpex uses the service locator pattern to resolve and inject dependencies, plus it's super-powered by Typescript inference for a super slick experience. But the really _really_ cool thing about it is you can test your react components with the provider component to stub your dependencies:

```tsx
<Provider
  onMount={(jpex) => {
    jpex.constant<IDep>(mockValue);
    // everything rendered inside this provider will use the mock value
    // everything outside of the provider will use the "real" value
  }}
>
  <ComponentUnderTest>
</Provider>
```

## Using jpex with node.js
However, we're talking about node.js right now, not react. How does jpex work with node? Well at first glance it's pretty similar to the front end:

```ts
import jpex from 'jpex';

const getMyDep = () => {
  const dep = jpex.resolve<IDep>();
  return dep.someValue;
};
```
```ts
import jpex from 'jpex';

const getMyDep = jpex.encase((dep: IDep) => () => {
  return dep.someValue;
});
```

Easy right? The problem is that it's then quite hard to create a "sandboxed" environment. How do you call these functions with mocked values?

### Option 1: mocking at the test level
```ts
it('returns some value', () => {
  jpex.constant<IDep>(mockValue);

  const result = getMyDep();

  expect(result).toBe(mockValue.someValue);
});
```
This method can be problematic because you're registering a test mock on the `global` instance. It will then be used as the resolved value for `IDep` everywhere in the file, unless you register it again in the next test. This sort of leaky test is a bad idea and will almost definitely cause bugs.

### Option 2: only using encase
```ts
it('returns some value', () => {
  const result = getMyDep.encased(mockValue)();

  expect(result).toBe(mockValue.someValue);
});
```
`encase` actually exposes the factory function so you can manually pass in your dependencies, which means you can test it safely like this. This works well for _some_ cases. But what if your function is called by another function?

```ts
const someOtherFn = () => {
  return getMyDep();
}
```
Now you cannot test `someOtherFn` without `getMyDep` attempting to resolve its dependencies!

### Option 3: the composite pattern
Another pattern for dependency injection is the composite pattern. Essentially your entire application is made up of factory functions that must compose at app start. In this case you'd be passing the `jpex` object through your composite chain like this:
```ts
export default (jpex) => {
  return {
    getMyDep: jpex.encase((dep: IDep) => () => dep.someValue),
  };
};
```
I'm not keen on this myself, it kinda defeats the point of a service locator!

So if you can't actually _invert the control_ of your dependencies, is jpex just useless in node applications? Yes... until now!

## A more robust solution to DI and testing
I have just published a new library: [`@jpex-js/node`](https://www.npmjs.com/package/@jpex-js/node). You use it like this:

```ts
import { resolve } from '@jpex-js/node';

const getMyDep = () => {
  const dep = resolve<IDep>();
  return dep.someValue;
};
```
```ts
import { encase } from '@jpex-js/node';

const getMyDep = encase((dep: IDep) => () => {
  return dep.someValue;
});
```

Looks familiar right? It's essentially the same syntax as `jpex` and `react-jpex` so far, and works exactly the same. The magic starts to happen when you want to sandbox and stub your dependencies...

The library exports a `provide` function. What this does is creates a new instance and then every `resolve` and `encase` call within is contextualised to this new instance. You can think of it as an equivalent to the `<Provider>` component in `react-jpex`.

If we attempt to write the same test as earier it might look like this:

```ts
import { provide } from '@jpex-js/node';

it('returns some value', () => {
  const result = provide((jpex) => {
    jpex.constant<IDep>(mockValue);

    return getMyDep();
  });

  expect(result).toBe(mockValue.someValue);
});
```

Regardless of whether this function used `resolve` or `encase`, we're able to control the dependencies it receives!

## One more thing
If the idea of a sandboxed DI context in which to run your tests seems cool, I should also point out that this supports asynchronous call stacks as well. Any promsies, callbacks, or timeouts, are kept within the same context:

```ts
provide(async (jpex) => {
  jpex.constant<IDep>(mockValue);

  await waitFor(200);

  setTimeout(() => {
    getMyDep(); // still retains the context
    done();
  }, 1000);
});
```

## Conclusion
As the author of jpex I am definitely biased but I am a big proponant of making dependency injection a core part of javascript development, but also a slick developer experience. I've been using jpex in react applications for a few years now and I love it. And now with this library, we should be able to bring the same patterns and ease of testing to node applications as well.

> Behind the scenes we're using node's new `async_hooks` module and the `AsyncLocalStorage` class. It's a really powerful concept for creating and managing contexts and there is so much potential for awesome uses of it!