我在 React 中写了一个 Register 组件,它是一个简单的表单,在提交时将发布到 API。对 API 的调用将返回一个包含特定数据的对象,然后该数据将被添加到 redux 存储中。
我为此编写了一些测试。我正在使用 Mock Service Worker (MSW) 来模拟 API 调用。这是我第一次编写此类测试,所以我不确定我是否做错了什么,但我的理解是 MSW 会拦截对 API 的调用并返回我在 MSW 配置中指定的任何内容,之后它应该遵循常规流程。
这是我的 reducer :
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = new User().register(
action.payload.email,
action.payload.firstName,
action.payload.lastName,
action.payload.password
)
console.log("User registered data back:");
console.log(newUser);
return {
...state,
'user': newUser
}
default:
return state;
}
}
这是执行实际调用的我的用户类:
import axios from "axios";
import { REGISTER_API_ENDPOINT } from "../../api";
export default class User {
/**
* Creates a new user in the system
*
* @param {string} email - user's email address
* @param {string} firstName - user's first name
* @param {string} lastName - user's last name
* @param {string} password - user's email address
*/
register(email, firstName, lastName, password) {
// console.log("registering...")
axios.post(REGISTER_API_ENDPOINT, {
email,
firstName,
lastName,
password
})
.then(function (response) {
return {
'email': response.data.email,
'token': response.data.token,
'active': response.data.active,
'loggedIn': response.data.loggedIn,
}
})
.catch(function (error) {
console.log('error');
console.log(error);
});
}
}
这是我的 Action 创作者:
export function createNewUser(userData) {
return {
type: REGISTER_NEW_USER,
payload: userData
}
}
这是我的 Register 组件中的 onSubmit
方法:
const onSubmit = data => {
// console.log(data);
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
dispatch(createNewUser(data))
}
这是我的实际测试:
describe('Register page functionality', () => {
const server = setupServer(
rest.post(REGISTER_API_ENDPOINT, (req, res, ctx) => {
console.log("HERE in mock server call")
// Respond with a mocked user object
return res(
ctx.status(200),
ctx.json({
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}))
})
)
// Enable API mocking before tests
beforeEach(() => server.listen());
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
// Disable API mocking after the tests are done.
afterAll(() => server.close())
it('should perform an api call for successful registration', async () => {
// generate random data to be used in the form
const email = faker.internet.email();
const firstName = faker.name.firstName();
const lastName = faker.name.lastName();
const password = faker.internet.password();
// Render the form
const { store } = renderWithRedux(<Register />);
// Add values to the required input fields
const emailInput = screen.getByTestId('email-input')
userEvent.type(emailInput, email);
const firstNameInput = screen.getByTestId('first-name-input');
userEvent.type(firstNameInput, firstName);
const lastNameInput = screen.getByTestId('last-name-input');
userEvent.type(lastNameInput, lastName);
const passwordInput = screen.getByTestId('password-input');
userEvent.type(passwordInput, password);
const confirmPasswordInput = screen.getByTestId('confirm-password-input');
userEvent.type(confirmPasswordInput, password);
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
// verify the store was populated
console.log(await store.getState())
});
});
所以我希望只要检测到 REGISTER_API_ENDPOINT url 就会拦截我的调用,并将模拟调用的值添加到我的 redux 状态,而不是 register
中的实际 API 调用的值> 方法,但这似乎没有发生。如果这不是在商店中测试值(value)的方法,我还能如何实现?
所以在我的测试结束时,在打印我期望看到的商店时:
{ auth: { user:
{
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}
}
但我看到的是:
{ auth: { user: null } }
这是测试的正确方法吗?
谢谢
编辑
根据评论进行一些重构。现在我的 onSubmit
方法看起来像:
const onSubmit = async data => {
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
let user = new User()
await user.register(data).
then(
data => {
// console.log("Response:")
// console.log(data)
// create cookies
cookie.set("user", data.email);
cookie.set("token", data.token);
dispatch(createNewUser(data))
}
).catch(err => console.log(err))
请注意,现在我在此处发送来自 User.register
的响应,而不是在 User.register
中执行。另请注意,此函数现在为 async
和 await
以完成 register
函数调用,届时它将填充存储。
register
方法现在如下所示:
async register(data) {
let res = await axios.post(REGISTER_API_ENDPOINT, {
'email': data.email,
'firstName': data.firstName,
'lastName': data.lastName,
'password': data.password
})
.then(function (response) {
return response
})
.catch(function (error) {
console.log('error');
console.log(error);
});
return await res.data;
}
现在它只负责执行 API 调用并返回响应。
reducer 也被简化为没有任何副作用的变化,所以它看起来像:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = action.payload
return {
...state,
'user': newUser
}
default:
return state;
}
}
我的测试基本相同,唯一的区别是我检查 store
值的部分:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
});
await waitFor(() => {
// verify the store was populated
console.log("Store:")
console.log(store.getState())
})
现在,这有时有效,有时无效。意思是,有时我得到正确的商店打印如下:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{
auth: {
user: {
email: 'Selena.Tremblay@hotmail.com',
token: '1a0fadc7-7c13-433b-b86d-368b4e2311eb',
active: true,
loggedIn: true
}
}
}
at test/pages/Register.test.js:220:21
但有时我会得到 null
:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{ auth: { user: null } }
at test/pages/Register.test.js:220:21
我想我在某处遗漏了一些异步代码,但我无法确定它在哪里。
最佳答案
这里有一些 Redux 规则被打破了:
- 不要在 reducers 中产生副作用: reducers 应该是纯函数:对于相同的输入,总是返回 相同的输出。这里不是执行 API 调用的地方。
- 状态应该是不可变的:您永远不应通过引用更改状态值,始终使用包含更改的新对象提供新状态。
因此,经典的 redux 方法是在 Redux 中执行三个操作:REGISTER_USER、REGISTER_USER_SUCCEEDED、REGISTER_USER_FAILED。
reducer
:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_USER:
return {
...state,
status: 'loading'
}
case actionTypes.REGISTER_USER_SUCCEEDED:
return {
...state,
status: 'idle',
user: action.user
}
case actionTypes.REGISTER_USER_FAILED:
return {
...state,
status: 'error'
}
default:
return state;
}
}
然后,异步工作应该在您的事件处理程序中完成:
提交
:
const onSubmit = async data => {
// ...
dispatch(registerNewUser());
const user = new User()
try {
await user.register(data);
dispatch(registerNewUserSucceeded(user));
} catch(e) {
console.error(e);
dispatch(registerNewUserFailed());
}
}
**不要忘记在您的注册函数中返回来自 axios 的 promise ,这样您就可以等待 promise 。目前,你只是在调用 axios,而不是更新或返回任何东西......
这有什么好处,就是测试您的商店不需要您进行任何网络调用!您可以放弃 MSW(尽管它是一个很棒的库,只是这里不需要)。
在您的测试中,只需在每次转换前后检查您的商店状态:
const mockUser = {...} // provide a mock user for your test
const store = createStore(authReducer);
store.dispatch(registerNewUserSucceeded(mockUser);
expect(store.getState()).toEqual({user: mockUser, status: 'idle'});
编辑
响应提问者的编辑,现在有一个错误,因为 await
与 .then
的混淆组合。
具体来说,在 onSubmit
中,您在同一个 promise 上执行 await
和 .then
。在这种情况下,存在竞争条件。 .then
调用首先发生,然后 await
发生。
所以不是 await user.register(data).then(...)
:
const onSubmit = async data => {
// ...
try {
await user.register(data);
} catch(e) {
console.log(e);
}
dispatch(createNewUser(data));
}
这里我只使用 await。 try
/catch
子句不是在 promise 上调用 .catch
。
使用 await
可以让您像在编写同步代码一样编写代码,因此只需在 await
之后的下一行中写入您要放入 .then
中的任何内容> 表达。
同样在你的注册函数中:
async register(data) {
try {
let res = await axios.post(...);
return res;
} catch(e) {
console.log("error: ", e);
}
}
关于javascript - 模拟 API 调用时未填充 Redux 存储,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70208379/