變異測試是測試驅動開發(TDD)的演變

Alex Bunardzic發表於2019-10-17

測試驅動開發技術是根據大自然的運作規律建立的,變異測試自然成為 DevOps 演變的下一步。

在 “故障是無懈可擊的開發運維中的一個特點”,我討論了故障在通過徵求反饋來交付優質產品的過程中所起到的重要作用。敏捷 DevOps 團隊就是用故障來指導他們並推動開發程式的。測試驅動開發Test-driven development(TDD)是任何敏捷 DevOps 團隊評估產品交付的必要條件。以故障為中心的 TDD 方法僅在與可量化的測試配合使用時才有效。

TDD 方法仿照大自然是如何運作的以及自然界在進化博弈中是如何產生贏家和輸家為模型而建立的。

自然選擇

查爾斯·達爾文

1859 年,查爾斯·達爾文Charles Darwin在他的《物種起源On the Origin of Species》一書中提出了進化論學說。達爾文的論點是,自然變異是由生物個體的自發突變和環境壓力共同造成的。環境壓力淘汰了適應性較差的生物體,而有利於其他適應性強的生物的發展。每個生物體的染色體都會發生變異,而這些自發的變異會攜帶給下一代(後代)。然後在自然選擇下測試新出現的變異性 —— 當下存在的環境壓力是由變異性的環境條件所導致的。

這張簡圖說明了調整適應環境條件的過程。

環境壓力對魚類的影響

圖1. 不同的環境壓力導致自然選擇下的不同結果。圖片截圖來源於理查德·道金斯的一個視訊

該圖顯示了一群生活在自己棲息地的魚。棲息地各不相同(海底或河床底部的礫石顏色有深有淺),每條魚長的也各不相同(魚身圖案和顏色也有深有淺)。

這張圖還顯示了兩種情況(即環境壓力的兩種變化):

  1. 捕食者在場
  2. 捕食者不在場

在第一種情況下,在礫石顏色襯托下容易凸顯出來的魚被捕食者捕獲的風險更高。當礫石顏色較深時,淺色魚的數量會更少一些。反之亦然,當礫石顏色較淺時,深色魚的數量會更少。

在第二種情況下,魚完全放鬆下來進行交配。在沒有捕食者和沒有交配儀式的情況下,可以預料到相反的結果:在礫石背景下顯眼的魚會有更大的機會被選來交配並將其特性傳遞給後代。

選擇標準

變異性在進行選擇時,絕不是任意的、反覆無常的、異想天開的或隨機的。選擇過程中的決定性因素通常是可以度量的。該決定性因素通常稱為測試或目標。

一個簡單的數學例子可以說明這一決策過程。(在該示例中,這種選擇不是由自然選擇決定的,而是由人為選擇決定。)假設有人要求你構建一個小函式,該函式將接受一個正數,然後計算該數的平方根。你將怎麼做?

敏捷 DevOps 團隊的方法是快速驗證失敗。謙虛一點,先承認自己並不真的知道如何開發該函式。這時,你所知道的就是如何描述你想做的事情。從技術上講,你已準備好進行單元測試。

單元測試unit test”描述了你的具體期望結果是什麼。它可以簡單地表述為“給定數字 16,我希望平方根函式返回數字 4”。你可能知道 16 的平方根是 4。但是,你不知道一些較大數字(例如 533)的平方根。

但至少,你已經制定了選擇標準,即你的測試或你的期望值。

進行故障測試

.NET Core 平臺可以演示該測試。.NET 通常使用 xUnit.net 作為單元測試框架。(要跟隨進行這個程式碼示例,請安裝 .NET Core 和 xUnit.net。)

開啟命令列並建立一個資料夾,在該資料夾實現平方根解決方案。例如,輸入:

mkdir square_root

再輸入:

cd square_root

為單元測試建立一個單獨的資料夾:

mkdir unit_tests

進入 unit_tests 資料夾下(cd unit_tests),初始化 xUnit 框架:

