How test usestate and useEffect with click events using react testing library jest?

I am trying to test the useState functionality which is also updates when useEffect is called.

The thing I am trying to test is at the initial stage the useState value is false then it changes on a click event.

Please see the code I tried below:

   it("should have initial state false", async () => {
        const setStateMock = jest.fn();
        const useSateMock = (useState) => [useState, setStateMock];

        jest.spyOn(React, "useState").mockImplementation(useSateMock);

        render(<Dropdown placeholder="placeholder name" options={testOptions} />, { wrapper: BrowserRouter });
        const dropdwonBtn = screen.getByTestId("dropdownBtn");
        fireEvent.click(dropdwonBtn);
        expect(setStateMock).toBeFalsy();
    });

The code of my dropdown component;

const [showDropdown, setShowDropdown] = useState(false);`


useEffect(() => {
    const handler = () => setShowDropdown(false);

    window.addEventListener("click", handler);
    return () => {
        window.removeEventListener("click", handler);
    };
}, [""]);

const handleDropdownClick = (e) => {
    e.stopPropagation();
    setShowDropdown(!showDropdown);
};

the error I am geeting

Any help is appreciated thanks a million

I don’t have time to dig in right now, but yeah, testing hooks can be a pain. I love hooks, but man, they can be a lot harder to test than life cycle methods were.

A few things that I have done to make things easier… Use more custom hooks. Then I could wrap things like useEffect in them and then I could mock them in my component and test the hook itself outside the context of the component, just as a function. Things like React Hooks Testing Library have been helpful.

There is a lot of discussion out there about this topic. I would look on youtube and Stack Overflow to see some of it.

1 Like

I’ve edited your code for readability. When you enter a code block into a forum post, please precede it with a separate line of three backticks and follow it with a separate line of three backticks to make it easier to read.

You can also use the “preformatted text” tool in the editor (</>) to add backticks around text.

See this post to find the backtick on your keyboard.
Note: Backticks (`) are not single quotes (').

useEffect(() => {
  // ...
}, [""]);

What’s up with the dep list there? What does it mean to have an empty string literal in your dep list? I mean, it is a primitive type so I guess it will always evaluate as unchanged, but still, that’s weird.

Hi @kevinSmith that is just an empty array which I removed it now but is there any luck on how we can test such functionality? I also have shared the test coverage screenshot which is asking me to cover function and statement.

Test coverage report screenshot showing the missing function to be tested and a statement to be tested.

Thanks a million for the support as usual since the start of learning.

To test the handler it has to run during the test. To test it meaningfully, you have to make an assertion about what it does. If that is difficult, you can pull it out into it’s own partial function to be able to test it, like:

const createHandler = setShowDropdown => () => setShowDropdown(false)

That could be moved out to the root level and exported for easy testing. Then in your code you can have:

const handler = createHandler(setShowDropdown)

That is an indirect way to test it, but will work as a fallback.

I’ll see if I can tackle the hook testing. I’ll have to create a project to do it so it may take a few.

Hi @kevinSmith , thanks a million, if possible, could you please share the solution or a codesnadbox link. Just for a reference.

Please .

Thanks again

OK, I’m mostly React Native so this has been interesting…

There are different ways of testing. This tests the behaviors or the page. I didn’t have an actual component from you so I had to make up one that was close to what you had:

import React, {useState, useEffect, useCallback} from 'react'

const App = () => {
  const [showDropdown, setShowDropdown] = useState(false)

  const handler = useCallback(() => setShowDropdown(false), [setShowDropdown])

  useEffect(() => {
      window.addEventListener('click', handler)
      return () => {
          window.removeEventListener('click', handler)
      }
  }, [handler])
    
  const handleDropdownClick = (e) => {
    e.stopPropagation()
    setShowDropdown(showDropdown => !showDropdown)
  }

  return (
    <div>
      <h1>My App</h1>
      {showDropdown && (
        <div data-testid="dropdown">
          <h2>My Dropdown</h2>
        </div>
      )}
      <button type="button" onClick={handleDropdownClick} data-testid="toggle-dropdown-button">Toggle Dropdown</button>
    </div>
  )
}

export default App

I tested it thusly:

import App from './App';
import {fireEvent, render, screen } from '@testing-library/react'

describe('App', () => {
  describe('the component', () => {
    const { container } = render(<App />)
    it('should match snapshot', () => {
      expect(container).toMatchSnapshot()
    })
  })

  describe('the dropdown', () => {
    describe('on initial render', () => {
      it('should not show', async () => {
        render(<App />)
        expect(screen.queryByTestId('dropdown')).not.toBeInTheDocument()
      })

      it('should show toggle show button', () => {
        render(<App />)
        expect(screen.getByTestId('toggle-dropdown-button')).toBeInTheDocument()
      })
    })
  })

  describe('the toggle button', () => {
    describe('with a single press', () => {
      it('should reveal dropdown', () => {
        render(<App />)
        expect(screen.queryByTestId('dropdown')).toBeNull()
        fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
        expect(screen.getByTestId('dropdown')).toBeTruthy()
      })
    })

    describe('with one press opens, a second closes', () => {
      it('should reveal dropdown', () => {
        render(<App />)
        expect(screen.queryByTestId('dropdown')).toBeNull()
        fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
        expect(screen.getByTestId('dropdown')).toBeTruthy()
        fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
        expect(screen.queryByTestId('dropdown')).toBeNull()
      })
    })
  })

  describe('the document', () => {
    it('should close an open dropdown', () => {
      render(<App />)
      expect(screen.queryByTestId('dropdown')).toBeNull()
      fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
      expect(screen.getByTestId('dropdown')).toBeTruthy()
      fireEvent.click(document.body)
      expect(screen.queryByTestId('dropdown')).toBeNull()
    })
  })
})

In reality, I probably would have pulled that useState and useEffect guts into a custom hook to simplify this component. Then I could just test the hook exhaustively as a function.

In order to test this, I added some test ids, but there are other ways to do that. I also used a useCallback on your handler function. I think that that has to be constant reference otherwise the teardown in useEffect won’t work properly - how will it know what to tear down?

To give you an idea about what I mean by custom hook, we could do:

import React, {useState, useEffect, useCallback} from 'react'

const useDropdown = () => {
  const [showDropdown, setShowDropdown] = useState(false)

  const handler = useCallback(() => setShowDropdown(false), [setShowDropdown])

  useEffect(() => {
      window.addEventListener('click', handler)
      return () => {
          window.removeEventListener('click', handler)
      }
  }, [handler])
    
  const handleDropdownClick = (e) => {
    e.stopPropagation()
    setShowDropdown(showDropdown => !showDropdown)
  }

  return { showDropdown, setShowDropdown, handleDropdownClick }
}

// *************

const App = () => {
  const { showDropdown, handleDropdownClick } = useDropdown()

  return (
    <div>
      <h1>My App</h1>
      {showDropdown && (
        <div data-testid="dropdown">
          <h2>My Dropdown</h2>
        </div>
      )}
      <button type="button" onClick={handleDropdownClick} data-testid="toggle-dropdown-button">Toggle Dropdown</button>
    </div>
  )
}

export default App

Normally I would put that hook it a file like MyComponent.hooks.js or I would put it in a more general location if I wanted to use it in multiple places. It’s just really nice to clean up component files and any time I can pull logic out, I can - it makes it cleaner and easier to test and mock. It’s just a good practice to always be thinking - how can I logically break this apart.

In this case, you can still use the exact same test - since we were testing behaviors - the behaviors haven’t changed.

But with complicated hooks sometimes it’s nice to break things out like this so you can test the hook directly with things like react hooks testing library. And sometimes when testing the component, it’s helpful to mock the hook to easily set up different states to test the component and then test the hook separately.

1 Like