Ervin E
Ervin E

Reputation: 452

React - Where to unit test state change when parent component passes method as prop: parent or child component?

I am trying to figure out the best way to unit test the following situation

The question is: do I test that the state is changing properly in App.tsx or Attendees.tsx? I've seen examples where I should test the state change in the parent component but those examples show that the parent component displays the value in the DOM instead of the child.

Code is below

App.tsx

import React, { FC, Component } from 'react';
import {Attendees} from './Attendees';


interface AppState {
  attendees: number
}
export default class App extends Component<{}, AppState> {
  constructor(props:any) {
    super(props);
    this.handleAttendeesChange = this.handleAttendeesChange.bind(this);
    this.state = {
      attendees: 0
    };
  }

  handleAttendeesChange(value: number) {
    this.setState({ attendees: this.state.attendees + value});
  }
  
  render() {
    return (
      <div>
        <h1>Parent Component</h1>
        <Attendees currentCount={this.state.attendees} onValueChange={this.handleAttendeesChange} />
      </div>
    )
  }
}

Attendees.tsx

import React, { FC } from 'react';

type AttendeesProps = {
  currentCount: number,
  onValueChange: (value: number) => void
}

export const Attendees:FC<AttendeesProps> = ({ currentCount, onValueChange }) => {
  return (
      <div>
        <button data-testid="countUp" onClick={() => onValueChange(1)}>
          Up
        </button >
        <button data-testid="countDown" onClick={() => onValueChange(-1)}>
          Down
        </button >
        <p data-testid="currentCount">
          {currentCount}
        </p>
        
      </div>
    
  )
}

Here's what I am currently testing in Attendeees.test.tsx using react-testing-library

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { unmountComponentAtNode } from "react-dom";
// import { act } from "react-dom/test-utils";
import {Attendees} from './Attendees';
let container: any = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("Main");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});
it("Attendees should respond to callback props", () => {
  const onValueChange = jest.fn();
  const { getByTestId } = render(<Attendees currentCount={0} onValueChange={onValueChange} />, container)

  fireEvent.click(getByTestId('countUp'))
  expect(onValueChange).toBeCalledWith(1);
  expect(onValueChange).toHaveBeenCalledTimes(1);
  expect(getByTestId('currentCount').textContent).toBe('1');

})

Upvotes: 2

Views: 8281

Answers (1)

HelloWorld101
HelloWorld101

Reputation: 4366

Late answer, but may help someone having same question.

"Do I test that the state is changing properly in App.tsx or Attendees.tsx?"

We don't have to test whether setState is working properly or not. However, if we want to test that the updated count is displayed in the Attendees component, we could test it in App.test.tsx, because that is where the state resides.

Couple more ideas about the tests

We don't have to create a container in beforeEach and then remove it in afterEach. The react-testing-library takes care of it.

Here's my version of the Attendees.test.tsx:

// Attendees.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Attendees } from './Attendees';

describe('Attendees component', () => {
  it('should respond to Up button click', () => {
    const onValueChange = jest.fn();
    render(<Attendees currentCount={0} onValueChange={onValueChange} />);

    // Don't have to use data-testid all the time. Here's another way
    fireEvent.click(screen.getByText('up', { exact: false }));
    expect(onValueChange).toBeCalledWith(1);
    expect(onValueChange).toHaveBeenCalledTimes(1);
  });

  it('should respond to Down button click', () => {
    const onValueChange = jest.fn();
    render(<Attendees currentCount={0} onValueChange={onValueChange} />);

    fireEvent.click(screen.getByText('down', { exact: false }));
    expect(onValueChange).toBeCalledWith(-1);
    expect(onValueChange).toHaveBeenCalledTimes(1);
  });
});

I would place the test that checks for the current count display in the App.test.tsx:

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

describe('App component', () => {

  it('should display increased attendee count when the Up button is clicked', () => {
    render(<App />);

    // Sometimes it may be a good idea to ensure that the precondition is true
    expect(screen.getByLabelText('current-attendee-count').textContent).toBe('0');

    fireEvent.click(screen.getByText('up', { exact: false }));

    expect(screen.getByLabelText('current-attendee-count').textContent).toBe('1');
  });
});

Note

The examples above were tested using React v17.0.1, and @testing-library/react v11.2.3.

Upvotes: 4

Related Questions