本文以DDM为例,简单地介绍一下如何用测试驱动开发(TDD, Test-Driven Development)的方法来驱动出这个函数库。
DDM简介
DDM是一个简洁的前端领域模型库,如我在《DDM: 一个简洁的前端领域模型库》一文中所说,它是我对于DDD在前端领域中使用的一个探索。
简单地来说,这个库就是对一个数据模型的操作——增、删 、改,然后生成另外一个数据模型。
如以Blog模型,删除Author,我们就可以得到一个新的模型。而实现上是因为我们需要RSS模型,我们才需要对原有的模型进行修改。
预先式设计
如果你对TDD有点了解的话,那么你可能会预先式设计有点疑问。
等等,什么是测试驱动开发?
> 测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。
流程大概就是这样的,先写个测试 -> 然后运行测试,测试失败 -> 让测试通过 -> 重构。
换句简单的话来说,就是 红 -> 绿 -> 重构。
在DDM项目里,就是一个比较适合TDD的场景。我们知道我们所有的功能,我们也知道我们需要对什么内容进行测试,并且它很简单。因为这个场景下,我们已经知道了我们所需要的功能,所以我们就可以直接设计主要的函数:
export class DDM {
constructor() {}
from() {};
get(array) {};
to() {};
handle() {};
add() {};
remove(field) {};
}
上面的就是我们的需要函数,不过在后来因为需要就添加了replace和replaceWithHandle方法。
然后,我们就可以编写我们的第一个测试了。
第一个驱动开发的测试
我们的第一个测试,比较简单,但是也比较麻烦——我们需要构建出基本的轮廓。我们的第一个测试就是要测试我们可以从原来的对象中取出title的值:
let ddm = new DDM();
var originObject = {
title: 'hello',
blog: 'fdsf asdf fadsf ',
author: 'phodal'
};
var newObject = {};
ddm
.get(['title'])
.from(originObject)
.to(newObject);
expect(newObject.title).toBe("hello");
对应的,为了实现这个需要基本的功能,我们就可以写一个简单的return来通过测试。
from(originObject) {
return this;
};
get(array) {
return this;
};
to(newObject) {
newObject.title = 'hello';
return this;
};
但是这个功能在我们写下一个测试的时候,它就会出错。
ddm
.get(['title', 'phodal'])
.from(originObject)
.to(newObject);
expect(newObject.title).toBe("hello");
expect(newObject.author).toBe("phodal");
但是这也是我们实现功能要做的一步,下一步我们就可以实现真正的功能:
from(originObject) {
this.originObject = originObject;
return this;
};
get(array) {
this.newObjectKey = array;
return this;
};
to(newObject) {
for (var key of this.newObjectKey) {
newObject[key] = this.originObject[key];
}
return this;
};
现在,我们已经完成了基本的功能。
在我实现的过程中,我发现如果我传给get函数的array如果是空的话,那么就不work了。于是,就针对这个情况写了个测试,然后实现了这个功能:
get(keyArray) {
if(keyArray) {
this.newObjectKey = keyArray;
} else {
this.newObjectKey = [];
}
return this;
};
to(newObject) {
if(this.newObjectKey.length > 0){
for (var key of this.newObjectKey) {
newObject[key] = this.originObject[key];
}
} else {
// Clone each property.
for (var prop in this.originObject) {
newObject[prop] = clone(this.originObject[prop]);
}
}
return this;
};
在这个过程中,我还找到了一个clone函数,来替换from中的"="。
from(originObject) {
this.originObject = clone(originObject);
return this;
};
第三个驱动开发的测试
---
因为有了第一个测试的基础,我们要写下一测试变得非常简单:
```javascript
dlm.get(['title'])
.from(originObject)
.add('tag', 'hello,world,linux')
.to(newObject);
expect(newObject.tag).toBe("hello,world,linux");
expect(newObject.title).toBe("hello");
expect(newObject.author).toBe(undefined);
在实现的过程中,我又投机取巧了,我创建了一个对象来存储新的对象的key和value:
add(field, value) {
this.objectForAddRemove[field] = value;
return this;
};
同样的,在to
方法里,对其进行处理:
to(newObject) {
function cloneObjectForAddRemove() {
for (var prop in this.objectForAddRemove) {
newObject[prop] = this.objectForAddRemove[prop];
}
}
function cloneToNewObjectByKey() {
for (var key of this.newObjectKey) {
newObject[key] = this.originObject[key];
}
}
function deepCloneObject() {
// Clone each property.
for (var prop in this.originObject) {
newObject[prop] = clone(this.originObject[prop]);
}
}
cloneObjectForAddRemove.call(this);
if (this.newObjectKey.length > 0) {
cloneToNewObjectByKey.call(this);
} else {
deepCloneObject.call(this);
}
return this;
};
在这个函数里,我们用cloneObjectForAddRemove函数来复制将要添加的key和value到新的对象里。
remove和handle函数
对于剩下的remove和handle来说,他们实现起来都是类似的:
存储相应的对象操作
然后在to函数里进行处理
编写测试:
function handler(blog) {
return blog[0];
}
ddm.get(['title', 'blog', 'author'])
.from(originObject)
.handle("blog", handler)
.to(newObject);
expect(newObject.blog).toBe('A');
然后实现功能:
remove(field) {
this.objectKeyForRemove.push(field);
return this;
};
handle(field, handle) {
this.handleFunction.push({
field: field,
handle: handle
});
return this;
}
这一切看上去都很自然,然后我们就可以对其进行重构了。
100%的测试覆盖率
由于,我们先编写了测试,再实现代码,所以我们编写的代码都有对应的测试。因此,我们可以轻松实现相当高的测试覆盖率。
在这个Case下,由于业务场景比较简单,要实现100%的测试覆盖率就是一件很简单的事。
正在做测试的朋友可以进来交流,群里给大家整理了大量学习资料和面试题项目简历等等....