Mocking in TypeScript with `factoree`

schniz

Gal Schlezinger

Posted on April 6, 2021

Mocking in TypeScript with `factoree`

A couple of days ago I had a fantastic experience. Two ambitious developers asked me to review an open source project they are working on in a short video chat. I was flattered and happily accepted the offer.

We found ourselves talking about mocks in TypeScript. Since I started using TypeScript, I adopted a practice where I try to push as much as I can to the type system and use tools like io-ts to hook just enough runtime validations to ensure I can trust it.

A couple of months ago I needed to mock something in one of our tests. We have a pretty big config, generated from a confirmation system, and I needed to use a property from it in my test.

The first idea was to do something like setAppConfig({ myKey: value } as any). This worked well but it stinks from the any, which also have a very big drawback: what if the test is implicitly using a property I did not set?

Enter factoree. A simple factory generator function which will immediately throw an error when accessing a missing property. So, the previous example would be something like:

import { factory } from "factoree";

const createAppConfig = factory<AppConfig>();

setAppConfig(createAppConfig({ myKey: value }));
Enter fullscreen mode Exit fullscreen mode

Can you see that we no longer have as any? Instead of skipping the type system, we generate an object which will throw an error if we accessed a missing key. Instead of playing pretend — we specify rules for the computer to enforce at runtime:

import { factory } from "factoree";
const createAppConfig = factory<AppConfig>();

const config = createAppConfig({ myKey: "hello" });
config.myKey; // => 'hello'
config.otherKey; // => Error: Can't access key 'otherKey' in object { myKey: 'hello' }
Enter fullscreen mode Exit fullscreen mode

Why does it matter?

Imagine the following code under test:

export type User = {
  firstName: string;
  lastName: string;

  // more data
  website: string;
};

export function printName(user: User): string {
  return `${user.firstName} ${user.lastName}`;
}
Enter fullscreen mode Exit fullscreen mode

And when we test it, we can use as unknown as User to provide only the things that are in use in our function:

test(`prints the name`, () => {
  const userDetails = ({
    firstName: "Gal",
    lastName: "Schlezinger",
  } as unknown) as User;
  const result = printName(userDetails);
});
Enter fullscreen mode Exit fullscreen mode

Now, the product manager asked us to add another feature: allow a user’s name to be written in reverse. So, our code changes into:

export type User = {
  firstName: string;
  lastName: string;
  prefersReversedName: boolean;

  // more data
  website: string;
};

export function printName(user: User): string {
  if (user.prefersReversedName) {
    return `${user.lastName} ${user.firstName}`;
  }

  return `${user.firstName} ${user.lastName}`;
}
Enter fullscreen mode Exit fullscreen mode

After the code change, tests should still pass. This is a problem because we access a property (prefersReversedName) which should be a non-null boolean, but we don’t pass value into it, which is undefined. This creates a blind spot in our test. When using factoree, this would not happen: if you forget to define a property, factoree will throw an error — ensuring you can trust your types.


This tiny library helped us make more maintainable code with easier assertions and better tests. No more undefined errors in tests if types have changed. Let the computer do the thinking for us.

Try it, and let me know how that worked for you!

💖 💪 🙅 🚩
schniz
Gal Schlezinger

Posted on April 6, 2021

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

Sign up to receive the latest update from our blog.

Related