如何对 Vuex 进行单元测试

简介: 如何对 Vuex 进行单元测试

Vuex 是一个专为 Vue.js 应用程序开发的状态管理库,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

通常来说 Vue 组件会在以下方面和 Vuex 发生交互:

  1. commit 一个 mutation
  2. dispatch 一个 action
  3. 通过 $store.state 或 getters 访问 state

本文将探讨在单元测试中,如何覆盖 Vuex 部分的问题。

store 所执行的任何逻辑,诸如 mutations 和 getters,都能被单独地测试 -- 因为 Vuex stores 由普通 JavaScript 函数组成,所以它们也易于被单元测试;同时这些测试都是基于 Vuex store 的当前 state 来断言组件行为是否正常的,并不需要知道  mutators、actions 或 getters 的实现。

1. 测试 Mutations

创建 mutation

mutations 易于遵循一套模式:取得一些数据,可能进行一些处理,然后将数据赋值给 state。比如一个 ADD_POST mutation 的概述如下:一旦被实现,它将从 payload 中获取一个 post 对象,并将 post.id 添加到 state.postIds 中;它也会将那个 post 对象以 post.id 为 key 添加到 state.posts 对象中。这即是在应用中使用 Vuex 的一个通常的模式。

我们将使用 TDD 进行开发。mutation 是这样开头的:


export default {
  SET_POST(state, { post }) {
  }
}

让我们开始写测试,并让报错信息指引我们的开发:


import mutations from "@/store/mutations.js"
describe("SET_POST", () => {
  it("adds a post to the state", () => {
    const post = { id: 1, title: "Post" }
    const state = {
      postIds: [],
      posts: {}
    }
    mutations.SET_POST(state, { post })
    expect(state).toEqual({
      postIds: [1],
      posts: { "1": post }
    })
  })
})

yarn test:unit 运行测试将产生以下错误信息:


FAIL  tests/unit/mutations.spec.js
 SET_POST  adds a post to the state
  expect(received).toEqual(expected)
  Expected value to equal:
    {"postIds": [1], "posts": {"1": {"id": 1, "title": "Post"}}}
  Received:
    {"postIds": [], "posts": {}}

让我们从将 post.id 加入 state.postIds 开始:


export default {
  SET_POST(state, { post }) {
    state.postIds.push(post.id)
  }
}

现在 yarn test:unit 会产生:


Expected value to equal:
  {"postIds": [1], "posts": {"1": {"id": 1, "title": "Post"}}}
Received:
  {"postIds": [1], "posts": {}}

postIds 看起来挺好了。现在我们只需要将 post 加入 state.posts。限于 Vue 反应式系统的工作方式我们无法简单地写成 post[post.id] = post 来添加 post。更多细节可以在 这里找到。基本上,你需要使用 Object.assign... 操作符创建一个新的对象。此处我们将使用 ... 操作符将 post 赋值到 state.posts


export default {
  SET_POST(state, { post }) {
    state.postIds.push(post.id)
    state.posts = { ...state.posts, [post.id]: post }
  }
}

测试都通过了~

2. 测试 actions

创建 action

我们会遵循一个通常的 Vuex 模式创建一个 action:

  1. 发起一个向 API 的异步请求
  2. 对数据进行一些处理(可选)
  3. 根据 payload 的结果 commit 一个 mutation

这里有一个 认证 action,用来将 username 和 password 发送到外部 API 以检查它们是否匹配。然后其认证结果将被用于通过 commit 一个 SET_AUTHENTICATED mutation 来更新 state,该 mutation 将认证结果作为 payload。


import axios from "axios"
export default {
  async authenticate({ commit }, { username, password }) {
    const authenticated = await axios.post("/api/authenticate", {
      username, password
    })
    commit("set_authenticated", authenticated)
  }
}

action 的测试应该断言:

  1. 是否使用了正确的 API 端?
  2. payload 是否正确?
  3. 根据结果,是否有正确的 mutation 被 commit

让我们进行下去并编写测试,并让报错信息指引我们。

编写测试


describe("authenticate", () => {
  it("authenticated a user", async () => {
    const commit = jest.fn()
    const username = "alice"
    const password = "password"
    await actions.authenticate({ commit }, { username, password })
    expect(url).toBe("/api/authenticate")
    expect(body).toEqual({ username, password })
    expect(commit).toHaveBeenCalledWith(
      "SET_AUTHENTICATED", true)
  })
})

因为 axios 是异步的,为保证 Jest 等到测试完成后才执行,我们需要将其声明为 async 并在其后 await 那个 actions.authenticate 的调用。不然的话(译注:即假如不使用 async/await 而仅仅将 3 个 expect 断言放入异步函数的 then() 中)测试会早于 expect 断言完成,并且我们将得到一个常绿的 -- 一个不会失败的测试。

