Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test invalid function arguments with TypeScript and JestJS

I've beend creating a node application with typescript and I'm using jest to write unit tests.

My issue: I can't really write unit tests simulating invalid function argument types, because typescript won't compile. I mean it's nice that typescript realises that I try to put wrong data in those functions, but the way I understood ts so far is, it only works during compile time. There won't be any checks of argument types during runtime.

So I still have to test the correct behaviour of my IO depending functions with jest, right?

I thought I'll just write a xyz.spec.js that and leave all the interfaces and types behind for that specific test. But then I receive an error from jest regarding the import of modules. I guess because it's not a ts file.

Do I have to change my jest or ts setup to make it work?

Here is a screenshot of the test and the error:

enter image description here

here my package.json:

{
  "name": "state",
  "version": "0.0.0",
  "files": [
    "build"
  ],
  "main": "build/index",
  "types": "build/index",
  "scripts": {
    "clean": "rimraf build && rimraf coverage",
    "format": "prettier --write \"{src,__tests__}/**/*.ts\" --single-quote --trailing-comma es5",
    "lint": "tslint --force --format verbose \"src/**/*.ts\"",
    "prepublishOnly": "npm run build",
    "start": "node ./build/index.js",
    "prebuild": "npm run clean && npm run format && npm run lint && echo Using TypeScript && tsc --version",
    "build": "tsc --pretty",
    "build:watch": "nodemon --legacy-watch src/index.ts",
    "test": "jest --no-cache",
    "test:watch": "jest --no-cache --watch",
    "coverage": "jest --no-cache --coverage"
  },
  "dependencies": {
    "mongoose": "^5.6.0"
  },
  "devDependencies": {
    "@types/jest": "^24.0.13",
    "@types/mongoose": "^5.5.6",
    "@types/node": "^10.14.7",
    "coveralls": "^3.0.2",
    "jest": "^24.8.0",
    "nodemon": "^1.19.0",
    "prettier": "^1.14.3",
    "rimraf": "^2.6.2",
    "ts-jest": "^24.0.2",
    "ts-node": "^8.1.0",
    "tslint": "^5.11.0",
    "tslint-config-prettier": "^1.15.0",
    "typescript": "^3.1.1"
  },
  "engines": {
    "node": ">=10.0.0"
  },
  "jest": {
    "preset": "ts-jest"
  }
}

and here my tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "target": "es2015",
    "outDir": "./<%= buildpath %>",
    "removeComments": true,
    "inlineSourceMap": true,
    "inlineSources": true,
    "preserveConstEnums": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

Best regards fea

like image 444
fea17e86 Avatar asked Jun 21 '19 14:06

fea17e86


People also ask

How do I test a TypeScript function?

For TypeScript, unit tests are run against the generated JavaScript code. In most TypeScript scenarios, you can debug a unit test by setting a breakpoint in TypeScript code, right-clicking a test in Test Explorer, and choosing Debug.

Can you write jest tests in TypeScript?

Via ts-jest ​ts-jest is a TypeScript preprocessor with source map support for Jest that lets you use Jest to test projects written in TypeScript.

Does TypeScript replace testing?

The type system cannot replace all testing, but there are certainly places where it can make testing superfluous. Increasingly I strive to make more and more of a codebase like that.


2 Answers

You can use as any in the argument of your test. That way you can add the "wrong" type and test your code with it. Assuming you want to test the someFunction() function which expects some sort of Prop object, for instance

interface Prop {
    id: number;
    type: string;
}

function someFunction(props: Prop): void {
    // ... do some stuff
}

Then your test could look something like this:

it('should not accept the wrong argument type', () => {
    const props = { id: 123 };
    const testSomeFunction = () => {
        someFunction(props as any);
    };
    expect(testSomeFunction).toThrow();
});

You could also have a look at Type Guards, to do runtime type checks: http://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types

function isValidProp(prop: Prop): prop is Prop {
    const keys = ['id', 'type'];
    return Object.keys(prop).every(key => keys.includes(key));
}

function someFunction(props: Prop): void {
    if(!isValidProp(prop)){
        throw new Error('invalid prop!');
    }
    // ... do some stuff
}
like image 143
magikMaker Avatar answered Oct 12 '22 08:10

magikMaker


This is what @ts-expect-error is for. It tells typescript to suppress the following error so that you can write tests that otherwise would not compile. Unlike @ts-ignore, it also generates an error if the following statement does not actually produce an error. Here is an example:

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

function acceptsNumber(x: number): void {
  if (typeof x !== 'number') {
    throw new NotANumberError(`expected number, received ${x}`);
  }
}

describe('acceptsNumber', () => {
  it('throws NotANumberError when called with string', () => {
    // @ts-expect-error test passing invalid string argument
    expect(() => acceptsNumber('hello')).toThrow(NotANumberError);
  });
});

The acceptsNumber('hello') would normally not compile, but that error gets suppressed by the @ts-expect-error comment above it, allowing us to run and test the code even when types mismatch.

like image 2
daz Avatar answered Oct 12 '22 09:10

daz