在上一篇文章裡,我們介紹了透過『單一職責原則』來化解Feature Envy這個程式壞味道的方法。現在看起來PhoneBook與Contact都各司其職,並且功能正確了,很好。
那麼,我們這麼想吧:『既然程式可以各司其職,測試又何嘗不可呢?』當然可以啊!在這個電話簿與聯絡人的例子中,兩者都是我們自己寫的,但如果裡面有一個類是引用第三方套件呢?難道我們也要順便測試他的功能嗎?答案是『可以,但不一定需要。』這要看情形。但如果你不想的話,那麼Mock技術可以幫你達到這個目的。
Mock技術簡單來說,就是當你要叫某個物件做某件事時,他幫助你『強迫』該物件返回你指定的值。舉個例子吧,當你的程式需要對Google發一個Query,當Google回傳InternalError 500你就走A邏輯,否則就走B邏輯。你可以想像當你真的對google下了一個query,你要調整你的query去強迫他回InternalError 500,這是多麻煩的事啊?況且,你的測試案例天天這樣跑,沒過多久你的ip就會被ban掉了,實在不划算。
我們冷靜想,Google在什麼情況下回傳什麼值,是我們該關心的嗎?不是喔!
我們該關心的,其實是當Google回傳InternalError 500時,我們應該採取什麼行動,對吧!
同樣地,當我們測試PhoneBook時,我們如果可以假設Contact的邏輯是完全正確的,那麼我們就只要關心『當Contact順利回傳某個值時,PhoneBook採取的行動對不對』就好了。
...好我知道很抽象,我們直接來看code吧:
原始的code是長這樣的:
public class PhoneBookTest {
@Test
public void testGenerateFormattedPrint() throws Exception {
// Prepare data
PhoneBook phoneBook = new PhoneBook();
phoneBook.addContact(new Contact("Kuma", "kuma@aaa.bbb", "123456789"));
phoneBook.addContact(new Contact("Maku", "maku@ccc.ddd", "987654321"));
// Execute method
String result = phoneBook.generateFormattedPrint();
// Check result
String expected = "Kuma: kuma@aaa.bbb | 123456789. \nMaku: maku@ccc.ddd | 987654321. \n";
assertEquals(expected, result);
}
}
其實還好,沒有很長,但是這裡有個問題:
當Contact的格式內容被改變時,就連PhoneBook的測試也要改變。
這實在是沒道理,明明PhoneBook的程式是一行都沒動啊!A class的更動會影響B class的測試,這代表測試本身也存在著解耦合的空間。於是我們用上述的Mock技術來試著調整看看:
public class PhoneBookTest {
private final int LINE_LENGTH = 10;
@Test
public void testGenerateFormattedPrint() throws Exception {
// Prepare data
Contact mockedContact = Mockito.mock(Contact.class);
String fakeLine = RandomStringUtils.randomAlphanumeric(LINE_LENGTH);
Mockito.doReturn(fakeLine).when(mockedContact).generateFormattedPrint();
PhoneBook phoneBook = new PhoneBook();
phoneBook.addContact(mockedContact);
phoneBook.addContact(mockedContact);
// Execute method
String result = phoneBook.generateFormattedPrint();
// Check result
String expected = fakeLine + "\n" + fakeLine + "\n";
assertEquals(expected, result);
System.out.println(result);
}
}
注意這裡我用了 Mockito.mock(Contact.class)來製造要塞給PhoneBook的聯絡人物件,並且用doReturn()來強迫他吐出一個我指定的字串,而這裏我則是刻意指定成一個隨機(random)的字串。這代表什麼意思?這代表:
Contact會吐出什麼字串我根本不在意,我是PhoneBookTest,我只在乎PhoneBook有沒有乖乖做好他該做的事。
至此,『測試的解耦合』也被我們搞定了。從今以後不管Contact的格式怎麼變,只要PhoneBook的邏輯不改,PhoneBookTest也就不用動了。而Contact的邏輯對不對,就讓寫Contact的人去擔心嚕!
回到最一開始的問題,被引用的類別我們要不要順便測?其實很簡單,看哪樣比較方便測。如果被引用的類別是可以很輕鬆控制回傳值的,那我們不用mock他也無所謂,反正一起測的話我也可以少寫一點測試代碼。相反地,如果被引用的套件很難控制其行為,那不如用Mock技術來控制他。
最後我們來回想,如果當初我們沒有把PhoneBook對Contact的Feature Envy壞味道去除,你認為我們現在的測試有解耦合空間嗎?其實沒有,我們會變得無從選擇。因此,程式解耦合,會讓測試變容易;而測試的存在則可以讓程式的解耦合過程減少邏輯錯誤。這兩件事實在是相輔相成的。
本文程式碼載點:https://github.com/bearhsu2/ithelp