Skip to content
On this page

更好地组织测试代码

随着测试用例的增加,你会发现存在很多重复代码,避免这种情况的方法之一就是使用工厂函数(factory function)。

工厂函数:执行时返回新对象或者新实例的函数。

工厂函数可让让代码易于阅读和理解,是一种常见的代码组织模式

本文将介绍如何使用工厂函数来组织测试代码,以减少重复,以及如何更好的组织代码。

重复代码

ContractList.vue 的测试用例如下:

js
import {
  shallowMount
} from '@vue/test-utils'
import ContractList from './ContractList.vue'
import ContractItem from './ContractItem.vue'

describe('ContractList.vue', () => {
  const richFriends = [{
      name: '马爸爸',
      city: '杭州',
      img: 'https://jsd.cdn.zzko.cn/gh/jackchoumine/jack-picture@master/ma-yun.png',
      phone: '123456789',
      position: 'CEO',
      company: '阿里巴巴',
      twitter: 'https://twitter.com/jack-ma',
      // fortune: '400亿美元',
    },
    {
      name: '麻花藤',
      city: '深圳',
      img: 'https://jsd.cdn.zzko.cn/gh/jackchoumine/jack-picture@master/pony-ma.png',
      phone: '99988123',
      position: 'CTO',
      company: '腾讯',
      twitter: 'https://twitter.com/pony-ma',
      // fortune: '600亿美元',
    },
  ]
  it("ContractItem's size", () => {
    const wrapper = shallowMount(ContractList, {
      propsData: {
        persons: richFriends,
      },
    })
    expect(wrapper.findAllComponents(ContractItem)).toHaveLength(richFriends.length)
  })
  it('测试 props', () => {
    const wrapper = shallowMount(ContractList, {
      propsData: {
        persons: richFriends,
      },
    })
    const items = wrapper.findAllComponents(ContractItem)
    items.wrappers.forEach((wrapper, index) => {
      // console.log(wrapper.props())
      expect(wrapper.props()).toEqual(richFriends[index])
    })
  })
  it('should contain contract-list class in root ele', () => {
    const wrapper = shallowMount(ContractList, {
      propsData: {
        persons: richFriends,
      },
    })
    expect(wrapper.classes()).toContain('contract-list')
  })
  it('test inline style', () => {
    const wrapper = shallowMount(ContractList, {
      propsData: {
        persons: richFriends,
      },
    })
    // expect(wrapper.attributes('style')).toBe('color: red;')
    expect(wrapper.element.style.color).toBe('red')
    // console.log(
    //   wrapper.findComponent(ContractItem).element.style['margin-bottom'],
    //   'zqj log'
    // )
    // expect(wrapper.find(ContractItem).element.style['margin-bottom']).toBe('20px')
    expect(wrapper.findComponent(ContractItem).element.style['margin-bottom']).toBe(
      '20px'
    )
  })
})

你会发现,每个用例在断言之前,我们都需要挂载组件,创建一个 wrapper

可将这部分代码提取成工厂函数:

JS
function createWrapper(propsData = {
  persons: richFriends
}) {
  return shallowMount(ContractList, {
    propsData,
  })
}

重构后的测试用例如下:

js
import {
  shallowMount
} from '@vue/test-utils'
import ContractList from './ContractList.vue'
import ContractItem from './ContractItem.vue'

describe('ContractList.vue', () => {
  const richFriends = [{
      name: '马爸爸',
      city: '杭州',
      img: 'https://jsd.cdn.zzko.cn/gh/jackchoumine/jack-picture@master/ma-yun.png',
      phone: '123456789',
      position: 'CEO',
      company: '阿里巴巴',
      twitter: 'https://twitter.com/jack-ma',
      // fortune: '400亿美元',
    },
    {
      name: '麻花藤',
      city: '深圳',
      img: 'https://jsd.cdn.zzko.cn/gh/jackchoumine/jack-picture@master/pony-ma.png',
      phone: '99988123',
      position: 'CTO',
      company: '腾讯',
      twitter: 'https://twitter.com/pony-ma',
      // fortune: '600亿美元',
    },
  ]

  function createWrapper(
    propsData = {
      persons: richFriends,
    }
  ) {
    return shallowMount(ContractList, {
      propsData,
    })
  }
  it("ContractItem's size", () => {
    const wrapper = createWrapper()
    expect(wrapper.findAllComponents(ContractItem)).toHaveLength(richFriends.length)
  })
  it('测试 props', () => {
    const wrapper = createWrapper()
    const items = wrapper.findAllComponents(ContractItem)
    items.wrappers.forEach((wrapper, index) => {
      expect(wrapper.props()).toEqual(richFriends[index])
    })
  })
  it('should contain contract-list class in root ele', () => {
    const wrapper = createWrapper()
    expect(wrapper.classes()).toContain('contract-list')
  })
  it('test inline style', () => {
    const wrapper = createWrapper()
    expect(wrapper.element.style.color).toBe('red')
    expect(wrapper.findComponent(ContractItem).element.style['margin-bottom']).toBe(
      '20px'
    )
  })
})

