/cavy

An integration test framework for React Native.

Primary LanguageJavaScriptMIT LicenseMIT

Cavy

We are in the process of consolidating all Cavy documentation on cavy.app. In the mean time you can use either the README or the site. Once we are finished, we'll tidy up the README. Thanks for your patience!

npm version CircleCI

Cavy logo

Cavy is a cross-platform integration test framework for React Native, by Pixie Labs. You can run tests in-app, or via Cavy's command line interface cavy-cli.

This README covers installing and setting up Cavy, writing tests and FAQs. For information on how to use Cavy's command line interface, check out the corresponding README.

Table of Contents

How does it work?

Cavy (ab)uses React ref generating functions to give you the ability to refer to, and simulate actions upon, deeply nested components within your application. Unlike a tool like enzyme which uses a simulated renderer, Cavy runs within your live application as it is running on a host device (e.g. your Android or iOS simulator).

CLI and continuous integration

When your app boots, Cavy will run your test suite and output the results to the console. Cavy will also check whether there is a cavy-cli server running and, if so, send a report of the test results.

You can also run Cavy tests directly using cavy-cli.

Further details on how you can use cavy-cli to fully automate your tests with continuous integration can be found in the cavy-cli README.

Where does it fit in?

We built Cavy because, at the time of writing, React Native had only a handful of testing approaches available:

  1. Unit testing components (Jest).
  2. Shallow-render testing components (enzyme).
  3. Testing within your native environment, using native JS hooks (Appium).
  4. Testing completely within your native environment (XCTest).

Cavy fits in between shallow-render testing and testing within your native environment.

Installation

To get started using Cavy, install it using yarn:

yarn add cavy --dev

or npm:

npm i --save-dev cavy

If you're using TypeScript, you'll also need to install the types package:

yarn add @types/cavy

Usage

Check out the sample app for example usage. Here it is running:

Sample app running

1. Set up the Tester

Import Tester, TestHookStore and your specs in your top-level JS file (typically this is your index.{ios,android}.js files). Instantiate a new TestHookStore and render your app inside a Tester.

// index.ios.js

import React, { Component } from 'react';
import { Tester, TestHookStore } from 'cavy';
import AppSpec from './specs/AppSpec';
import App from './app';

const testHookStore = new TestHookStore();

export default class AppWrapper extends Component {
  render() {
    return (
      <Tester specs={[AppSpec]} store={testHookStore}>
        <App />
      </Tester>
    );
  }
}

Tester props

Prop Type Description Default
specs (required) Array Your spec functions -
store (required) TestHookStore The newly instantiated TestHookStore component -
reporter Function Called once all tests have finished. Takes the test report as an argument. If undefined, Cavy will send a test report to cavy-cli if it is running. undefined
waitTime Integer Time in milliseconds that your tests should wait to find a component 2000
startDelay Integer Time in milliseconds before test execution begins 0
clearAsyncStorage Boolean If true, clears AsyncStorage between each test e.g. to remove a logged in user false

2. Hook up components

To add test hooks to components, first add a ref using the generateTestHook function then export a hooked version of the parent component.

If you need to test a function component, create a testable version of it using the wrap function. Then assign it a ref using generateTestHook (see example below).

generateTestHook takes a string as its first argument - this is the identifier used in tests. It takes an optional second argument in case you also want to set your own ref generating function.

// src/Scene.js

import React, { Component } from 'react';
import { View, TextInput } from 'react-native';
import { FunctionComponent } from 'some-ui-library';
import { hook, wrap } from 'cavy';

class Scene extends Component {
  render() {
    // If you need to test a function component, use `wrap` so that you can
    // assign it a ref.
    const TestableFunctionComponent = wrap(FunctionComponent);

    return (
      <View>
        <TextInput
          ref={this.props.generateTestHook('Scene.TextInput')}
          onChangeText={...}
        />
        <TestableFunctionComponent
          ref={this.props.generateTestHook('Scene.FunctionComponent')}
          otherProp={...}
        />
      </View>      
    );
  }
}

const TestableScene = hook(Scene);
export default TestableScene;

If your component is functional, you can call the custom React Hook useCavy() to obtain a generateTestHook function:

// src/components/MyComponent.js

