Why Native Testing Library Exists

Brandon Carroll
7 min readApr 16, 2019

--

The elephant in the room

One of the questions I’ve fielded since open-sourcing Native Testing Library is why it exists. The concern is that there was already another testing solution in the React Native community, so this might create a difficult decision for the community when it comes to following good testing practices and choosing what to use.

The first thing I want to address is that I personally don’t see the two existing as a problem. The fact that they both exist makes a whopping two viable testing solutions for the React Native community, which is still very sparse. I also think options are generally a pro, not a con. Are Jest and Mocha a bad UX? React and Reach Router? React and Preact? Heck, Enzyme and Testing Library? No way! I think part of what strengthens an ecosystem is healthy mixture of competition, collaboration, and choice.

Another matter that I feel should be addressed is that I did not make an attempt to contribute to the existing solution. For the sake of brevity, I can’t dive too deep into that decision, but I’ll admit that could have handled it more tactfully. I had intended for it to be a tool used mainly by my team at Express, I needed to move/iterate quickly, and didn’t realize the attention it would get so quickly. That said, I have learned from the mistake, apologized for it privately, and have moved on. I don’t care to speak to that any further, so please be respectful and don’t open Github issues about it, tweet about it, or comment here about it.

The primary motivation

Now that those things are out of the way, I want to reiterate that my only success criteria when I started was making sure the dom-testing-library and react-testing-library test cases passed in my implementation. If I couldn’t do that, I was going to stop building it, and wouldn’t even recommend it for my team. There’s an important part of that goal that wasn’t addressed in my first article though. In order for that to happen, there needed to be a very high level of adhesion to the guiding principles.

…the guiding principles are why I felt I needed to start a separate project.

Folks: if you don’t get anything else from this article, I need you to get that the guiding principles are why I felt this warranted starting a separate project. I didn’t feel that any other solution for native adequately implemented the guiding principles, and others I talked to agreed. I believe that the spirit of testing-library is one that forces you to be considerate of all of your users and makes it almost impossible to test implementation details. It prevents bad testing practices and encourages thoughtfulness. These things are important. They make you and your team better developers and more considerate people. They’re not an afterthought, I believe they’re the core of what the ecosystem is all about!

The native implementation

I want to give you three examples of how I tried my absolute best to stick to the guiding principles in the native ecosystem. I have to do some comparison in order for this to work. Please know it isn’t meant to be malicious, it’s meant to demonstrate why I believed things needed to happen the way they did to adhere to the guiding principles. People have asked for a technical comparison, and I’m somewhat reluctantly obliging out of fear of the way it will sound. I’m also not claiming native-testing-library is perfect — it’s brand new and I’m sure there’s plenty we’ll need to iron out.

Example 1

Check out this quote, and try to really think about it:

The more your tests resemble the way your software is used, the more confidence they can give you.

To me, this means that more than anything, what’s important is that your tests resemble real life as much as possible. There are trade offs, there are compromises, but as close as possible in an emulated test environment is the key.

This would work in react-native-testing-library:

const { getByTestId } = render(
<Image onPress={() => this.doSomething()} />
);
fireEvent.press(getByTestId('image'));

This isn’t valid in react-native. You can’t onPress an Image, but the tests would pass. I built a custom event handling system in order to prevent unintended consequences like this one that could totally happen in day-to-day development.

A lot of teams would just say “yeah but this is okay because you wouldn’t make this mistake, it would throw an error in the app”—but that’s not the point! The point is your tests should be giving you as much confidence as possible without you having to scavenger hunt for unintended errors after a refactor.

This test would fail in native-testing-library, because it won’t find a valid target for a press event. This makes it resemble your software much more closely, therefore making it align more closely to the guiding principles.

Example 2

Let’s look at an example of querying props in react-native-testing-library:

