In our project we are using react-oidc-context (which uses oidc-client-ts) to authenticate a user.
react-oidc-context exposes useAuth
, which contains information like isAuthenticated
, isLoading
, the user
object when authenticated, etc.
In order to fully test my components I need to mock it with different return values in each test. Normally I would do this with jest.spyOn
and mockImplementation
to set the right conditions for my test.
However, the number of (nested) properties that useAuth
returns is pretty big and may be prone to change in the future, so I don't want to type everything out. I just want to pass the properties that I care about. But typescript doesn't allow that.
An example:
// 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;
// Login.test.tsx
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,
})
);
// renderWithProviders wraps the normal testing-library render with the routing provider
renderWithProviders(<Login />);
expect(screen.findByRole('button', { name: 'Sign in' }));
});
});
In the above code typescript will complain that the mockImpelmentation doesn't match AuthContextProps. And it is right! 12 direct props are missing and many more deeply nested ones.
If I try to trick 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' }));
});
});
Now I get a runtime error: TypeError: Cannot redefine property: useAuth
Crap.
I have tried many different mocking tricks but everything fails at some point.
Going back to the drawing board, I tried to forego the whole mocking thing and just setup a Provider with fake credentials. Basically what I do for react-router.
This will work for the unauthenticated state, but to my knowledge I can't fake the authenticated state.
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' }));
});
});
So the last thing I can think of is to write some helper to generate a big return object for useAuth
that satisfies TS.
Like I said I've been putting it off, because this doesn't seem very future-proof.
Anyone an idea how to fix this and make it pretty?
After a good night sleep, I finally found a way. Inspired by this answer: https://stackoverflow.com/a/73761102/1783174
In jest-setup I create the default mocked return values for
useAuth
For reference, here is the Login component file that we tested