How to Apply SOLID with Testing JS/TS Class Methods
Tags: jest, testing, solidprinciples, typescript
Published: 2024-01-13 07:55:08

Are your class test files growing out of control? Are you struggling with writing tests for class methods because there are too many variables to control? I was in the same situation recently and found a way out using [SOLID](https://stackoverflow.blog/2021/11/01/why-solid-principles-are-still-the-foundation-for-modern-software-architecture/) principles.

```ts
// ...
export const MyClass implements IMyClass {
  constructor(
    @multiInject(IDENTIFIERS.FOO)
    protected foos: IFoo[]
    @inject(IDENTIFIERS.BAR) protected bar: IBar
  ) {}
  someMethod = (): Baz | undefined => {
    const baz: Baz | undefined = undefined;
    for (const foo of this.foos) {
      if ("baz" in foo) {
        baz = foo.baz();
      }
    }
    return baz;
  };
  // ...
}
```

Take a class for which we are tasked to write a unit test. This class may have a dozen methods and a dozen more attributes. In my environment we were already using [inversify](https://inversify.io/) to dependency inject into this class, and using container snapshot and restore as setup and teardown operators, in our jest test file. But it began getting out of control even after refactoring into test cases and test runners.

```ts

container.bind<IBar>(IDENTIFIERS.BAR).to(BarMock);
container.bind<IMyClass>(IDENTIFIER.MY_CLASS).to(MyClass);

beforeEach(() => {
  container.snapshot();
})
afterEach(() => {
  container.restore();
});

const someMethodTests: {
  description: string;
  foo: IFoo[];
  expected: Baz | undefined
}[] = [
  {
    description: 'without foo',
    foo: [],
    expected: undefined,
  },
  // ...
];

describe('MyClass', () => {
  // ...
  describe('someMethod', () => {
    it.each(someMethodTests)('$description returns: $expected', ({ foos, expected }) => {
      foos.forEach(foo => container.bind(IDENTIFIERS.FOO).to(foo));
      const myClass = container.get(IDENTIFIER.MY_CLASS);
      expect(myClass.someMethod()).toEqual(expected)
    });
  });
});

```

Our unit test files became more complex than the actual classes we were testing. The comment was even made that we need tests for our test files. As funny as that was, it was partially correct because of the complexity of the unit test files.

> we need tests for our test files

While researching to find a better method to jest test JavaScript classes, I came upon [How to test classes with jest](https://dev.to/dstrekelj/how-to-test-classes-with-jest-jif) which provided insight into using spies with `jest.spyOn` methods. While this did provide a little bit of help, it just added again even more complexity, and as we were already using Inversify dependency injection spies offered little to no help.

While learning from Uncle Bob's design principles, I feel like I have strengthened myself in writing quality function tests. I wasn't however applying the SOLID principle one layer deeper into our class methods. What would it look like if we:

- **S**ingled the responsibility of the method into an external function?
- Converted methods into **O**pen-closed functions?
- Made it possible to later **L**iskov substitute class methods?
- Applied **I**nterface segregation?
- And took another step in **D**ependency inversion?

I took a step back and looked at another project I had been working on that also used classes and jest. I had written a parsing method that became so convoluted I had little choice but to separate it into an external function. My unit tests then covered this function separately from the rest of the class. That was it! I could separate class methods into external functions and thereby easily do unit testing on the external functions irrelevant to the class as a whole.

```ts
// types.ts
export type SomeMethod = (foos: IFoo) => Baz | undefined;
```
```ts
// someMethod.ts
import { SomeMethod } from './types';

export const someMethod: SomeMethod = (foos) => {
  const baz: Baz | undefined = undefined;
  for (const foo of foos) {
    if ("baz" in foo) {
      baz = foo.baz();
    }
  }
  return baz;
};
```
```ts
// MyClass.ts
import { SomeMethod } from './types';
import { someMethod } from './someMethod';

export const MyClass implements IMyClass {
  constructor(
    @multiInject(IDENTIFIERS.FOO)
    protected foos: IFoo[]
    @inject(IDENTIFIERS.BAR) protected bar: IBar
  ) {}
  someMethod: (() => SomeMethod) = () => someMethod(this.foos);
  // ...
}
```

The unit tests for the method then become much easier to write as well.

```ts
// someMethod.spec.ts
import { someMethod } from './someMethod';

const someMethodTests: {
  description: string;
  foo: IFoo[];
  expected: Baz | undefined
}[] = [
  {
    description: 'without foo',
    foo: [],
    expected: undefined,
  },
  // ...
];

describe('someMethod', () => {
  it.each(someMethodTests)(
    '$description returns: $expected',
    ({ foos, expected }) => {
      expect(someMethod(foos)).toEqual(expected)
    }
  );
});

```

We end up with:

- A unit test that has the **S**ingle responsibility of testing a single function.
- A class that is still **O**pen for extension, while the function is _closed_ for modification.
- A function that can follow the **L**iskov substitution principle by using an argument referencing the property of the class without knowing the class itself.
- Another implementation of the class does not have to use the same function if not needed, and can still make their implementation of the **I**nterface.
- Also the class is **D**ependent upon the type of the function and _not_ the inline function itself. This allows for easier replacements when a better function may be found later that follows the same type or similar abstraction of such.

I would love to hear your feedback on this SOLID principle application and learn how you might better jest test classes in your JavaScript/TypeScript application. (If you have feedback on my horrible example code, yes I know... I wrote almost all of it without the help of an IDE to cross-check/correct my work).