用junit5編寫一個類ZeroCode的測試框
- 2019 年 11 月 12 日
- 筆記
技術點:
最近筆者在嘗試基於應用日誌來自動生成測試用例。這其中就需要一個配套的簡易測試框架。梳理了一下,其中的技術點有: 0.使用csv文件來定義測試用例及步驟 1.使用自定義測試註解來定義測試用例(參考ZeroCode) 2.使用Junit5提供的extension機制來實現測試執行 3.使用簡單工廠類提供執行驅動 4.使用OpenCsv來實現解析 5.使用Lombok來定義Java Bean 6.使用Junit5提供的參數化測試解決方案junit-jupiter-params來實現測試用例集
來自ZeroCode的參考
ZeroCode是一個輕量級的開源測試框架。它通過使用JSON或者YAML文件格式來定義測試用例,進而讓測試用例的編寫變得更為容易。 以下是其github項目首頁提供的案例
@Test @Scenario("test_customer_get_api.yml") public void getCustomer_happyCase(){ // No code goes here. This remains empty. }其中test_customer_get_api.yml中就描述了這個介面測試用例的全部要素,具體如下:
--- url: api/v1/customers/123 method: GET request: headers: Content-Type: application/json retry: max: 3 delay: 1000 verify: status: 200 headers: Content-Type: - application/json; charset=utf-8 body: id: 123 type: Premium Visa addresses: - type: Billing line1: 10 Random St verifyMode: LINIENT
這個YAML文件中包括了http介面測試中所需要的請求(含url、head、類型)以及返回、驗證模式等內容,是一個不錯的用例DSL。本身這是一個很好的開源測試框架,涵蓋的測試類型也比較多,參與維護的人員和更新速度也不錯。
A community-developed, free, open source, declarative API automation and load testing framework built using Java JUnit core runners for Http REST, SOAP, Security, Database, Kafka and much more. It enables to create and maintain test-cases with absolute ease.
實際項目中的需求
在實際的測試過程中,對於文本格式的測試用例,往往有以下的需求:
- 測試用例的步驟描述通常是自定義的,而不是根據工具提供的DSL來編寫。
- 用例編寫儘可能少一些冗餘的內容,以便節約用例編寫時間。例如在前述介面測試案例中的head,Content-Type等等,在某個系統的介面規範中,往往都是規定了固定格式的。
- 可能的話,@Test之類的Java程式碼也不用寫了。測試人員只寫用例文件,框架通過掃碼文件目錄和文件來執行用例。
為了實現上述需求,這就要求根據測試的特點,來訂製一個類似的簡易測試框架。
使用文件來定義測試用例和步驟
當設計一個自動化測試用例框架時,有一個很重要的三聯問問題:
如何定義一個用例?如何定義用例的步驟?如何定義一個用例集?
在本案例中,我們約定
- 一個文件(csv)是一個用例
- 文件中的一行是用例的一個步驟
- 包含若干文件的目錄,組成了一個用例集
至於用csv文件來作為用例的載體,而不是json/yaml等更新的文件類型,或者xml/excel等傳統文件類型,主要是基於以下兩方面考慮
- 介面自動化測試,尤其是面向業務功能的測試,其請求、入參、出參的結構相對固定。
- csv擅長表達結構固定的數據內容,且格式冗餘最小。
因此,如果以前述ZeroCode的介面為例,一個簡單的介面自動化測試的用例格式可以是
num | type | url | params | response |
|---|---|---|---|---|
1 | get | 「api/v1/customers/123」 | {"id": 123,"type": "Premium High Value","addresses": [{"type":"home","line1":"10 Random St"}]}" |
讀者可能會問,那麼head,content-type這些不要了么?status code 都等於200么?實際項目中經常用到的token怎麼沒有體現?等等問題。 這裡我們假設,
- head,content-type,token這些內容在功能測試時,基本屬於不變化的內容。
- status code =200,如果有業務層面的錯誤,在response中可以通過errorCode的方式體現。 每個框架都有其應用場合。上面這樣的框架,主要是應用於業務層面的測試,而不是介面自身的魯棒性測試等場合。這樣簡單的格式,也非常適合不太能寫程式碼的同學來寫自動化測試用例。
以下是編寫完成以後的一個測試用例的樣例
package org.codefx.demo.junit5.extensions; import org.junit.jupiter.api.Test; import com.demo.junit5.Scenario; class ScenarioTest { @Test @Scenario(value=".\tests\demo1\sample.csv") public void sampleTest() { } }其中的sample.csv中的內容就是前述表格中的內容
[未完待續]
自定義註解
我們來看下測試用例中的註解的具體實現:
package com.demo.junit5; import java.lang.annotation.*; import org.junit.jupiter.api.extension.ExtendWith; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(ScenarioExtension.class) public @interface Scenario { String[] value() default ""; }通過@Scenario 在某個方法上的註解,可以調用 @ExtendWith(ScenarioExtension.class) 中的具體功能。這也是JUnit5提供的一種回調機制,來擴展Junit5測試框架的功能。
具體的Extension
JUnit5提供了非常友好的擴展性,最常用的是Before/After配套的一些Callback介面上,如下圖所示:

這裡我們就使用了一個BeforeTestExecutionCallback的介面來進行擴展,在被註解的用例執行之前,Junit5會首先調用該介面,實現自定義的功能。
package com.demo.junit5; import java.io.IOException; import java.io.Reader; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Iterator; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.util.AnnotationUtils; import com.demo.junit5.bean.TestStep; import com.demo.junit5.runner.Runner; import com.demo.junit5.runner.RunnerFactory; import com.opencsv.bean.CsvToBean; import com.opencsv.bean.CsvToBeanBuilder; class ScenarioExtension implements BeforeTestExecutionCallback { private Runner runner = RunnerFactory.getRunner(""); //實際項目中一般通過配置來傳入具體的runner類型。這裡只是一個Dummy樣例。 // EXTENSION POINTS public void beforeTestExecution(ExtensionContext context) throws Exception{ Scenario scenario = AnnotationUtils.findAnnotation( context.getRequiredTestMethod(), Scenario.class) .orElse(null); if (scenario == null) { scenario = AnnotationUtils.findAnnotation( context.getRequiredTestClass(), Scenario.class) .orElse(null); } for(String v:scenario.value()) { runCase(runner,v); } } private static void runCase(Runner runner,String testCase) throws IOException { Reader reader = Files.newBufferedReader(Paths.get(testCase)); CsvToBean<TestStep> csvToBean = new CsvToBeanBuilder<TestStep>(reader) .withType(TestStep.class) .withIgnoreLeadingWhiteSpace(true) .withSeparator(',') .build(); Iterator<TestStep> csvIterator = csvToBean.iterator(); while(csvIterator.hasNext()) { TestStep testStep = csvIterator.next(); runner.run(testStep); } } }通過實現BeforeTestExecutionCallback 介面中的beforeTestExecution方法,可以將傳入的用例文件內容(測試步驟)進行解析,並交給一個Runner進行執行。
執行器Runner相關
再來看一下Runner介面
package com.demo.junit5.runner; import com.demo.junit5.bean.TestStep; public interface Runner { public void run(TestStep testStep); }其中的run介面用於具體的執行。作為示例,這裡先給一個MockRunner
package com.demo.junit5.runner; import com.alibaba.fastjson.JSON; import static org.assertj.core.api.Assertions.assertThat; import com.demo.junit5.bean.TestStep; import lombok.extern.slf4j.Slf4j; @Slf4j public class MockRunner implements Runner { public void run(TestStep testStep) { log.info("mock runner called"); log.info("step #{}",testStep.getNum()); log.info(JSON.toJSONString(testStep)); ssertThat(testStep.getResponse()).isEqualToIgnoringCase(testStep.getResponse()); } }實際工作中可以使用Rest-Assured等工具來實現HTTP介面的調用,並進行結果的驗證。如果是TCP等類型的介面,換一種具體實現即可。 有經驗的讀者可能已經想到了,這就是一個典型的工廠設計模式的使用場景。我們用一個簡單工廠作為示例:
package com.demo.junit5.runner; public class RunnerFactory { public static Runner getRunner(String runner) { return new MockRunner(); } }目前這個工廠只提供MockRunner一種實現。
業務Bean -TestStep
測試步驟的Bean 如下:
package com.demo.junit5.bean; import com.opencsv.bean.CsvBindByName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class TestStep { @CsvBindByName(column="num") private int num; @CsvBindByName(column="type") private String type; @CsvBindByName(column="url") private String url; @CsvBindByName(column="params") private String params; @CsvBindByName(column="response") private String response; }通過Lombok極大地簡化了程式碼。而通過opencsv,可以極為方便地實現csv文件和bean之間地轉換。
CsvToBean<TestStep> csvToBean = new CsvToBeanBuilder<TestStep>(reader) .withType(TestStep.class) .withIgnoreLeadingWhiteSpace(true) .withSeparator(',') .build();只要通過opencsv5.0提供的建造者方法一行程式碼就能完成了。
小節
至此,一個簡單的自定義文件的測試框架就構建完畢了,從測試用例來看,測試方法體可以是ZeroCode,基本實現了全部測試用例在文件中體現的目標。總結一下使用到的技術點:
0.使用csv文件來定義測試用例及步驟 1.使用自定義測試註解來定義測試用例(參考ZeroCode) 2.使用Junit5提供的extension機制來實現測試執行 3.使用簡單工廠類提供執行驅動 4.使用OpenCsv來實現解析 5.使用Lombok來定義Java Bean
至於參數化構建,我們將在後續完成。