dotnet new xunit

現在,轉到 square_root 下, 建立 app 資料夾:

mkdir app
cd app

如果有必要的話,為你的程式碼建立一個腳手架:

dotnet new classlib

現在開啟你最喜歡的編輯器開始編碼!

在你的程式碼編輯器中,導航到 unit_tests 資料夾,開啟 UnitTest1.cs

UnitTest1.cs 中自動生成的程式碼替換為:

using System;
using Xunit;
using app;

namespace unit_tests{

   public class UnitTest1{
       Calculator calculator = new Calculator();

       [Fact]
       public void GivenPositiveNumberCalculateSquareRoot(){
           var expected = 4;
           var actual = calculator.CalculateSquareRoot(16);
           Assert.Equal(expected, actual);
       }
   }
}

該單元測試描述了變數的期望值應該為 4。下一行描述了實際值。建議通過將輸入值傳送到稱為calculator 的元件來計算實際值。對該元件的描述是通過接收數值來處理CalculateSquareRoot 資訊。該元件尚未開發。但這並不重要,我們在此只是描述期望值。

最後,描述了觸發訊息傳送時發生的情況。此時,判斷期望值是否等於實際值。如果是,則測試通過,目標達成。如果期望值不等於實際值,則測試失敗。

接下來,要實現稱為 calculator 的元件,在 app 資料夾中建立一個新檔案,並將其命名為Calculator.cs。要實現計算平方根的函式,請在此新檔案中新增以下程式碼:

namespace app {
   public class Calculator {
       public double CalculateSquareRoot(double number) {
           double bestGuess = number;
           return bestGuess;
       }
   }
}

在測試之前,你需要通知單元測試如何找到該新元件(Calculator)。導航至 unit_tests 資料夾,開啟 unit_tests.csproj 檔案。在 <ItemGroup> 程式碼塊中新增以下程式碼:

<ProjectReference Include="../app/app.csproj" />

儲存 unit_test.csproj 檔案。現在,你可以執行第一個測試了。

切換到命令列,進入 unit_tests 資料夾。執行以下命令:

dotnet test

執行單元測試,會輸出以下內容:

單元測試失敗後xUnit的輸出結果

圖2. 單元測試失敗後 xUnit 的輸出結果

正如你所看到的,單元測試失敗了。期望將數字 16 傳送到 calculator 元件後會輸出數字 4,但是輸出(Actual)的是 16。

恭喜你!建立了第一個故障。單元測試為你提供了強有力的反饋機制,敦促你修復故障。

修復故障

要修復故障,你必須要改進 bestGuess。當下,bestGuess 僅獲取函式接收的數字並返回。這不夠好。

但是,如何找到一種計算平方根值的方法呢? 我有一個主意 —— 看一下大自然母親是如何解決問題的。

效仿大自然的迭代

在第一次(也是唯一的)嘗試中要得出正確值是非常難的(幾乎不可能)。你必須允許自己進行多次嘗試猜測,以增加解決問題的機會。允許多次嘗試的一種方法是進行迭代。

要迭代,就要將 bestGuess 值儲存在 previousGuess 變數中,轉換 bestGuess 的值,然後比較兩個值之間的差。如果差為 0,則說明問題已解決。否則,繼續迭代。

這是生成任何正數的平方根的函式體:

double bestGuess = number;
double previousGuess;

do {
   previousGuess = bestGuess;
   bestGuess = (previousGuess + (number/previousGuess))/2;
} while((bestGuess - previousGuess) != 0);

return bestGuess;

該迴圈(迭代)將 bestGuess 值集中到設想的解決方案。現在,你精心設計的單元測試通過了!

單元測試通過了

圖 3. 單元測試通過了。

迭代解決了問題

正如大自然母親解決問題的方法,在本練習中,迭代解決了問題。增量方法與逐步改進相結合是獲得滿意解決方案的有效方法。該示例中的決定性因素是具有可衡量的目標和測試。一旦有了這些,就可以繼續迭代直到達到目標。

