【问题标题】:Jest & React Testing Library:Type of Events dont matchJest 和 React 测试库:事件类型不匹配
【发布时间】:2022-01-23 13:46:42
【问题描述】:

我在测试领域尝试了一些冒险,特别是我想测试一些基本的反应组件,但我在第一步被卡住了。这是我正在使用的简化形式 我的申请。

表单正在运行。我正在为视图 atm 的测试而苦苦挣扎。

我想要什么:

我想在没有容器的情况下测试视图,并在视图中测试它们。

我写了一些基本的测试,我认为它们应该是这样的,例如

  • 查看:测试change 是否被正确的数据调用
  • presenter:在change 被调用后测试输入的值是什么

我期望发生的事情:

如果我在测试中调用fireEvent,我希望他通过测试。

发生了什么:

第一个测试有效(显然),因为组件被初始化为空值。 onChange 容器组件中的测试也正常工作。我认为onChange 测试已损坏,事件类型不匹配。

我怎样才能测试这个或获得正确的类型?

代码:

LoginView.ts(演示者)

import { ChangeEvent, createElement, FunctionComponent } from "react";

export interface LoginViewProps {
    username: string;
    password: string;
    onChange: (event: ChangeEvent<HTMLInputElement>) => void;
    onSubmit: () => void;
}

export const LoginView: FunctionComponent<LoginViewProps> = (props: LoginViewProps) => {

    return createElement("div", {},
        createElement("label", {
            htmlFor: "username",
        },
            "Username:",
        ),
        createElement("input", {
            "data-testid": "username",
            type: "text",
            name: "username",
            id: "username",
            value: props.username,
            onChange: props.onChange,
        }),
        createElement("label", {
            htmlFor: "password",
        },
            "Password:",
        ),
        createElement("input", {
            "data-testid": "password",
            type: "password",
            name: "password",
            id: "password",
            value: props.password,
            onChange: props.onChange,
        }),
        createElement("button", {
            type: "button",
            "data-testid": "submit",
            onClick: props.onSubmit,
        },
            "Sign in",
        ),
    );
};

export default LoginView;

LoginView.test.ts - 测试视图

import "@testing-library/jest-dom";
import { createElement } from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import LoginView, { LoginViewProps } from "./LoginView";

afterEach(cleanup);

describe("Login Presenter", () => {

    /**
     * This works, more coincidence than knowledge
     */
    it("should display div with blank values", async () => {
        const { findByTestId } = renderLoginForm();

        const username = await findByTestId("username");
        const password = await findByTestId("password");

        expect(username).toHaveValue("");
        expect(password).toHaveValue("");
    });

    /**
     * This is not working
     */
    it("should allow entering a username", async () => {
        const onChange = jest.fn();
        const { findByTestId } = renderLoginForm({
            onChange,
        });
        const username = await findByTestId("username");

        fireEvent.change(username, {
            target: {
                id: "username",
                value: "test",
            },
        });

        /**
         * This expect is wrong,
         * received: Object {...}
         * expected: SyntheticBaseEvent {...}
         */
        expect(onChange).toHaveBeenCalledWith({
            target: {
                id: "username",
                value: "test",
            },
        });
    });

    /**
     * This is not working
     */
    it("should allow entering a password", async () => {
        const onChange = jest.fn();
        const { findByTestId } = renderLoginForm({
            onChange,
        });
        const password = await findByTestId("password");

        fireEvent.change(password, {
            target: {
                id: "password",
                value: "test",
            },
        });

        /**
         * This expect is wrong,
         * received: Object {...}
         * expected: SyntheticBaseEvent {...}
         */
        expect(onChange).toHaveBeenCalledWith({
            target: {
                id: "password",
                value: "test",
            },
        });
    });

    it("should submit the form with username, password", async () => {
        /**
         * What to write here?
         *
         * How can i test the values that i provided
         */
    });
});


function renderLoginForm(props: Partial<LoginViewProps> = {}) {
    const defaultProps: LoginViewProps = {
        username: "",
        password: "",
        onChange() {
            return;
        },
        onSubmit() {
            return;
        },
    };
    return render(createElement(LoginView, {
        ...defaultProps,
        ...props,
    }));
}

错误:

