0

我们如何确保自定义钩子实际上调用了另一个钩子公开的方法?

比方说,我有一个useName内部利用的自定义钩子useState

import { useState } from 'react'

export const useName = () => {

    const [name, setState] = useState()

    const setName = (firstName: string, lastName: string) => setState([firstName, lastName].join(' '))

    return {name, setName}
}

我需要断言调用setName实际上调用了“setState”。我的测试用例编写如下:

/**
* @jest-environment jsdom
*/

import * as React from 'react'
import { renderHook, act } from '@testing-library/react-hooks'
import { useName } from './useName'

jest.mock('react')

const setState = jest.fn()

React.useState.mockReturnValue(['ignore', setState]) //overwriting useState

test('ensures that setState is called', () => {
    
    const {result} = renderHook(() => useName())

    act(() => {
        result.current.setName("Kashif", "Nazar") //I am expecting this to hit jest.fn() declared above.
    })

    expect(setState).toBeCalled()

})

我得到以下结果。

 FAIL  src/useName.test.ts
  ✕ ensures that setState is called (3 ms)

  ● ensures that setState is called

    TypeError: Cannot read property 'setName' of undefined

      18 |
      19 |     act(() => {
    > 20 |         result.current.setName("Kashif", "Nazar")
         |                        ^
      21 |     })
      22 |
      23 |     expect(setState).toBeCalled()

      at src/useName.test.ts:20:24
      at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:22380:12)
      at act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1042:14)
      at Object.<anonymous> (src/useName.test.ts:19:5)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:404:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.32 s, estimated 1 s
Ran all test suites.

这可能吗,我这样做是否正确?

4

1 回答 1

0

您应该测试返回的状态而不是实现 detail( setState)。Mock 可能会破坏setState. 这会导致测试用例通过,但被测代码将在实际运行时失败。并且模拟也使测试容易受到攻击,当您的实现细节发生变化时,您的测试用例必须相应地更改,例如模拟新对象。

我只测试是否满足接口,不管实现细节如何变化,对吧

useName.ts

import { useState } from 'react';

export const useName = () => {
  const [name, setState] = useState('');
  const setName = (firstName: string, lastName: string) => setState([firstName, lastName].join(' '));
  return { name, setName };
};

useName.test.ts

import { renderHook, act } from '@testing-library/react-hooks';
import { useName } from './useName';

describe('70381825', () => {
  test('should pass', () => {
    const { result } = renderHook(() => {
      console.count('render');
      return useName();
    });
    expect(result.current.name).toBe('');
    act(() => {
      result.current.setName('Kashif', 'Nazar');
    });
    expect(result.current.name).toBe('Kashif Nazar');
    act(() => {
      result.current.setName('a', 'b');
    });
  });
});

测试结果:

 PASS  examples/70381825/useName.test.ts
  70381825 - mock way
    ○ skipped should pass
  70381825
    ✓ should pass (29 ms)

  console.count
    render: 1

      at examples/70381825/useName.test.ts:31:15

  console.count
    render: 2

      at examples/70381825/useName.test.ts:31:15

  console.count
    render: 3

      at examples/70381825/useName.test.ts:31:15

Test Suites: 1 passed, 1 total
Tests:       1 skipped, 1 passed, 2 total
Snapshots:   0 total
Time:        1.251 s, estimated 8 s

现在,如果您坚持使用模拟方式。你应该只模拟useStateReact 的钩子。jest.mock('react')将为 React 导出的所有方法、属性和函数创建模拟,这将破坏它们的函数。

例如

useName.test.ts

import { renderHook, act } from '@testing-library/react-hooks';
import { useName } from './useName';
import React from 'react';

jest.mock('react', () => {
  return { ...(jest.requireActual('react') as any), useState: jest.fn() };
});

describe('70381825 - mock way', () => {
  test('should pass', () => {
    const setState = jest.fn();
    (React.useState as jest.MockedFunction<typeof React.useState>).mockReturnValue(['ignore', setState]);
    const { result } = renderHook(() => {
      console.count('render');
      return useName();
    });
    act(() => {
      result.current.setName('a', 'b');
    });
    expect(result.current.name).toBe('ignore');
    expect(setState).toBeCalled();
    act(() => {
      result.current.setName('c', 'd');
    });
  });
});

测试结果:

 PASS  examples/70381825/useName.test.ts (7.885 s)
  70381825 - mock way
    ✓ should pass (29 ms)
  70381825
    ○ skipped should pass

  console.count
    render: 1

      at examples/70381825/useName.test.ts:14:15

Test Suites: 1 passed, 1 total
Tests:       1 skipped, 1 passed, 2 total
Snapshots:   0 total
Time:        8.487 s

好的。你知道为什么在我们调用 的时候 mock 方式只渲染一次,而另一种方式渲染 3 次setName吗?正如我之前所说。

于 2021-12-17T02:43:50.223 回答