「知足需求」是全部軟件存在的必要條件,單元測試必定是爲它服務的。從這一點出發,咱們能夠總結出寫單元測試的兩個動機:驅動(如:TDD)和驗證功能實現。另外,軟件需求「易變」的特徵決定了修改代碼成爲必然,在這種狀況下,單元測試能保護已有的功能不被破壞。node
基於用例的測試(By Example)程序員
單元測試最多見的套路就是Given、When、Then三部曲。面試
但這樣的測試方式也有壞處。安全
對於第一個問題,咱們換種思路思考一下。假設咱們不寫具體的測試用例,而是直接描述意圖,那麼問題也就迎刃而解了。想法很美好,但如何實踐Given、When、Then呢?答案是讓程序自動生成入參並驗證結果。這也就引出「生成式測試」的概念——咱們先聲明傳入數據可能的狀況,而後使用生成器生成符合入參狀況的數據,調用待測方法,最後進行驗證。bash
Clojure 1.9(Alpha)新內置的Clojure.spec能夠很輕鬆地作到這點:數據結構
;; 定義輸入參數的可能狀況:兩個整型參數
(s/def ::add-operators (s/cat :a int? :b int?))
;; 嘗試生成數據
(gen/generate (s/gen ::add-operators))
;; 生成的數據
-> (1 -122)
複製代碼
首先,咱們嘗試聲明兩個參數可能出現的狀況或者稱爲規格(specification),即參數a和b都是整數。而後調用生成器產生一對整數。整個分析和構造的過程當中,都沒有涉及具體的數據,這樣會強制咱們揣摩輸入數據可能的模樣,並且也能避免測試意圖被掩蓋掉——正如前面所說,return 3 when add 1 and 2並不表明什麼,return the sum of two integers才具備廣泛意義。app
數據是生成了,待測方法也能夠調用,可是Then這個斷言階段又讓人頭疼了,由於咱們根本無法預知生成的數據,也就沒法知道正確的結果,怎麼斷言?框架
拿定義好的加法運算爲例:ide
(defn add [a b]
(+ a b))
複製代碼
咱們嘗試把斷言改爲一個全稱命題: 任取兩個整數a、b,a和b加起來的結果老是a、b之和。 藉助test.check,咱們在Clojure能夠這樣表達:性能
(def test-add
(prop/for-all [a (gen/int)
b (gen/int)]
(= (add a b) (+ a b))))
複製代碼
不過,咱們把add方法的實現(+ a b)寫到了斷言裏,這幾乎喪失了單元測試的基本意義。換一種斷言方式,咱們使用加法的逆運算進行描述: 任取兩個整數,把a和b加起來的結果減去a總會獲得b。
(def test-add
(prop/for-all [a (gen/int)
b (gen/int)]
(= (- (add a b) a) b))))
複製代碼
咱們經過程序陳述了一個已知的真命題。變換之後,就可使用quick-check對多組生成的整數進行測試。
;; 隨機生成100組數據測試add方法
(tc/quick-check 100 test-add)
;; 測試結果
-> {:result true, :num-tests 100, :seed 1477285296502}
複製代碼
測試結果代表,剛纔運行了100組測試,而且都經過了。理論上,程序能夠生成無數的測試數據來驗證add方法的正確性。即使不能窮盡,咱們也得到一組統計上的數字,而不只僅是幾個純手工挑選的用例。
至於第二個問題,首先得明確測試是沒法作到完備的。不少指導方法保證使用較少的用例作到有效覆蓋,好比:等價類、邊界值、斷定表、因果圖、pairwise等等。可是在實際使用過程中,依然存在問題。舉個例子,假如咱們有一個接收天然數並直接返回這個參數的方法identity-nat,那麼對於輸入參數而言,全體天然數都互爲等價類,其中的一個有效等價類能夠是天然數1;假定入參被限定在整數範圍,咱們很容易找到一個無效等價類,好比-1。 用Clojure測試代碼表現出來:
(deftest test-with-identity-nat
(testing "identity of natural integers"
(is (= 1 (identity-nat 1))))
(testing "throw exception for non-natural integers"
(is (thrown? RuntimeException (identity-nat -1)))))
複製代碼
不過若是有人修改了方法identity-nat的實現,單獨處理入參爲0的狀況,這個測試仍是可以照常經過。也就是說,實現發生改變,基於等價類的測試有可能起不到防禦做用。固然你徹底能夠反駁:規則改變致使等價類也須要從新定義。道理確實如此,可是反過來想一想,咱們寫測試的目的不正是構建一張安全網嗎?咱們信任測試能在代碼變更時給予警告,但此處它失信了,這就尷尬了。
若是使用生成式測試,咱們規定:
任取一個天然數a,在其上調用identity-nat的結果老是返回a。
(def test-identity-nat
(prop/for-all [a (s/gen nat-int?)]
(= a (identity-nat a))))
(tc/quick-check 100 test-identity-nat)
-> {:result false,
:seed 1477362396044,
:failing-size 0,
:num-tests 1,
:fail [0],
:shrunk {:total-nodes-visited 0,
:depth 0,
:result false,
:smallest [0]}}
複製代碼
這個測試嘗試對100組生成的天然數(nat-int?)進行測試,但首次運行就發現代碼發生過變更。失敗的數據是0,並且還給出了最小失敗集[0]。拿着這個最小失敗集,咱們就能夠快速地重現失敗用例,從而修正。
固然也存在這樣的可能:在一次運行中,咱們的測試沒法發現失敗的用例。可是,若是100個測試用例都經過了,至少代表咱們程序對於100個隨機的天然數都是正確的,和基於用例的測試相比,這就如同編織出一道更加緊密的安全網——網孔越小,漏掉的狀況也越少。
Clojure語言之父Rich Hickey推崇Simple Made Easy哲學,受其影響生成式測試在Clojure.spec中有更爲簡約的表達。以上述爲例:
(s/fdef identity-nat
:args (s/cat :a nat-int?) ; 輸入參數的規格
:ret nat-int? ; 返回結果的規格
:fn #(= (:ret %) (-> % :args :a))) ; 入參和出參之間的約束
(stest/check `identity-nat)
複製代碼
fdef宏定義了方法identity-nat的規格,默認狀況下會基於參數的規格生成1000組數據進行生成式測試。除了這一好處,它還提供部分類型檢查的功能。
若是對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。感興趣能夠175317069,羣內會有不按期的發放免費的資料連接,這些資料都是從各個技術網站蒐集、整理出來的,若是你有好的學習資料能夠私聊發我,我會註明出處以後分享給你們。
TDD(測試驅動開發)是一種驅動代碼實現和設計的過程。咱們說要先有測試,再去實現;保證明現功能的前提下,重構代碼以達到較好的設計。整個過程就比如演繹推理,測試就是其中的證實步驟,而最終實現的功能則是證實的結果。
對於開發人員而言,基於用例的測試方式是友好的,由於它能簡單直接地表達實現的功能並保證其正確性。一旦進入紅、綠、重構的節(guai)奏(quan),開發人員根本停不下來,彷彿遁入一種心流狀態。只不過問題是,基於用例驅動出來的實現可能並非剛好經過的。咱們經常會發現,在寫完上組測試用例的實現以後,無需任何改動,下組測試照常能運行經過。換句話說,實現代碼可能作了多餘的事情而咱們卻渾然不知。在這種狀況下,咱們能夠利用生成式測試準備大量符合規格的數據探測程序,以此檢查程序的健壯性,讓缺陷無處遁形。
凡是想到的狀況都能測試,可是想不到狀況也須要測試,這纔是生成式測試的價值所在。有人把TDD概念化爲「展現你的功能」(Show your work),而把生成式測試概括爲「檢查你的功能「(Check your work),我深覺得然。
回到咱們寫單元測試的動機上:
一、驅動和驗證功能實現;
二、保護已有的功能不被破壞。
基於用例的單元測試和生成式測試在這兩點上是相輔相成的。咱們能夠藉助它們儘量早地發現更多的缺陷,避免它們逃逸到生產環境。
Clojure.spec是Clojure內置的一個新特性,它容許開發人員將數據結構用類型和其餘驗證條件(例如容許的取值範圍)進行封裝。這種數據結構一旦創建,Clojure就能利用這種規格來爲程序員提供大量的便利:自動生成的測試代碼、合法性驗證、析構數據結構等等。Clojure.spec提供方法頗有前景,它可讓開發者在須要的時候,就能從類型和取值範圍中獲益。
另外,除了Clojure,其它語言也有相應的生成式測試的框架,你不妨在本身的項目中試一試。