Skip to content
This repository has been archived by the owner on Jul 30, 2020. It is now read-only.

Can't query for or fire events on mocked components #17

Closed
OzzieOrca opened this issue May 15, 2019 · 5 comments
Closed

Can't query for or fire events on mocked components #17

OzzieOrca opened this issue May 15, 2019 · 5 comments

Comments

@OzzieOrca
Copy link

Relevant code or config:

Component:

export const ComponentUnderTest = () => (
  <View>
    <ButtonWrapper testID="my-button" onPress={buttonHandler}>
      <Text testID="main-content">Main Content</Text>
    </ButtonWrapper>
  </View>
);

Test:

jest.mock('./ButtonWrapper', () => ({ ButtonWrapper: 'ButtonWrapper' }));

it('should press button', () => {
  const { getByTestId } = render(<ComponentUnderTest />);
  // Throws error: Unable to find an element with the testID of: my-button
  fireEvent.press(getByTestId('my-button'));
  expect(buttonHandler).toHaveBeenCalled();
});

What you did:

Tried to use getByTestId to find the mocked component.

What happened:

Test output:
Screen Shot 2019-05-15 at 3 07 46 PM

Reproduction:

https://github.com/OzzieOrca/repro-native-testing-library-mock-wrapper

Problem description:

Our repo is filled with wrappers around native components and complicated child components. Being able to mock these makes the snapshot output much cleaner and prevents us from having to mock everything needed to render a child component. react-native-testing-library works fine for finding and firing events on these mocked components. native-testing-library doesn't seem to be able to query for these components.

Suggested solution:

  1. Fix the rendered output in the error message. In blue, it says the rendered output is <View /> (hopefully that's what it's meant to be) which doesn't match the snapshot or the output of debug(). I spent a while trying to figure out why my component wasn't rendering anything more than just the root element before realizing it actually was. This error is misleading. If you have a typo in the test id, it won't show you the rendered output with the correct ID and could lead you down a wild goose chase before finding the real issue.
  2. Support querying on mocked components. I believe this would also address Impossible to get FlatList element #12. Feel free to suggest other testing patterns but we've got a large repo of enzyme shallow tests that I'm trying to migrate to something that supports context and hooks and just looking for an easy path forward.

Thanks for your help with this! Just trying to figure out the best way to test modern React Native components and your library seems promising :)

@bcarroll22
Copy link
Collaborator

Hey thanks for the detailed report, and we're happy you checked the library out!

Just like dom-testing-library, this library won't accept events on implementations like your own components. In your repro, the real ButtonWrapper renders a Touchable* component, and that's where your app will accept a press event. The DOM wouldn't render ButtonWrapper or know anything about it, and that's what we emulate here. That's a HUGE departure from the Enzyme model where all of your components can be tested and asserted against.

Snapshots output in 2.0 isn't great, which is something we're addressing in the upcoming 3.0 release that should be landing any day now. I'm just putting some finishing touches on it and polishing up the docs to get it out the door. That said, the 3.0 release does dig in more on testing only the native components, not implementation components. I think it's much more aligned to the guiding principles and you'll love it once it's out ☺️

Sort of related, as a general rule of thumb, in the testing-library world, testIDs aren't a preferred way to search . Check out this page or this page for more info about that. Usually there's a better user-centric query that will help you isolate an event target.

All of that said, this is what I'd recommend for your tests be:

import React from 'react';
import { render, fireEvent } from 'native-testing-library';

import { buttonHandler } from './buttonHandler';
import { ComponentUnderTest } from './ComponentUnderTest';

it('should match snapshot', () => {
  const { container } = render(<ComponentUnderTest />);
  expect(container).toMatchSnapshot();
});

it('should press button', () => {
  const { getByText } = render(<ComponentUnderTest />);

  fireEvent.press(getByText('Main Content'));
  expect(buttonHandler).toHaveBeenCalled();
});

These pass, other than the fact buttonHandler just console logs, but it can't pass because it's not a jest.fn(). The reason I omitted the middle test of finding the text is that getBy* will throw an error if the element isn't found. So, you'd be asserting that the text content of the node is the text you just searched for. The third test case will cover your scenario because you can't click the button if you can't find the text ☺️ less work, more confidence -- it's a win win!

Good luck to you, and keep an eye out for 3.0!

@bcarroll22
Copy link
Collaborator

Responded on #12 as well now that 3.0 is released, and wanted to show you what things would be like using the latest. Given these tests:

import React from 'react';
import { render, fireEvent } from 'native-testing-library';

import { buttonHandler } from './buttonHandler';
import { ComponentUnderTest } from './ComponentUnderTest';

it('should match snapshot', () => {
  const { container } = render(<ComponentUnderTest />);
  expect(container.children[0]).toMatchSnapshot();
});

it('should press button', () => {
  const { getByText } = render(<ComponentUnderTest />);

  fireEvent.press(getByText('Main Content'));
  expect(buttonHandler).toHaveBeenCalled();
});

