那些台灣軟體產業所缺少的 – 自動化測試

你是否有計算過,你在寫專案的過程中,測試過了多少次的程式? 我想是沒有,我也沒有,但是你是否有曾想過,或是感覺過,隨著專案的膨漲,你要測試的項目也跟著變多了? 這是理所當然的事情,當專案小,測試還算很輕鬆,因為程式的功能不外乎就那幾樣,一轉眼就測完了,常見的寫程式流程會像這樣

撰寫新功能

測試新功能

當然,也有修正bug的情況

修正bug

測試bug

如此一直循環,當你寫了新功能,理所當然地會去測試新功能,看是否如你預期地執行,那舊功能呢? 或許你記憶力不錯,在寫新功能的同時,想到先前某個舊功能是依賴現在改的東西,這麼一改可能會造成舊的功能出問題,於是你也順便測了一下舊的功能,當程式還小

撰寫新功能

測試新功能

測試舊功能

嘿,不怎麼樣吧? 只佔了開發時間的三分之一,好吧那如果有更多的舊功能要測呢?

撰寫新功能

測試新功能

測試舊功能

測試舊功能

測試舊功能

….

發現了沒有? 隨著你的專案越來越大,如果要確保整個系統所有的功能都是正常運作的,無可避免地,在你修改程式之後要測試的項目會越來越多

這表示你每寫一行新程式的成本增加了,身為以減低成本為傲的島國 國民: 台灣人…,你說,簡單! 不要測舊功能不就好了? 是的,我想這可能就是最常見的情況,不要測試舊功能理所當然地,每寫一行的程式成本都保持一樣很低,但這代表著舊程式可能出錯的風險也跟著增加了,當你喜滋滋地覺得你幫公司省了成本,結果在一個月後因為舊程式缺乏測試,因改動了核心的部份造成舊的功能將所有資料外洩,公司損失慘重,這就是不重視軟體品質的後果

舉真實生活上發生過的例子,PTT曾經有過改程式未經好好地測試,造成每個人都能以管理員的權限登入的事情,知名的檔案同步平台Dropbox,也曾經發生過因為認證的程式改版有bug,造成任何人都可以登入別人帳號的事,我也有曾聽聞一些網站因為工程師為了測試方便,把認證的函數暫時改成

function authenticate(user_id, password) {
        return true;
        // do authentication here
        // ....
}

然後又不小心commit,因此讓任何人都通過認證的事情,但這些都不能只怪工程師本身,誰能無過? 人總會犯錯的,問題出問於工程本身的制度、專案的管理、和工具的使用上

在未來,網路的應用越來越多,而軟體的品質重要程度只會越來越高,所以,要如何維持軟體的品質又同時能不讓測試的成本隨著專案的擴張而跟著無限制地成長呢? 答案就是 – 自動化測試

自動化測試

自動化測試聽起來好像很美妙,讓電腦自動幫你測試程式? 有這麼好的事情嗎? 事實上不是那樣,自動化測試,是透過寫好的規則,自動對於程式進行測試,所以終究還是得需要人力的介入,那你或許會問,結果倒頭來還不是得用人力? 那到底有什麼好處? 答案就跟我們先前提到的一樣,如果你的專案很小,用人力測試其實可能就已足夠,但當你的專案夠大,如果沒有自動化測試,那麼光是在測舊的程式就是相當龐大的成本上

引入了自動化測試不代表程式就不會出錯,它不是萬能的,但是它至少保證了程式一定的品質,只要使用得當,就能降低測試的成本,也能讓大部份有經過自動測試的程式都不會出現太離譜的錯誤,至於要怎麼做,讓我們看下去

單元測試

最常見的測試,就是單元測試(Unit test),通常是針對單一個或是少數類別,確保這些類別單獨運作是正確的,舉個例子,你寫了一個類別,是用來找輸入的地圖的最短路徑,那麼你就得替這類別,寫一個單元測試,餵入你準備好的資料,然後取得輸出的結果,看是否和你準備的預期答案是一樣的,舉一個最簡單的例子,一個用Python來將輸入文字拆解成一行一行的解析器

class LineParser(object):
    def __init__(self, newline='rn', remain=''):
        self.newline = newline
        self._buffer = [remain]
        self._size = len(remain)

    def feed(self, data):
        self._buffer.append(data)
        self._size += len(data)

    def getLine(self):
        data = ''.join(self._buffer)
        index = data.find(self.newline)
        if index != -1:
            line = data[:index]
            self._buffer = [data[index + len(self.newline):]]
            self.length = len(self._buffer[0])
            return line

    def iterLines(self):
        line = self.getLine()
        while line is not None:
            yield line
            line = self.getLine()

它的單元測試就長這樣

import unittest

