+6

Viết Unit Test dễ dàng hơn với phương pháp này

Nhìn qua về unit test

image.png Bài viết này phù hợp với những người đã biết về unit test, nên mình sẽ không giải thích các khái niệm căn bản nữa và sẽ đi thẳng vào những phần chính

Unit test giống như chúng ta viết lại một lần component đấy, nhưng khi ứng dụng càng lớn, nhiều thư viện được sử dụng như Vuex, Vue Router... nó sẽ khiên cho các file test trở nên rất dài, đôi khi còn dài hơn cả component cần test

Ví dụ về một component có sử dụng store và router như thế này

<!-- CreateTodo.vue -->
<script>
export default {
  name: 'CreateTodo',
  computed: {
    user() {
      return this.$store.state.user
    }
  },
  methods: {
    submit() {
      this.$router.push({ name: 'List' })
    }
  }
}
</script>

Khi tiến hành test chúng ta sẽ cần setup router và store để có thể test được component này

// CreateTodo.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import CreateTodo from '@/components/CreateTodo'
import userModule from '@/store/modules/user'
import VueRouter from 'vue-router'
import Vuex from 'vuex'

const localVue = createLocalVue()
localVue.use(VueRouter)
localVue.use(Vuex)
// Tạo localVue sử dụng VueRouter và Vuex

const router = new VueRouter({
  routes: [
    {
      path: '/list',
      name: 'List',
      component: () => import('@/components/List.vue')
    }
  ]
})
// Tạo router liên kết với component List.vue

const store = new Vuex.Store({
  modules: {
    user: userModule
  }
})
// Tạo store với user module

let wrapper
beforeEach(() => {
  wrapper = shallowMount(CreateTodo, {
    localVue,
    router,
    store
  })
  // Tạo một wrapper từ shallowMount với router và store để sử dụng
})

afterEach(() => {
  wrapper.destroy()
})

describe('Test CreateTodo', () => {
  ...
})

Như vậy, khi có nhiều component sử dụng store và router, đều phải setup lại những phần router và store lặp đi lặp lại thế này, nên bài viết này mình sẽ cấu trúc lại để có thể viết test một cách ngắn ngọn và đơn giản hơn

Tổ chức thư mục

test
└── unit               
    ├── components
    |   └── CreateTodo.spec.js
    └── methods.js                 

Tạo các phương thức tái sử dụng

// methods.js
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
// có ba thứ cần dùng là shallowMout, mount và createLocalVue
import Vuex from 'vuex';
import VueRouter from 'vue-router';
import { routes } from '@/router/index';
import user from '@/store/modules/user';
// import routes và các module từ router và store của bạn

export const localVue = createLocalVue();
localVue.use(VueRouter);
localVue.use(Vuex);
// Tạo localVue sử dụng VueRouter và Vuex

export const createStore = (customModule = {}) => new Vuex.Store({
  modules: {
    user,
    ...customModule,
  },
});
// createStore đã được cấu hình sẵn với các module được import từ store của bạn 
// Có thể custom với giá trị đầu vào nhận một custom module có kiểu dữ liệu object, sẽ được merge với các module mặc định

export const createRouter = (customRoute = []) => new VueRouter({
  routes: [
    ...routes,
    ...customRoute
  ],
});
// createRouter đã được cấu hình sẵn với các routes được import từ router của bạn
// Có thể custom với giá trị đầu vào nhận vào một custom route có kiểu dữ liệu array chứ các object routes, sẽ được merge với các routes mặc định


export const createMock = ((customMock = {}) => ({
  // thêm các mock mặc định ở đây
  $t: () => '', // ví dụ với i18n
  ...customMock,
}));
// createMock với các mock mặc định và customMock dùng để custom

export const createStub = ((customStub = {}) => ({
  // thêm các stub mặc định ở đây
  'ButtonComponent': { template: '<div></div>' },
  ...customStub,
}));
// createStub với các stub mặc định và customStub dùng để custom

