iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 20
0
自我挑戰組

再戰軟體工程系列 第 19

『程式都解耦合了,那測試呢?』 -- 談測試解耦合神器:Mock技術

在上一篇文章裡,我們介紹了透過『單一職責原則』來化解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


上一篇
『你儂我儂的程式碼』 -- 談Code Smell 之 Feature Envy
下一篇
『就決定是你了』 -- 談PO的適合人選
系列文
再戰軟體工程30

尚未有邦友留言

立即登入留言