class TestLineParser(unittest.TestCase):

    def makeOne(self):
        return LineParser()

    def testParser(self):
        p = self.makeOne()

        p.feed('abc')
        line = p.getLine()
        self.assertEqual(line, None)

        p.feed('rn')
        line = p.getLine()
        self.assertEqual(line, 'abc')
        line = p.getLine()
        self.assertEqual(line, None)

        # write lots line
        p.feed('111rn222rn3333')
        lines = list(p.iterLines())
        self.assertEqual(['111', '222'], lines)
        line = p.getLine()
        self.assertEqual(line, None)

        p.feed('rntext')
        line = p.getLine()
        self.assertEqual(line, '3333')
        line = p.getLine()
        self.assertEqual(line, None)

        # write nothing
        p.feed('')
        line = p.getLine()
        self.assertEqual(line, None)

        p.feed('rn')
        lines = list(p.iterLines())
        self.assertEqual(lines, ['text'])

        self.assertEqual(list(p.iterLines()), [])

def suite():
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(TestLineParser))
    return suite

if __name__ == '__main__':
    unittest.main(defaultTest='suite')

很簡單的想法就是列出幾種常見的case,還有你能想到的特例代進去,好的測試資料要能夠測到每一行程式,但是要做到那樣需要花不少心力,其實能夠做到大部份常見的情況和常見的特例,就已經相當足夠

整合測試

有些程式,無可避免地會依賴其它程式,如果我們針對這兩個程式同時測試,會無法分出出錯到底是誰的錯,再者,很多依賴的部份可能會牽扯到IO或是其它系統資源,讓測試變得更複雜,例如有個類別是負責輸出文件到印表機的,那你要如何確認印表機印出來的東西是正確的? 答案就是做一個假的 (Mocking)印表機丟給那個類別去做列印的動作,再去讀取裡面的資料,確認跟你預期的一樣

雖然單元測試在相當單純的模擬環境下測過了我們的程式,然而世界並不是那樣的美好,總有些事情沒有經過真槍實彈操演過可能會有差錯,因此有時我們會引入部份受控制的真實環境來測試,例如你想測試網路連線,或許你可以寫一段script在Amazon EC2上建起幾個instance,並上傳程式到那些機器中,自動讓他們連線來確保這些功能是正常的,然而越真實的環境變因就越多,因此測試也就相對困難

每日建構的好幫手 – Jenkins

Joel有說過 每日建構是你的朋友 (Daily build is your friend),也有提過 軟體開發成功的12個法則 (The Joel Test: 12 Steps to Better Code),裡面的daily build是指利用工具每天自動建構整個專案,通常對於編譯式的語言,如C語言寫的大型專案會較需要這類的工具,但是這樣的工具還有一個目的,在於確保程式是可以正常編譯的,並且讓測試員容易拿到最新的程式進行測試,然而自動化測試,同樣的也需要類似的工具,因為通常你在改完程式就會進行測試,那每次一改完程式就得跑一次測試指令,這不是一件很煩的事情嗎?

記得,工具是為我們服務的,不是我們為工具服務,這樣重複的瑣事理所當然也是由工具來幫忙,謝天謝地現今有好用又免費的工具,可以幫你做到這點,那就是Jenkins,它是一套基於網頁的自動化測試管理工具,它可以做到什麼呢? 它可以做到幫你定時去版本控制系統取程式回來,用預先設定好的流程進行測試,並且記錄測試的結果,如果有某個測試出錯了,當然也可以發Email通知你,以Now.in的開發為例,因為專案為數眾多,其中又有依賴關係,有了Jenkins的幫忙,程式只要一改送到BitBucket,它就會自動進行測試

如此一來就省下了大量的時間,同時,也可以專心在於開發上

Jenkins除了功能強大以外,他還有一項特色令我驚訝,就是非常簡單易用,從安裝到設定完所有的測試,除了clone hg檔案庫和設置測試環境以外,我從沒因為Jenkins打過一行指令,全部都可以透過它友善的網頁介面完成,同時它也有內建資料庫,也沒因此設定MySQL,在Windows下安裝更是容易,一個安裝檔執行完就是安裝完成,如果你希望有工具幫你自動定時測試或是建構,請不要懷疑,Jenkins是你最佳選擇

部署前的自動化測試

執行自動化測試的時機,除了剛改完程式,還有一個重要的時機,那就是在你把程式部署到伺服器以前,讓你的自動化部署的script先跑過一次自動測試,確認測試通過了再進行部署,為什麼要這樣做呢? 還記得先前提到的PTT和Dropbox以及一些網站對於authentication的return true慘劇嗎? 為了不讓那種事情發生,或著至少讓機會降低,在deploy前讓自動化測試跑過一次,確保測試的範圍內都是正確的,可以大大降低那種情況發生的機會,除此之外,也比較不會因為改出bug,自己沒發現,等到使用者來抱怨了才知道問題在哪

測試的幾項重點

自動化測試雖然是一項利器,但是得經過正確的使用才會有好的效果,自動化測試有所謂的覆蓋率,也就是你的程式裡以行為單位,有多少行是在跑測試時有執行過的? 這些工具都可以幫你統計出來,但是切記

