模拟 API 调用时未填充 Redux 存储

Posted

技术标签:

【中文标题】模拟 API 调用时未填充 Redux 存储【英文标题】:Redux store not being populated when mocking API call 【发布时间】:2022-01-09 11:19:01 【问题描述】:

我在 react 中编写了一个注册组件,它是一个简单的表单,提交时会发布到 API。调用 API 会返回一个带有特定数据的对象,然后这些数据会被添加到 redux 存储中。

我为此写了一些测试。我正在使用 Mock Service Worker (MSW) 来模拟 API 调用。这是我第一次编写此类测试,所以我不确定我是否做错了什么,但我的理解是 MSW 会拦截对 API 的调用并返回我在 MSW 配置中指定的任何内容,之后它应该遵循常规流程。

这是我的减速器:

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);
            );
    

这是我的动作创建者:

export function createNewUser(userData) 
    return 
        type: REGISTER_NEW_USER,
        payload: userData
    

这是我的注册组件中的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 调用的值,但这并没有似乎没有发生。如果这不是在商店中测试价值的方法,我还能如何实现呢?

所以在我的测试结束时,在打印我期望看到的商店时:

 auth:  user:

                'email': faker.internet.email(),
                'token': faker.datatype.uuid(),
                'active': true,
                'loggedIn': true,
            

但我看到的是:

  auth:  user: null  

这是本次测试的正确方法吗?

谢谢


编辑

基于 cmets 进行一些重构。现在我的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。另请注意,此函数现在为 asyncawait,以便完成对 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

我想我在某处遗漏了一些异步代码,但我无法确定它在哪里。

【问题讨论】:

当您在浏览器中正常运行您的应用程序时,这行console.log(newUser); 是否记录newUser 的正确值?看来您没有从 user 类中的 register 方法返回任何内容。 @MrCujo 您没有正确等待 onSubmit 处理程序的 xcompletion。根据 gunwin 的回答,也许尝试等待大约 200 毫秒的延迟 怎么回事? await user.register(data)不是等待数据返回的方式吗?老实说,我不认为添加延迟是最好的选择,同步/等待就足够了,我可能肯定做错了,但应该有一个正确的解决方案,只使用同步/等待而不需要添加延迟跨度> 【参考方案1】:

这里有一些 Redux 规则被打破:

    不要在减速器中产生副作用: reducer 应该是纯函数:对于相同的输入,总是返回 相同的输出。这里不是进行 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;
    

然后,应该在您的事件处理程序中完成异步工作:

onSubmit:

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());
        
    

**不要忘记在你的 register 函数中从 axios 返回承诺,这样你就可以等待承诺。目前,你只是调用 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 中,您在同一承诺上同时执行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 子句不是在承诺上调用 .catch。 使用await 可以让您像编写同步代码一样编写代码,因此只需在await 表达式之后的下一行写下您要放入.then 的任何内容。

也在你的注册函数中:

async register(data) 
    try 
        let res = await axios.post(...);
        return res; 
     catch(e) 
        console.log("error: ", e);
    

【讨论】:

谢谢@deckele!我实施了您的大部分建议,并且似乎部分有效。部分原因是有时我会在商店中看到正确的值,而有时我会看到 null,我想我在某处遗漏了一些异步代码,请检查我更新的问题(在 EDIT 标记之后) @MrCujo 你很亲密!有一个错误你将await.then 组合在一起以获得令人困惑的结果......特别是在onSubmitawait user.register(data).then(...) 是一个错误,因为你正在等待.then 之后的结果,但你实际上想要首先等待register 调用,然后才对结果进行处理。 @MrCujo 用示例查看我编辑的答案 谢谢。我现在了解await.then 之间的区别。我已经进行了建议的代码更改,但结果似乎是一样的。有时我会得到null,有时我会在商店中得到预期值【参考方案2】:

状态不会立即更新,因为服务器调用是一个承诺。您应该等待页面上的某些内容表明该过程已完成,如下所示:

        // Click on the Submit button
        await act(async () => 
            userEvent.click(screen.getByTestId('register-submit-button'));

            await wait(() => getByText('Some text that appears after success '));

            // verify the store was populated
            console.log(await store.getState())
        );

或者你可以等待更新:

        // Click on the Submit button
        await act(async () => 
            userEvent.click(screen.getByTestId('register-submit-button'));
            
            await act(() => sleep(500));

            // verify the store was populated
            console.log(await store.getState())
        );

【讨论】:

这正是我正在做的,除了这部分 await wait(() =&gt; getByText('Some text that appears after success ')); 因为页面中不应该显示任何内容,在现实生活中它应该被重定向到不同的页面并且应该填充商店跨度> 我已经更新了答案 这似乎不起作用。我添加了一个setTimeout,但结果还是一样。我认为这不是原因,我相信在 act 语句中添加内容的全部意义在于等待承诺得到解决 这是正确的,您应该只在api调用完成后检查商店。 - 但还要确保new User(). register () 调用返回一个承诺并等待。目前它返回undefined @MarcRo 是的,我实现了这些更改,现在有时当我运行我的测试时,我会从商店获得正确的价值,而其他时候我会得到null。请在 EDIT 标记后查看我更新的问题。

以上是关于模拟 API 调用时未填充 Redux 存储的主要内容,如果未能解决你的问题,请参考以下文章

Gmail API 游乐场:发送方法,转换后的 MIME 原始标头在发送时未填充电子邮件字段

如何使用 props 在 React 中自动填充可编辑的 redux-form 字段?

React - 使用 Redux 状态填充第一个选择选项

使用 OpenIdConnect 时未填充 .Net Core Identity 用户

在 Redux 中更改 props 但不更改状态的 API 调用应该放在哪里?

直接导航到组件时未呈现道具 - React Redux