How to Properly Mock Typed Variables in Unit Tests with TypeScript
Tags: typescript, testing
Published: 2020-08-16 12:41:24

Visit my Blog for the original post: [How to Properly Mock Typed Variables in Unit Tests with TypeScript](https://yizhiyue.me/2020/08/16/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](#The-Good-Way-to-Solve-It-Use-the-Native-Type-Partial-in-TypeScript) if you want to check the solution directly).

If you have the `Order` type has the following structure: 

```typescript
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;
}
```

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:

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

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](#The-Good-Way-to-Solve-It-Use-the-Native-Type-Partial-in-TypeScript) 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:

```typescript
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;
}
```

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.

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

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.

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

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:

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

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](https://www.typescriptlang.org/docs/handbook/utility-types.html), `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.

```typescript
// 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,
};
```

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](https://stackoverflow.com/a/47914631))

```typescript
type RecursivePartial<T> = {
  [P in keyof T]?: RecursivePartial<T[P]>;
};
```

Then you can complete the test like:

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

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](https://github.com/ZhiyueYi/demo-in-blog/tree/master/mock-typed-variables)

Cheers!

_Featured image is credited to [Chokniti Khongchum from Pexels](https://www.pexels.com/photo/person-holding-laboratory-flask-2280571/)_