關鍵點!

好的,這是一個有趣的試驗,但是更有趣的發現來自於使用這種新建立的解決方案。到目前為止,bestGuess 從開始一直把函式接收到的數字作為輸入引數。如果更改 bestGuess 的初始值會怎樣?

為了測試這一點,你可以測試幾種情況。 首先,在迭代多次嘗試計算 25 的平方根時,要逐步細化觀察結果:

25 平方根的迭代編碼

圖 4. 通過迭代來計算 25 的平方根。

以 25 作為 bestGuess 的初始值,該函式需要八次迭代才能計算出 25 的平方根。但是,如果在設計 bestGuess 初始值上犯下荒謬的錯誤,那將怎麼辦? 嘗試第二次,那 100 萬可能是 25 的平方根嗎? 在這種明顯錯誤的情況下會發生什麼?你寫的函式是否能夠處理這種低階錯誤。

直接來吧。回到測試中來,這次以一百萬開始:

逐步求精法

圖 5. 在計算 25 的平方根時,運用逐步求精法,以 100 萬作為 bestGuess 的初始值。

哇! 以一個荒謬的數字開始,迭代次數僅增加了兩倍(從八次迭代到 23 次)。增長幅度沒有你直覺中預期的那麼大。

故事的寓意

啊哈! 當你意識到,迭代不僅能夠保證解決問題,而且與你的解決方案的初始猜測值是好是壞也沒有關係。 不論你最初理解得多麼不正確,迭代過程以及可衡量的測試/目標,都可以使你走上正確的道路並得到解決方案。

圖 4 和 5 顯示了陡峭而戲劇性的燃盡圖。一個非常錯誤的開始,迭代很快就產生了一個絕對正確的解決方案。

簡而言之,這種神奇的方法就是敏捷 DevOps 的本質。

回到一些更深層次的觀察

敏捷 DevOps 的實踐源於人們對所生活的世界的認知。我們生活的世界存在不確定性、不完整性以及充滿太多的困惑。從科學/哲學的角度來看,這些特徵得到了海森堡的不確定性原理Heisenberg’s Uncertainty Principle(涵蓋不確定性部分),維特根斯坦的邏輯論哲學Wittgenstein’s Tractatus Logico-Philosophicus(歧義性部分),哥德爾的不完全性定理Gödel’s incompleteness theorems(不完全性方面)以及熱力學第二定律Second Law of Thermodynamics(無情的熵引起的混亂)的充分證明和支援。

簡而言之,無論你多麼努力,在嘗試解決任何問題時都無法獲得完整的資訊。因此,放下傲慢的姿態,採取更為謙虛的方法來解決問題對我們會更有幫助。謙卑會給為你帶來巨大的回報,這個回報不僅是你期望的一個解決方案,還會有它的副產品。

總結

大自然在不停地運作,這是一個持續不斷的過程。大自然沒有總體規劃。一切都是對先前發生的事情的迴應。 反饋迴圈是非常緊密的,明顯的進步/倒退都是逐步實現的。大自然中隨處可見,任何事物的都在以一種或多種形式逐步完善。

敏捷 DevOps 是工程模型逐漸成熟的一個非常有趣的結果。DevOps 基於這樣的認識,即你所擁有的資訊總是不完整的,因此你最好謹慎進行。獲得可衡量的測試(例如,假設、可測量的期望結果),進行簡單的嘗試,大多數情況下可能失敗,然後收集反饋,修復故障並繼續測試。除了同意每個步驟都必須要有可衡量的假設/測試之外,沒有其他方法。

在本系列的下一篇文章中,我將仔細研究變異測試是如何提供及時反饋來推動實現結果的。


via: https://opensource.com/article/19/8/mutation-testing-evolution-tdd

作者:Alex Bunardzic 選題:lujun9972 譯者:Morisun029 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

變異測試是測試驅動開發(TDD)的演變

訂閱“Linux 中國”官方小程式來檢視

相關文章