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

标签 javascript reactjs unit-testing redux msw

我在 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 中执行。另请注意,此函数现在为 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

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

最佳答案

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

  1. 不要在 reducers 中产生副作用: reducers 应该是纯函数:对于相同的输入,总是返回 相同的输出。这里不是执行 API 调用的地方。
  2. 状态应该是不可变的:您永远不应通过引用更改状态值,始终使用包含更改的新对象提供新状态。

因此,经典的 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/

相关文章:

java - 如何对同步代码进行单元测试

c# - 测试 MVC Controller 方法上的属性数量

javascript - Jquery - 拦截由 ajax 请求创建的链接

javascript - React.js 上下文 API : How to only update a value pair of an object while maintaining other pairs?

javascript - 如何使用knockout 3.0及更高版本在UI上渲染数据

javascript - ReactJS(初学者)使用 Youtube API - 返回的对象不允许我访问 VideoId 属性

javascript - 将 Javascript 图像对象直接分配给页面

javascript - 如何在 Meteor 中重新渲染 React 组件?Meteor 中的 React component.forceUpdate 错误

reactjs - 当使用 React Component 作为参数时为 "Type is missing the following properties, context, setState"

web-services - 如何使用 Rhino Mocks 模拟 WCF Web 服务