小黃瓜的驗收測試

Cucumber是一個以驗收測試為目標的框架,對碼農來說,就是有人(PM或參與的用戶)把使用者情境(或者說是使用者腳本)的過程寫出來,然後我們想法子匹配到我們的程式中,看看程試跑出的結果是否有符合腳本所要描述的狀態。
廢話少說,先體驗下:
  1. 首先開個目錄 cuke-first,然後在cuke-first/lib裡面放入cucumber-java.jarcucumber-core.jarcucumber-jvm-deps.jargherkin.jar
  2. 然後在cuke-first/features寫入我們的第一個腳本如下checkout.feature
# language: zh-TW
功能: 結帳
 生鮮超市結帳測試腳本
 場景: 香蕉結帳
   假設"香蕉"每斤40元
   當我買了1斤"香蕉"
   那麼總價要付40元
  1. 看一下雛形寫法
cuke-first]$java -cp "lib/*:." cucumber.api.cli.Main -p pretty --snippets camelcase -d features
功能: 結帳
 生鮮超市結帳測試腳本

 場景: 香蕉結帳      # checkout.feature:4
    假設"香蕉"每斤40元
    當我買了1斤"香蕉"
    那麼總價要付40元

1 Scenarios (1 undefined)
3 Steps (3 undefined)
0m0.000s


您可以用下面片段的程式來實作遺漏的步驟:

@假設("^\"([^\"]*)\"每斤(\\d+)元$")
public void 每斤元(String arg1, int arg2) throws Throwable {
   // Write code here that turns the phrase above into concrete actions
   throw new PendingException();
}

@當(("^我買了(\\d+)斤\"([^\"]*)\"$"))
public void 我買了斤(int arg1, String arg2) throws Throwable {
   // Write code here that turns the phrase above into concrete actions
   throw new PendingException();
}

@那麼("^總價要付(\\d+)元$")
public void 總價要付元(int arg1) throws Throwable {
   // Write code here that turns the phrase above into concrete actions
   throw new PendingException();
}
請注意:腳本中以雙引號(")夾起的文字會變成String參數,而所有的數字會變成int 參數
上述的參數說明:
-p
表示輸出結果,若是值為 pretty,則會將整本腳本完整印出,若值為progress,則會用 . F - U P 分別表示 通過 失敗 跳過 未定義 暫停
其它可用的有 junit,html:報表目錄,json:輸出路徑與檔名 等用法
--snippets
雛形函式的名稱,值可用 underscore (以底線分隔), camelcase(第一字小寫,而後面每個字的第一個字大寫),因為我們使用中文腳本,函式名稱又自定義,所以這個參數沒有什麼用
-d
腳本所在的目錄

  1. 我不太喜歡Java用變數與方法用中文,所以先把英中對照表找出來
cuke-first]$java -cp "lib/*:." cucumber.api.cli.Main --i18n zh-TW
feature
"功能"
background
"背景"
scenario
"場景", "劇本"
scenario_outline
"場景大綱", "劇本大綱"
examples
"例子"
given
"* ", "假如", "假設", "假定"
when
"* ", "當"
then
"* ", "那麼"
and
"* ", "而且", "並且", "同時"
but
"* ", "但是"
given (code)
"假如", "假設", "假定"
when (code)
"當"
then (code)
"那麼"
and (code)
"而且", "並且", "同時"
but (code)
"但是"
上面標(code)的表示是用在程式中的@Annotation,而沒有標(code)的表示是用在腳本的語法中
  1. 開始寫第一個測試程式到cuke-first/steps,並加以編譯
cuke-first]$ vi ./steps/CheckoutSteps.java
package steps;

import cucumber.api.java.en.*;
import java.util.*;

public class CheckoutSteps {
   Map<String,Integer> pricesMap = new HashMap<>();

   @Given("^\"([^\"]*)\"每斤(\\d+)元$")
   public void thePerKiloPriceOfFruitI(String fruit, int price) throws Throwable {
       pricesMap.put(fruit,price);
   }

   @When(("^我買了(\\d+)斤\"([^\"]*)\"$"))
   public void iCheckout(int kilo, String fruit) throws Throwable {
       //TODO:把水果加入結帳
       summary += kilo * pricesMap.get(fruit);
   }

