jestjs / jest

Delightful JavaScript Testing.
https://jestjs.io
MIT License
44.22k stars 6.46k forks source link

Asserting against a custom error class using .toThrow and .toThrowError never work in Typescript #8279

Closed JasonShin closed 5 years ago

JasonShin commented 5 years ago

🐛 Bug Report

I'm reporting a potential bug with using .toThrow and .toThrowError against custom error classes that only happens in Typescript environment. Both assertions fail to validate custom error classes unlike in the Babel environment. Please see the #To Reproduce section for more detail.

To Reproduce

Steps to reproduce the behavior:

First, get a Jest environment ready, I've used

jest@20.0.4 & typescript@2.4.0 and jest@24.7.1 & typescript@3.4.1, and I was able to reproduce the same bug in both environments.

Typescript code:

class BaseError extends Error {
  constructor(message: string) {
    super();
    this.message = message;
    this.name = 'Error';
  }
}

class ValidationError extends BaseError {}
class StrangeError extends BaseError {}

describe('exceptions', () => {
  it('should throw ValidationError', () => {
    function errorProneFunc () {
      throw new ValidationError('zzz');
    }
    expect(errorProneFunc).toThrow(ValidationError); // fails
  });
});

I get below error messages

● exceptions › should throw ValidationError

    expect(function).toThrow(type)

    Expected the function to throw an error of type:
      "ValidationError"
    Instead, it threw:
      zzz      
          at ValidationError.BaseError [as constructor] (src/__tests__/HelloWorld.test.ts:3:5)
          at new ValidationError (src/__tests__/HelloWorld.test.ts:25:42)
          at errorProneFunc (src/__tests__/HelloWorld.test.ts:15:13)
          at Object.<anonymous> (src/__tests__/HelloWorld.test.ts:17:28)
              at new Promise (<anonymous>)

However, on a standard Babel setup with the exact same code (except for the type declarations), I cannot reproduce the bug at all. Please check out

https://repl.it/repls/FrankHarshVaporware

and run add_test.js

Expected behavior

I expect toThrow and toThrowError to work consistently in both Typescript and Babel environment.

Link to repl or repo (highly encouraged)

As a result, I cannot write good unit tests to assert based on Error type and have to assert based on error messages, which breaks all the time. Please check my project https://github.com/machinelearnjs/machinelearnjs/tree/master/test

Issues without a reproduction link are likely to stall.

Run npx envinfo --preset jest

System:
  Linux & Mac
Binaries:
    Node: 10.15.1 - /usr/local/bin/node
    Yarn: 1.13.0 - /usr/local/bin/yarn
    npm: 6.9.0 - /usr/local/bin/npm
npmPackages:
    jest: ^20.0.4 => 20.0.4 

Also I'm using jest@24.7.1 in the other repository.

JasonShin commented 5 years ago

As a workaround in Typescript, I have to declare the custom error classes like

export const ValidationError = function (message) {
  Error.captureStackTrace(this, this.constructor);
  this.name = this.constructor.name;
  this.message = message;
};

and assert like

 try {
      lr.fit('abc' as any, y1);
} catch (e) {
      expect(e).toBeInstanceOf(ValidationError);
}

Or if you are using promises

lr.fit(...).catch((e) => {
  expect(e).toBeInstanceOf(ValidationError);
});
azorng commented 5 years ago

Thanks for the workaround, I also made one for async/await

class CustomException {
    message: string

    constructor(message: string) {
        this.message = message
    }
}

class SomethingThatThrows {
    async throwIt() {
        throw new CustomException('hello')
    }
}

const thrower = new SomethingThatThrows()

// In your jest async test
await expect(thrower.throwIt()).rejects.toBeInstanceOf(CustomException)
SimenB commented 5 years ago

How do you transpile the TS? Would you mind putting together a repository (or code sandbox) showing the error you get?

GillesDebunne commented 5 years ago

Here is a codesandbox reproducing the problem.

https://codesandbox.io/embed/crazy-ptolemy-odtff

class MyError extends Error {}

it("should throw MyError", () => {
    function errorProneFunc() {
      throw new MyError("my error subclass");
    }
    expect(errorProneFunc).toThrow(MyError); // fails
});

I encountered this error when using ts-jest on a bare jest project.

ayliao commented 5 years ago

I think this may be a TypeScript limitation rather than a Jest issue. According to this changelog, extending built-ins in TypeScript comes with its caveats.

As a workaround, you can do what they suggest in the changelog and explicitly set the prototype of your custom error. So your example would be:

class MyError extends Error {
  constructor(m: string) {
    super(m);

    // Set the prototype explicitly.
    Object.setPrototypeOf(this, MyError.prototype);
  }
}

it("should throw MyError", () => {
  function errorProneFunc() {
    throw new MyError("my error subclass");
  }

  expect(errorProneFunc).toThrow(MyError); // passes, yay!
});
GillesDebunne commented 5 years ago

Thanks,

This is indeed a TS limitation as you found out. This issue can be closed.

clintharris commented 3 years ago

I have also noticed that the arg passed to the custom error constructor must be of type unknown.

class MyError extends Error {
  constructor(m: string) { // 👎
    super(m);
    Object.setPrototypeOf(this, MyError.prototype);
  }
}

it("should throw MyError", () => {
  function errorProneFunc() {
    throw new MyError("my error subclass");
  }
  expect(errorProneFunc).toThrow(MyError); // ❌ typescript error (see below)
});

This generates the following TypeScript error:

Argument of type 'typeof MyError' is not assignable to parameter of type 'string | RegExp | Constructable | Error'.
  Type 'typeof MyError' is not assignable to type 'Constructable'.
    Types of parameters 'm' and 'args' are incompatible.
      Type 'unknown' is not assignable to type 'string'.ts(2345)

Changing the constructor arg type to unknown fixes this:

class MyError extends Error {
  constructor(m: unknown) { // 👍
    super(m);
    Object.setPrototypeOf(this, MyError.prototype);
  }
}
github-actions[bot] commented 3 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.