1

I wrote a functional component using the useState hook, the component itself works fine but I'm having trouble testing it. I don't understand how to create a initial state in my component. I've tried multiple ways of mocking useState, none of which worked:

describe('<Component/>', () => {
    beforeEach() = > {
        jest.spyOn(Readt, 'useState').mockImplementation( () => React.useState(['test']));
        //Also tried this:
        //React['useState'] = jest.fn().mockImplementation( () => React.useState(['test']));
    }
    it('should match snapshot', () => {
        expect(wrapper).toMatchSnapshot()
    }
}

I've seen this post too but was unable to get it to work for me

How to set initial state for useState Hook in jest and enzyme?

I've also read that your not supposed to mock out useState in your unit test but then how do you set an initial state for your components if you don't mock it out?

Thanks.

EDIT: In regards to an answer below:

Passing in a prop doesn't change the state I'm trying to test.

I have a button which when clicked (one state) renders an input box which, when the user types in (another state) and presses the okay button (yet another state) creates a button based on the content in the input box.

The props which is passed helps renders a list of buttons but it has no control over what the user types into the input box. I'm trying to set a state in the input box and then simulate the click on the okay button and test that. I'm not trying to test reacts native useState functionality. I'm just trying to use what enzyme already has 'wrapper.setState' but how do I do that with useState?

Ideally I'd like to set up my component, set up the state in which the user has typed into the input box and then test to see if the ok button does what it's supposed to do. Without simulating all the clicks before that.

MatTaNg
  • 923
  • 8
  • 23
  • 41

2 Answers2

2

A component's state is internal to it, and this is an essential part of understanding React components. Once you "grok" that, you can understand that no test should ever try to change it.

but then how do you set an initial state for your components?

You pass a value to your useState hook, inside the component. Your tests should then test your component using that initial state ... they should just pass in props to change it, the same way as your non-test code would.

In fact, even if you somehow could change your initial state (which is impossible AFAIK), your test code should NEVER do anything non-test code does (except to set up testing environments which simulate real ones), since the entire point of making tests is to verify that your non-test code works.

Similarly, you never want to test the internals of anything you're testing, whether it's a React component or anything else.

If you do that, every test you write will break the moment any of those internals change, and a HUGE part of the value of tests is that they let you refactor more easily (if you don't test internals). Instead, you want to use your testing tools to measure the output of your component (ie. what HTML it generates).

And again, this really isn't even about React. Good software tests of anything don't try and mess with that thing's internals, either to change or measure them. Instead, good tests focus on the externals, because again that's all non-test code every works with, and because conceptually everything you test should be a "black box" to your test code.

EDIT: Response to comment/question edit

Your edit doesn't really change my answer to be honest, except that it reminded me Enzyme does have a way to change state in tests (never using it myself I forgot it existed).

But again, just because something is possible doesn't mean it's a good idea, and again, you want to test the exterior of anything you test (not just React components). The state of a React component is like a variable inside a function: it's internal to that function/component, and something you should avoid testing directly.

What you want to test instead is the exterior piece, and (based on your edit) it seems that isn't props ... but it's still something exterior: the DOM.

So what I would instead recommend doing is (again) the same thing the non-test code is doing: "clicking" on your buttons. Enzyme makes this very easy with its "simulate" method (https://airbnb.io/enzyme/docs/api/ReactWrapper/simulate.html).

If you just simulate the button sequence that creates the state you want, you'll only be testing externals (ie. testing your code the same way it will be used in production), but you will be able to test whatever circumstances you want.

machineghost
  • 33,529
  • 30
  • 159
  • 234
  • I replied. To be fair, I should note that I'm injecting a fair bit of "good testing philosophy" into my answer, so like any programer's philosophy take it with a grain of salt. However, I'm basing it off a decade-plus long career leading teams (and working with tests), and I strongly suspect that if you consult any testing expert they'll give the same/similar advice about interior/exterior testing. A HUGE part of the value of tests is that they let you safely refactor your code, and if you "test the interior" you lose most of that value. – machineghost Feb 04 '20 at 00:16
  • Your answer surprises me as it goes against everything I know about unit testing. Shouldn't you be testing your code in as small units as possible and try to isolate your logic? If I simulate a bunch of clicks and inputs then not only will it be pretty ugly code bcuz I have to dig through the DOM but I'll also be calling multiple functions before I am able to test the function. This now begins to sound more of an integration test than a unit test. – MatTaNg Feb 04 '20 at 14:46
  • I see it as being a lot like state itself. You want your state as *low* in your component hierarchy as possible right? But you still need it to be high enough to be shared by every component that needs it. Likewise, you want to test as low as you can ... without testing internals. What you have is a very common misunderstanding of the word "unit": it's not meant to be a single function. Look it up: the word used to mean a class, and in JS a module is the closest equivalent. You want to test the *exterior* of that unit, not how it does what it does, again because of refactoring. – machineghost Feb 04 '20 at 15:24
  • Think of it like this: you have function A that uses function B. You could just export and test A, or you could export and test B too. If you test just A, then you can change how B works whenever you want, ie. you can *refactor* it to improve its code. And you can do so *safely*, because your unit tests on A will catch any mistakes you make in B. But if you test B, then when you change B you also have to change B's test. Now instead of helping you (by catching refactoring errors) your test are *hurting* you, making more busy work (test fixing) that you have to do because you changed B. – machineghost Feb 04 '20 at 15:26
  • It's the same core idea with your React components (which, if they're significant, will usually have their own module/unit right?). You want to treat the component itself as a black box, *because* you're allowed to refactor the inside of that black box at will, and the tests on the outside of it will catch your mistakes. If you test what's going on inside, all you do is make busywork for yourself the next time those internals change. – machineghost Feb 04 '20 at 15:27
  • P.S. Again, I want to emphasize that this is a common misunderstanding, which many programmers have: Mocha/Jest/etc. *doesn't* explain it in their docs, and again the term "unit" goes way back, to testing *classes* (and remember classes had *private* methods). When programmers forget and test the"private" parts of a module, they wind up feeling like tests are busywork and a waste of time ... *because they are*. Good tests are an *asset* to the team, not a hindrance. To make them good, you have to avoid testing too deep (but again, like React state, you still want it as deep as possible). – machineghost Feb 04 '20 at 15:39
  • "But if you test B, then when you change B you also have to change B's test" do you mean "...you also have to change A's test"? i think you can just mock out A and any other dependency B has and it wouldn't be a problem. For me personally, refactoring hasn't really been an issue, sure tests fail but if you mock everything out then you don't need to spend a lot of time fixing tests. In any case what you say makes sense too, the 'unit' is the react component itself. – MatTaNg Feb 04 '20 at 15:56
  • But I don't generally like looking for DOM elements bcuz it makes tests brittle and you have multiple tests failing when you change the JSX and these tests which dig through the DOM can be difficult to debug. Do you have any resources which talk about this? – MatTaNg Feb 04 '20 at 15:56
  • Also what if its a long chain of HOCs? If A renders B and B renders C and C renders D then to test A as a black box you'd have to mount it which will render all its child components. Now you're essentially testing 4 components at once. If the logic for D changes you would have to change the tests for A, B and C. – MatTaNg Feb 04 '20 at 16:06
  • A Stack Overflow comment section is ill suited for conversations. Suffice it to say, the answers to your questions will come from a better understanding of testing, and there are many great resources out there which you can use to educate yourself. Or (and I mean this honestly/genuinely), sometimes we have to learn things for ourselves ... if you do, just keep doing what you're doing (testing internals). I can all but guarantee that in a few years, if you're on the same codebase, you'll have felt the pain of tests being more hindrance than help, and you'll appreciate my points organically. – machineghost Feb 04 '20 at 23:01
  • I was just trying to gain a better understanding of this new testing paradigm. If it matters to you, I've already written my test to not test the internals of my component (a lot due to because there's still no solution to my original question). In any case, thanks for spending the time answering my questions! – MatTaNg Feb 05 '20 at 14:59
  • :) glad I could help. – machineghost Feb 06 '20 at 15:02
-1

How about setting a value depending on the environment? Jest sets the environment variable NODE_ENV to 'test':

const inTest = process.env.NODE_ENV === 'test'

[value, setValue] = useState(inTest ? mockValue : defaultValue)
Brent Washburne
  • 12,904
  • 4
  • 60
  • 82