export const createShallow = (component, options = {}) => shallowMount(component, {
  stubs: createStub(),
  store: createStore(),
  router: createRouter(),
  mocks: createMock(),
  localVue,
  ...options,
});
// Khởi tạo hàm createShallow, hàm này sẽ trả về hàm shallowMount của vue-test-utils
// Nó đã được nhận vào sẵn các stubs, store, router, mocks từ các hàm bên trên
// Và đầu vào sẽ nhận 2 giá trị là component và object chứa các options như shallowMount 

export const createDeep = (component, options = {}) => mount(component, {
  stubs: createStub(),
  store: createStore(),
  router: createRouter(),
  mocks: createMock(),
  localVue,
  ...options,
});
// Tương tự như createShallow chỉ khác là sử dụng mount thay vì shallow

Hướng dẫn sử dụng

Và sau đó khi sử dụng chỉ cần dùng

import CreateTodo from '@/components/CreateTodo'
import { createShallow } from '../methods'

let wrapper
beforeEach(() => {
  wrapper = createShallow(CreateTodo)
})

afterEach(() => {
  wrapper.destroy()
})

describe('Test CreateTodo', () => {
  ...
})

So sánh với ví dụ ban đầu thì số lượng code đã được giảm thiểu rõ rệt

Từ giờ thay vì gọi hàm shallowMount từ vue-test-utils chúng ta chỉ cần sử dụng createShallow được viết sẵn từ file methods.js

wrapper được tạo từ createShallow đã có đầy đủ store, router, mock và stub...

Tùy chỉnh

Những trường hợp cần tùy chỉnh trong quá trình test sẽ xảy ra không nhiều. Mặc dù vậy, các function vẫn cho phép chúng ta sử dụng trong những trường hợp cần phải tùy chỉnh

Router

import { createShallow, createRouter } from '../methods'
import TodoComponent from '@/components/Todo'

const customRouter = [
  {
    path: '/custom-router',
    name: 'CustomRouter',
    component: () => import('@/components/CustomRouter.vue')
  }
]
// Tạo một custom router

const router = createRouter(customRouter)
// createRouter() sẽ merge customRouter với các router hiện tại

let wrapper
beforeEach(() => {
  wrapper = createShallow(TodoComponent, {
    router
  })
  // truyền options là router mới sẽ thay thế router cũ
})

Store

import { createShallow, createStore } from '../methods'
import TodoComponent from '@/components/Todo'
import todo from '@/store/modules/todo'

const state = {
  todos: [
    { title: "newTodo" }
  ]
}
// Tạo state với dữ liệu mẫu để test

const store = createStore({
  todo: { 
    ...todo, 
    state 
  }
})
// Replace state và giữ lại getters, mutations, actions
// createStore() sẽ merge các module vào vuex

let wrapper
beforeEach(() => {
  wrapper = createShallow(TodoComponent, {
    store
  })
  // truyền options là store mới sẽ thay thế store cũ
})

Mock

import { createShallow, createMock } from '../methods'
import TodoComponent from '@/components/Todo'

const mocks = createMock({
  $t: () => '',
})
// createMock() sẽ merge các mock mới với các mock mặc định

let wrapper
beforeEach(() => {
  wrapper = createShallow(TodoComponent, {
    mocks
  })
  // truyền options là mocks mới sẽ thay thế mocks cũ
})

Stub

import { createShallow, createStub } from '../methods'
import TodoComponent from '@/components/Todo'

const stubs = createStub({
  'CustomStub': { template: '<div></div>' },
})
// createStub() sẽ merge các stubs mới với các stubs mặc định

let wrapper
beforeEach(() => {
  wrapper = createShallow(TodoComponent, {
    stubs
  })
  // truyền options là stubs mới sẽ thay thế stubs cũ
})

Kết luận

Đây là một phương pháp giúp chúng ta giảm thiểu số lượng code cần viết mỗi khi test một các đơn giản và ngắn gọn


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí