如何测试自定义钩子?

huangapple go评论64阅读模式
英文:

How to test custom hook?

问题

Here is the translation of the code portion you provided:

我的自定义钩子

```javascript
import { useAppDispatch } from "../../app/redux";
import { registrationFormSlice } from "../../entities/registration";

export interface IgetFields {
  label: string;
  valueKey: string;
  setValue: any;
  disabled?: boolean;
}

export const useFields = (): (() => IgetFields[][]) => {
  const dispatch = useAppDispatch();

  const { setEmail } =
    registrationFormSlice.actions;

  const getFields = (): IgetFields[][] => {
    return [
      [
        {
          label: "Email",
          valueKey: "email",
          setValue: (value: string) =>
            dispatch(setEmail({value, error:false, text:''})),
        },
      ],
    ];
  };
  return getFields;
};

我的 reducer:

import { initialState } from "./initialState";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IRegistrationState } from "./models";

export const registrationFormSlice = createSlice({
  name: "registrationForm",
  initialState,
  reducers: {
  
    setEmail(
      state: IRegistrationState,
      action: PayloadAction<{ value: string; error: boolean; text: string }>
    ) {
      state.data.email.value = action.payload.value;
      state.data.email.error = action.payload.error;
      state.data.email.errorText = action.payload.text;
    },
  },
});

我的存储:

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { registrationFormSlice } from "./registration";

const rootReducer = combineReducers({
  registrationForm: registrationFormSlice.reducer,
});

export const setupStore = () => {
  return configureStore({
    reducer: rootReducer,
    devTools: process.env.NODE_ENV !== "production",
  });
};

export type RootState = ReturnType<typeof rootReducer>;
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore["dispatch"];

我的测试:

import { renderHook } from "@testing-library/react";
import { useFields } from "./data";
import * as useAppDispatch from "../../app/redux";

describe("useFields", () => {
  const useDispatchMock = jest.spyOn(useAppDispatch, "useAppDispatch");
  useDispatchMock.mockReturnValue(jest.fn());

  const setup = () => {
    const { result } = renderHook(() => useFields());

    return result.current();
  };

  it("必须是一个数组的数组", () => {
    const getFields = setup();

    expect(Array.isArray(getFields)).toBeTruthy();
    expect(useDispatchMock).toBeCalled();
  });

  it("索引为 0 的数组必须具有特定的属性和结构", () => {
    const getFields = setup()[0];

    expect(getFields.length).toBe(1);
    expect(typeof getFields[0].label).toBe("string");
    expect(getFields[0].valueKey).toBe("email");
    expect(typeof getFields[0].setValue).toBe("function");
  });
});

一切正常,测试通过,我只是不喜欢测试覆盖率:

如何测试自定义钩子?

我不知道如何测试这行代码

我尝试检查我的 useDispatchMock 被调用时的参数:

import { renderHook } from 'testing-library/react';
import { Provider } from 'react-redux';
import { useFields } from './data';
import { setupStore } from '../../entities/store';


describe('useFields', () => {
  const store = setupStore();
  
  const setup = () => {
    const { result } = renderHook(useFields, {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });
    return result.current();
  };

  it('必须是一个数组的数组', () => {
    const getFields = setup();
    expect(Array.isArray(getFields)).toBeTruthy();
  });

  it('索引为 0 的数组必须具有特定的属性和结构', () => {
    const getFields = setup()[0];
    expect(getFields).toHaveLength(1);
    expect(getFields[0]).toEqual(
      expect.objectContaining({
        label: expect.any(String),
        valueKey: expect.any(String),
        setValue: expect.any(Function),
      })
    );
  });

  test('应正确设置电子邮件值', () => {
    expect.assertions(1);
    const getFields = setup()[0];
    const firstField = getFields[0];
    store.subscribe(() => {
      expect(store.getState().regsitration.data).toEqual({
        email: {
          value: 'test@example.com',
          error: false,
          errorText: '',
        },
      });
    });
    firstField.setValue('test@example.com');
  });
});

但是我得到了一个错误:

[![enter image description here][1]][1]

我的方法是否不好?我如何摆脱这个问题?欢迎任何建议、链接和信息。

谢谢!


<details>
<summary>英文:</summary>

My custom hook:

import { useAppDispatch } from "../../app/redux";
import { registrationFormSlice } from "../../entities/registration";

export interface IgetFields {
label: string;
valueKey: string;
setValue: any;
disabled?: boolean;
}

export const useFields = (): (() => IgetFields[][]) => {
const dispatch = useAppDispatch();

const { setEmail } =
registrationFormSlice.actions;

const getFields = (): IgetFields[][] => {
return [
[
{
label: "Email",
valueKey: "email",
setValue: (value: string) =>
dispatch(setEmail({value, error:false, text:''})),
},
],
];
};
return getFields;
};

My reducer: 

import { initialState } from "./initialState";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IRegistrationState } from "./models";

export const registrationFormSlice = createSlice({
name: "registrationForm",
initialState,
reducers: {

setEmail(
  state: IRegistrationState,
  action: PayloadAction&lt;{ value: string; error: boolean; text: string }&gt;
) {
  state.data.email.value = action.payload.value;
  state.data.email.error = action.payload.error;
  state.data.email.errorText = action.payload.text;
},

},
});

My Store: 

    import { combineReducers, configureStore } from &quot;@reduxjs/toolkit&quot;;
    import { registrationFormSlice } from &quot;./registration&quot;;
    
    const rootReducer = combineReducers({
      registrationForm: registrationFormSlice.reducer,
    });
    
    export const setupStore = () =&gt; {
      return configureStore({
        reducer: rootReducer,
        devTools: process.env.NODE_ENV !== &quot;production&quot;,
      });
    };
    
    export type RootState = ReturnType&lt;typeof rootReducer&gt;;
    export type AppStore = ReturnType&lt;typeof setupStore&gt;;
    export type AppDispatch = AppStore[&quot;dispatch&quot;];


My test:

import { renderHook } from "@testing-library/react";
import { useFields } from "./data";
import * as useAppDispatch from "../../app/redux";

describe("useFields", () => {
const useDispatchMock = jest.spyOn(useAppDispatch, "useAppDispatch");
useDispatchMock.mockReturnValue(jest.fn());

const setup = () => {
const { result } = renderHook(() => useFields());

return result.current();

};

it("Must be an array of arrays", () => {
const getFields = setup();

expect(Array.isArray(getFields)).toBeTruthy();
expect(useDispatchMock).toBeCalled();

});

it("An array with index 0 must have certain properties and structure", () => {
const getFields = setup()[0];

expect(getFields.length).toBe(1);
expect(typeof getFields[0].label).toBe(&quot;string&quot;);
expect(getFields[0].valueKey).toBe(&quot;email&quot;);
expect(typeof getFields[0].setValue).toBe(&quot;function&quot;);

});
});


Everything works, the test passes, I don&#39;t like only the test coverage:

[![enter image description here](https://i.stack.imgur.com/uPEIb.png)](https://i.stack.imgur.com/uPEIb.png)

**I do not know how to test this line of code**

I tried to check with which argument my useDispatchMock is called:   

    import { renderHook } from &#39;@testing-library/react&#39;;
    import { Provider } from &#39;react-redux&#39;;
    import { useFields } from &#39;./data&#39;;
    import { setupStore } from &#39;../../entities/store&#39;;
    
    
    describe(&#39;useFields&#39;, () =&gt; {
      const store = setupStore();
      
      const setup = () =&gt; {
        const { result } = renderHook(useFields, {
          wrapper: ({ children }) =&gt; &lt;Provider store={store}&gt;{children}&lt;/Provider&gt;,
        });
        return result.current();
      };
    
      it(&#39;Must be an array of arrays&#39;, () =&gt; {
        const getFields = setup();
        expect(Array.isArray(getFields)).toBeTruthy();
      });
    
      it(&#39;An array with index 0 must have certain properties and structure&#39;, () =&gt; {
        const getFields = setup()[0];
        expect(getFields).toHaveLength(1);
        expect(getFields[0]).toEqual(
          expect.objectContaining({
            label: expect.any(String),
            valueKey: expect.any(String),
            setValue: expect.any(Function),
          })
        );
      });
    
      test(&#39;should set email value correctly&#39;, () =&gt; {
        expect.assertions(1);
        const getFields = setup()[0];
        const firstField = getFields[0];
        store.subscribe(() =&gt; {
          expect(store.getState().regsitration.data).toEqual({
            email: {
              value: &#39;test@example.com&#39;,
              error: false,
              errorText: &#39;&#39;,
            },
          });
        });
        firstField.setValue(&#39;test@example.com&#39;);
      });
    });

But I got an error:

[![enter image description here][1]][1]

Is my approach a bad one? What can I do to get rid of this issue? Any suggestion, link and information is welcome.

Thank you in advance!


  [1]: https://i.stack.imgur.com/UGMpn.png

</details>


# 答案1
**得分**: 2

以下是您要翻译的内容:

"不要嘲笑`useAppDispatch`,它是一个使用`react-redux`包的hook,使用`useDispatch` hook。错误的嘲笑会破坏其原始实现。相反,我们可以提供一个模拟的存储,并测试Redux操作是否被分发以及Redux状态是否正确更新。

例如,

`registration.ts`:
```ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