this is the snapshot output:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should match snapshot 1`] = `
<View>
  <TouchableHighlight
    activeOpacity={0.85}
    delayPressOut={100}
    underlayColor="black"
  >
    <Text
      testID="main-content"
    >
      Main Content
    </Text>
  </TouchableHighlight>
</View>
`;

Much more reasonable, huh? ☺️ hope this helps. One last thing, if you really need to mock the button for a shallow render as you transition over from Enzyme, try this (the event still won't work, you can't fire them on your components):

import React from 'react';
import { render, fireEvent } from 'native-testing-library';

import { buttonHandler } from './buttonHandler';
import { ComponentUnderTest } from './ComponentUnderTest';

jest.mock('./ButtonWrapper', () => ({
  ButtonWrapper: ({ children, ...props }) =>
    require('react').createElement('ButtonWrapper', props),
}));

it('should match snapshot', () => {
  const { container } = render(<ComponentUnderTest />);
  expect(container.children[0]).toMatchSnapshot();
});

The important thing is not passing children to your mock, otherwise it's just a regular deep render with a wasted mock. It's not really good for anything other than getting a shallow snapshot, honestly. Here's the output:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should match snapshot 1`] = `
<View>
  <ButtonWrapper
    testID="my-button"
  />
</View>
`;

Hope it helps, good luck on the transition from Enzyme with your team! ☺️

@OzzieOrca
Copy link
Author

OzzieOrca commented May 16, 2019

Thanks for the detailed responses 😃 I'll address your specific responses before getting back to the bigger question.

Ya we have a huge codebase of enzyme shallow tests... Diving through HOCs is annoying but maybe that'll get better with hooks (if enzyme ever supports them). Deep rendering and then mocking everything below the component under test seemed like a good way to approximate a shallow test. I'm just starting to slowly convert things to TypeScript, Hooks, GraphQL, and some testing library that supports those. Rewriting tests that have been focused on implementation detail is going to be an undertaking. I'm just trying to figure out how to best get started. It'll be a process over time. Hooks make it harder to write tests that depend on the component instance anyway which you can't get at with hooks and functional components.

Thanks for the work on improving the snapshots and the 3.0 release! I'll have to try it sometime soon.

Ya the testIDs here were just for the example. But I think I'm reaching for them cuz I'm still trying to interact with the implementation components, not the native components in some cases.

The middle test was just me proving to myself that I could query for children INSIDE a mocked component. In reality the snapshot would cover testing for that. I wasn't really sure what my issue was at the beginning. Hopefully an improved rendered output in the unable to find element error message makes it clearer what you can and can't query.

When I was mocking a child component, I was interested in rendering the children since they are IN my component since they won't get tested anywhere else. That's a good way to toss the children though if I need to. Returning a string is a super simple way to mock a component but still get the children.

Your comment in the other issue about firing a scroll event on any child of the FlatList makes sense and will be helpful. I was wondering where it was possible to fire that...

I guess I'm struggling with the testing-library world forcing me out of writing unit tests and into writing integration tests, especially in light of the existing codebase. I've also got a co-worker who believes very strongly in writing unit tests for everything and having them only interact with the unit and not anything else outside. I think I was coming here with a solution/bug fix in mind instead of presenting the complete problem 😬

Starting a conversation about the merits of the testing-library approach is probably not the most appropriate in an issue in a library for one specific implementation but I'll fire off one message and maybe you can point me to the best place to ask about it haha (Stack Overflow/other repo/chat/etc). I'm also trying to convince myself strongly enough of this pattern to be able to convince a coworker :)

So I guess my concerns are:

  1. Are we giving up on unit tests? (If you say yes, idk that I have a good argument lol). Seems like with the testing-library world you can only write a unit test for a leaf component (the deepest children that don't have dependencies on other user-created components). Everything else seems like it becomes an integration test. Maybe this by itself isn't a bad thing or may even be a good thing as it matches how the user would experience the component tree. I would love some docs to boldly say the testing-library world doesn't support unit tests and why. Might help me accept it by knowing what I'm getting into up front. But it leads my into number 2...
  2. How should I snapshot a complex, top level component. I just tested removing the mocks from one of the components of my main tabs and it turned my my 236 line snapshot (which already seemed a little overwhelming) into 1116 lines https://gist.github.com/OzzieOrca/ba41111d80035db1fcc1c2771b086e42/revisions. I'm scared to ever have to do code review on something that big. And seems like editing a leaf component could potentially make tons of changes to lots of different files. Is the solution to not do snapshot testing? Or to just blindly update snapshots if the diff gets too big to reason about? Or should mocking be allowed in some cases? Maybe there are some other possibilities I can't come up with right now.
  3. Would you consider it a best practice to have some global, default mocks for things like a Redux store or Apollo or other providers? When I was removing those mocks I broke a couple mapStateToProps since they tried to access a key that didn't exist and I had to add a few things to the store state I was using in the test. Having to provide a lot of data for external components in each test would get old fast. Any downsides to globally mocking the Redux store and then tweaking it for certain tests? Guess that's what I've started doing with Apollo. I'm just worried that there will be other components that need some specific testing setup that I won't want to do in every component that uses them. But maybe that just means my global setup and mocks aren't complete enough.

All that to say, I think you resolved my initial question and the issue with "that doesn't fit our guiding principles". So feel free to close the issue :) But I would like to continue this conversation/learn more. I'm not opposed to any of the guiding principles and do see the value in them, I'm just trying to figure out how to best apply them to a large codebase. At this point, I'm not convinced that I can test what I want to in a sane way without some amount of mocking, in which case maybe I should stick with react-native-testing-library. But I would love to be convinced otherwise 😃 The answer very well may be that we're writing our components wrong...

