A System For Automatically Adding TestIds To React Code
Tags: react, automation, testing, ui
Published: 2023-10-11 00:17:03

While at [OfferUp](offerup.com) I had to create automation using [Detox](https://wix.github.io/Detox/) and [Appium](https://appium.io/docs/en/2.1/) for a React native app. To ensure we could reliably find UI elements, it was decided to use the `data-testid` HTML attribute.

React has its own specific one for the purpose of identifying elements during automation named `testID` that gets rendered in HTML `data-testid`. It does not interfere with any other React functionality. TestIDs should have unique values to avoid fragility and simplify test code.

## The Problem And The Solution

Usually testIDs are coded in by developers when adding markup in their React code. However, this is unreliable since people forget to do it, assign too many or too few, can create duplicate values, and use inconsistent naming schemes. This makes it difficult to write tests when you are not sure what to look for or if it exists at all. Adding/fixing testIDs while writing tests is inefficient and painful, and leads to a giant mess.

It would be nice if there was a system in place to automatically assign testIDs in the markup of the React components. Since a React app is typically a hierarchy of custom components, it should be possible to assigned unique values to each component, concatenate these as child components are added, then use the final value as the `testID` of a React tag.

This solution would also fix another wrinkle. Sometimes Detox/Appium cannot access the `data-testid` attribute for whatever reason. In this case, the `accessibilityLabel` attribute can be used instead. In order for this to work, we need to defined it as well in the product code with the same value as the React `testID`.

## Implementation

TestIDs are generally constructed by defining a top level base string at a root component, passing it to children, then having them append to it. At the lowest level, where the actual React tags are used, the value is assigned to the `testID` and `accessibilityLabel` attributes. 

### Example overview

For this article we will lay out things like this for a React native app:

![Diagram of the example component layout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2kyl5l6ooqf0belc9fb0.png)

There are our own components which can contain other custom components or React ones. There is a component library (CL) at the low level which define our custom version of UI primitives (ie: buttons, text, etc...). These can use one or more React components.

Assigning our testIDs is mostly done at the CL level, but can also be done at higher levels where `View` or `Touchable` are used. Constructing the IDs, however, happens throughout the hierarchy.

### Top level components

At the top we have a screen container. The relevant code should look similar to this:

```react
export const MyScreen: FC = () => {
  const topLevelTestID = 'my-screen'}

  // A CL component, so add a descriptor to the id
  <MyCLButton baseTestID={topLevelTestID + '.do-not-press'}>Do not press</MyCLButton> 

  // An intermediate component, so just pass the base and it will append more specific info to the id
  <ChildThing baseTestID={topLevelTestID} />  
}
```

For ease of use, we define a top level testID base string, `topLevelTestID`, which will have additional id segments appended to it. This modified string is passed to lower components via their `baseTestID` attributes. 

> The name "baseTestID" was used to indicate that the id is not final and to not be confused with React's `testID`. This attribute is enforced on children via an interface.

If the child component is a CL one, a suffix based on the type, such as ".button" or ".text" will be appended inside the CL class. Therefore, all we need to provide here is a more specific descriptor for the component. The code above will end up assigning `my-screen.do-not-press.button` to the CL button.

If the component is an intermediate one, we just pass the top level id as a base. This component will then append its own specific identifying info and repeat the process.

### Intermediate level components

There are a few things we need for the `ChildThing` component. The first is an interface to define its properties:

```react
interface ChildThingProps extends BaseTestable {
}
```

We want it to have the `baseTestId` attribute and the best way to that is with an interface that can be applied to all of our child components:

```react
interface BaseTestable {
  baseTestId: string
}
```

Using this interface consistently helps enforce the testIDs and also labels components that use this system.

Now our component itself:

```react
export const ChildThing: FC<ChildThingProps> = ({ baseTestID }) => {

  // Assign a default baseID in case one was not passed in
  const localTestID = (baseTestID ?? 'default') + '.child-thing'

  // A React component so we need to assign the testID and accessibilityLabels directly
  <View testID={localTestID} accessibilityLabel={localTestID}>  
    <ChildThing baseTestID={localTestID} /> 
    <MyContainer testID={localTestID + '.actions'}>
      <MyCLButton baseTestID={localTestID + '.actions.donuts'}>Donuts, please</MyCLButton> 
    </MyContainer>
  </View>   
}
```

The component first gets the passed in base id and appends a string that describes the component. This is stored in a `localTestID` variable, which serves as the new base. We also take care to assign a default value in case a `baseTestID` was not passed in.  

There is a React component is here, and we need to assign the `testID` and `accessibilityLabel` attributes directly. For most other components, this happens in the CL class which wraps React primitives.

You can also see how there can be components which have a `testID` attribute instead of a `baseTestID` one. In this case, the component just uses the passed id as is. The attribute name “testID” is used to connote that it is a final value and will not be appended to. This is mostly used for container components, since we mostly don’t care what type of container it is, so we don’t need to specify any additional information.

### Low level components

Finally we have the lowest level components: `MyCLButton` and `MyContainer`. `MyCLButton` is part of our component library, which are leaf nodes in our UI tree. `MyContainer` is not a leaf node, but is considered a primitive. 

`MyCLButton` will use the `baseTestId` property since it will append the control type to it:

```react
interface MyCLButtonProps extends BaseTestable {
  buttonText: string,
}
```

Also like the mid level components it will append information to the testID (here, the control type), but then assign that directly to the `testID` and `accessibilityLabel` attributes on the React tags used:

```react
export const MyCLButton: FC<MyCLButtonProps> = ({ buttonText, baseTestID }) => {

  const localTestID = (baseTestID ?? 'cl') + '.button' 

    // React components so we need to assign the testID and accessibilityLabels directly
    <Touchable testID={localTestID} accessibilityLabel={localTestID}>  
      <Text testID={localTestID + '.text'} accessibilityLabel={localTestID + '.text'}>{buttonText}</Text>  
    </Touchable>   
}
```

`MyContainer` is very similar, but uses `testID` instead of `baseTestID` as its property. This is enforced by having `MyContainerProps` extend `FinalTestable` instead of `BaseTestable`:

```react
interface MyContainerProps extends FinalTestable {}

export const MyContainer: FC<FinalTestable> = ({ children, testID }) => {  

    // A React component so we need to assign the testID and accessibilityLabels directly
    <View testID={testID} accessibilityLabel={testID}>  
      {children}
    </View>    
}
```

Like the View in the previous section,`MyContainer` is a React tag and needs a final testID. The child controls will already have their testID information, so they just get passed along.

> It is helpful to print out the final testID value in the CL components right before using the React tags. This helps the development of UI tests since you know exactly what is on a screen and what the names are. 

> A lot of info can be dumped, but you can always send it through a custom logger with options to clean it up.

## Conclusion

Here is a diagram of the testID values as they exist in our UI hierarchy:

![Tree diagram of final testID values from the example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tu6ir8by9lcuc6lfhkbb.png)

You can see how a top level base testID is defined and passed down to children, which append their own information and pass that on. Containers do not append their own information since it add noise to its children. Finally, leaf components append their own details about the control type and assign the values directly to the React tag's `testID` and `accessibilityLabel`.

This system works pretty well, but can be tricky to use with components like navigation bars and modal dialogs because of their complexity. However, it is not too difficult to implement, creates unique, descriptive names, and reduces the burden on everyone to generate and keep track of testIDs.