type IRegistrationState = {
  data: {
    email: {
      value: string;
      error: boolean;
      errorText: string | null;
    };
  };
};

export const registrationFormSlice = createSlice({
  name: 'registrationForm',
  initialState: {
    data: {
      email: {
        value: '',
        error: false,
        errorText: null,
      },
    },
  },
  reducers: {
    setEmail(state: IRegistrationState, action: PayloadAction<{ value: string; error: boolean; text: string }>) {
      state.data.email.value = action.payload.value;
      state.data.email.error = action.payload.error;
      state.data.email.errorText = action.payload.text;
    },
  },
});

store.ts

import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';
import { registrationFormSlice } from './registration';

const rootReducer = combineReducers({
  registrationForm: registrationFormSlice.reducer,
});

export const setupStore = () => {
  return configureStore({
    reducer: rootReducer,
    devTools: process.env.NODE_ENV !== 'production',
  });
};

export type RootState = ReturnType<typeof rootReducer>;
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore['dispatch'];
export const useAppDispatch: () => AppDispatch = useDispatch;

useFields.ts

import { useAppDispatch } from './store';
import { registrationFormSlice } from './registration';

export interface IgetFields {
  label: string;
  valueKey: string;
  setValue: any;
  disabled?: boolean;
}

export const useFields = (): (() => IgetFields[][]) => {
  const dispatch = useAppDispatch();

  const { setEmail } = registrationFormSlice.actions;

  const getFields = (): IgetFields[][] => {
    return [
      [
        {
          label: 'Email',
          valueKey: 'email',
          setValue: (value: string) => dispatch(setEmail({ value, error: false, text: '' })),
        },
      ],
    ];
  };
  return getFields;
};