import React, { Component } from 'react';
import { View, TextInput } from 'react-native';

import { useCavy } from 'cavy';

export default () => {
  const generateTestHook = useCavy();

  return (
    <View>
      <TextInput
        ref={generateTestHook('MyComponent.TextInput')}
        onChangeText={...}
      />
    </View>   
  )
};

3. Write test cases

Write your spec functions referencing your hooked-up components. See below for a list of currently available spec helper functions.

You can use spec.beforeEach to call a function before each test runs. The beforeEach function will be called after AsyncStorage is cleared but before the app re-renders and the test is run i.e. the order of actions for each test execution is:

  1. AsyncStorage is cleared (if the clearAsyncStorage prop is set to true in Tester)
  2. The beforeEach function is called (if defined for this test)
  3. The app is re-rendered
  4. The test is run

If you need to run shared code at the start of multiple tests after the app is re-rendered, create your own helper function to call from within your tests.

// specs/AppSpec.js

export default function(spec) {

  spec.beforeEach(function() {
    // This function will run before each test in this spec file.
  });

  spec.describe('My feature', function() {
    spec.it('works', async function() {
      await spec.fillIn('Scene.TextInput', 'some string')
      await spec.press('Scene.button');
      await spec.exists('NextScene');
    });
  });
}

4. Run tests

Congratulations! You are now all set up to start testing your app with Cavy.

Following the set up above, your tests will run automatically when you boot your app. However, if using cavy-cli, you can configure your app to only run tests when initiated through the command line. See the cavy-cli README for further instructions.

Apps that use native code

If you're not using Create React Native App, you'll need to register your AppWrapper as the main entry point with AppRegistry instead of your current App component:

AppRegistry.registerComponent('AppWrapper', () => AppWrapper);

Available spec helpers

Function Description
fillIn(identifier, str) Fills in the identified component with the string
Component must respond to onChangeText
press(identifier) Presses the identified component
Component must respond to onPress
pause(integer) Pauses the test for this length of time (milliseconds)
Useful if you need to allow time for a response to be received before progressing
exists(identifier) Returns true if the component can be identified (i.e. is currently on screen)
notExists(identifier) As above, but checks for the absence of the component
findComponent(identifier) Returns the identified component
Can be used if your component doesn't respond to either onChangeText or onPress
For example:
const picker = await spec.findComponent('Scene.modalPicker');
picker.open();

Writing your own spec helpers

Want to test something not included above? Write your own spec helper function!

Your function will need to be asynchronous and should throw an error in situations where you want the test to fail. For example, the following tests whether a <Text> component displays the correct text.

// specs/helpers.js

export async function containsText(component, text) {
  if (!component.props.children.includes(text)) {
    throw new Error(`Could not find text ${text}`);
  };
}
// specs/AppSpec.js

import { containsText } from './helpers';

export default function(spec) {
  spec.describe('Changing the text', function() {
    spec.it('works', async function() {
      await spec.press('Scene.button');
      const text = await spec.findComponent('Scene.text');
      await containsText(text, 'you pressed the button');
    });
  });
}

Writing your own reporter

Don't want to use cavy-cli to handle your tests results? Write your own reporter!

By default, Cavy will send a test report to cavy-cli if it detects it is running. However, passing your own custom reporter function as a prop into the <Tester> component overrides this functionality - Cavy will call your function with the report as an argument instead of sending the results to cavy-cli.

For an example of a custom test reporter, check out cavy-native-reporter, which reports test results to native Android or iOS test runners.

FAQs

How does Cavy compare to Appium? What is the benefit?

Cavy is a comparable tool to Appium. The key difference is that Appium uses native hooks to access components (accessibility IDs), wheras Cavy uses React Native refs. This means that Cavy sits directly within your React Native environment (working identically with both Android and iOS builds), making it easy to integrate into your application very quickly, without much overhead.

What does this allow me to do that Jest does not?

Jest is a useful tool for unit testing individual React Native components, whereas Cavy is an integration testing tool allowing you to run end-to-end user interface tests.

Contributing

Before contributing, please read the code of conduct.

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
  • Fork the project.
  • Start a feature/bugfix branch.
  • Commit and push until you are happy with your contribution.
  • Please try not to mess with the package.json, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so we can cherry-pick around it.