> jest

 FAIL  src/react/LoginForm/LoginView.test.ts
  ● Login Presenter › should allow entering a username

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

    - Expected
    + Received

    - Object {
    -   "target": Object {
    -     "id": "username",
    -     "value": "test",
    + SyntheticBaseEvent {
    +   "_reactName": "onChange",
    +   "_targetInst": null,
    +   "bubbles": true,
    +   "cancelable": false,
    +   "currentTarget": null,
    +   "defaultPrevented": false,
    +   "eventPhase": 3,
    +   "isDefaultPrevented": [Function functionThatReturnsFalse],
    +   "isPropagationStopped": [Function functionThatReturnsFalse],
    +   "isTrusted": false,
    +   "nativeEvent": Event {
    +     "isTrusted": false,
        },
    +   "target": <input
    +     data-testid="username"
    +     id="username"
    +     name="username"
    +     type="text"
    +     value=""
    +   />,
    +   "timeStamp": 1640165302072,
    +   "type": "change",
      },

    Number of calls: 1

      31 |              });
      32 |
    > 33 |              expect(onChange).toHaveBeenCalledWith({
         |                               ^
      34 |                      target: {
      35 |                              id: "username",
      36 |                              value: "test",

      at _callee2$ (src/react/LoginForm/LoginView.test.ts:33:20)
      at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
      at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:294:22)
      at Generator.next (node_modules/regenerator-runtime/runtime.js:119:21)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:9)

  ● Login Presenter › should allow entering a password

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

    - Expected
    + Received

    - Object {
    -   "target": Object {
    -     "id": "password",
    -     "value": "test",
    + SyntheticBaseEvent {
    +   "_reactName": "onChange",
    +   "_targetInst": null,
    +   "bubbles": true,
    +   "cancelable": false,
    +   "currentTarget": null,
    +   "defaultPrevented": false,
    +   "eventPhase": 3,
    +   "isDefaultPrevented": [Function functionThatReturnsFalse],
    +   "isPropagationStopped": [Function functionThatReturnsFalse],
    +   "isTrusted": false,
    +   "nativeEvent": Event {
    +     "isTrusted": false,
        },
    +   "target": <input
    +     data-testid="password"
    +     id="password"
    +     name="password"
    +     type="password"
    +     value=""
    +   />,
    +   "timeStamp": 1640165302102,
    +   "type": "change",
      },

    Number of calls: 1

      53 |              });
      54 |
    > 55 |              expect(onChange).toHaveBeenCalledWith({
         |                               ^
      56 |                      target: {
      57 |                              id: "password",
      58 |                              value: "test",

      at _callee3$ (src/react/LoginForm/LoginView.test.ts:55:20)
      at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
      at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:294:22)
      at Generator.next (node_modules/regenerator-runtime/runtime.js:119:21)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:9)

 PASS  src/react/LoginForm/Login.test.ts

Test Suites: 1 failed, 1 passed, 2 total
Tests:       2 failed, 6 passed, 8 total
Snapshots:   0 total
Time:        4.035 s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

要点:

https://gist.github.com/simann/9cbf01f28602d59ba988ef608df99bc0

最后的评论:

除了上面的错误,我还需要对submit函数进行一些测试,如果有人能给我提供一些信息,我将非常感激。

也欢迎任何其他提示或改进。

编辑

为了澄清,我也会添加容器的代码

登录.ts

import { ChangeEvent, Component, createElement } from "react";
import LoginForm from "./LoginView";

interface LoginState {
    password: string;
    username: string;
}

export class Login extends Component<null, LoginState> {

    constructor(props: null) {
        super(props);
        this.state = {
            password: "",
            username: "",
        };

        this.onChange = this.onChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);
    }

    onChange(event: ChangeEvent<HTMLInputElement>) {
        const { id, value } = event.target;
        this.setState({
            ...this.state,
            [id]: value,
        });
    }


    onSubmit() {
        console.log(this.state.username, this.state.password);
        /**
         * Do a websocket request with the values
         */
    }

    render() {
        return createElement(LoginForm, {
            password: this.state.password,
            username: this.state.username,
            onChange: this.onChange,
            onSubmit: this.onSubmit,
        });
    }

}

export default Login;

登录.test.ts

import "@testing-library/jest-dom";
import { createElement } from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import Login from "./Login";

afterEach(cleanup);

describe("Login Container", () => {
    it("should display a blank login form with blank values", async () => {
        const { findByTestId } = renderLogin();

        const username = await findByTestId("username");
        const password = await findByTestId("password");

        expect(username).toHaveValue("");
        expect(password).toHaveValue("");
    });

    it("should allow entering a username", async () => {
        const { findByTestId } = renderLogin();
        const username = await findByTestId("username");

        fireEvent.change(username, {
            target: {
                id: "username",
                value: "test",
            },
        });

        expect(username).toHaveValue("test");
    });

    it("should allow entering a password", async () => {
        const { findByTestId } = renderLogin();
        const password = await findByTestId("password");

        fireEvent.change(password, {
            target: {
                id: "password",
                value: "test",
            },
        });

        expect(password).toHaveValue("test");
    });

    it("should submit the form with username, password", async () => {
        /**
         * What to write here?
         *
         * How do i test the values that are in my state?
         */
    });
});


function renderLogin() {
    return render(createElement(Login));
}

【问题讨论】:

    标签: reactjs unit-testing jestjs react-testing-library


    【解决方案1】:

    测试失败主要是因为您正在比较两个不同的对象。

    React 传递给事件处理程序的对象是 SyntheticEvent 的一个实例,它是浏览器原生事件的包装器,比您要与之比较的对象更复杂。

    尽管 SyntheticEvent 类具有与原生事件类似的接口,但它是 React 的一个实现细节,您应该避免围绕其结构实现测试,尤其是对象匹配测试。

    更好的方法是将您的 onChange 道具包装在另一个函数中,该函数将调用它仅传递事件值。

    ...
    createElement("input", {
        "data-testid": "username",
        type: "text",
        name: "username",
        id: "username",
        value: props.username,
        onChange: (event) => {
            props.onChange(event.target.value);
        },
    })
    ...
    

    然后,在你的测试中,你可以写:

    ...
    fireEvent.change(password, {
        target: {
            value: "test",
        },
    });
    
    expect(onChange).toHaveBeenCalledWith("test");
    ...
    

    要测试提交的值,请使用 React useState 保存输入字段的值。

    ...
    const [username, setUsername] = React.useState("");
    const [password, setPassword] = React.useState("");
    ...
    createElement("input", {
        "data-testid": "username",
        type: "text",
        name: "username",
        id: "username",
        value: props.username,
        onChange: (event) => {
            setUsername(event.target.value);
            props.onChange(event.target.value);
        },
    }),
    createElement("input", {
        "data-testid": "password",
        type: "password",
        name: "password",
        id: "password",
        value: props.password,
        onChange: () => {
            setPassword(event.target.value);
            props.onChange(event.target.value);
        },
    }),
    createElement("button", {
        type: "button",
        "data-testid": "submit",
        onClick: () => {
            props.onSubmit(username, password)
        },
    },
        "Sign in",
    )
    

    那么在你的测试中你可以

    it("should submit the form with username, password", async () => {
        const onSubmit = jest.fn();
        const { findByTestId } = renderLoginForm({
            onSubmit,
        });
    
        const username = await findByTestId("username");
        const password = await findByTestId("password");
        const submit = await findByTestId("submit");
    
        fireEvent.change(username, { target: { value: "username" } });
        fireEvent.change(password, { target: { value: "password" } });
        fireEvent.click(submit);
    
        expect(onSubmit).toHaveBeenCalledWith("username", "password");
    });
    

    除此之外,您应该使用 JSX 来创建组件,而不是 createElement。 ?

    【讨论】:

    • 感谢您的回复。我用 onChange 函数得到了那个部分。我没有得到的部分是关于const [username, setUsername] = React.useState("");。我将为相同的数据引入第二个变量,并在登录函数中使用它。那会使容器中的变量过时吗?
    • 另一种方法是使用refs 保留对输入字段的引用,然后您可以在提交处理程序中访问其值。
    【解决方案2】:

    通过你当前的组件代码和测试

    expect(onChange).toHaveBeenCalledWith({
      target: {
        id: "username",
        value: "test",
      },
    });
    

    您实际上想检查“存在包含target: { id: "username", value: "test" } 的对象,而当前您检查的是“正是那个对象”。这里expect.objectContaining 有帮助:

    expect(onChange).toHaveBeenCalledWith(
      expect.objectContaining({
        target: {
          id: "username",
          value: "test",
        },
      })
    );
    

    【讨论】:

      猜你喜欢
      • 2020-03-24
      • 2022-12-14
      • 2021-05-26
      • 2020-06-14
      • 2020-06-01
      • 2021-09-20
      • 2021-07-17
      • 2019-04-22
      • 2021-12-06
      相关资源
      最近更新 更多