Setup Jest
For unit testing, we will be using Jest and react-native-testing-library.
Install Dependencies
% yarn add -D jest-expo ts-jest@^26 @types/jest react-test-renderer @types/react-test-renderer \ @testing-library/react-native @testing-library/jest-native jest-coverage-badges
The version of ts-jest has to match the version of jest included with jest-expo. You can check this with npm list jest.
Create Jest Setup
This file will be used to setup some global imports that we want for all our tests. In this case, we will load our environmental variables.
// jest.setup.js require('dotenv').config({ path: `envs/.env.${process.env.NODE_ENV}` });
Create Test Environment
Jest setsÂ
NODE_ENV
 to test
 so we will create a test environment file. You can just duplicate .env.development
 and change NAME
 to test
. Don't forget to add this file to your .gitignore
.// envs/.env.test NAME=test ...
// .gitignore ... envs/.env.test
Update Jest Config
This file should already exist and contain our module mappings.
// jest.config.js module.exports = { testEnvironment: 'jsdom', preset: 'jest-expo', setupFiles: ['<rootDir>/jest.setup.js'], setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], globals: { 'ts-jest': { tsconfig: { jsx: 'react', }, }, }, transform: { '^.+\\.js$': '<rootDir>/node_modules/react-native/jest/preprocessor.js', '^.+\\.tsx?$': 'ts-jest', }, testMatch: ['**/?(*.)+(spec|test).ts?(x)'], collectCoverageFrom: [ '**/*.{ts,tsx}', '!**/coverage/**', '!**/node_modules/**', '!**/babel.config.js', '!**/jest.setup.js', ], moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], // 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. 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/.*)', ], coverageReporters: ['json-summary', 'text', 'lcov'], moduleNameMapper: { '^@/api/(.*)': '<rootDir>/src/api/$1', '^@/assets/(.*)': '<rootDir>/src/assets/$1', '^@/components/(.*)': '<rootDir>/src/components/$1', '^@/locales/(.*)': '<rootDir>/locales/$1', '^@/navigators/(.*)': '<rootDir>/src/navigators/$1', '^@/screens/(.*)': '<rootDir>/src/screens/$1', '^@/services/(.*)': '<rootDir>/src/services/$1', '^@/styles/(.*)': '<rootDir>/src/styles/$1', '^@/utilities/(.*)': '<rootDir>/src/utilities/$1', }, };
Add Scripts
// package.json { ... "scripts": { ... "test": "jest", "test:dev": "jest --watch --changedSince=origin/master", "test:debug": "jest -o --watch", "test:updateSnapshots": "jest -u", "test:ci": "jest --coverage", "test:badges": "npm run test:ci && jest-coverage-badges --input coverage/coverage-summary.json --output __badges__" }, ... }
Update .gitignore
// .gitignore ... # testing coverage/
First Tests
Before we write our tests, we have to do two things. First, delete the tests inÂ
backend/test/
 since we don't want to deal with testing the CDK code right now. Second, rename src/utilities/test.ts
 to src/utilities/testUtility.ts
 and fix the import in App.tsx
. The old filename would be picked up as a test.For our first tests we will make sureÂ
App.tsx
 matches a snapshot and also check to make sure that process.env.NODE_ENV
 and process.env.NAME
 successfully load and are displayed. In order to do this, we have to also mock the imports that we don't care about right now.// src/utilities/App.test.tsx import React from 'react'; import { render } from '@testing-library/react-native'; import App from './App'; jest.mock('aws-amplify'); jest.mock('@/services/sentry'); jest.mock('@/api/amplify'); jest.mock('@/services/location'); jest.mock('@/services/analytics'); jest.mock('@/utilities/testUtility'); describe('<App />', () => { it('renders correctly', () => { const tree = render(<App />).toJSON(); expect(tree).toMatchSnapshot(); }); it('displays process.env.NODE_ENV', () => { const { getByText } = render(<App />); const text = getByText(new RegExp('^process\.env\.NODE_ENV: [a-zA-z0-9]+')); expect(text).not.toBeNull(); }); it('displays process.env.NAME', () => { const { getByText } = render(<App />); const text = getByText(new RegExp('^process\.env\.NAME: [a-zA-z0-9]+')); expect(text).not.toBeNull(); }); });
