A ce life is a mock life.

preface

Through vivid and detailed examples to guide you through all the puzzles and problems in the vUE unit testing process.

Must first collect, it is not difficult to foresee, when you really need and go to see, will come from the heart of a sentence: not empty this hide.

Technology stack: JEST, VUe-test-utils. There are four parts: runtime, Mock, Stub, Configuring, and CLI.

The runtime

When running test cases, your first stumbling block is bound to be various Undifned errors.

There is a history of blood and tears to resolve these errors, most of which are due to lack of runtime variables or asynchrony. Here we only talk about runtime, basically there are two types:

1. Environment variables such as Window are missing

This is usually solved by introducing global-Jsdom, which is also officially recommended. Of course, we can also declare the definition directly in the test code ourselves.

For example, we used sessionStorage in our business code.

// procudtpay.vue
<script>
const sessionParams = window.sessionStorage.getItem('sessionParams')
export default {
  data () { }
}
</script>
Copy the code

We then redefine it directly in our test code so that at run time, we actually get the values we defined here.

// procudtpay.spec.js
window.sessionStorage = {
  getItem: () = > {
    return { name:'name'.type:'type'}}}import procudtpay from '.. /views/procudtpay.vue'
Copy the code

Here’s a little extra information about the order of execution:

The assignment of sessionParams in the example is introduced in import. The vue module performs this assignment, so the assignment of the sessionStorage definition needs to come before the import.

If your sessionStorage value is created after vUE instantiation, such as in Created, then this is not a problem.

If you want to override or mock some read-only properties, such as window.loAction or document.getelementById. Direct assignment is invalid

document.getElementById = jest.fn() / / not to take effect
Copy the code

We can use the Object.defineProperty() method

Object.defineProperty(document.'getElementById', {
  value: () = >{
  	return { offsetWidth: 100}}});Copy the code

2. Missing global properties and methods defined/registered in main.js

This requires introducing the same model in the test code and mocks or stubs via the mount configuration items mocks and stubs, respectively.

// main.js
import Vue from 'vue'
import Mint from 'mint-ui'
import '.. /filter'
import axios from 'axios'
Vue.use(Mint)
Vue.prototype.$post = (url, params) = > {
  return axios.post(url, params).then(res= > res.data)
}
Vue.filter('filterxxx'.function (value) {
  / / bala bala ba...
})

// xxx.spec.js
import Vue from 'vue'
import '.. /.. /filter/filter'   // Introduce the registration filter
Vue.filter('filterxxx'.function (value) {
  / / bala bala ba...
})
import { $post } from './http.js' 
it('Snapshot Test'.() = > {
    const wrapper = shallowMount(ProductPay, {
      mocks: {
        $post  // Replace real HTTP requests with self-defined mock data
      },
      stubs: ['mt-header'] // Stub component
    })
    // ...
})
Copy the code

Usually other test files will also rely on these global variables, which can be reused by configuring Jest’s setupFiles.

Mock

I looked at the code. There were no comments, and every crooked line said ‘Assert true.’ I can not sleep, carefully look at the middle of the night, just from the word seam to see the word, full screen are written with two words: ‘fake’!

A ce is a shi, and he mocks it. Mock is important anyway.

Mock simple functions

Let’s start with the simplest mock function.

For example, we now want to test that when the user makes a successful purchase, they expect the page to jump to the results page.

// productpay.vue
<script>
export default {
  methods: {
    onCommmit() {
      this.$post('xxx', params).then(data= > {
        this.$router.push(`/payresult`)
      })
    }
  }
}
</script>
Copy the code

We can mock out the $router push method and then assert that it has been called with the correct parameters for testing purposes.

// productpay.spec.js
it('When the purchase is successful, the page should be redirected to the results page'.async() = > {const mockFunc = jest.fn()
  const wrapper = shallowMount(ProductPay, {
    mocks: {
      $post,
      $router: {
        push: mockFunc
      }
    }
  })
  wrapper.vm.commmit() // Submit the purchase
  expect(mockFunc).toHaveBeenCalledWith('/payresult')})Copy the code

2. Mock the Http request to specify the return result