function MyComponent(props) {
return (<View {...props} />
}
test('it gets results', () => {
const { getAllByProps } = render(
<MyComponent accessibilityLabel="hello" />
);
const results = getAllByProps({ accessibilityLabel: 'hello' })
});

First issue: props are an implementation detail. What if you refactor MyComponent and change it to accept a11yLabel prop which then gets assigned to the View accessibilityLabel? Your users don’t care, and they never will! DOM Testing Library forces you to not write tests like this because they’re harmful to your application. That said, you probably haven’t even realize what the worst consequence of this snippet is. Take a look at this assertion:

expect(results).toHaveLength(1);

The biggest issue? This assertion will fail.

The reason is that both MyComponent and the View now have an accessibilityLabel prop, so the length is 2. But hang on, it’s actually even more sinister than that. The real answer? The length is 3. This is because React Native’s Jest mocks clone the View component along with all of its props and make a “string” version of it as a child of the View component you actually rendered. They do this to “stub out” the native components that communicate over the bridge to your device.

Pop quiz. Do you know why this wouldn’t happen in DOM Testing Library on the web? Because it never cares about your props! The only thing it cares about are the DOM attributes a user cares about. In Native Testing Library, we use this Jest mock system to our advantage because its basically pinpointing the equivalent of the “DOM”. The components they mock that have the string type are literally the elements that communicate with native APIs just like, you know, the DOM.

What we do in this case is filter query results so that the only thing you’ll get are the instances we specifically want you to get. Think of them as the native DOM nodes. The only result we’d return you? The “string” View component. Here’s what it would look like (we don’t have ByProps):

function MyComponent(props) {
return (<View {...props} />
}
test('it gets results', () => {
const { getAllByA11yLabel } = render(
<MyComponent accessibilityLabel="hello" />
);
const results = getAllByA11yLabel('hello')
});

The resulting assertion:

expect(results).toHaveLength(1);

This will pass, and the result will be the “View” that gets rendered natively. The “DOM”, if you will. This makes it align more closely to the guiding principles because it forces your tests to look for things your users look for, and the results your tests get are the results your users ultimately get.

Example 3

Whitespace is unpredictable sometimes, especially in JSX. Sometimes you’ll get line breaks, sometimes too many spaces, but really that’s not the concern of your tests. I want to give a silly example to show one last technical difference. Consider this example:

const { getByText } = render(<Text>   abc   def   </Text>);getByText('abc def');

I’ll admit this may be the most petty example of the 3, but anyone who’s worked with JSX knows whitespace can be janky sometimes. Line breaks can mess you up, extra spaces can sometimes mess you up, but you as a developer aren’t always thinking about it and browsers/native normalize it pretty well.

This will throw in react-native-testing-library, but you will get one result as expected in native-testing-library. I mention this because it’s still important. Your users do not care about a few extra whitespaces because almost always, they can be normalized by the platform. Also even if at one point it was “abc def” and then changed to a string that has a billion spaces, the test will pass. Once again, this is relevant to the guiding principles because what matters to your users is what matters to your tests; the end result, not the code.

Wrap it up, Brandon

Okay okay, this was longer than I intended. There are more technical differences, but I hope you see the point well enough.

If you read this, please know, I didn’t intend to offend or minimize anyone else’s work. In all 3 examples I listed, I hope I very clearly outlined how these differences are very relevant to the guiding principles that I set out to follow. I’ve always said that I believe the biggest difference in the two libraries is philosophical, not technological.

If you read this, and you’re not that familiar with the teachings of the testing-library family, please, please, please go check them out. You will become a better developer who is more cognizant of your users. Your tests will be better. Your team will be more confident. Your users will reap the benefits.

Testing Library isn’t just a library, it’s a way of thinking. Native Testing Library is just a library, but it’s intended to mimic that way of thinking.

I can’t be any more concise than that. “What’s the big difference between your library and X?” or “What about Y?” or “How about Z?”

The mindset and guiding principles.

If that still doesn’t make sense to you, then respectfully, you should learn them better, because you, those you work with, and your users, will be better off for it.

Thanks for reading, happy coding! 💯

--

--