前提・実現したいこと
ReduxのcreateAsyncThunkで作成した、API通信関数を、
コンポーネントのテスト内で呼び出すことができません。
具体的には、
expect(store.dispatch).toHaveBeenCalledWith();
の、toHaveBeenCalledWithの引数内で呼び出したいです。
解決法をご教示いただけますと幸いでございます。
発生している問題・エラーメッセージ
● <Auth /> › Register mode test expect(jest.fn()).toHaveBeenCalledWith(...expected) Expected: [Function anonymous] Received 1: {"payload": undefined, "type": "user/toggleMode"} 2: [Function anonymous] 3: [Function anonymous] Number of calls: 4 95 | expect(store.dispatch).toHaveBeenCalledTimes(4); 96 | expect(store.dispatch).toHaveBeenCalledWith(toggleMode()); > 97 | expect(store.dispatch).toHaveBeenCalledWith(fetchAsyncRegister(cred)); | ^ 98 | }); 99 | }); 100 | at Object.<anonymous> (src/features/user/Auth.test.tsx:97:28)
該当のソースコード
以下は、テストに用いるための、前提となるコード群です。
//types.ts export interface CRED { email: string; password: string; } export interface JWT { token: string; } export interface USER { uuid: string; email: string; } export interface PROFILE { id: number; user_id: string; name: string; image: string; } export interface USER_STATE { isLoginView: boolean; }
//userSlice.ts export const fetchAsyncLogin = createAsyncThunk( "user/login", async (auth: CRED) => { const res = await axios.post<JWT>( `${process.env.REACT_APP_API_URL}/login`, auth, { headers: { "Content-Type": "application/json", }, } ); return res.data; } ); export const fetchAsyncRegister = createAsyncThunk( "user/register", async (auth: CRED) => { const res = await axios.post<USER>( `${process.env.REACT_APP_API_URL}/signup`, auth, { headers: { "Content-Type": "application/json", }, } ); return res.data; } ); export const fetchAsyncCreateProf = createAsyncThunk( "user/createProfile", async () => { const res = await axios.post<PROFILE>( `${process.env.REACT_APP_API_URL}/user/profile`, { image: null }, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.localJWT}`, }, } ); return res.data; } ); const initialState: USER_STATE = { isLoginView: true, }; export const userSlice = createSlice({ name: "user", initialState, reducers: { toggleMode(state) { state.isLoginView = !state.isLoginView; }, }, extraReducers: (builder) => { builder.addCase( fetchAsyncLogin.fulfilled, (state, action: PayloadAction<JWT>) => { localStorage.setItem("localJWT", action.payload.token); } ); }, }); export const { toggleMode, } = userSlice.actions; export const selectIsLoginView = (state: RootState) => state.user.isLoginView; export default userSlice.reducer;
//Auth.tsx const Auth: React.FC = () => { const classes = useStyles(); const dispatch: AppDispatch = useDispatch(); const isLoginView = useSelector(selectIsLoginView); const [credential, setCredential] = useState({ email: "", password: "" }); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; const name = e.target.name; setCredential({ ...credential, [name]: value }); }; const login = async () => { if (isLoginView) { await dispatch(fetchAsyncLogin(credential)); } else { await dispatch(fetchAsyncRegister(credential)); await dispatch(fetchAsyncLogin(credential)); await dispatch(fetchAsyncCreateProf()); } }; return ( <Container component="main" maxWidth="xs"> <CssBaseline /> <div className={classes.paper}> <Avatar className={classes.avatar}> <LockOutlinedIcon /> </Avatar> <Typography component="h1" variant="h5"> {isLoginView ? "Login" : "Register"} </Typography> <div className={classes.form}> <TextField variant="outlined" margin="normal" required fullWidth label="Email" type="text" name="email" value={credential.email} onChange={handleInputChange} data-testid="email" /> <TextField variant="outlined" margin="normal" required fullWidth label="Password" type="password" name="password" value={credential.password} onChange={handleInputChange} data-testid="password" /> <Button type="submit" fullWidth variant="contained" color="primary" className={classes.submit} onClick={login} data-testid="loginButton" > {isLoginView ? "Login" : "Register"} </Button> <Grid container alignItems="center" justify="center" className={classes.toggle} > <Grid item> <Link variant="body2" onClick={() => dispatch(toggleMode())} data-testid="toggleButton" > {isLoginView ? "Create new account ?" : "Back to Login"} </Link> </Grid> </Grid> </div> </div> </Container> ); }; export default Auth;
//testTools.ts import { configureStore, EnhancedStore } from "@reduxjs/toolkit"; import userReducer from "./user/userSlice"; import boardReducer from "./board/boardSlice"; const makeStore = () => { const store: EnhancedStore = configureStore({ reducer: { user: userReducer, board: boardReducer, }, }); return store; }; export const makeTestStore = () => { const store = makeStore(); const origDispatch = store.dispatch; store.dispatch = jest.fn(origDispatch); return store; };
そして、以下がテストコードになります。
//Auth.test.tsx const cred: CRED = { email: "test@gmail.com", password: "test" }; describe("<Auth />", () => { it("Register mode test", async () => { const store = makeTestStore(); render( <Provider store={store}> <Auth /> </Provider> ); const emailInputField: HTMLInputElement = screen .getByTestId("email") .querySelector("input") as HTMLInputElement; const passwordInputField: HTMLInputElement = screen .getByTestId("password") .querySelector("input") as HTMLInputElement; const loginButton = screen.getByTestId("loginButton") as HTMLElement; const toggleButton = screen.getByTestId("toggleButton") as HTMLElement; expect(emailInputField).toBeTruthy; expect(passwordInputField).toBeTruthy; expect(loginButton).toBeTruthy; expect(toggleButton).toBeTruthy; //ここまでは全て成功。要素の取り出しはできている。 userEvent.type(emailInputField, cred.email); expect(emailInputField.value).toEqual("test@gmail.com"); userEvent.type(passwordInputField, cred.password); expect(passwordInputField.value).toEqual("test"); userEvent.click(toggleButton); userEvent.click(loginButton); await new Promise((resolve) => setTimeout(resolve, 3000)); expect(store.dispatch).toHaveBeenCalledTimes(4); /*これも成功。useDispatchが問題なく作動していること、 そしてそれらがボタンのクリックで起動することが確認できた。 */ expect(store.dispatch).toHaveBeenCalledWith(toggleMode()); //これも成功。createSliceのreducerは呼び出せている。 expect(store.dispatch).toHaveBeenCalledWith(fetchAsyncRegister(cred)); //これが通らない…。 }); });
試したこと
toHaveBeenCalledWithの引数内を、{type: "user/login"}や、
{type: fetchAsyncLogin.fulfilled.type}にしても、うまくいきません。
{"type": "user/toggleMode"}を引数にすると通りますが…。
補足情報(FW/ツールのバージョンなど)
nodeのバージョン:v12.18.4
yarnのバージョン:1.22.4
また、testTools.ts内の関数は、下記を参考にしております。
https://blog.krawaller.se/posts/unit-testing-react-redux-components/
あなたの回答
tips
プレビュー