发布于 2026-01-06 0 阅读
0

为 Vuex 编写优秀的测试

为 Vuex 编写优秀的测试

读完这篇文章后,你应该去Manning 的网站购买并阅读 Edd Yerburgh 所著的《Testing Vue.js Applications》。Edd Yerburgh 是@vue/test-utils的作者,也是 Vue 核心团队的测试专家。

在使用 Vuex 两年后,我将解释我用来测试应用程序 store 的两种方法,以及我发现哪种方法在不断发展的企业应用程序中更有效。

测试整个模块

通过创建 Vuex store 的实例并通过 store 的界面进行测试,将我们的整个模块(包括 actions / commits / getters)一起进行测试。

这种程度的测试已经越过了界限,进入了集成测试(它有其优点和缺点),但由于我们的操作、变更和获取器本身就高度耦合,因此出于以下几个原因,这样做是有意义的。

+ 我们会同时测试提交和操作。先分发操作,然后检查所有外部操作、服务调用和状态更改是否都已发生,这似乎是一种合理且直观的模块测试方法。

+ 重构时,我们更有可能发现操作、提交和获取器之间的通信错误。

+ 通过只能在测试 getter 时通过我们的操作来构建状态,我们的代码覆盖率工具会在模块中代码分支不再可访问时立即给出反馈。

然而,

我们需要创建一个包含所有其他模块依赖项的 store 实例。这可能会产生额外的样板代码,这些代码需要维护和更新。

- 我发现,虽然这种方法对于模块相对解耦的小型应用程序效果很好,但随着应用程序变得越来越复杂,它就无法扩展了。

- 如果落入水平不高的开发人员手中,这种方法很快就会变成难以阅读、难以维护的混乱局面。

例如,

// app.module.spec.js
import Vuex from 'vuex';
import AppModule from '~store/app.module';
import merge from 'lodash/merge';

// a factory function is a good way 
// of DRY-ing up your tests
function createStore() {
    const getPosts = jest.fn();
    return {
        store: new Vuex.Store({
            modules: {
                app: AppModule,
                blog: { // a mocked dependency
                    namespaced: true,
                    actions: {
                        getPosts,
                    },
                },
            },
        }),
        spies: {
            // use the full path to the action
            // to make it clear what module it is in
            'blog/getPosts': getPosts, 
        },
    };
}

test('refreshing app state', async () => {
    const {store, spies} = createStore();
    const refreshPromise = store.dispatch('app/refresh');

    expect(store.getters['app/isLoading']).toBeTruthy();
    expect(spies['blog/getPosts']).toHaveBeenCalled();
    await refreshPromise;
    expect(store.getters['app/isLoading']).toBeFalsy();
});

test('refreshing app state failure', async () => {
    const error = new Error();
    const {store, spies} = createStore();
    spies['blog/getPosts'].mockImplementation(() => throw error);

    const refreshPromise = store.dispatch('app/refresh');
    expect(store.getters['app/isLoading']).toBeTruthy();
    expect(spies['blog/getPosts']).toHaveBeenCalled();
    await refreshPromise;
    expect(store.getters['app/error']).toBe(error);
    expect(store.getters['app/isLoading']).toBeFalsy();
});

测试模块的各个部分

通过直接测试构成模块的每个部分(操作、提交、获取器)来测试我们的模块。

+ 这是测试模块最快捷、最易于维护的方法,尤其是在重构时。

+ 它可以随着我们模块的复杂性而扩展,因为我们可以完全控制输入到单元中的参数。

+ 通过坚持单元测试,我们可以编写出易于编写的测试。

然而,

由于我们没有测试模块各部分之间的集成,因此这种方法无法防止此类错误。这是单元测试的主要缺陷。

- 水平较差的开发人员很容易陷入编写包含过多实现细节的测试的常见陷阱,例如 test('当 isLoading 为真时不要调用 getBlogs')。

例如,

// app.actions.spec.js
import Vuex from 'vuex';
import {refresh} from '~store/app.actions';
import merge from 'lodash/merge';

test('refreshing app state', async () => {
    const store = {
        commit: jest.fn(),
        dispatch: jest.fn(),
    };

    await refresh(store);
    expect(store.dispatch).toHaveBeenCalledWith('blog/getPosts', null, {root: true});
});

test('refreshing app state failure', async () => {
    const error = new Error();
    const store = {
        commit: jest.fn(),
        dispatch: jest.fn().mockImplementationOnce(() => throw error),
    };

    await refresh(store);
    expect(store.dispatch).toHaveBeenCalledWith('blog/getPosts', null, {root: true});
    expect(store.commit).toHaveBeenCalledWith('setError', error)
});

最后想说的

归根结底,作为开发人员,你需要审视你的测试策略,并在不同类型的测试之间找到平衡,以最大限度地减少错误并提高应用程序的可靠性。

我在 Zoro 工作期间,曾使用上述两种方式编写测试。为了确保我的应用程序 Vuex store 的变更能够顺利发布且无 bug,直接测试 actions、commits 和 getter 方法,并配合一套端到端测试,能够在易于编写和可靠性之间取得最佳平衡。

文章来源:https://dev.to/shannonarcher/write-great-tests-for-vuex-1p9d