useFields.test.tsx

import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { Provider } from 'react-redux';
import { setupStore } from './store';
import { useFields } from './useFields';

describe('useFields', () => {
  const store = setupStore();
  const setup = () => {
    const { result } = renderHook(useFields, {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });
    return result.current();
  };

  it('Must be an array of arrays', () => {
    const getFields = setup();
    expect(Array.isArray(getFields)).toBeTruthy();
  });

  it('An array with index 0 must have certain properties and structure', () => {
    const getFields = setup()[0];
    expect(getFields).toHaveLength(1);
    expect(getFields[0]).toEqual(
      expect.objectContaining({
        label: expect.any(String),
        valueKey: expect.any(String),
        setValue: expect.any(Function),
      })
    );
  });

  test('should set email value correctly', () => {
    expect.assertions(1);
    const getFields = setup()[0];
    const firstField = getFields[0];
    store.subscribe(() => {
      expect(store.getState().registrationForm.data).toEqual({
        email: {
          value: 'test@example.com',
          error: false,
          errorText: '',
        },
      });
    });
    firstField.setValue('test@example.com');
  });
});

测试结果:

 PASS  stackoverflow/76031614/useFields.test.tsx (7.997 s)
  useFields
    ✓ Must be an array of arrays (14 ms)
    ✓ An array with index 0 must have certain properties and structure (3 ms)
    ✓ should set email value correctly (4 ms)

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |                   
 registration.ts |     100 |      100 |     100 |     100 |                   
 store.ts        |     100 |      100 |     100 |     100 |                   
 useFields.ts    |     100 |      100 |     100 |     100 |                   
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        8.462 s, estimated 10 s

请注意,已经进行了代码的翻译,如有任何其他需要,请告诉我。

英文:

Don't mock useAppDispatch which is a hook use the useDispatch hook from the react-redux package. An incorrect mock will break its original implementation. Instead, we can provide a mock store and test if the redux actions are dispatched and redux state is updated correctly.

E.g.

registration.ts:

import { createSlice, PayloadAction } from &#39;@reduxjs/toolkit&#39;;