I suppose I could make mock components that would, say, hook up the onPress of my mocked component to a native component and then fire a press event on native component. But that's a hack and you'll probably hate me for proposing it :) I'm just grasping for ways to reduce the size of the render tree and the snapshot and still be able to test things that only get called by user interactions with a child component. Maybe I just need to accept the size of those and deal with it, it just feels way too complex. I suppose it could get better as components are refactored but it still feels wrong and overwhelming. I feel like there's something I'm missing here and maybe I'm approaching it wrong. Or maybe there are ways to make such large snapshot diffs more approachable.

Thanks for reading all this lol. I've appreciated your thoughts so far 👍

@bcarroll22
Copy link
Collaborator

bcarroll22 commented May 16, 2019

Edit: sorry I closed this early, clicked the button on accident while I was writing my comment lol. Still going to close just for housekeeping’s sake, since the original concern is addressed.

I totally get where you’re coming from, so I’ll try to address a few of the biggest concerns.

  1. This tweet kinda summarizes the testing library mentality. I don’t think it means you totally give up on units, but I do think it means you a) prioritize integration or b) consider components to be your unit instead of methods inside components. Generally speaking, unit testing in the Enzyme train of though won’t work with testing library. Here’s some more about that
  2. You’ll wanna check this out for more thoughts on that question. Snapshots like that are prone to break and the testing philosophy would probably tell you there’s more valuable assertions you could make to give you confidence and be more resilient than that snapshot would. Mocking is okay, but as that thread points out, in the TL mentality it should be a conscious decision rather than the outcome of the library itself.
  3. I’d check out this example which is generally the recommended approach for this type of thing regardless of which testing library implementation you’re using. In practice you should rarely import straight from NTL, you’ll likely want to create a custom utility for your team that adds things like providers and wrappers. Here’s some more information about that.

Hopefully that helps, I definitely felt the pain points as well going through a transition from Enzyme. Biggest thing to keep in mind, it’s a completely different mentality and tests you currently have likely won’t map 1:1 to testing library; there’s probably a lot of tests you could/should get rid of in favor of something that’s more focused around user actions and outcomes.

I won’t try to convince you to use NTL over RNTL because I’ve always believed there’s merits to both. I think it’s ultimately about what helps your team be confident your app is working. I can tell you there will be trade offs, but if you’ve not run into any issues with it and you’re generally happier with it, no complaints here, and I’m happy it’s working for you guys 👍 the only alternative I could give you to consider (just for the sake of helping you make a decision, not to convince you) is using NTL for most tests and importing the react-test-renderer directly to shallow render for snapshots, which is totally possible to ease a transition. If you make a custom utility like I linked earlier, you could even export shallow from there so most of the team wouldn’t even care where the shallow renderer was coming from.

Hope this helps, and best of luck to you and your team in your huge overhaul!

@TAGraves
Copy link
Collaborator

Are we giving up on unit tests? (If you say yes, idk that I have a good argument lol). Seems like with the testing-library world you can only write a unit test for a leaf component (the deepest children that don't have dependencies on other user-created components). Everything else seems like it becomes an integration test.

One thing I'd add here to what Brandon said - I'd encourage you to read Martin Fowler's article on mocking and stubbing, and specifically his discussion about the difference between "classical" and "mockist" testing. Component tests in testing-library world tend to still be unit tests, but they're unit tests under the classical philosophy. That is, they test a single "unit" (a component), but they allow that unit to exercise its full behavior and then verify the state of things after it does so.

One dumb example I like to point to for this: Imagine you have an calculate function. Here's two ways you could write it:

function calculate() {
  return add(3, 5);
}

function add(x, y) {
  return x + y;
}

or

function calculate() {
  return 3 + 5;
}

These two implementations are equivalent in terms of how they behave. On the naive version of the mockist testing approach, though, you'd actually write different tests for each! (Viz., for the first you'd mock out the add function, but for the latter you wouldn't). That means that the tests you write for a feature are tightly coupled to the implementation, then (since the only difference here is a difference in implementation).

On the classical testing philosophy, the tests for calculate are identical in both cases and you can refactor from one to the other without having to make any changes to the tests.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants