用typescript模拟jest中react-oidc-context的返回值

tp5buhyn  于 2023-11-15  发布在  Jest
关注(0)|答案(1)|浏览(143)

在我们的项目中,我们使用react-oidc-context(它使用oidc-client-ts)对用户进行身份验证。
react-oidc-context公开useAuth,其中包含诸如isAuthenticatedisLoading、验证时的user对象等信息。
为了完全测试我的组件,我需要在每个测试中用不同的返回值来模拟它。通常我会用jest.spyOnmockImplementation来设置正确的测试条件。
然而,useAuth返回的(嵌套)属性的数量相当大,而且将来可能会发生变化,所以我不想把所有的东西都输入出来,我只想传递我关心的属性,但typescript不允许这样做。
一个例子:

// Login.tsx
// AuthProvider has redirect_uri set that points back to this file

import { Button } from 'react-components';
import { useAuth } from 'react-oidc-context';
import { Navigate } from 'react-router-dom';

const Login = (): JSX.Element => {
  const auth = useAuth();

  if (auth.isAuthenticated) {
    return <Navigate to="/dashboard" replace />;
  }

  return (
    <main style={{ display: 'flex', justifyContent: 'center', paddingTop: '5rem' }}>
      <Button variant="secondary" onClick={() => auth.signinRedirect()}>
        Sign in
      </Button>
    </main>
  );
};

export default Login;

个字符
在上面的代码中,打字机会抱怨mockImplementation与AuthContextProps不匹配。这是正确的!缺少12个直接属性和更多的深层嵌套属性。
如果我试图欺骗TS:

// Login.test.tsx with type casting

import { renderWithProviders, screen } from '@test/utils';
import * as oidc from 'react-oidc-context';

import Login from '../Login';

describe('Login view', () => {
  it('Shows a login button', () => {
    jest.spyOn(oidc, 'useAuth').mockImplementation(
      () =>
        ({
          isAuthenticated: false,
        }) as oidc.AuthContextProps // <--- NEW
    );

    renderWithProviders(<Login />);
    expect(screen.findByRole('button', { name: 'Sign in' }));
  });
});


现在我得到一个运行时错误:TypeError: Cannot redefine property: useAuth
废话了
我试过许多不同的嘲笑技巧,但在某个时候都失败了。
回到绘图板上,我试图放弃整个嘲笑的事情,只是设置一个提供者与假证书。基本上是我做的React路由器。
这将适用于未验证状态,但据我所知,我无法伪造已验证状态。

import { renderWithProviders, screen } from '@test/utils';

import Login from '../Login';

describe('Login view', () => {
  it('Shows a login button', () => {
    renderWithProviders(
      <AuthProvider
        {...{
          authority: 'authority',
          client_id: 'client',
          redirect_uri: 'redirect',
        }}
      >
        <Login />
      </AuthProvider>
    );
    expect(screen.findByRole('button', { name: 'Sign in' }));
  });
});


所以我能想到的最后一件事就是编写一些helper来为useAuth生成一个满足TS的大的返回对象。
就像我说的,我一直在推迟,因为这似乎不是很未来的证据。
有谁知道怎么把它修好,弄得漂亮一点吗?

inkz8wg9

inkz8wg91#

睡了一个好觉之后,我终于找到了一个方法。灵感来自这个答案:https://stackoverflow.com/a/73761102/1783174

// jest.config.ts

{
  setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
}

字符串
在jest-setup中,我为useAuth创建了默认的模拟返回值

// test/jest-setup.ts

jest.mock('react-oidc-context', () => ({
  // The result of this mock can be altered by changing `mockAuthenticatedStatus` in your test
  useAuth() {
    const { isLoading, isAuthenticated } = getMockAuthStatus();
    return {
      isLoading,
      isAuthenticated,
      signinRedirect: jest.fn(),
      removeUser: jest.fn(),
      settings: {},
    };
  },
}));
// test/utils.ts

// ..in this file is also the renderWithProviders function that adds the react-router MemoryBrowser..

export const mockAuthenticatedStatus = {
  isLoading: false,
  isAuthenticated: false,
};

export const getMockAuthStatus = () => {
  return mockAuthenticatedStatus;
};
// views/__tests__/Login.test.tsx

import { mockAuthenticatedStatus, renderWithProviders, screen } from '@test/utils';
import * as router from 'react-router-dom';

import Login from '../Login';

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  Navigate: jest.fn(),
}));

describe('Login view', () => {
  it('Shows a login button', () => {
    // BY DEFAULT WE ARE LOGGED OUT
    renderWithProviders(<Login />);
    expect(screen.findByRole('button', { name: 'Sign in' }));
  });

  it('Navigates when logged in', () => {
    // HERE WE CHANGE THE AUTH STATUS FOR THIS TEST
    mockAuthenticatedStatus.isAuthenticated = true;
    const navigateSpy = jest.spyOn(router, 'Navigate');
    renderWithProviders(<Login />);
    expect(navigateSpy).toHaveBeenCalled();
  });
});

作为参考,下面是我们测试的Login组件文件

// views/Login.tsx

import { Button } from 'react-components';
import { useAuth } from 'react-oidc-context';
import { Navigate } from 'react-router-dom';

import { signIn } from '../auth/utils';

const Login = (): JSX.Element => {
  const auth = useAuth();

  if (auth.isAuthenticated) {
    return <Navigate to="/" replace />;
  }

  return (
    <main style={{ display: 'flex', justifyContent: 'center', paddingTop: '5rem' }}>
      <Button variant="secondary" onClick={() => signIn(auth)}>
        Sign in
      </Button>
    </main>
  );
};

export default Login;

相关问题