How to Properly Mock Typed Variables in Unit Tests with TypeScript

zhiyueyi

Zhiyue Yi

Posted on August 16, 2020

How to Properly Mock Typed Variables in Unit Tests with TypeScript

Visit my Blog for the original post: How to Properly Mock Typed Variables in Unit Tests with TypeScript

The Problem I Faced

When we are doing unit tests, most of the time, we have to create some variables with existing types and some mock values to test our functions. However, in TypeScript, you might have a headache creating the variable, if the mocked variable has many properties and you only want to mock some of the property values for specific tests.

Let's illustrate this problem with an example. (Jump to the bottom if you want to check the solution directly).

If you have the Order type has the following structure:

interface Order {
  id: number;
  buyer: string;
  items: {
    id: number;
    description: string;
    unit: number;
    unitPrice: number;
    discount: number;
  }[];
  promoCode: string;
  totalPrice: number;
  buyerMessage: string;
  remarks: string;
  createdAt: Date;
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

And you want to test function called calculateTotalPrice(order: Order): boolean, which takes an order object with Order type as an input, calculate total price for all the items, compare the result with the totalPrice property, and lastly output the boolean of comparison result, which identifies if the calculation matches the existing total price record.

So, it's very simple to write a test for it:

describe('# calculateTotalPrice', () => {
  it('should return true if total price matches', () => {
    const mockOrder: Order = {
      // mock some values with total price matching with calculation from records
    }

    expect(calculateTotalPrice(mockOrder)).toBe(true);
  });

  it('should return false if total price does not match', () => {
    const mockOrder: Order = {
      // mock some values with total price NOT matching with calculation from records
    }

    expect(calculateTotalPrice(mockOrder)).toBe(false);
  });
});
Enter fullscreen mode Exit fullscreen mode

Now the problem is, I ONLY need items and totalPrice properties for the test, but TypeScript linter yells to me for a complete data structure like this, how should I do?

The Not-so-good Ways to Solve It

Well, we can directly jump to the correct answer for this problem, because it's simple to adopt. But, I think it's more educational if we can learn from the bad cases and understand why they are bad.

Basically, there are 3 not-so-good ways to solve it, which I've tried them all for the past a few years. They are ranked by how bad it is in descending order.

#1: Make them optional, why bother? [BAD]

When I was an absolute beginner to TypeScript, I tend to use this simple and brutal method to solve this issue, marking all the properties optional like this:

interface Order {
  id?: number;
  buyer?: string;
  items?: {
    id?: number;
    description?: string;
    unit?: number;
    unitPrice?: number;
    discount?: number;
  }[];
  promoCode?: string;
  totalPrice?: number;
  buyerMessage?: string;
  remarks?: string;
  createdAt?: Date;
  updatedAt?: Date;
}
Enter fullscreen mode Exit fullscreen mode

This is definitely a BIG no no!

That's because the properties are not really optional! They are only optional when you are doing tests. While in the actual code base, these properties can be mandatory. Marking them optional makes you convenient, but the consequence can be disastrous. In this way, you lose the capability to do null / undefined check for the properties, which results your code to likely have careless mistakes not being able to uncover!

In a word, never give in with your actual codes just because you want to have some conveniences in testing, or whatever other aspects!

#2: ANY way, we did it [Not-so-good]

The second way, is just turning TypeScript to "AnyScript" by adding any to the mocked variable, so that the type check can be ignored and less annoying.

it('should return true if total price matches', () => {
  const mockOrder: any = {
    items: [
      {
        unit: 2,
        unitPrice: 10,
        discount: 0,
      },
      {
        unit: 1,
        unitPrice: 5,
        discount: 1,
      }
    ],
    totalPrice: 24,
  };

  expect(calculateTotalPrice(mockOrder)).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

Well, it actually works, at least in our case. However, the order type is lost. It is not convenient especially when you want to have the intellisense feature when you are constructing the mockOrder variable, as the text editor (mine is VS Code) won't be able to intelligently notify you what properties an Order object should have, which can be less convenient and annoying most of the time.

#3: I say it is AS it is! [Looks probably ok]

The third way, it is one step smarter than using any, which is to use as keyword to force adding the Order type at the variable declaration stage.

it('should return true if total price matches', () => {
  const mockOrder = {
    items: [
      {
        unit: 2,
        unitPrice: 10,
        discount: 0,
      },
      {
        unit: 1,
        unitPrice: 5,
        discount: 1,
      }
    ],
    totalPrice: 24,
  } as Order;

  expect(calculateTotalPrice(mockOrder)).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

It actually works very well, and I really thought it was the ultimate answer to this problem because It has types and does not affect actual code base.

But I still feel something is missing here.

Hey, what if I put the property which doesn't belong to the Order object? For instance:

const mockOrder = {
  iShouldNotBeHereButHereIam: true,
  items: [
    {
      unit: 2,
      unitPrice: 10,
      discount: 0,
    },
    {
      unit: 1,
      unitPrice: 5,
      discount: 1,
    }
  ],
  totalPrice: 24,
} as Order;
Enter fullscreen mode Exit fullscreen mode

In this case, you can't really lint the "illegal" property inside of the object, because you have forced the object to be an Order object, which is obviously not so right here.

The Good Way to Solve It: Use the Native Type Partial in TypeScript!

So here comes the best approach to this problem, which I just discovered it today.

It is to use the native type Partial in TypeScript.

According to TypeScript: Handbook - Utility, Partial constructs a type with all properties of Type set to optional. This utility will return a type that represents all subsets of a given type.

However, we cannot solely use partial here, because our object is nested.

// Lint error occurs:
// Type '{ unit: number; unitPrice: number; discount: number; }' is missing the following properties from type 'Item': id, description ts(2739)
const mockOrder: Partial<Order> = {
  items: [
    {
      unit: 2,
      unitPrice: 10,
      discount: 0,
    },
    {
      unit: 1,
      unitPrice: 5,
      discount: 1,
    }
  ],
  totalPrice: 24,
};
Enter fullscreen mode Exit fullscreen mode

It's because the Partial only applies to the first level (totalPrice level), but not to the second level (unitPrice level). Hence, we can consider to add a custom RecursivePartial type to a global declaration file like src/declarations.d.ts. (Solution referred to use Partial in nested property with typescript - Stack Overflow)

type RecursivePartial<T> = {
  [P in keyof T]?: RecursivePartial<T[P]>;
};
Enter fullscreen mode Exit fullscreen mode

Then you can complete the test like:

describe("# calculateTotalPrice", () => {
  it("should return true if total price matches", () => {
    const mockOrder: RecursivePartial<Order> = {
      items: [
        {
          unit: 2,
          unitPrice: 10,
          discount: 0,
        },
        {
          unit: 1,
          unitPrice: 5,
          discount: 1,
        },
      ],
      totalPrice: 24,
    };

    // Now we can safely transform the type to Order here
    expect(calculateTotalPrice(mockOrder as Order)).toBe(true);
  });
});
Enter fullscreen mode Exit fullscreen mode

This method is good in our circumstances because your object is typed all the time (both in creation and in use) and actual code based does not get affected!

GitHub Demo is here ZhiyueYi/demo-in-blog

Cheers!

Featured image is credited to Chokniti Khongchum from Pexels

💖 đŸ’Ș 🙅 đŸš©
zhiyueyi
Zhiyue Yi

Posted on August 16, 2020

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

Sign up to receive the latest update from our blog.

Related