React: Expose closures in function components

Created on 18 Nov 2019  路  3Comments  路  Source: facebook/react

Do you want to request a feature or report a bug?
Feature (I think)
What is the current behavior?
React Hooks are pretty great. We are able to use function components for everything now which has been sweet 馃馃徎 But back in the class days, if we wanted to test a function internal to a class component, we could simply pass our component to shallow or mount and dig into it using instance. For all the research I've done, it doesn't seem possible to test functions inside of function components.

We are using jest as our unit testing framework with enzyme to handle DOM manipulation. I brought this up in an issue to the jest team and they referred me to the enzyme team (issue logged) and I thought'd I give here a try.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
I figure the best way to explain this is through an example. Fist I have an example of a class component with accompanying test

// Component definition
class MyComponent extends React.Component {
...
  foo(bar) {
    // Some processing on bar that depends on state and imported libraries that don't make sense importing into a helper file
    return fooBar;
  }
...
}

// Test
it('should return fooBar', () => {
  const sut = shallow(<MyComponent />);
  expect(sut.instance().foo(testValue)).toBe(fooBar);
});

Here is an example of something I might expect if this were implemented (which is really similar to the above, but just for clarity sake I re-wrote it):

// Component definition
const MyComponent = (props) => {
...
  const foo = (bar) => {
     // Some processing on bar that depends on state and imported libraries that don't make sense importing into a helper file
    return fooBar;
  }
...
}

// Test
it('should return fooBar', () => {
  const sut = shallow(<MyComponent />);
  expect(sut.instance().foo(testValue)).toBe(fooBar);
});

I understand that it is impossible to reference closures in JS, but is there some way that react might be engineered to expose functions in these situations for testing purposes?
What is the expected behavior?
That I would be able to test functions internal to function components.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
React vs 16.8

All 3 comments

If you must, you can useImperativeHandle() to continue exposing methods via ref. However, some dislike this pattern because it tests implementation details. A less fragile test would only rely on inputs and outputs.

As @ljharb mentioned in https://github.com/airbnb/enzyme/issues/2287 this is just how functions and closures work, there's no way to perform introspection like this without some kind of compiler pass.

Regardless, we generally recommend against testing internal utility methods like this. As @nortonwong stated, you should try and test the behavior of the component directly rather than its implementation. You can find some of our recommended testing strategies in the Testing Overview doc page.

@aweary

Late to the party but it's still relevant.

It's not "testing implementation details". When the React team talks about that what they are saying is this:

"We want you to test everything through heavy DOM wiring such as buttons, forms, etc."

The problem with this, while most JS developers see that as the best route to go, actually isn't for a lot of us out here, we disagree. "Testing as the user" can also be done not through that heavy wiring. And unfortunately, the react community has misguided developers saying you should test everything through heavy DOM details/events/wiring.

Now, back to "testing implementation details". "Implementation Details" means nothing unless you talk about test approach and WHAT.

Those of us who do not drink the koolaid of "test as a user tests it => automatically means test through wiring, never shallow, never expose anything", we like to test some of our UI modules a little more direct.

NOW, one can immediately throw out again to us "Stop! You're testing implementation details!!!!!!". STOP. Most the time Dodds and others try to show an example of shallow or an example where people are actually writing tests that do actually hit implementation details such as trying to update internals directly such as trying to mess with React API stuff like setState, state, or say mapStateToProps in the Redux world, etc. That's just bad practice period. You don't have to write tests that way, and you should't but it's a bad excuse to totally shut down trying to expose some kind of subset of your React Class or Hook and test directly. You can do that without still hitting true implementation details and without having to go through button click wiring or form inputs.

Those of us who understand that you can bypass button wiring and buttons (sure we don't care that we're not testing that kind of wiring), we expose handler methods or custom behavioral based methods that btw still hide implementation details such as React API stuff or local data. We still want to be able to do this with hooks somehow by hitting that carefully exposed behavior.

So, with JS not exposing stuff via closure unless you use a Factory Method for example to expose some of that as your public testable API on your module, yes we can't test it directly. But for the React team, they need to realize we want to be able to expose a subset of those external functions somehow by returning an object literal or something like that so we can get at some of that as our "Public API".

we generally recommend against testing internal utility methods like this

These are not utility methods. You're painting it like nothing should ever be exposed in code. Imagine taking that and saying that to all languages and all layers of an application. You can't just paint a brush like that. We carefully expose the public behavioral contract of our components. Imagine a backend API that has routes and controllers underneath. A lot of us perfer to TDD at the controller level and start there instead of routes, etc. That's the same thing here. You can choose to test a little lower and still test well, still test in a way that's not fragile and that's what we can still do here with React...if you know how to write tests well and smart about what you expose as your public API.

@aweary So what are the options here going forward to allow that somehow with these Hooks? I don't see a way to expose some of what should be part of the public Contract by returning an object literal, you can't I don't think with a Hook.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gaearon picture gaearon  路  104Comments

gabegreenberg picture gabegreenberg  路  264Comments

gabegreenberg picture gabegreenberg  路  119Comments

gaearon picture gaearon  路  111Comments

addyosmani picture addyosmani  路  143Comments