   @Then("^總價要付(\\d+)元$")
   public void theTotalPriceShouldBeDollars(int total) throws Throwable {
       //TODO:測試總價是否正確
       assert summary == total : "結帳價格計算錯誤";
   }
}
:x
cuke-first]$ javac -cp ".:lib/*" ./steps/*.java
  1. 進行測試
cuke-first]$ java -cp "lib/*:." -ea cucumber.api.cli.Main -p pretty -g steps features
# language: zh-TW
功能: 結帳
 生鮮超市結帳測試腳本

 場景: 香蕉結帳              # checkout.feature:4
   假設"香蕉"每斤40 # CheckoutSteps.thePerKiloPriceOfFruitIs(String,int)
   當我買了1斤"香蕉"   # CheckoutSteps.iCheckout(int,String)
   那麼總價要付40    # CheckoutSteps.theTotalPriceShouldBeC(int)

1 Scenarios (1 passed)
3 Steps (3 passed)
0m0.075s
上述多了一個 -g 參數,目的在於將程式與腳本結合,因為測試程式放在 steps目錄下,而腳本放在 features下,所以必須用 -g 告訴 cucumber,將程式與腳本串聯(glue)在一起

腳本寫法說明

  1. 首先第一行使用 # language: zh-TW 說明我們的腳本是用正體中文書寫,每個階層習慣以兩個空白字元作縮排。
  2. 功能: 開頭,接著用短文字作為這腳本的名稱,例如上述的 結帳;若是有詳細說明,請換行寫(可容許多行),例如上述的 生鮮超市結帳測試腳本
  3. 然後以 場景:劇本:場景大綱:劇本大綱: 開頭,開始描述要測試的內容,內容至少要包含一句 假設假如假定 開頭 的句子,可用 # 來表示註解,範本如下:
# language: zh-TW
功能: 結帳

劇本大綱: 買香蕉結帳
   假設”香蕉”每斤40元
# <count>與 <total>變數會對映到下面的例子的 count、total 參數,
   當我買了<count>斤"香蕉"  
   那麼總價要付<total>元
#例子中的兩個參數會代入上面的 <count> 與 <tottal> ,也就是會用兩組參數各執行一次測試
   例子:
   | count | total |
   | 1       | 40    |
   | 2       | 80    |
   
 場景: 兩串蕉分別掃條碼
   假設”香蕉”每斤40元
   當我買了1斤"香蕉"
   並且結帳時又加了1斤"香蕉"
   那麼總價要付80元

 場景: 買1斤香蕉和一斤蘋果
   假設”香蕉”每斤40元
   同時"蘋果"每斤25元
   當我買了1斤"香蕉"
   並且結帳時又加了1斤"蘋果"
   那麼總價要付65元
  1. 照前述的第3點,重新產生一下雛形寫法,會發現腳本寫了那麼長,卻只產生幾個函試,所以可以確定一點,cucumber的測試是以讀取腳本,然後以 Regex 找尋符合的函式,一一執行,所以要小心語法相同,卻作不同事的描述。
  2. 腳本寫作的原則,一次只測一件事,最好只縮減到只剩 假設 當 那麼,就像作實驗一樣,過多的可變因子會導致錯誤時,卻不知道是因為什麼因素而失敗。

結合框架

回頭看上面的步驟,會發現,所有的工作的進入點都是一支 cucumber.api.cli.Main 程式,然後透過參數告訴cucumber.api.cli.Main應該怎麼作,而通常我們作測試,並不會用這麼粗爆的方式,最常見的就是結合測試框架,例如 JUnitTestNG
跟之前的測試一樣JUnitTestNG也要有一個進入點,官方的Github裡有很多的範例程式,不過我還是大略說明一下。

JUnit的進入點(測試程式,在此要用到cucumber-junit.jarcucumber-html.jar)

import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty", "html:target/cukeReport"}, snippets = SnippetType.CAMELCASE,
       features = {"src/test/resources/features"},glue = {"kentyeh.steps"})
public class RunCukesTest {
}
說明:
  1. Cucumber.class如同之前的cucumber.api.cli.Main一樣,是Cucumber Junit測試程式的進入點,@CucumberOptions而就相當於之前的參數設定,所以一個專案內就只應存在這一組,用多組進入程式或是多個@CucumberOptions可能會導致對可知的後果
  2. features指示Cucumber ,腳本的存放路徑,這樣子Cucumber才知道要從那裡讀取腳本
  3. glue指示腳本測試程式的package名稱,例如之前的 CheckoutSteps.java就應該放在這個package下,那麼cucumber才會在讀取腳本的時候,才知道到那個package下去找Matched的程式進行驗收測試。

TestNG的進入點(要用到cucumber-testng.jar)

import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import cucumber.api.testng.AbstractTestNGCucumberTests;

@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty", "html:target/cukeReport"}, snippets = SnippetType.CAMELCASE,
       features = {"src/test/resources/features"},glue = {"kentyeh.steps"})
public class RunCukesTest extends AbstractTestNGCucumberTests{
}
說明:跟Junit沒太多的差別,唯一不同的就是繼承AbstractTestNGCucumberTests

Spring 的 Autowired(要用到cucumber-spring.jar)

沒有什麼特別,直接在 XXXsteps.java內使用spring 的 annotation,這裡有兩個範例

若您是 spring + maven 的開發者,可以用以下指令產生範例專案
$mvn archetype:generate -DarchetypeGroupId=com.github.kentyeh \
-DarchetypeArtifactId=springJdbiArch -DarchetypeVersion=2.2
經問答式產出專案後進入專案目錄執行以下指令進行測試
$mvn test
或是可以以下指令,提示cucumber腳本step程式雛形
$mvn -Pcuke initialize
其它部分請參考專案首頁


Guice(要用到cucumber-guice.jar)

沒有用過,不過應該與Spring差不多

從這個索引目錄可以看出,有其它可以配合的框架,請自行從官方的GitHub學習。

留言

這個網誌中的熱門文章

企業人員的統一管理-FreeIPA學習筆記

證交所最佳五檔的程式解析

Postgresql HA