运行以上测试会给我们下面的报错信息:



FAIL  tests/unit/actions.spec.js
   authenticate  authenticated a user
    SyntaxError: The string did not match the expected pattern.
      at XMLHttpRequest.open (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:482:15)
      at dispatchXhrRequest (node_modules/axios/lib/adapters/xhr.js:45:13)
      at xhrAdapter (node_modules/axios/lib/adapters/xhr.js:12:10)
      at dispatchRequest (node_modules/axios/lib/core/dispatchRequest.js:59:10)

这个错误来自 axios 的某处。我们发起了一个对 /api... 的请求,并且因为我们运行在一个测试环境中,所以并不是真有一个服务器在处理请求,这就导致了错误。我们也没有定义 urlbody -- 我们将在解决掉 axios 错误后做那些。

因为使用了 Jest,我们可以用 jest.mock 容易地 mock 掉 API 调用。我们将用一个 mock 版本的 axios 代替真实的,使我们能更多地控制其行为。Jest 提供了 ES6 Class Mocks,非常适于 mock axios

axios 的 mock 看起来是这样的:


let url = ''
let body = {}
jest.mock("axios", () => ({
  post: (_url, _body) => { 
    return new Promise((resolve) => {
      url = _url
      body = _body
      resolve(true)
    })
  }
}))

我们将 urlbody 保存到了变量中以便断言正确的时间端点接收了正确的 payload。因为我们不想实现真正的端点,用一个理解 resolve 的 promise 模拟一次成功的 API 调用就够了。

yarn test:unit 现在测试通过了!

测试 API Error

咱仅仅测试过了 API 调用成功的情况,而测试所有产出的可能情况也是重要的。让我们编写一个测试应对发生错误的情况。这次,我们将先编写测试,再补全实现。

测试可以写成这样:


it("catches an error", async () => {
  mockError = true
  await expect(actions.authenticate({ commit: jest.fn() }, {}))
    .rejects.toThrow("API Error occurred.")
})

我们要找到一种强制 axios mock 抛出错误的方法。正如 mockError 变量代表的那样。将 axios mock 更新为:


let url = ''
let body = {}
let mockError = false
jest.mock("axios", () => ({
  post: (_url, _body) => { 
    return new Promise((resolve) => {
      if (mockError) 
        throw Error()
      url = _url
      body = _body
      resolve(true)
    })
  }
}))

只有当一个 ES6 类 mock 作用域外的(out-of-scope)变量以 mock 为前缀时,Jest 才允许访问它(译注:查看文档)。现在我们简单地赋值 mockError = true 然后 axios 就会抛出错误了。

运行该测试给我们这些报错:


FAIL  tests/unit/actions.spec.js
 authenticate  catchs an error
  expect(function).toThrow(string)
  Expected the function to throw an error matching:
    "API Error occurred."
  Instead, it threw:
    Mock error

成功的抛出了一个错误... 却并非我们期望的那个。更新 authenticate 以达到目的:


export default {
  async authenticate({ commit }, { username, password }) {
    try {
      const authenticated = await axios.post("/api/authenticate", {
        username, password
      })
      commit("SET_AUTHENTICATED", authenticated)
    } catch (e) {
      throw Error("API Error occurred.")
    }
  }
}

现在测试通过了。

3. 测试 getters

我们考虑一个用两个 getters 操作一个 store 的案例,看起来是这样的:


const state = {
  dogs: [
    { name: "lucky", breed: "poodle", age: 1 },
    { name: "pochy", breed: "dalmatian", age: 2 },
    { name: "blackie", breed: "poodle", age: 4 }
  ]
}

对于 getters 我们将测试:

  1. poodles: 取得所有 poodles
  2. poodlesByAge: 取得所有 poodles,并接受一个年龄参数

创建 getters

首先,创建 getters。


export default {
  poodles: (state) => {
    return state.dogs.filter(dog => dog.breed === "poodle")
  },
  poodlesByAge: (state, getters) => (age) => {
    return getters.poodles.filter(dog => dog.age === age)
  }
}

并没有什么特别令人兴奋的 -- 记住 getter 可以接受其他的 getters 作为第二个参数。因为我们已经有一个 poodles getter 了,可以在 poodlesByAge 中复用它。通过在 poodlesByAge 返回一个接受参数的函数,我们可以向 getters 中传入参数。poodlesByAge getter 用法是这样的:


computed: {
  puppies() {
    return this.$store.getters.poodlesByAge(1)
  }
}

