question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How are we meant to test controlled components?

See original GitHub issue
"@testing-library/user-event": "^12.1.10", (From CRA) -   "version": "12.6.0" in node modules. 
  • Testing Framework and version:

  • DOM Environment:

    Jest.

I’ve copied the code from this closed issue: https://github.com/testing-library/user-event/issues/307

import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("THE BUGGED TEST", async () => {
  let formValue = "";
  const setFormValue = jest.fn((newFormValue) => {
    formValue = newFormValue;
  });

  render(
    <input
      data-testid="thisinput"
      type="text"
      value={formValue}
      onChange={(event) => setFormValue(event.target.value)}
    />
  );

  await userEvent.type(screen.getByTestId("thisinput"), "Ben Mayer");

  expect(setFormValue).toHaveBeenCalledWith("Ben Mayer");

  expect(screen.getByTestId("thisinput")).toHaveDisplayValue("Ben Mayer");
});

The output I get is:

 FAIL  src/TestSandbox.test.tsx
  ✕ THE BUGGED TEST (55 ms)

  ● THE BUGGED TEST

    expect(jest.fn()).toHaveBeenCalledWith(...expected)

    Expected: "Ben Mayer"
    Received
           1: "B"
           2: "e"
           3: "n"

    Number of calls: 9

      20 |   await userEvent.type(screen.getByTestId("thisinput"), "Ben Mayer");
      21 |
    > 22 |   expect(setFormValue).toHaveBeenCalledWith("Ben Mayer");
         |                        ^
      23 |
      24 |   expect(screen.getByTestId("thisinput")).toHaveDisplayValue("Ben Mayer");
      25 | });

      at Object.<anonymous>.test (src/TestSandbox.test.tsx:22:24)

This makes sense to me as I can’t see that reassigning the ‘formValue’ variable will cause a rerender for RTL.

However, from reading the issue thread, it looks like Kent is saying that it should work?

I’ve searched ‘controlled components’ across the User Events github issues, and that’s the only issue for it.

How are we mean to test controlled components, like a simple text field like this? Can an example be added to the main readme?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:3
  • Comments:9 (4 by maintainers)

github_iconTop GitHub Comments

9reactions
ph-fritschecommented, Jan 29, 2021

Thanks for providing these informations. I understand your problem is more how to organize your tests than how to actually write them.

It feels less like unit tests and more like end to end testing. Like I should be able to test my component in an application-non-specific context.

Some might stone me for this, but I feel like tests should reflect your codes objective and the risks to produce bugs instead of some philosophy. So for a lot of simple components this means that a strict “unit test” would basically just protect you from a typo by requiring you to type it twice (once in the component and once in the test). This sounds useless to me and is rendered even more useless by autocompletion and the like. The DRY devil has possessed me regarding code as well as in the test, so whenever an additional test requires more copy and paste than original typing there is probably a better way. That said, let’s head over to your scenarios:

Setup function to test similar components

You should use a setup function in your component tests that reflects the expected environment for your components like:

// test/_setup.js
export function setup(FormComponent, props) {
  let onChange
  function TestEnvironment() {
    const [value, setValue] = useState(props.value)

    onChange = jest.fn(v => setValue(v))

    return <FormComponent id={props.id} value={value} onChange={onChange}/>
  }
  render(<TestEnvironment/>)
  return { onChange }
}
// test/components.js
import { setup } from './_setup'

test('render editable text input', () => {
  const { onChange } = setup(MyTextInput, {id: 'someField', value 'someValue'})

  userEvent.type(screen.getByLabel('someField'), 'Foo')

  expect(onChange).toBeLastCalledWith('someValueFoo')
  expect(screen.getByLabel('someField')).toHaveValue('someValueFoo')
})

This tests your exported components against the behavior and with the requirements you laid out for the consumers of your library. It prevents you from repeating yourself in the tests and also makes the test much more readable.

Testing wiring of components

If your DynamicForm does some magic with the props you can test if you pass down the correct results at the correct spots by inspecting the rendered React tree with react-test-renderer.

If you don’t do some magic, your tests will just be a repetition of FormItemMapper. (imho see above) You can however improve your confidence in passing down correct props at correct places by using TypeScript.

Dependency Injection

If you manage state up the tree, you can make your state managing component more testable by making the used components injectable.

// src/App.js
import { UserSignUp, PaymentDetails, AccountDetails, DynamicForm } from './components'

export function App({
  UserSignUpComponent = UserSignUp,
  PaymentDetailsComponent = PaymentDetails,
  AccountDetailsComponent = AccountDetails,
  DynamicFormComponent = DynamicForm,
} = {}) {
  // ...
  <DynamicFormComponent />
  // ...
}
// test/components/_setup.js
const mockComponents = {
  UserSignUpComponent = 'div',
  PaymentDetailsComponent = 'div',
  AccountDetailsComponent = 'div',
  DynamicFormComponent = 'div',
}
export function setup(components) {
  // ...
  render(<App {...mockComponents} {...components}/>)
  // ...
}
// test/components/DynamicForm.js
import { setup } from './_setup'
test('some test', () => {
  setup({MyDynamicForm})
  // ...
}
6reactions
dwjohnstoncommented, Jan 27, 2021

@ph-fritsche you might be misunderstanding what I mean by controlled component in this case What I mean is where ‘the value of the component is controlled by its parent.’

Eg.

function MyComponent(props) {
  const {value, onChange} = props; 

  return (
    <div>
      <input value={value} onChange={(event) => onChange(event.target.value)} />
      <span data-testid="output">{value}</span>
    </div>
  )
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

4 methods for testing controlled React components - Moxio
One of the ways you can test complex behavior that includes state changes, is by manually telling the component to re-render, and what...
Read more >
Controlled vs Uncontrolled Components in React - ITNEXT
In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component....
Read more >
Controlled vs. uncontrolled components in React
In this tutorial, we'll explain the difference between controlled and uncontrolled components in React with practical examples.
Read more >
How to Test React Components: the Complete Guide
Controlled component Forms. A controlled component form essentially means the form will work through the React state instead of the form ...
Read more >
How to properly test controlled input component which ...
I want to test controlled component using Jest/enzyme. ... Do you have an example of such test with react testing library.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found