You can put your tests in a __tests__ directory (or elsewhere) if you want but I prefer to co-locate my tests with the file being tested.
% npm run test
If the output is correct but your snapshot is failing because it's matching against an old, incorrect snapshot, run npm run test:updateSnapshots to update it.
We can also runÂ
npm run test:badges
 to run our tests, generate a coverage report, and create those cool badges people show in their repo README.Setup Storybook
I ran into issues setting up Storybook. I will update this section when I get it working.
Setup Detox
Detox is even more painful to get setup with Expo. I managed to get it working by making an EAS build for iOS that doesn't have the dev client. This isn't a great solution though so I will keep investigating and update this section when I make progress.
Setup Cypress
We will use Cypress to do end to end testing on the web app.
Install Dependencies
% yarn add -D start-server-and-test cypress
Add Scripts
// package.json { ... "scripts": { ... "cypress": "cypress open", "cypress:headless": "cypress run", "e2e": "start-server-and-test start http://localhost:3000 cypress", "e2e:headless": "start-server-and-test start http://localhost:3000 cypress:headless", } ... }
Configure Cypress
// cypress.json { "baseUrl": "http://localhost:19006/" }
% yarn cypress
// cypress/tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "es5", "dom" ], "types": [ "cypress" ] }, "include": [ "**/*.ts", "**/*.js" ] }
First E2E Test
Cypress is a super powerful testing framework but we will just keep our first test super simple and check that the app loads and we can see the textÂ
process.env.NODE_ENV
./// <reference types="cypress" /> describe('App', () => { it('loads', () => { cy.visit('/') cy.contains('process.env.NODE_ENV') .should('be.visible') }); });
We can test it in the terminal usingÂ
yarn cypress:headless
.Setup Linting & Formatting
Install ESLint
ESLint and it's numerous plugins will handle our linting.
% yarn add -D eslint
Install Plugins
Airbnb Config
This installs common ESLint defaults
% npx install-peerdeps --dev eslint-config-airbnb
JSON
% yarn add -D eslint-plugin-json
TypeScript
% yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-import-resolver-typescript
Babel
% yarn add -D babel-eslint
Prettier
For formatting we will be using Prettier and integrating it into ESLint.
% yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
Jest
% yarn add -D eslint-plugin-jest
Testing Library
% yarn add -D eslint-plugin-testing-library
Cypress
% yarn add -D eslint-plugin-cypress
Detox
% yarn add -D eslint-plugin-detox
Configure ESLint
This config is something you will update as you go along. Feel free to re-enable rules, disable others, etc.
// .eslintrc.js module.exports = { root: true, parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true, }, }, settings: { react: { version: 'detect', }, 'import/resolver': { typescript: {}, }, }, env: { browser: true, es6: true, node: true, 'jest/globals': true, }, plugins: [ '@typescript-eslint', 'react', 'react-hooks', 'testing-library', 'cypress', 'detox', 'import', 'prettier', 'jest', 'detox', ], extends: [ 'airbnb', 'airbnb/hooks', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript', 'plugin:json/recommended', 'plugin:jsx-a11y/recommended', 'plugin:react-hooks/recommended', 'plugin:testing-library/react', 'plugin:cypress/recommended', 'plugin:jest/recommended', 'plugin:jest/style', 'plugin:prettier/recommended', ], overrides: [ { files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], extends: ['plugin:testing-library/react'], rules: { 'testing-library/prefer-screen-queries': 0, }, }, ], rules: { 'prettier/prettier': [ 'error', { singleQuote: true, trailingComma: 'all', arrowParens: 'avoid', endOfLine: 'auto', printWidth: 120, }, { usePrettierrc: false, }, ], 'react/react-in-jsx-scope': 'off', 'jsx-a11y/anchor-is-valid': [ 'error', { components: ['Link'], specialLink: ['hrefLeft', 'hrefRight'], aspects: ['invalidHref', 'preferButton'], }, ], 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], 'react/jsx-one-expression-per-line': 0, 'react/jsx-props-no-spreading': 0, 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 0, '@typescript-eslint/ban-types': 0, '@typescript-eslint/no-unused-vars': 0, 'no-console': 0, '@typescript-eslint/ban-ts-comment': 0, 'import/no-extraneous-dependencies': 0, 'import/extensions': 0, 'lines-between-class-members': 0, '@typescript-eslint/no-empty-function': 0, '@typescript-eslint/no-var-requires': 0, 'import/prefer-default-export': 0, 'class-methods-use-this': 0, 'testing-library/no-debug': 0, 'no-case-declarations': 0, 'no-new': 0, }, };
Setup Commitlint
Commitlint ensures that our commits follow the Conventional Commit specification.
% yarn add -D @commitlint/{cli,config-conventional}
// .commitlintrc.js module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'header-max-length': [0, "always", 72], 'body-max-line-length': [0, "always", 100], 'footer-max-line-length': [0, 'always', 100] } };
Ensure Committed Code is Linted & Formatted
Husky & lint-staged work together to check that our files are properly linted and formatted before committing.
Install Husky & Lint-Staged
% npx mrm@2 lint-staged
Configure Lint-Staged
On commit, The Husky pre-commit hook will triggerÂ
lint-staged
. Lint-staged will run eslint --fix
 against all staged files that match the given glob pattern. If there are no errors, the files are staged and the commit goes through.// .lintstagedrc.js module.exports = { "*.{js,jsx,ts,tsx,json,css,scss,md,mdx}": ["eslint --fix"], };
