Testing Expo apps with Jest in TypeScript
Tags: expo, reactnative, typescript, jest
Published: 2021-01-16 08:57:09

---
date: "2021-01-16T08:57:09+00:00"
title: "Testing Expo apps with Jest in TypeScript"
tags:
- expo
- reactnative
- typescript
- jest
published: true
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/v3npjs8wxnzlgpgodm8s.jpg
canonical_url: https://metareal.blog/en/post/2021/01/16/setup-jest-for-expo-typescript-project/
series: "Testing React Native Apps with Jest and Expo"
---
_[Cover image](https://unsplash.com/photos/-DnKOro7GaQ) by [Quinton Coetzee](https://unsplash.com/@quinietjie?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)_

Recently I've been working on a new project with [Expo](https://expo.io/) and [React Native](https://reactnative.dev/) 🙂

I want to test my Expo project with Jest. Basically, you can follow [Testing with Jest - Expo Documentation](https://docs.expo.io/guides/testing-with-jest/), but in the case of TypeScript, you have to change some settings in following points:

1. [Jest configuration for Expo project in TypeScript](#configuring-jest-and-add-a-simple-test)
2. [Writing Jest configuration file with TypeScript](#writing-jest-configuration-file-with-typescript)

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

```bash
$ expo --version                                              
4.0.17
$ npm list expo
my-expo-app
└── expo@40.0.0
```

## Configuring Jest and add a simple test

First of all, install the necessary packages; it is the Expo way to install [jest-expo](https://www.npmjs.com/package/jest-expo) instead of the jest package.

```bash
$ npm i jest-expo react-test-renderer --save-dev
```

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

```bash
$ npm i @types/jest @types/react-test-renderer --save-dev
```

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

```json
{
  ...
  "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/.*)"
    ]
  }
}
```

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`](https://jestjs.io/docs/en/configuration#transformignorepatterns-arraystring) 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`.

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

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](https://testing-library.com/docs/react-testing-library/ intro/) instead of using react-test-renderer directly.

## Running test and configuring Jest properly

Okay, let's run the test.

```bash
$ 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.
```

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.
```

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

```typescript
import App from "./App";
```

The error was happened because the `import` loads **`app.json` instead of `App.tsx`**. To prevent this, configure Jest's [`moduleFileExtensions`](https://jestjs.io/docs/ja/configuration#) to prioritize `.tsx` and `.ts` over `.json` when loading modules.

```json
"jest": {
  ...
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js",
    "jsx"
  ]
},
```

After making this change, the test will pass successfully.

```bash
$ 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
```

Googled and found that someone has the same issue [^2], so I posted a [PR](https://github.com/expo/expo/pull/11580) to the documentation.

## Writing Jest configuration file with TypeScript

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

```json
"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"
  ]
}
```

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](https://github.com/facebook/jest/blob/master/CHANGELOG.md#2660), **TypeScript configuration file is supported**. [ts-node](https://www.npmjs.com/package/ts-node) is required and must be installed.

```bash
$ npm i ts-node --save-dev
```

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

```bash
$ npm list jest           
my-expo-app
└─┬ jest-expo@40.0.1
  └── jest@25.5.4 
$ npm i jest --save-dev 
```

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

```typescript:jest.config.ts
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;
```

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](https://github.com/expo/expo/blob/9f1c0850d814e131cb565dd3d0fc9a91481e1e06/packages/jest-expo/jest-preset.js#L40) 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.

```bash
$ npm list jest        
my-expo-app
├── jest@26.6.3 
└─┬ jest-expo@40.0.1
  └── jest@25.5.4
```

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`.

```bash
$ 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
```

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

```bash
$ 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
```

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`.

```json
"scripts": {
  ...
  "test": "./node_modules/jest/bin/jest.js"
}
```

You can now run it with `npm test`.

```bash
$ 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.
```

[^1]: [Regular expression syntax cheatsheet - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet)
[^2]: [reactjs - Jest-Expo crashes on example (React.createElement: type is invalid -- expected a string) - Stack Overflow](https://stackoverflow.com/questions/65549722/jest-expo-crashes-on-example-react-createelement-type-is-invalid-expected-a)