How to Apply SOLID with Testing JS/TS Class Methods

amaster507_59

Anthony Master

Posted on January 13, 2024

How to Apply SOLID with Testing JS/TS Class Methods

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 principles.

// ...
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;
  };
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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 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.


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)
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

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 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:

  • Singled the responsibility of the method into an external function?
  • Converted methods into Open-closed functions?
  • Made it possible to later Liskov substitute class methods?
  • Applied Interface segregation?
  • And took another step in Dependency 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.

// types.ts
export type SomeMethod = (foos: IFoo) => Baz | undefined;
Enter fullscreen mode Exit fullscreen mode
// 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;
};
Enter fullscreen mode Exit fullscreen mode
// 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);
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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

// 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)
    }
  );
});

Enter fullscreen mode Exit fullscreen mode

We end up with:

  • A unit test that has the Single responsibility of testing a single function.
  • A class that is still Open for extension, while the function is closed for modification.
  • A function that can follow the Liskov 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 Interface.
  • Also the class is Dependent 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).

💖 💪 🙅 🚩
amaster507_59
Anthony Master

Posted on January 13, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related