Install CommitLint Husky Hook
This hook will callÂ
commitlint
 to check that the commit message is properly formatted before allowing the commit to go through.% npx husky add .husky/commit-msg 'npx --no-install commitlint --edit'
Test Linting & Formatting
Let's add a script to runÂ
eslint
// package.json { "scripts": { ... "lint": "eslint . --fix" } }
Originally we had ~300 problems but thankfully ESLint fixed most of them. We still have 31 to fix (the warnings are optional). You will probably have less because I am going to put the updated ESLint config above but I'll leave it up to you to fix the rest or disable some of the rules if you think they are too strict.
Fix TypeScript Config
You will probably see some TypeScript errors now. Some may be from poor typing in dependencies and can be ts-ignored but the Jest and Cypress ones can be fixed with TypeScript configs.
// tsconfig.json { "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "baseUrl": "./", "paths": { "@/api/*": ["src/api/*"], "@/assets/*": ["src/assets/*"], "@/components/*": ["src/components/*"], "@/locales/*": ["locales/*"], "@/navigators/*": ["src/navigators/*"], "@/screens/*": ["src/screens/*"], "@/services/*": ["src/services/*"], "@/styles/*": ["src/styles/*"], "@/utilities/*": ["src/utilities/*"], } }, "exclude": [ "**/*.test.js", "**/*.test.ts", "**/*.test.tsx", "cypress/*" ] }
// tsconfig.test.json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": ["jest"], "module": "commonjs", "emitDecoratorMetadata": true, "allowJs": true }, "include": [ "**/*.test.js", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.js", "**/*.spec.ts", "**/*.spec.tsx", ], "exclude": [ "cypress/*" ] }
Test Commit Checks
Okay, now that we have fixed all the ESLint and TypeScript problems, we should be good to commit our changes.
% git add -A % git commit -m "feat: setup linting & formatting" âś” Preparing... âś” Running tasks... âś” Applying modifications... âś” Cleaning up... [linting-formatting-and-testing f49a574] feat: setup linting & formatting
Â