单元测试本身是一种技能, 这个技能或许当你学习一种新的语言或者新的框架的时候不是优先考虑的。当你对你的组件开始进行单元测试的时候, 你可能会感到有些失落, 下面这些问题你可能会想过。
- 如何进行单元测试
- 应该怎样进行单元测试
- 在单元测试中存在什么陷阱
- 直到尾部一直等待
- 测试错误的事情
- 测试错误
- 耦合的结构
- 测试每件事情
- 在单元测试中是否要遵循一些规则
下面,我将要展示给你在你每天的工作中能够用到的具体的步骤, 这些步骤基于 vue.js 的例子。
陷阱一: 直到代码完成之后才进行单元测试
我保证当你在正确的开发环境中使用单元测试的时候, 你会享受这个过程, 但是在这之前, 你还要有许多的东西需要学习,当你仍在弄清楚新的框架的时候, 于此同时, 那些烦人的同事却坚持添加测试。 这些需要测试的东西会拖慢你的进度。
甚至对于你的第一个组件而言, 你等待的时间越长, 对组件写单元测试也将变得愈加困难, 因为积极的单元测试者在写代码的同时也在考虑如何进行测试, 当他们写代码的时候, 他们让代码更加容易测试, 可测试的代码是一个好的软件架构的标志。
在写首次单元测试的时候将会拖慢你的进度,但是测试迭代的时候时间将会成倍加快, 当重构的时候,你的代码仍然可以继续运行, 第一次的测试给予你这样的自行。
你需要做的第一件事情是确保你拥有代码运行的环境。
import { shallow } from 'vue-test-utils';
import YourComponent from './YourComponent.vue';
describe('Your Component', () => {
it('renders a vue instance', () => {
expect(shallow(YourComponent).isVueInstance()).to.be.true;
});
});
第一次单测是非常重要的, 你想要确定无论你向组件传递什么值, 组件都能够被渲染, 毕竟, 你的组件将会使用来自不同地方的数据,你应该认为这些不同的地方不安全, 你不应该相信它们向你的组件提供有效的数据。
通过这个简单的测试, 我在确保我的组件在空数据时完全不会出现意外,
使用 shallow方法作为渲染的开始是十分重要的, shallow 将只会渲染特定的组件而不会渲染当前组件下的子组件, 当你只需要关注当前的一个组件的时候, 这让你的测试更加的健壮。
陷阱二: 测试错误的事情
第二个陷阱紧跟在第一个陷阱之后, 当你想要测试代码中本不需要测试的部分的时候, 你会很容易困惑卡住。
你不应该测试组件内部中特定的某一个函数被调用了, 这些都是细节, 你应该关注大的方向, 如果你关注这些细节部分, 你的测试将会和你的代码耦合, 当重构代码的同时, 你也必须重构测试代码, 单元测试应该帮助你重构, 如果当你进行重构的时候你需要改变你的测试代码, 这或许意味着你测试了错误的地方。
下面是错误测试的例子, 我在测试当我点击按钮的时候, 测试组件中的函数将会被调用了, 在这个测试中, 如果我改变我的代码, 例如我决定重新命名函数, 我必须同时改变组件中的代码以及组件测试中的代码。
import { shallow } from 'vue-test-utils';
import ExampleComponent from '../ExampleComponent.vue';
import Sinon from 'sinon';
describe('Bad Example', () => {
it('Do not do this: should toggle the visibility', () => {
const toggleParagraphDisplaySpy = Sinon.spy(
ExampleComponent.methods,
'toggleParagraphDisplay'
);
const wrapper = shallow(ExampleComponent);
wrapper.find('button').trigger('click');
expect(toggleParagraphDisplaySpy.calledOnce);
toggleParagraphDisplaySpy.restore();
});
});
因此, 你应该测试什么呢?
你应该测试组件的公共接口以及副作用。
测试公共接口
你需要测试当向你的组件传递具体的props值的时候, 你能得到具体的输出,
对于组件而言, 这种输出是被渲染的某种标记。
- 你可以测试元素中的class属性存在
import { shallow } from 'vue-test-utils';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('has a active class by default', () => {
expect(shallow(MyComponent, {
propsData: {
isActive: true }}).hasClass('active')).to.be.true;
});
});
- 你可以测试元素是否被渲染
import { shallow } from 'vue-test-utils';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('contains a list', () => {
expect(shallow(MyComponent).contains('ul')).to.be.true;
})
})
- 你可以确定子组件存在,在下面的这个例子中, 对我的存根组件添加了一个特定的class, 通过这种方式, 我确保将不会获得错误的元素
import { shallow } from 'vue-test-utils';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('has child components', () => {
expect(shallow(MyComponent, {
propsData: {
children: [1, 2, 3, 4 ,5],
},
stubs: {
'ChildComponent': '<li class="my-stub"></li>',
},
}).findAll('li.my-stub').length).to.equal(5);
})
})
测试副作用
你可以测试当组件中的事件发生的时候, 点击按钮, 滑动滚轮, 一些特定的副作用发生了, 这些副作用可能是下面这些事情:
- 一个http请求
import { shallow } from 'vue-test-utils';
// This can be any http library, axios, fetch…
import http from 'http';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('gets google.com', () => {
sinon.spy(http, 'get');
const wrapper = shallow(MyComponent);
wrapper.find('button').trigger('click');
expect(http.get.withArgs('http://www.google.com').calledOnce).to.be.true;
http.restore();
});
})
- 切换UI元素的显示
import { shallow } from 'vue-test-utils';
// This can be any http library, axios, fetch…
import http from 'http';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('disappears', () => {
const wrapper = shallow(MyComponent);
expect(wrapper.contains('div.foo')).to.be.true;
wrapper.find('button').trigger('click');
expect(wrapper.contains('div.foo')).to.be.false;
});
})
陷阱三 :Test Doubles(测试替身)
"Test doubles" 是指使用各种假对象替代需要进行测试的真正对象的专业术语,有多种类型的测试替身, 从最简单的 dummy 到完全的 mock。
当提到 test doubles 的时候, 下面是我喜欢使用的总类大纲:
- test double 是抽象的
- Dummy 拥有所有的方法, 但是不会做任何事情, 它能够通过向函数中传递空的参数来在测试环境下运行代码。
- stub 就像 Dummy, 与之不同的是 stub 会返回一个值而不是 null, undefined 或者 0,stub 允许你选择代码中的执行路径。
- spy 和 Dommy 类似, 不同的是能够晓得调用的是什么。
- mock 和 spy 类似, 不同的是他有一个校验方法用来检测函数方法是否按照正确的顺序被调用。
- Fake 是一种代替的执行功能, 能够允许你越过例如授权认证, 数据库, 或者其他昂贵的资源, 我发现在集成测试中非常的有用。
下面图表展示了这些测试替身之间是怎样关联的:
我应当如何使用 test doubles ?
为了创建一个spys 或者 mocks而使用框架是十分有用的,我喜欢 SinonJs, 因为其使用了上一部分中相同的测试大纲。
现在我或许因为这些新的词汇给你的大脑理解造成了困难, 但我有一个好消息, 对于最简单的 test doubles, 这也是你将要经常使用到的一种, 你不用必须使用框架, 在特定的情况下, 写一个空函数返回你想要的结果, 这种测试要简单的多。
例如, 对于使用vuex来追踪用户登录状态的组件:
import { shallow, createLocalVue } from 'vue-test-utils';
import Vuex from 'vuex';
import MyComponent from './MyComponent';
describe('MyComponent for a logged in User', () => {
it('shows the user ID', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store({
getters: {
isLoggedIn: () => true,
}
});
const wrapper = shallow(MyComponent, {
localVue,
store,
});
expect(wrapper.text()).contains('Thanks for coming back!');
});
})
在前面的例子上面, 我们增加一些东西, 我们可以使用 spys 来测试函数调用:
import { shallow } from 'vue-test-utils';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('gets google.com and save analytics', () => {
sinon.spy(http, 'get');
sinon.spy(http, 'post');
const wrapper = shallow(MyComponent);
wrapper.find('button').trigger('click');
expect(http.get.withArgs('http://www.google.com').calledOnce).to.be.true;
expect(http.post.withArgs('http://www.analytics.com').calledOnce).to.be.true;
http.restore();
});
});
Mocks 有一点不同, 你需要首先定义预期的动作, 然后进行验证:
import { shallow } from 'vue-test-utils';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('gets google.com', () => {
mock = sinon.mock(http);
mock.expects('get').withArgs('http://www.google.com').once;
const wrapper = shallow(ExampleComponent);
wrapper.find('button').trigger('click');
mock.verify();
mock.restore();
});
});
有一件事情需要记在心里, 不要忘记回收原始函数, 这被称为 ‘un-mocking’, 否则, 你接下来的测试或许会影响要其他使用 test doubles 的测试用例。
陷阱四: 结构耦合
你不用对于每一个组件都要有一个测试文件, 如果你这样做, 你的测试代码的结构将会成为本身代码结构的镜子, 这样将会使得代码重构以及在实施策略中切换的时候更加困难,这被称为结构耦合, 下面这个例子将会更好的说明这个问题。
想象下我们正在创建一个button组件, 这个组件基于一个简单的按钮, 在click的时候发送一些事件, 我们从一个组件以及一个用浅渲染按钮的组件测试用例开始, 浅渲染意味着测试不会渲染子组件, 我们可以关心特定的组件而不用测试子组件的行为。
接下来我们意识到我们对于这个组件想要做更多的事情, 根据传入的props值来进行不同的动作, 做一些初始化的动作, 做一个好的单元测试, 我们决定拆分这个按钮组件:
我们完成了这个工作, 我们的组件每一个都被单元测试文件覆盖了, 但是你有发现什么问题吗? 这些测试的结构和代码的结构相似。
如果因为一些原因我们想要改变代码的结构的时候, 增加或删除文件, 我们需要对于我们的测试文件做相同的调整, 这将会拖慢我们的进度。
考虑一下这样创建文件:
我们使用 mount 作为渲染方式, mount 意味着子组件将也会被渲染,这允许我们测试我们按钮组件的所有预期的动作而不用将测试组件变为代码的结构, 例如, 当我们决定重构时以对这些组件切换其他的状态时, 我们无需改变我们的测试代码。
陷阱五: 测试每件事情
最初的想法时你想要测试每件事情, 你想要测试组件正确渲染, 事件按照预期正常工作, 异步的代码能够正常执行等等。
测试边界: http请求, 与外部服务器的连接, 这些测试很难去做并且会很快让你的测试更加复杂。
我想你应该考虑一些完全不同的事情, 无需测试, 下面时一个 Humble Object [^1]模型:
根组件是简陋的, 因为其没有包含任何的逻辑, 其只负责调用 api 用来获取一些数据, 然后将这些数据传递给子组件,
如果你的组件时 humble 的, 你将会需要对其进行单元测试, 你需要测试子组件以及测试里面所有的逻辑, 当 User Story 完成之后, 你可以写一个总体的测试来确保每件事情都能够正常被触发。
- [^1]: http://xunitpatterns.com/Humble%20Object.html
注: 本文翻译自:
Five Traps to Avoid While Unit Testing Vue.jsengineering.doximity.com