The difference between the HTTP request and the $router in the above example is that it requires a return value. Jest has several ways of specifying return values, in this case mockImplementation.

// test/**.spec.js
it('When user XXXX, should XXXX'.async() = > {const respSuccess = { data: [...]. .code:0 }
	const respError = { data: [...]. .code:888 }
	// Define mock functions
	const mockPost = jest.fn() 
	const wrapper = shallowMount(index, { 
	   mocks: {
        $post:mockPost // Apply the mock function}})// Specify asynchronously to return data
   mockPost.mockImplementation(() = > Promise.resolve(respError))
   // The invocation can be asserted
   expect(mockPost).toHaveBeenCalled() 
  
   mockPost.mockImplementation(() = > Promise.resolve(respSuccess))
   // You can also wait for the asynchrony to end and assert the result
   await flushPromises()
   expect(wrapper.vm.list).toEqual(respSuccess.data)
})
Copy the code

In fact, our project calls a lot of interfaces and often returns a lot of data. If all of this is defined in the test code, it will be bloated. At this point, we can do a simple modularization of this feature.

// Common business code
// Axios is mounted to the vue instance in main.js
Vue.prototype.$post = (url, params) = > {
  return axios.post(url, params).then(res= > res.data)
}
// request in index.vue
getProductList () {
    this.$post('/ProductListQry', {}).then(data= > {
        this.ProductList = data.List
    })
}
Copy the code
// 1. Store simulation data data/ productListqry.js in a separate js file
export default {
	data: [{id:1.name:'name'. },... ] .code:0
}

// 2. Define the post method and match the data to test/http.js
import ProductListQry from '@/data/ProductListQry.js'
const mockData = {
  ProductListQry,
  ... // More mock data can be introduced in the same way
}
const $post = (url = ' ') = > {
  return new Promise((resolve, reject) = > {
    const jsName = String(url).split('/') [1]
    resolve(mockData[jsName])
  })
}
export { $post }

// 3. Import and use test/index.spec.js
import Index from '@/views/Index.vue'
import { $post } from './http.js'
it('... '.() = >{
    const wrapper = shallowMount(Index, {
      mocks: {
        $post
      }
    })
    wrapper.vm.getProductList() // Trigger the request
    await flushPromises() // Wait for the asynchronous request to end
    // We can see the data we specified in the wrapper
    console.log(wrapper.vm.ProductList) 
})
Copy the code

Similarly, if you want to test for failed requests, you can define a method that returns error data, such as $postError.

// test/**.spec.js
import { $postError } from './http.js'
it('... '.() = >{
    const wrapper = shallowMount(Index, {
        mocks: {
            $post:$postError
        }
    })
    
    wrapper.vm.getProductList() // Trigger the request
    await flushPromises() // Wait for the asynchronous request to end
    
    // We can test the case where we get the wrong data
    console.log(wrapper.vm.ProductList) 
})
Copy the code

Mock the entire module

When an imported component/method is used directly in business code, we may need to mock the entire module to test it. Here is a popover form validation scenario:

// productpay.vue
<script>
import { MessageBox } from '.. /Component'
export default {
    methods:{
        makeSurebuy () {
            let payAmount = delcommafy(this.payAmount)
                if(! payAmount) { MessageBox({message: 'Please enter the purchase amount first'
                })
                return
            }
            if (payAmount < this.resData.BaseAmt) {
                MessageBox({
                    message: 'Purchase amount should not be less than minimum deposit amount'
                })
                return
            }
            if (payAmount > this.Balance) {
                MessageBox({
                    message: 'Purchase amount cannot exceed available balance'
                })
                return
            }
            // Check pass, initiate transaction...
        }
    }
}
<script>
Copy the code
//productpay.spce.js
import Component from '.. /Component'
jest.mock('.. /.. /.. /components/ZyComponent')

it('When the user clicks the buy button, there should be an error message if the user enters an illegal amount.'.async () => {
    wrapper.findAll('.btn-commit').at(0).trigger('click')
    expect(Component.MessageBox.mock.calls[0] [0])
        .toEqual({ message: 'Please enter the purchase amount first' })
    
    wrapper.setData({payAmount: '100'})
    
    wrapper.findAll('.btn-commit').at(0).trigger('click')
    expect(Component.MessageBox.mock.calls[1] [0])
        .toEqual({ message: 'Purchase amount should not be less than minimum deposit amount' })
    
    wrapper.setData({payAmount: '100000000000000000'})
    
    wrapper.findAll('.btn-commit').at(0).trigger('click')
    expect(Component.MessageBox.mock.calls[2] [0])
        .toEqual({ message: 'Purchase amount cannot exceed available balance'})})Copy the code

Mock us through jest. Mock () throughout the module, when the module method is invoked after it will have a mock properties, by ZyComponent. ZyMessageBox. Mock a visit, Including ZyComponent. ZyMessageBox. Mock. Calls will return is called an array, we can according to the number of data to the function is invoked, the situation assertion test refs.

Stub component

With unit testing, we theoretically do not and should not test the child components in its test cases, otherwise it is called integration testing. Vue-test-utils mocks components by configuring stubs.

const wrapper = shallowMount(index, {
    stubs: ['mt-header'.'mt-loadmore']}Copy the code

However, there will inevitably be calls to subcomponent methods in the business, such as mint-UI’s loadMore.

// procuctlist.vue
<script>
export default {
  methods: {
    getProductList() {
      this.$post('xxx', params).then(data= > {
        this.productList = this.productList.concat(data.List)
        this.$refs.loadmore.onBottomLoaded() / / this
      })
    }
  }
}
</script>
Copy the code

In this case, we can use the mount method to render the child component, so that $refs will normally get the child component instance. However, it would be more appropriate to customize the internal implementation of the stub component to meet testing requirements.

// procuctlist.spec.js
it('When users pull up the product list, they should be able to see more products'.() = > {
    const mockOnBottomLoaded = jest.fn()
    const mtLoadMore = {
      render: () = >{},methods: {
        onBottomLoaded: mockOnBottomLoaded
      }
    }
    const mtHeader = {
      render: () = >{}}const wrapper = shallowMount(Index, {
      stubs: { 'mt-loadmore': mtLoadMore, 'mt-header': mtHeader },
      mocks: {
        $post
      }
    })
    const currentPage = wrapper.vm.currentPage

    wrapper.vm.loadMoreProduction()

    expect(wrapper.vm.currentPage).toEqual(currentPage + 1)
    expect(mockOnBottomLoaded).toHaveBeenCalled()
})
Copy the code

Finally, after stub components, business code subcomponents are still introduced, but not instantiated and rendered.

You and CLI

1. Statistical code coverage ignores certain files

Jest have to provide the corresponding configuration items coveragePathIgnorePatterns, but did not take effect after use.

// jest.config.js
{
    coveragePathIgnorePatterns: ['<rootDir>/src/assets/']}Copy the code

It turns out from official documentation that the babel-plugin-Istanbul plugin is responsible. When using the Babel – plugin – Istanbul, Babel processing each file has collect code coverage, so coveragePathIgnorePatterns won’t ignore it. The Babel-plugin-Istanbul plugin has a exclude configuration to solve this problem, but it’s not what I want. And I ended up commenting out Istanbul in balelrc.

2. In T mode, only the specified test case can be executed

When you write a lot of test cases, running a lot of test cases at a time is inefficient, and if you have a lot of consoles in your code, it’s even harder to find an error. I thought it would be nice to test only the current use case. T mode is found, jest command with –watch parameter into the listening mode, and then enter t, then enter the matching rule. Suddenly the world was quiet and comfortable…

// package.json
{
    "scripts": {"tets":"jest --watch"}}Copy the code

3. Vue-awes-swiper test fails

If swiper is introduced in the component, js in vue-awesome-swiper will report an error when executing the test case. A reference is an error and is third-party code. Finally, the swiper component was changed from local registration to global registration.

Thank you

Finally list the articles and materials that helped me a lot in the learning process

  • Jest official documentation
  • Official guide to vue-test-utils
  • Learn to unit test Vue projects
  • Preliminary practice of using Jest in conjunction with VUe-test-utils