type IRegistrationState = {
  data: {
    email: {
      value: string;
      error: boolean;
      errorText: string | null;
    };
  };
};

export const registrationFormSlice = createSlice({
  name: &#39;registrationForm&#39;,
  initialState: {
    data: {
      email: {
        value: &#39;&#39;,
        error: false,
        errorText: null,
      },
    },
  },
  reducers: {
    setEmail(state: IRegistrationState, action: PayloadAction&lt;{ value: string; error: boolean; text: string }&gt;) {
      state.data.email.value = action.payload.value;
      state.data.email.error = action.payload.error;
      state.data.email.errorText = action.payload.text;
    },
  },
});

store.ts:

import { combineReducers, configureStore } from &#39;@reduxjs/toolkit&#39;;
import { useDispatch } from &#39;react-redux&#39;;
import { registrationFormSlice } from &#39;./registration&#39;;

const rootReducer = combineReducers({
  registrationForm: registrationFormSlice.reducer,
});

export const setupStore = () =&gt; {
  return configureStore({
    reducer: rootReducer,
    devTools: process.env.NODE_ENV !== &#39;production&#39;,
  });
};

export type RootState = ReturnType&lt;typeof rootReducer&gt;;
export type AppStore = ReturnType&lt;typeof setupStore&gt;;
export type AppDispatch = AppStore[&#39;dispatch&#39;];
export const useAppDispatch: () =&gt; AppDispatch = useDispatch;

useFields.ts:

import { useAppDispatch } from &#39;./store&#39;;
import { registrationFormSlice } from &#39;./registration&#39;;

export interface IgetFields {
  label: string;
  valueKey: string;
  setValue: any;
  disabled?: boolean;
}

export const useFields = (): (() =&gt; IgetFields[][]) =&gt; {
  const dispatch = useAppDispatch();

  const { setEmail } = registrationFormSlice.actions;

  const getFields = (): IgetFields[][] =&gt; {
    return [
      [
        {
          label: &#39;Email&#39;,
          valueKey: &#39;email&#39;,
          setValue: (value: string) =&gt; dispatch(setEmail({ value, error: false, text: &#39;&#39; })),
        },
      ],
    ];
  };
  return getFields;
};

useFields.test.tsx:

import { renderHook } from &#39;@testing-library/react-hooks&#39;;
import React from &#39;react&#39;;
import { Provider } from &#39;react-redux&#39;;
import { setupStore } from &#39;./store&#39;;
import { useFields } from &#39;./useFields&#39;;

describe(&#39;useFields&#39;, () =&gt; {
  const store = setupStore();
  const setup = () =&gt; {
    const { result } = renderHook(useFields, {
      wrapper: ({ children }) =&gt; &lt;Provider store={store}&gt;{children}&lt;/Provider&gt;,
    });
    return result.current();
  };

  it(&#39;Must be an array of arrays&#39;, () =&gt; {
    const getFields = setup();
    expect(Array.isArray(getFields)).toBeTruthy();
  });

  it(&#39;An array with index 0 must have certain properties and structure&#39;, () =&gt; {
    const getFields = setup()[0];
    expect(getFields).toHaveLength(1);
    expect(getFields[0]).toEqual(
      expect.objectContaining({
        label: expect.any(String),
        valueKey: expect.any(String),
        setValue: expect.any(Function),
      })
    );
  });

  test(&#39;should set email value correctly&#39;, () =&gt; {
    expect.assertions(1);
    const getFields = setup()[0];
    const firstField = getFields[0];
    store.subscribe(() =&gt; {
      expect(store.getState().registrationForm.data).toEqual({
        email: {
          value: &#39;test@example.com&#39;,
          error: false,
          errorText: &#39;&#39;,
        },
      });
    });
    firstField.setValue(&#39;test@example.com&#39;);
  });
});

Test result:

 PASS  stackoverflow/76031614/useFields.test.tsx (7.997 s)
  useFields
    ✓ Must be an array of arrays (14 ms)
    ✓ An array with index 0 must have certain properties and structure (3 ms)
    ✓ should set email value correctly (4 ms)

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |                   
 registration.ts |     100 |      100 |     100 |     100 |                   
 store.ts        |     100 |      100 |     100 |     100 |                   
 useFields.ts    |     100 |      100 |     100 |     100 |                   
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        8.462 s, estimated 10 s

huangapple
  • 本文由 发表于 2023年4月17日 11:56:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76031614.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定