使用工厂函数的两大好处:

  • 减少重复代码,DRY(Don't repeat yourself)是编写代码的编程原则
  • 提供了一种可沿用的模式

通过重构,把重复创建包装起的代码放在了工厂函数中,保证了代码的简洁和可读性。

DRY 原则:多次编写相似或者相同的代码,应该把这些代码抽取出来,放在一个函数或文件中,以便复用,而不是在各个地方重复编写。

沿用相同的模式提高代码的质量

在编写测试时,大部分人不考虑代码模式。但是,随着测试用例越来越多,同一份代码被不同的人修改后,代码很快变得乱成一团,难以维护。

规模较大的开发人员流动性大的或者维护周期长的项目,非常容易出现这种情况,代码质量会越来越差,最终导致项目维护不下去。为了避免这种问题,除了遵循相同的代码规范外,沿用相同的模式也是一种有效的方法。

通常,大型代码库都会出现计划外的模式,这些模式可能是由于开发人员的个人喜好,也可能是由于项目的历史原因。这些模式会导致代码难以理解,也会导致代码质量下降。

起先,某个函数的参数只有 2 个,某天,一个开发 A,希望增加一个参数 a, 但是,另一个开发 B,希望增加一个参数 b。

js
function foo(msg, agea, b) {
  // ...
}

慢慢地,foo 函数的参数越来越多,10 个参数,15 个参数,导致代码难以理解,也会导致代码质量下降。

我在最近的项目中,看到同一个人写的函数,使用了 12 个参数,我那时的心情就是想骂娘。

不沿用相同的模式,每个人就会创造自己的模式,代码质量就会越来越差,甚至差到无法维护,也就是所谓的 代码腐化

上面的测试用中,在每个测试用例执行之前,都是创建了一个 wrapper ,其实可以在 beforeEach 中创建 wrapper ,这样可以避免重调用工厂函数。

js
/* eslint-disable quotes */
import {
  shallowMount
} from '@vue/test-utils'
import ContractList from './ContractList.vue'
import ContractItem from './ContractItem.vue'

describe('ContractList.vue', () => {
  const richFriends = [{
      name: '马爸爸',
      city: '杭州',
      img: 'https://jsd.cdn.zzko.cn/gh/jackchoumine/jack-picture@master/ma-yun.png',
      phone: '123456789',
      position: 'CEO',
      company: '阿里巴巴',
      twitter: 'https://twitter.com/jack-ma',
      // fortune: '400亿美元',
    },
    {
      name: '麻花藤',
      city: '深圳',
      img: 'https://jsd.cdn.zzko.cn/gh/jackchoumine/jack-picture@master/pony-ma.png',
      phone: '99988123',
      position: 'CTO',
      company: '腾讯',
      twitter: 'https://twitter.com/pony-ma',
      // fortune: '600亿美元',
    },
  ]

  function createWrapper(
    propsData = {
      persons: richFriends,
    }
  ) {
    return shallowMount(ContractList, {
      propsData,
    })
  }
  let wrapper = null
  beforeEach(() => {
    wrapper = createWrapper()
  })
  it("ContractItem's size", () => {
    expect(wrapper.findAllComponents(ContractItem)).toHaveLength(richFriends.length)
  })
  it('测试 props', () => {
    const items = wrapper.findAllComponents(ContractItem)
    items.wrappers.forEach((wrapper, index) => {
      expect(wrapper.props()).toEqual(richFriends[index])
    })
  })
  it('should contain contract-list class in root ele', () => {
    expect(wrapper.classes()).toContain('contract-list')
  })
  it('test inline style', () => {
    expect(wrapper.element.style.color).toBe('red')
    expect(wrapper.findComponent(ContractItem).element.style['margin-bottom']).toBe(
      '20px'
    )
  })
})

beforeEach 会在每个测试用例执行之前执行。

工厂函数的缺点

不管是工厂函数还是 beforeEach,为了减少重复代码,都对代码进行了封装和抽象,虽然维护性增加了,但是可理解性却下降了。新来的开发人员可能难以理解这些代码,除非他深入到代码中。

通常情况下,代码的封装和抽象程度越高,复用和维护性越好,但是可理解性越差,这是一个矛盾的问题。好的设计或者经常需要修改的代码,应该要很好的平衡这个矛盾。

小结

  • 使用工厂函数可以减少重复代码,提高代码的可读性
  • beforeEach 在每个测试用例执行之前执行

Released under the MIT License.