Type-safe Jasmine Spies

TypeScript for all its glory is pretty handy when it comes to better Intellisense and type safety. Yet one of those situations that constantly drove me crazy was writing Jasmine tests. Specifically talking about Jasmine Spies.

The test candidate

So imagine we have this little app with a small Service fetching a server-side API and a MyComponent, consuming the services via constructor injection.

class Service {  
  public async getServerData() {
    const response = await fetch("http://my-endpoint.com/api/foo");
    return await response.json();
  }
}

class MyComponent {  
  constructor(private service: Service) {}

  public async doStuff() {
    const response = await this.service.getServerData();
    if (response.msg === "success") {
      return true;
    }
  }
}

Calling the method doStuff should fetch the data, check whether the property msg is success and return true in that case.

Creating the test

A simple describe and it should be enough to depict what we're trying to achieve

describe("My very special scenario", () => {  
  it("should support spies the nice type safe way", async () => {
    const mockedService: Service = jasmine
      .createSpyObj("Service", ["getServerData"]);

    mockedService.getServerData
      .and
      .returnValue(Promise.resolve({ msg: "success" }));

    const sut = new MyComponent(mockedService);

    expect(await sut.doStuff()).toBe(true);
  });
});

Running your test will now successfully pass the test but there is this one ugly squiggle line at the .and. which results in an ugly typescript error:

[ts] Property 'and' does not exist on type '() => Promise<any>'.

Thats understandable since our mocked service actually doesn't provide a the doStuff method as spy. And most of the Stackoverflow questions and blog posts so far recommend an ugly cast like this:

(mockedService.getServerData as jasmine.Spy)
      .and.returnValue(Promise.resolve({ msg: "success" }));

Meeeh if you ask me

The fix

Now what if I told you the solution would be just to initially cast the mockedService as a Spied<Service> vs Service:

it("should support spies the nice type safe way", async () => {  
    const mockedService: Spied<Service> = jasmine
      .createSpyObj("Service", ["getServerData"]);

    mockedService.getServerData
      .and
      .returnValue(Promise.resolve({ msg: "success" }));
    ...
  });

Reads much nicer doesn't it? Here's the code that makes that possible:

export type Spied<T> = {  
  [Method in keyof T]: jasmine.Spy;
};

The generic type Spied, a mapped type, takes any other type, and extends all of its methods using the x in keyof T syntax and mapping it to a jasmine.Spy. This means now every method not only acts as a function but also as an extended Spy object.

And the red squiggles are gone !!!

Conclusion

With this simple trick now you can save tons of strict casts throughout your test code and again enjoy the type-safe life, the easy way. Man all of this wouldn't even be needed in VanillaJS. Just sayin ... :)

photo credit: peterbwiberg: Spy camera via Pixabay (license)