本文以DDM為例,簡單地介紹一下如何用測試驅動開發(TDD, Test-Driven Development)的方法來驅動出這個函數庫。
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來說,他們實現起來都是類似的:
編寫測試:
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;
}
這一切看上去都很自然,然后我們就可以對其進行重構了。
由于,我們先編寫了測試,再實現代碼,所以我們編寫的代碼都有對應的測試。因此,我們可以輕松實現相當高的測試覆蓋率。
在這個Case下,由于業務場景比較簡單,要實現100%的測試覆蓋率就是一件很簡單的事。
(PS: 我不是TDD的死忠,只是有時候它真的很美。)
原文轉自:https://www.phodal.com/blog/use-tdd-drive-100-percent-test-coverage/