不要為了追求高測試覆蓋率,替foo bar寫測試

這只是在浪費時間,如果某段程式已經簡單到沒測試的必要,你寫了也是多餘

除此之外,寫測試事實上也是成本,因此如果時間有限,請

優先針對重要的核心、資料模型、商業邏輯測試

因為就算你測再多無關緊要的程式,最重要的核心出錯了,可能整個系統就完蛋了,所以盡量以重要的程式做為測試的優先考量

優先針對安全性相關、存取權限、身份認證、常見攻擊手法測試

雖然身份認證這種事情算不上是核心,但這關係到你的系統會不會被輕易地攻擊,除此之外,如果你的程式是網站應用程式,SQL Injection、XSS、buffer overflow這類攻擊也會很常發生,因此,你也需要優先自行設計一些攻擊,針對這些常見的問題餵一些資料,雖然這無法保證一定不會犯錯,至少確保不會發生太低等級的錯誤,因為常見的case都已經有自動測試過了,搭配先前所提到的,deploy前跑過一次測試,如此一來就能將犯錯的機會降低許多

雖然你的程式可能大多都已經有自動化測試在幫你測試,但即使如此,你還是會發現新的bug,如果說,你直接改了bug,就這樣了事,很有可能在下幾次改版bug又回來了,因此

每當你發現你先前沒想到的bug,請加到你的測試中

如此一來,隨著你針對的bug測試case越多,你的程式品質就越高,未發現的bug也會越少,在未來確保這些bug不會再出現

最後

再一次,自動化測試不是萬能的,除此之外也需要正確的運用,如果台灣軟體業界能夠好好運用自動化測試,軟體的品質可以有所提升,開發者也不會因為除錯除到死加班到天亮,雖然寫測試是額外的負擔,但是對於大形專案長期看來是非常值得的投資

This entry was posted in 中文文章, 分享, 嘴砲, 資訊安全 and tagged , , , , , , . Bookmark the permalink.

5 Responses to 那些台灣軟體產業所缺少的 – 自動化測試

  1. pjt says:

    一點小經驗分享:
    Unit Test和其他的測試(包括System Test, Deploy Test,和Acceptance Test)一般來說是視為不同的.Unit Test主要是開發人員的責任,其餘的則主要是測試人員的責任.(雖然大家都心知肚明在台灣很多時候開發和測試是同一組人,甚至同一個人).UT可以在每個component build好後立即(各自)測試,事實上,現代的IDE或是build環境幾乎都有整合UT工具在內,UT沒過會被視為build fail.在我以前的工作中,UT沒過跟compile沒過的code一樣被視為junk code,不能進revision control system.
    另外一提,如果沒試過test driven development的話,強烈建議試一下.TDD的研發流程正好跟你寫的相反:先寫test code,然後再寫code讓那些預先寫好的test code能通過測試.TDD好處多多,但是一用就會發現兩個立竿見影的效果:第一,絕對會有test code(廢話).第二,寫出來的東西大多能符合功能要求(因為test code就是照著功能要求寫的).
    還有一點,跟直覺相反的,根據經驗,TDD配合上pair programming效率絕佳(如果配合的兩個人功力差不多而且士氣高昂的話更可怕,你會以為他們合體變成悟天克斯了).雖然這兩樣一個增加了工作負擔一個增加了人力,不過產出/成本”似乎”不減反增(但是這邊要強調的是我沒有數字證據,只是個人的感覺).
    最後一提,要使用TDD或是pair programming前請跟你的老闆溝通,並做好心理準備會被打槍(前面說過直覺上這兩樣東西都會妨礙產出,而老闆一般相信直覺).老闆沒說好千萬不要做,千萬別想說先偷偷做出成績給老闆看.切記切記!!

  2. 路人甲 says:

    關於TDD的部分,我也來回應一下。我強烈建議「不要試TDD」,除非以下某些條件成立:
    1. 你對TDD有了夠深入的了解
    2. 有一個有經驗的人能夠跟你Pair Programming
    3. 你對這種開發方式有強烈的偏好
    4. 如果原程式碼不是Testable的,常得再多花一份功把他調整成Testable的。你深刻感受到那是種不必要的成本浪費
    5. 你想要很自然地達到某種程度的Test Coverage

    我強烈建議去了解TDD,但在做好足夠的心理建設前,不要在關鍵產品上實驗TDD。

  3. Swift says:

    TDD不是一兩個人可以玩起來的,必須整個專案組織觀念與共識都具備下才能玩

  4. 雅虎測試工程師 says:

    在雅虎開發就是用TDD
    但是光是TDD還不夠
    因為TDD只涵蓋到UT和UAT

    至於Jenkins是雅虎Weekly build的工具之一
    最近雅虎在徵人
    聽說明年要增加100個工程職位
    歡迎大家來雅虎投履歷

  5. Pingback: 有關自動化測試與測試自動化 | 科科和測試

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>