DEV Community

Cover image for Testing Expo apps with Jest in TypeScript
Takanori Ishikawa
Takanori Ishikawa

Posted on • Originally published at metareal.blog

Testing Expo apps with Jest in TypeScript

Cover image by Quinton Coetzee on Unsplash

Recently I've been working on a new project with Expo and React Native 🙂

I want to test my Expo project with Jest. Basically, you can follow Testing with Jest - Expo Documentation, but in the case of TypeScript, you have to change some settings in following points:

  1. Jest configuration for Expo project in TypeScript
  2. Writing Jest configuration file with TypeScript

First, let me show what the version. of Expo in my machine.

$ expo --version                                              
4.0.17
$ npm list expo
my-expo-app
└── expo@40.0.0
Enter fullscreen mode Exit fullscreen mode

Configuring Jest and add a simple test

First of all, install the necessary packages; it is the Expo way to install jest-expo instead of the jest package.

$ npm i jest-expo react-test-renderer --save-dev
Enter fullscreen mode Exit fullscreen mode

Since it is a TypeScript project, we should install .d.ts of the package too.

$ npm i @types/jest @types/react-test-renderer --save-dev
Enter fullscreen mode Exit fullscreen mode

Add the Jest configuration to package.json as mentioned in the documentation.

{
  ...
  "jest": {
    "preset": "jest-expo",
    "transformIgnorePatterns": [
      "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice transformIgnorePatterns contains a long regular expression 🤔

Jest doesn't transpile files under the node_modules directory by default, but there are some React Native libraries that are distributed without being transpiled. By setting transformIgnorePatterns as above, we can make them the target of the transpile.

  • x(?!y) Negative lookahead assertion: Matches "x" only if "x" is not followed by "y". Such long regular expressions should be commented like Perl, but unfortunately the /x modifier is not available in JavaScript. 1

Now, let's write a sample test App.test.tsx.

import React from "react";
import renderer from "react-test-renderer";

import App from "./App";

describe("<App />", () => {
  it("has 1 child", () => {
    const tree = renderer.create(<App />).toJSON();
    // @ts-ignore
    expect(tree.children.length).toBe(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

In fact, the above code can't be compiled by a type error, but here I've ignored it with @ts-ignore. This is because fixing the type error is out of the scope of this article, and in the future I will use the React Testing Library instead of using react-test-renderer directly.

Running test and configuring Jest properly

Okay, let's run the test.

$ npx jest
 FAIL  ./App.test.tsx
  <App />
    ✕ has 1 child (45ms)

  ● <App /> › has 1 child

    Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

      at createFiberFromTypeAndProps (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:16180:21)
      at createFiberFromElement (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:16208:15)
      at reconcileSingleElement (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5358:23)
      at reconcileChildFibers (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5418:35)
      at reconcileChildren (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7991:28)
      at updateHostRoot (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:8547:5)
      at beginWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:9994:14)
      at performUnitOfWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13800:12)
      at workLoopSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13728:5)
      at renderRootSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13691:7)

  console.error
    Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.

       6 | describe("<App />", () => {
       7 |   it("has 1 child", () => {
    >  8 |     const tree = renderer.create(<App />).toJSON();
         |                                  ^
       9 |     expect(tree.children.length).toBe(1);
      10 |   });
      11 | });

      at printWarning (node_modules/react/cjs/react.development.js:315:30)
      at error (node_modules/react/cjs/react.development.js:287:5)
      at Object.createElementWithValidation [as createElement] (node_modules/react/cjs/react.development.js:1788:7)
      at Object.<anonymous> (App.test.tsx:8:34)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.254s
Ran all test suites.
Enter fullscreen mode Exit fullscreen mode

It failed. The error is also mysterious.

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.
Enter fullscreen mode Exit fullscreen mode

Getting a warning that the App component is an object. When I run Jest, I get this error on the line below.

import App from "./App";
Enter fullscreen mode Exit fullscreen mode

The error was happened because the import loads app.json instead of App.tsx. To prevent this, configure Jest's moduleFileExtensions to prioritize .tsx and .ts over .json when loading modules.

"jest": {
  ...
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js",
    "jsx"
  ]
},
Enter fullscreen mode Exit fullscreen mode

After making this change, the test will pass successfully.

$ npx jest
 PASS  ./App.test.tsx
  <App />
    ✓ has 1 child (2341ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.855s
Enter fullscreen mode Exit fullscreen mode

Googled and found that someone has the same issue 2, so I posted a PR to the documentation.

Writing Jest configuration file with TypeScript

Okay, I was able to run the test, but the configuration became a bit complicated.

"jest": {
  "preset": "jest-expo",
  "transformIgnorePatterns": [
    "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
  ],
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js",
    "jsx"
  ]
}
Enter fullscreen mode Exit fullscreen mode

In particular, transformIgnorePatterns is complex, and I'd rather take it out than put it in package.json. And I want to write it with TypeScript if possible.

From Jest version 26.6.0, TypeScript configuration file is supported. ts-node is required and must be installed.

$ npm i ts-node --save-dev
Enter fullscreen mode Exit fullscreen mode

If the jest which jest-expo depends on is old, install it as well.

$ npm list jest           
my-expo-app
└─┬ jest-expo@40.0.1
  └── jest@25.5.4 
$ npm i jest --save-dev 
Enter fullscreen mode Exit fullscreen mode

Then create a new jest.config.ts and migrate the configuration in package.json

import { Config } from "@jest/types";

// By default, all files inside `node_modules` are not transformed. But some 3rd party
// modules are published as untranspiled, Jest will not understand the code in these modules.
// To overcome this, exclude these modules in the ignore pattern.
const untranspiledModulePatterns = [
  "(jest-)?react-native",
  "@react-native-community",
  "expo(nent)?",
  "@expo(nent)?/.*",
  "react-navigation",
  "@react-navigation/.*",
  "@unimodules/.*",
  "unimodules",
  "sentry-expo",
  "native-base",
  "react-native-svg",
];

const config: Config.InitialOptions = {
  preset: "jest-expo",
  transformIgnorePatterns: [
    `node_modules/(?!${untranspiledModulePatterns.join("|")})`,
  ],
  moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
};

export default config;
Enter fullscreen mode Exit fullscreen mode

Simplifing the lengthy regex expression made things look a lot better. Compared to package.json, which doesn't allow comments, it's nice to be able to write comments😅.

[2021.01.24] Actually, transformIgnorePatterns is included in the preset of jest-expo, so there is no need to specify them if you are fine with the default. However, since this list needs to be added depending on the library used, I personally use this method. The above example has been updated with the latest list.

One last point: if you re-installed jest instead of the version that the jest-expo depends on, you will have two versions of jest mixed under your node_modules.

Check it with the npm list command.

$ npm list jest        
my-expo-app
├── jest@26.6.3 
└─┬ jest-expo@40.0.1
  └── jest@25.5.4
Enter fullscreen mode Exit fullscreen mode

You can see that there are two versions of Jest. There is one problem with this: we don't know which version the npx command will execute.

The npx command executes commands placed under node_modules/.bin. These are symbolic links to files in each package, and they will be overwritten depending on the order of installation.

For example, ./node_modules/.bin/jest is now pointing to . /node_modules/.bin/jest/bin/jest.js.

$ ls -l ./node_modules/.bin/jest
lrwxr-xr-x  1 takanori_is  staff  19  1 16 16:31 ./node_modules/.bin/jest -> ../jest/bin/jest.js
Enter fullscreen mode Exit fullscreen mode

However, if you re-install jest-expo, ./node_modules/jest-expo/bin/jest.js will replace it.

$ ls -l ./node_modules/.bin/jest
lrwxr-xr-x  1 takanori_is  staff  24  1 16 16:34 ./node_modules/.bin/jest -> ../jest-expo/bin/jest.js
Enter fullscreen mode Exit fullscreen mode

So, until the version of jest on which jest-expo depends is upgraded, it is safe to run . /node_modules/jest/bin/jest.js directly instead of via npx command. Let's register it as a script in package.json.

"scripts": {
  ...
  "test": "./node_modules/jest/bin/jest.js"
}
Enter fullscreen mode Exit fullscreen mode

You can now run it with npm test.

$ npm test                  

> @ test /Users/ishikawasonkyou/Developer/Workspace/my-expo-app
> ./node_modules/jest/bin/jest.js

 PASS  ./App.test.tsx
  <App />
    ✓ has 1 child (150 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.767 s, estimated 1 s
Ran all test suites.
Enter fullscreen mode Exit fullscreen mode

  1. Regular expression syntax cheatsheet - JavaScript | MDN 

  2. reactjs - Jest-Expo crashes on example (React.createElement: type is invalid -- expected a string) - Stack Overflow 

Top comments (0)