新聞中心
作者 | 杜沁園(懸衡)

重構代碼時,我們常常糾結于這樣的問題:
- 需要進一步抽象嗎?會不會導致過度設計?
- 如果需要進一步抽象的話,如何進行抽象呢?有什么通用的步驟或者法則嗎?
單元測試是我們常用的驗證代碼正確性的工具,但是如果只用來驗證正確性的話,那就是真是 “大炮打蚊子”--大材小用,它還可以幫助我們評判代碼的抽象程度與設計水平。本文還會提出一個以“可測試性”為目標,不斷迭代重構代碼的思路,利用這個思路,面對任何復雜的代碼,都能逐步推導出重構思路。為了保證直觀,本文會以一個 “生產者消費者” 的代碼重構示例貫穿始終。最后還會以業(yè)務上常見的 Excel 導出系統(tǒng)為例簡單闡述一個業(yè)務上的重構實例。閱讀本文需要具有基本的單元測試編寫經驗(最好是 Java),但是本文不會涉及任何具體的單元測試框架和技術,因為它們都是不重要的,學習了本文的思路,可以將它們用在任意的單測工具上。
不可測試的代碼
程序員們重構一段代碼的動機是什么?可能眾說紛紜:
- 代碼不夠簡潔?
- 不好維護?
- 不符合個人習慣?
- 過度設計,不好理解?
這些都是比較主觀的因素,在一個老練程序員看來恰到好處的設計,一個新手程序員卻可能會覺得過于復雜,不好理解。但是讓他們同時坐下來為這段代碼添加單元測試時,他們往往能夠產生類似的感受,比如
- “單測很容易書寫,很容易就全覆蓋了”,那么這就是可測試的代碼;
- “雖然能寫得出來,但是費了老大勁,使用了各種框架和技巧,才覆蓋完全”,那么這就是可測試性比較差的代碼;
- “完全不知道如何下手寫”,那么這就是不可測試的代碼;
一般而言,可測試的代碼一般都是同時是簡潔和可維護的,但是簡潔可維護的代碼卻不一定是可測試的,比如下面的“生產者消費者”代碼就是不可測試的:
public void producerConsumer() {
BlockingQueue blockingQueue = new LinkedBlockingQueue<>();
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
blockingQueue.add(i + ThreadLocalRandom.current().nextInt(100));
}
});
Thread consumerThread = new Thread(() -> {
try {
while (true) {
Integer result = blockingQueue.take();
System.out.println(result);
}
} catch (InterruptedException ignore) {
}
});
producerThread.start();
consumerThread.start();
} 上面這段代碼做的事情非常簡單,啟動兩個線程:
- 生產者:將 0-9 的每個數字,分別加上 [0,100) 的隨機數后通過阻塞隊列傳遞給消費者;
- 消費者:從阻塞隊列中獲取數字并打?。?/li>
這段代碼看上去還是挺簡潔的,但是,算得上一段好代碼嗎?嘗試下給這段代碼加上單元測試。僅僅運行一下這個代碼肯定是不夠的,因為我們無法確認生產消費邏輯是否正確執(zhí)行。我也只能發(fā)出“完全不知道如何下手”的感嘆,這不是因為我們的單元測試編寫技巧不夠,而是因為代碼本身存在的問題:1、違背單一職責原則:這一個函數同時做了 數據傳遞,處理數據,啟動線程三件事情。單元測試要兼顧這三個功能,就會很難寫。2、這個代碼本身是不可重復的,不利于單元測試,不可重復體現(xiàn)在
需要測試的邏輯位于異步線程中,對于它什么時候執(zhí)行?什么時候執(zhí)行完?都是不可控的;
邏輯中含有隨機數;
消費者直接將數據輸出到標準輸出中,在不同環(huán)境中無法確定這里的行為是什么,有可能是輸出到了屏幕上,也可能是被重定向到了文件中;
因為第 2 點的原因,我們就不得不放棄單測了呢?其實只要通過合理的模塊職責劃分,依舊是可以單元測試。這種劃分不僅僅有助于單元測試,也會“順便”幫助我們抽象一套更加合理的代碼。
可測試意味著什么?
所有不可測試的代碼都可以通過合理的重構與抽象,讓其核心邏輯變得可測試,這也重構的意義所在。本章就會詳細說明這一點。
首先我們要了解可測試意味著什么,如果說一段代碼是可測試的,那么它一定符合下面的條件:
- 可以在本地設計完備的測試用例,稱之為 完全覆蓋的單元測試;
- 只要完全覆蓋的單元測試用例全部正確運行,那么這一段邏輯肯定是沒有問題的;
第 1 點常會令人感到難以置信,但事實比想象的簡單,假設有這樣一個分段函數:
f(x) 看起來有無限的定義域,我們永遠無法窮舉所有可能的輸入。但是再仔細想想,我們并不需要窮舉,其實只要下面幾個用例可以通過,那么就可以確保這個函數是沒有問題的:
- <-50
f(-51) == -100
- [-50, 50]
f(-25) == -50
f(25) == 50
- >50
f(51) == 100
- 邊界情況
f(-50) == -100
f(50) == 100
日常工作中的代碼當然比這個復雜很多,但是沒有本質區(qū)別,也是按照如下思路進行單元測試覆蓋的:
- 每一個分段其實就是代碼中的一個條件分支,用例的分支覆蓋率達到了 100%;
- 像 2x 這樣的邏輯運算,通過幾個合適的采樣點就可以保證正確性;
- 邊界條件的覆蓋,就像是分段函數的轉折點;
但是業(yè)務代碼依舊比 f(x) 要復雜很多,因為 f(x) 還有其他好的性質讓它可以被完全測試,這個性質被稱作引用透明:
- 函數的返回值只和參數有關,只要參數確定,返回值就是唯一確定的
現(xiàn)實中的代碼大多都不會有這么好的性質,反而具有很多“壞的性質”,這些壞的性質也常被稱為副作用:
- 代碼中含有遠程調用,無法確定這次調用是否會成功;
- 含有隨機數生成邏輯,導致行為不確定;
- 執(zhí)行結果和當前日期有關,比如只有工作日的早上,鬧鐘才會響起;
- 好在我們可以用一些技巧將這些副作用從核心邏輯中抽離出來。
高階函數
“引用透明” 要求函數的出參由入參唯一確定,之前的例子容易讓人產生誤解,覺得出參和入參一定要是數據,讓我們把視野再打開一點,出入參可以是一個函數,它也可以是引用透明的。
普通的函數又可以稱作一階函數,而接收函數作為參數,或者返回一個函數的函數稱為高階函數,高階函數也可以是引用透明的。
對于函數 f(x) 來說,x 是數據還是函數,并沒有本質的不同,如果 x 是函數的話,僅僅意味著 f(x) 擁有更加廣闊的定義域,以至于沒有辦法像之前一樣只用一個一維數軸表示。
對于高階函數 f(g) (g 是一個函數)來說,只要對于特定的函數 g,返回邏輯也是固定,它就是引用透明的了, 而不用在乎參數 g 或者返回的函數是否有副作用。利用這個特性,我們很容易將一個有副作用的函數轉換為一個引用透明的高階函數。
一個典型的擁有副作用的函數如下:
public int f() {
return ThreadLocalRandom.current().nextInt(100) + 1;
}它生成了隨機數并且加 1,因為這個隨機數,導致它不可測試。但是我們將它轉換為一個可測試的高階函數,只要將隨機數生成邏輯作為一個參數傳入,并且返回一個函數即可:
public Supplierg(Supplier integerSupplier) {
return () -> integerSupplier.get() + 1;
}
上面的 g 就是一個引用透明的函數,只要給 g 傳遞一個數字生成器,返回值一定是一個 “用數字生成器生成一個數字并且加1” 的邏輯,并且不存在分支條件和邊界情況,只需要一個用例即可覆蓋:
public void testG() {
Supplier result = g(() -> 1);
assert result.get() == 2;
} 實際業(yè)務中可以稍微簡化一下高階函數的表達, g 的返回的函數既然每次都會被立即執(zhí)行,那我們就不返回函數了,直接將邏輯寫在方法中,這樣也是可測試的:
public int g2(SupplierintegerSupplier) {
return integerSupplier.get() + 1;
}
這里我雖然使用了 Lambda 表達式簡化代碼,但是 “函數” 并不僅僅是指 Lambda 表達式,OOP 中的充血模型的對象,接口等等,只要其中含有邏輯,它們的傳遞和返回都可以看作 “函數”。
因為這個例子比較簡單,“可測試” 帶來的收益看起來沒有那么高,真實業(yè)務中的邏輯一般比 +1 要復雜多了,此時如果能構建有效的測試將是非常有益的。
面向單測的重構
第一輪重構
我們本章回到開頭的生產者消費者的例子,用上一章學習到的知識對它進行重構。那段代碼無法測試的第一個問題就是職責不清晰,它既做數據傳遞,又做數據處理。因此我們考慮將生產者消費者數據傳遞的代碼單獨抽取出來:
publicvoid producerConsumerInner(Consumer > producer,
Consumer> consumer) {
BlockingQueueblockingQueue = new LinkedBlockingQueue<>();
new Thread(() -> producer.accept(blockingQueue::add)).start();
new Thread(() -> consumer.accept(() -> {
try {
return blockingQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})).start();
}
這一段代碼的職責就很清晰了,我們給這個方法編寫單元測試的目標也十分明確,即驗證數據能夠正確地從生產者傳遞到消費者。但是很快我們又遇到了之前提到的第二個問題,即異步線程不可控,會導致單測執(zhí)行的不穩(wěn)定,用上一章的方法,我們將執(zhí)行器作為一個入參抽離出去:
publicvoid producerConsumerInner(Executor executor,
Consumer> producer,
Consumer> consumer) {
BlockingQueueblockingQueue = new LinkedBlockingQueue<>();
executor.execute(() -> producer.accept(blockingQueue::add));
executor.execute(() -> consumer.accept(() -> {
try {
return blockingQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}));
}
這時我們就為它寫一個穩(wěn)定的單元測試了:
private void testProducerConsumerInner() {
producerConsumerInner(Runnable::run,
(Consumer>) producer -> {
producer.accept(1);
producer.accept(2);
},
consumer -> {
assert consumer.get() == 1;
assert consumer.get() == 2;
});
} 只要這個測試能夠通過,就能說明生產消費在邏輯上是沒有問題的。一個看起來比之前的分段函數復雜很多的邏輯,本質上卻只是它定義域上的一個恒等函數(因為只要一個用例就能覆蓋全部情況),是不是很驚訝。如果不太喜歡上述的函數式編程風格,可以很容易地將其改造成 OOP 風格的抽象類,就像上一章提到的,傳遞對象和傳遞函數沒有本質的區(qū)別:
public abstract class ProducerConsumer{
private final Executor executor;
private final BlockingQueueblockingQueue;
public ProducerConsumer(Executor executor) {
this.executor = executor;
this.blockingQueue = new LinkedBlockingQueue<>();
}
public void start() {
executor.execute(this::produce);
executor.execute(this::consume);
}
abstract void produce();
abstract void consume();
protected void produceInner(T item) {
blockingQueue.add(item);
}
protected T consumeInner() {
try {
return blockingQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
此時單元測試就會像是這個樣子:
private void testProducerConsumerAbCls() {
new ProducerConsumer(Runnable::run) {
@Override
void produce() {
produceInner(1);
produceInner(2);
}
@Override
void consume() {
assert consumeInner() == 1;
assert consumeInner() == 2;
}
}.start();
} 看到這些類,熟悉設計模式的讀者們一定會想到 “模板方法模式”,但是我們在上面的過程從來沒有刻意去用任何設計模式,正確的重構就會讓你在無意間 “重新發(fā)現(xiàn)” 這些常用的設計模式,一般這種情況下,設計模式的使用都是正確的,因為我們一直在把代碼往更加可測試的方向推薦,而這也是衡量設計模式是否使用正確的重要標準,錯誤的設計模式使用則會讓代碼更加的割裂和不可測試,后文討論“過度設計”這個主題時會進一步深入討論這一部分內容。
很顯然這種測試無法驗證多線程運行的情況,但我故意這么做的,這部分單元測試的主要目的是驗證邏輯的正確性,只有先驗證邏輯上的正確性,再去測試并發(fā)才比較有意義,在邏輯存在問題的情況下就去測試并發(fā),只會讓問題隱藏得更深,難以排查。一般開源項目中會有專門的單元測試去測試并發(fā),但是因為其編寫代價比較大,運行時間比較長,數量會遠少于邏輯測試。
經過第一輪重構,主函數變成了這個樣子(這里我最終采用了 OOP 風格):
public void producerConsumer() {
new ProducerConsumer(Executors.newFixedThreadPool(2)) {
@Override
void produce() {
for (int i = 0; i < 10; i++) {
produceInner(i + ThreadLocalRandom.current().nextInt(100));
}
}
@Override
void consume() {
while (true) {
Integer result = consumeInner();
System.out.println(result);
}
}
}.start();
} 在第一輪重構中,我們僅僅保障了數據傳遞邏輯是正確的,在第二輪重構中,我們還將進一步擴大可測試的范圍。
第二輪重構
代碼中影響我們進一步擴大測試范圍因素還有兩個:
- 隨機數生成邏輯
- 打印邏輯
只要將這兩個邏輯像之前一樣抽出來即可:
public class NumberProducerConsumer extends ProducerConsumer{
private final SuppliernumberGenerator;
private final ConsumernumberConsumer;
public NumberProducerConsumer(Executor executor,
SuppliernumberGenerator,
ConsumernumberConsumer) {
super(executor);
this.numberGenerator = numberGenerator;
this.numberConsumer = numberConsumer;
}
@Override
void produce() {
for (int i = 0; i < 10; i++) {
produceInner(i + numberGenerator.get());
}
}
@Override
void consume() {
while (true) {
Integer result = consumeInner();
numberConsumer.accept(result);
}
}
}
這次采用 OOP 和 函數式 混編的風格,也可以考慮將 numberGenerator 和 numberConsumer 兩個方法參數改成抽象方法,這樣就是更加純粹的 OOP。它也只需要一個測試用例即可實現(xiàn)完全覆蓋:
private void testProducerConsumerInner2() {
AtomicInteger expectI = new AtomicInteger();
producerConsumerInner2(Runnable::run, () -> 0, i -> {
assert i == expectI.getAndIncrement();
});
assert expectI.get() == 10;
}此時主函數變成:
public void producerConsumer() {
new NumberProducerConsumer(Executors.newFixedThreadPool(2),
() -> ThreadLocalRandom.current().nextInt(100),
System.out::println).start();
}經過兩輪重構,我們將一個很隨意的面條代碼重構成了很優(yōu)雅的結構,除了更加可測試外,代碼也更加簡潔抽象,可復用,這些都是面向單測重構所帶來的附加好處。
你可能會注意到,即使經過了兩輪重構,我們依舊不會直接對主函數 producerConsumer 進行測試,而只是無限接近覆蓋里面的全部邏輯,因為我認為它不在“測試的邊界”內,我更傾向于用集成測試去測試它,集成測試則不在本篇文章討論的范圍內。下一章則重點討論測試邊界的問題。
單元測試的邊界
邊界內的代碼都是單元測試可以有效覆蓋到的代碼,而邊界外的代碼則是沒有單元測試保障的。
上一章所描述的重構過程本質上就是一個在探索中不斷擴大測試邊界的過程。但是單元測試的邊界是不可能無限擴大的,因為實際的工程中必然有大量的不可測試部分,比如 RPC 調用,發(fā)消息,根據當前時間做計算等等,它們必然得在某個地方傳入測試邊界,而這一部分就是不可測試的。
理想的測試邊界應該是這樣的,系統(tǒng)中所有核心復雜的邏輯全部包含在了邊界內部,然后邊界外都是不包含邏輯的,非常簡單的代碼,比如就是一行接口調用。這樣任何對于系統(tǒng)的改動都可以在單元測試中就得到快速且充分的驗證,集成測試時只需要簡單測試下即可,如果出現(xiàn)問題,一定是對外部接口的理解有誤,而不是系統(tǒng)內部改錯了。
清晰的單元測試邊界劃分有利于構建更加穩(wěn)定的系統(tǒng)核心代碼,因為我們在推進測試邊界的過程中會不斷地將副作用從核心代碼中剝離出去,最終會得到一個完整且可測試的核心,就如同下圖的對比一樣:
重構的工作流
好代碼從來都不是一蹴而就的,都是先寫一個大概,然后逐漸迭代和重構的,從這個角度來說,重構別人的代碼和寫新代碼沒有很大的區(qū)別。從上面的內容中,我們可以總結出一個簡單的重構工作流:
按照這個方法,就能夠逐步迭代出一套優(yōu)雅且可測試的代碼,即使因為時間問題沒有迭代到理想的測試邊界,也會擁有一套大部分可測試的代碼,后人可以在前人用例的基礎上,繼續(xù)擴大測試邊界。
過度設計
最后再談一談過度設計的問題。按照本文的方法是不可能出現(xiàn)過度設計的問題,過度設計一般發(fā)生在為了設計而設計,生搬硬套設計模式的場合,但是本文的所有設計都有一個明確的目的--提升代碼的“可測試性”,所有的技巧都是在過程中無意使用的,不存在生硬的問題。而且過度設計會導致“可測試性”變差,過度設計的代碼常常是把自己的核心邏輯都給抽象掉了,導致單元測試無處可測。如果發(fā)現(xiàn)一段代碼“寫得很簡潔,很抽象,但是就是不好寫單元測試”,那么大概率是被過度設計了。另外一種過度設計是因為過度依賴框架而無意中導致的,Java 往往習慣于將自己的設計耦合進 Spring 框架中,比如將一段完整的邏輯拆分到幾個 Spring Bean 中,而不是使用普通的 Java 類,導致根本就無法在不啟動容器的情況下進行完整的測試,最后只能寫一堆無效的測試提升“覆蓋率”。這也是很多人抱怨“單元測試沒有用”的原因。
和 TDD 的區(qū)別
本文到這里都還沒有提及到 TDD,但是上文中闡述的內容肯定讓不少讀者想到了這個名詞,TDD 是 “測試驅動開發(fā)” 的簡寫,它強調在代碼編寫之前先寫用例,包括三個步驟:
- 紅燈:寫用例,運行,無法通過用例
- 綠燈:用最快最臟的代碼讓測試通過
- 重構:將代碼重構得更加優(yōu)雅
在開發(fā)過程中不斷地重復這三個步驟。但是會實踐中會發(fā)現(xiàn),在繁忙的業(yè)務開發(fā)中想要先寫測試用例是很困難的,可能會有以下原因:
代碼結構尚未完全確定,出入口尚未明確,即使提前寫了單元測試,后面大概率也要修改
產品一句話需求,外加對系統(tǒng)不夠熟悉,用例很難在開發(fā)之前寫好
因此本文的工作流將順序做了一些調整,先寫代碼,然后再不斷地重構代碼適配單元測試,擴大系統(tǒng)的測試邊界。不過從更廣義的 TDD 思想上來說,這篇文章的總體思路和 TDD 是差不多的,或者標題也可以改叫做 “TDD 實踐”。
業(yè)務實例 - 導出系統(tǒng)重構
釘釘審批的導出系統(tǒng)是一個專門負責將審批單批量導出成 Excel 的系統(tǒng):
大概步驟如下:
- 啟動一個線程,在內存中異步生成 Excel
- 上傳 Excel 到釘盤/oss
- 發(fā)消息給用戶
釘釘審批導出系統(tǒng)比常規(guī)導出系統(tǒng)要更加復雜一些,因為它的表單結構并不是固定的。而用戶可以通過設計器靈活配置:
從上面可以看出單個審批單還具有復雜的內部結構,比如明細,關聯(lián)表單等等,而且還能相互嵌套,因此邏輯很十分復雜。
我接手導出系統(tǒng)的時候,已經維護兩年了,沒有任何測試用例,代碼中導出都是類似 patchXxx 的方法,可見在兩年的歲月中,被打了不少補丁。系統(tǒng)雖然總體能用,但是有很多小 bug,基本上遇到邊界情況就會出現(xiàn)一個 bug(邊界情況比如明細里只有一個控件,明細里有關聯(lián)表單,而關聯(lián)表單里又有明細等等)。代碼完全不可測試,完成的邏輯被 Spring Bean 隔離成一小塊,一小塊,就像下圖一樣:
我決定將這些代碼重構,不能讓它繼續(xù)荼毒后人,但是面對一團亂麻的代碼完全不知道如何下手(以下貼圖僅僅是為了讓大家感受下當時的心情,不用仔細看):
我決定用本文的工作流對代碼進行重新梳理。
確定測試邊界
首先需要確定哪些部分是單元測試可以覆蓋到的,哪些部分是不需要覆蓋到的,靠集成測試保證的。經過分析,我認為導出系統(tǒng)的核心功能,就是根據表單配置和表單數據生成 excel 文件:
這部分也是最核心,邏輯也最復雜的部分,因此我將這一部分作為我的測試邊界,而其他部分,比如上傳,發(fā)工作通知消息等放在邊界之外:
圖中 “表單配置” 是一個數據,而 “表單數據” 其實是一個函數,因為導出過程中會不斷批量分頁地去查詢數據。
不斷迭代,擴大測試邊界到理想狀態(tài)
我迭代的過程如下:
- 異步執(zhí)行導致不可測試:抽出一個同步的函數;
- 大量使用 Spring Bean 導致邏輯割裂:將邏輯放到普通的 Java 類或者靜態(tài)方法中;
- 表單數據,流程與用戶的相關信息查詢是遠程調用,含有副作用:通過高階函數將這些副作用抽出去;
- 導入狀態(tài)落入數據庫,也是一個副作用:同樣通過高階函數將其抽象出去;
最終導出的測試邊界大約是這個樣子:
public byte[] export(FormConfig config, DataService dataService, ExportStatusStore statusStore) {
//... 省略具體邏輯, 其中包括所有可測試的邏輯, 包括表單數據轉換,excel 生成
}
- config:數據,表單配置信息,含有哪些控件,以及控件的配置
- dataService: 函數,用于批量分頁查詢表單數據的副作用
- statusStore: 函數,用于變更和持久化導出的狀態(tài)的副作用
public interface DataService {
PageList batchGet(String formId, Long cursor, int pageSize);
}
public interface ExportStatusStore {
/**
* 將狀態(tài)切換為 RUNNING
*/
void runningStatus();
/**
* 將狀態(tài)置為 finish
* @param fileId 文件 id
*/
void finishStatus(Long fileId);
/**
* 將狀態(tài)置為 error
* @param errMsg 錯誤信息
*/
void errorStatus(String errMsg);
}在本地即可驗證生成的 Excel 文件是否正確(代碼經過簡化):
public void testExport() {
// 這里的 export 就是剛剛展示的導出測試邊界
byte[] excelBytes = export(new FormConfig(), new LocalDataService(),
new LocalStatusStore());
assertExcelContent(excelBytes, Arrays.asList(
Arrays.asList("序號", "表格", "表格", "表格", "創(chuàng)建時間", "創(chuàng)建者"),
Arrays.asList("序號", "物品編號", "物品名稱", "xxx", "創(chuàng)建時間", "創(chuàng)建者"),
Arrays.asList("1", "22", "火車", "而非", "2020-10-11 00:00:00", "懸衡")
));
}其中 LocalDataService,LocalStatusStore 分別是內存中的數據服務,和狀態(tài)變更服務實現(xiàn),用于進行單元測試。assertExcelContent 是我用 poi 寫的一個工具方法,用于測試內存中的 excel 文件是否符合預期。所有邊界的用例都可以直接在本地測試和沉淀用例。
最終的代碼結構大約如下(經過簡化):
雖然到現(xiàn)在為止我的目的都是提升代碼的可測試性,但是實際上我一不小心也提升了代碼的拓展性,在完全沒有相關產品需求的情況下:
- 通過 DataService 的抽象,系統(tǒng)可以支持多種數據源導出,比如來自搜索,或者來自 db 的,只要傳入不同的 DataService 實現(xiàn)即可,完全不需要改動和性邏輯;
- ExportStatusStore 的抽象,讓系統(tǒng)有能力使用不同的狀態(tài)存儲,雖然目前使用的是 db,但是也可以在不改核心邏輯的情況下輕松切換成 tair 等其他中間件;
果然在我重構后不久,就接到了類似的需求,比如要支持從不同的數據源導出。我們又新增了一個導出入口,這個導出狀態(tài)是存儲在不同的表中。每次我都暗自竊喜,其實這些我早就從架構上準備好了。
單元測試的局限性
雖然本文是一篇單元測試布道文章,前文也將單元測試說得“神通廣大”,但是也不得不承認單元測試無法解決全部的問題。
單元測試僅僅能保證應該的代碼邏輯是正確的,但是應用開發(fā)中還有很多更加要緊的事情,比如架構設計,中間件選型等等,很多系統(tǒng) bug 可能不是因為代碼邏輯,而是因為架構設計導致的,此時單元測試就無法解決。因此要徹底保障系統(tǒng)的穩(wěn)健,還是需要從單元測試,架構治理,技術選項等多個方面入手。
另外一點也不得不承認,單元測試是有一定成本的,一套工作流完成的話,可能會有數倍于原代碼量的單元測試,因此并不是所有代碼都需要這樣的重構,在時間有限的情況下,應該優(yōu)先重構系統(tǒng)中核心的穩(wěn)定的代碼,在權衡好成本與價值的情況下,再開始動手。
最后,單元測試也是對人有強依賴的技術,側重于前期預防,沒有任何辦法量化一個人單元測試的質量如何,效果如何,這一切都是出于工程自己內心的“工匠精神” 以及對代碼的敬畏,相信讀到最后的你,也一定有著一顆工匠的心吧。
當前題目:代碼重構:面向單元測試
網站地址:http://m.fisionsoft.com.cn/article/cdphohi.html


咨詢
建站咨詢