让我们从测试 poodles 开始吧。

编写测试

鉴于一个 getter 只是一个接收一个 state 对象作为首个参数的 JavaScript 函数,所以测试起来非常简单。我将把测试写在 getters.spec.js 文件中,代码如下:


import getters from "../../src/store/getters.js"
const dogs = [
  { name: "lucky", breed: "poodle", age: 1 },
  { name: "pochy", breed: "dalmatian", age: 2 },
  { name: "blackie", breed: "poodle", age: 4 }
]
const state = { dogs }
describe("poodles", () => {
  it("returns poodles", () => {
    const actual = getters.poodles(state)
    expect(actual).toEqual([ dogs[0], dogs[2] ])
  })
})

Vuex 会自动将 state 传入 getter。因为我们是单独地测试 getters,所以还得手动传入 state。除此之外,我们就是在测试一个普通的 JavaScript 函数。

poodlesByAge 则更有趣一点了。传入一个 getter 的第二个参数是其他 getters。我们正在测试的是 poodlesByAge,所以我们不想将 poodles 的实现牵扯进来。我们通过 stub 掉 getters.poodles 取而代之。这将给我们对测试更细粒度的控制。


describe("poodlesByAge", () => {
  it("returns poodles by age", () => {
    const poodles = [ dogs[0], dogs[2] ]
    const actual = getters.poodlesByAge(state, { poodles })(1)
    expect(actual).toEqual([ dogs[0] ])
  })
})

不同于向 getter 传入真实的 poodles(译注:刚刚测试过的另一个 getter),我们传入的是一个它可能返回的结果。因为之前写过一个测试了,所以我们知道它是工作正常的。这使得我们把测试逻辑单独聚焦于 poodlesByAge

async 的 getters 也是可能的。它们可以通过和测试 async actions 的相同技术被测试。



相关文章
|
JavaScript 测试技术 API
vue項目加入单元测试模块,使用jest
vue項目加入单元测试模块,使用jest
115 0
|
6天前
|
JavaScript 测试技术 API
常见的 Vue 3 单元测试问题及解决方案
常见的 Vue 3 单元测试问题及解决方案
20 2
|
6天前
|
JavaScript 测试技术
Vue 3 单元测试实例
Vue 3 单元测试实例
17 4
|
6天前
|
缓存 JavaScript 测试技术
Vue 3 单元测试最佳实践
Vue 3 单元测试最佳实践
10 1
|
3月前
|
JavaScript 测试技术 API
顺藤摸瓜🍉:用单元测试读懂 vue3 中的 defineComponent
顺藤摸瓜🍉:用单元测试读懂 vue3 中的 defineComponent
|
6月前
|
资源调度 JavaScript 测试技术
Vue的集成测试:使用VueTestUtils进行单元测试的技术博文
【4月更文挑战第24天】本文介绍了如何使用VueTestUtils进行Vue.js项目的集成测试。首先,需安装VueTestUtils和vue-template-compiler。接着,展示了如何编写测试用例,包括使用`mount`和`shallowMount`方法挂载组件,以及通过`wrapper`操作和断言组件行为。文章还讨论了单元测试与集成测试的区别,并提到了模拟依赖、交互、组件状态管理和断言的策略。最后,强调了测试的可读性和可维护性对代码质量的重要性。通过VueTestUtils,开发者能更高效地进行Vue组件的测试。
|
6月前
|
资源调度 JavaScript 测试技术
单元测试:编写和运行Vue组件的单元测试
【4月更文挑战第23天】本文探讨了为Vue组件编写单元测试的重要性,以及如何设置测试环境、编写和运行测试。通过使用Jest或Mocha作为测试框架,结合Vue Test Utils,可以独立测试组件的功能,如渲染、事件处理和状态管理。编写测试用例时,应注意覆盖各种行为,并使用断言验证组件状态。运行测试并观察结果,确保测试独立性和高覆盖率。单元测试是保证代码质量和维护性的关键,应随着项目发展持续更新测试用例。
134 3
|
前端开发 JavaScript 测试技术
用Jest做前端单元测试
前端单元测试概念听着很高大上,应该也是从后端的单元测试借鉴过来的,但在工作中我其实从来没做过。前端各种开发调试工具本身比较优秀了,最简单的 console、debugger 完全可以测试,虽说是一次性的,但是本身前端变化就比较快。
88 0
|
资源调度 JavaScript 前端开发
react+jest+enzyme配置及编写前端单元测试UT
react+jest+enzyme配置及编写前端单元测试UT
140 0
|
运维 JavaScript 前端开发
单元测试(jest):理解、安装、使用
单元测试(jest):理解、安装、使用
139 0