java测试案例_微服务实战测试之Java实例篇
測試分為好多種類型
測試用例的組織
單元測試
集成測試
端到端測試
消費者測試
契約測試
這是著名的測試金字塔, 也叫測試冰淇淋, 意謂我們應該從下到上地組織編寫我們的測試, 大量地單元測試覆蓋80%的代碼行數, 有了一個堅實的基礎, 我們再添加組織測試, 集成測試, API 測試, 端到端和用戶界面測試, 越往上維護成本越高, 測試代碼越脆弱, 越有可能由于環境問題導致并非代碼錯誤引起的失敗
測試用例的組織
首先要胸有成竹, 哪些測試應該做, 在[微服務實戰測試之理論篇] 中已提過一些指導原則, 我們要根據這些原則, 結合要測試的特性, 把所有有可能出錯的地方覆蓋到, 防患于未然.
借用 Cucumber 中的定義的 Gherkin 語法, 一個特性 Feature 有若干個場景 Scenario
而每個場景都必須有獨立的意義, 并且不依賴任何其他場景而獨立運行.
以表格的形式組織測試用例是比較常見的做法
特性Feature
場景Scenario
給定Given
事件 When
結果 Then
作為系統用戶, 我想在任務即將截止設置三次提醒來通知我, 從而我能即時采取措施而不至于超時
今天周一, 我要在周日前交稿
截止日期前一天要有郵件提醒
周六到了
收到提醒郵件
也可以用 wiki 或其他文件格式來存儲用例, 推薦用格式化, 易解析的格式化文本文件, 比如 json.
結構層次為 1) Test Suite -- 2) Test Case -- 3) Test Steps(Given, When, Then)
例如:
{
"testsuites": [
{
"name": "login_module_test",
"testcases": [
{
"name": "login_by_phone_step1",
"feature": "login",
"scenario": "login by mobile phone",
"given": "input mobile phone number",
"when": "submit",
"then": "send a sms for authenticate code"
},
{
"name": "login_by_phone_step2",
"feature": "login",
"scenario": "login by mobile phone",
"given": "input mobile phone number and authenticate code",
"when": "submit",
"then": "redirect the user's home paeg"
},
{
"name": "login_by_error_password",
"feature": "login",
"scenario": "login by username and password",
"given": "input username, password, and captcha",
"when": "submit",
"then": "dispatch login failure message: you inputed improper username or password"
}
]
}
]
}
也可以自己寫一個注解來自己生成測試用例, 我們在文末給出一個例子
單元測試
在微服務實戰測試之理論篇中我們提到測試的分類和測試金字塔, 單元測試是基石, 做好單元是首要的測試工作, 以我熟悉的幾種語言為例
測試的寫法就四步 SEVT (TVes 許多電視倒過來)
準備 setup
執行 exercise
驗證 verify
清理 teardown
簡單測試可以忽略1) 和 4) 步
Java 單元測試
哪些庫我們可以用呢
如果你使用 spring-boot-starter-test ‘Starter’ (test scope), 你會發現它所提供的下列庫:
JUnit?—?The de-facto standard for unit testing Java applications.
Spring Test & Spring Boot Test?—?Utilities and integration test support for Spring Boot applications.
AssertJ?—?A fluent assertion library.
Hamcrest?—?A library of matcher objects (also known as constraints or predicates).
Mockito?—?A Java mocking framework.
JSONassert?—?An assertion library for JSON.
JsonPath?—?XPath for JSON.
單元測試框架的鼻祖是 junit, 為什么不用 junit 呢? TestNG 有什么獨到之處可以后來居上呢? 原因就在于 testng 更為強大的功能, 如 @Test 注解, 可以指定 testcase 的依賴關系, 調用次數, 調用順序, 超時時間, 并發線程數以及期望的異常, 考慮得非常周到.
當然, 這只是個人喜好, Junit 新版本也多了很多改進.
舉個例子, Fibonacci 數列大家很熟悉, 用 Java8 的 stream, lambda 的新寫法比老的寫法酷很多, 代碼行數少了許多.
老寫法
public List fibonacci1(int size) {
List list = new ArrayList<>(size);
int n0 = 1, n1 = 1, n2;
list.add(n0);
list.add(n1);
for(int i=0;i < size - 2; i++) {
n2 = n1 + n0;
n0 = n1;
n1 = n2;
list.add(n2);
}
return list;
}
新寫法
public List fibonacci2(int size) {
return Stream.iterate(new int[]{1, 1}, x -> new int[]{x[1], x[0] + x[1]})
.limit(size).map(x -> x[0])
.collect(Collectors.toList());
}
然而性能如何呢? 寫個單元測試吧
package com.github.walterfan.example.java8;
import com.google.common.base.Stopwatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Created by walter on 24/03/2017.
* @see http://testng.org/doc/documentation-main.html
*/
public class LambdaPerfTest {
private static final Logger logger = LoggerFactory.getLogger(LambdaPerfTest.class);
private Map oldFibonacciResults;
private Map newFibonacciResults;
@BeforeClass
public void init() {
oldFibonacciResults = new ConcurrentSkipListMap<>();
newFibonacciResults = new ConcurrentSkipListMap<>();
}
@AfterClass
public void summarize() {
int rounds = oldFibonacciResults.size();
System.out.println("--- old vs. new ---");
oldFibonacciResults.forEach((key, value) -> {
System.out.println(key + ": " + value + " vs. " + newFibonacciResults.get(key));
//TODO: add assert for performance compare
});
}
public List fibonacci1(int size) {
List list = new ArrayList<>(size);
int n0 = 1, n1 = 1, n2;
list.add(n0);
list.add(n1);
for(int i=0;i < size - 2; i++) {
n2 = n1 + n0;
n0 = n1;
n1 = n2;
list.add(n2);
}
return list;
}
public List fibonacci2(int size) {
return Stream.iterate(new int[]{1, 1}, x -> new int[]{x[1], x[0] + x[1]})
.limit(size).map(x -> x[0])
.collect(Collectors.toList());
}
@DataProvider
public Object[][] getFibonacciSize() {
return new Object[][]{
{10},
{50},
{100},
{1000},
{10000}
};
}
@Test(dataProvider = "getFibonacciSize", description = "old fibonacci", timeOut = 1000)
public void testOldFibonacci(int size) {
long duration = testFibonacci("testFibonacci1", size, x->fibonacci1(x));
oldFibonacciResults.put(size, duration);
}
@Test(dataProvider = "getFibonacciSize", description = "lambda fibonacci", timeOut = 1000)
public void testNewFibonacci(int size) {
long duration = testFibonacci("testFibonacci2", size, x->fibonacci2(x));
newFibonacciResults.put(size, duration);
}
public long testFibonacci(String name, int size, Function > func) {
Stopwatch stopwatch = Stopwatch.createStarted();
List list = func.apply(size);
stopwatch.stop();
long duration = stopwatch.elapsed(TimeUnit.MICROSECONDS);
list.stream().forEach(x -> System.out.print(x +", "));
System.out.println(String.format("\n--> %s (%d): %d\n" , name, size, duration));
return duration;
}
}
做了5組數列長度從10到10000 的測試, 輸出結果如下
--- old vs. new ---
10: 34 vs. 28965
50: 9 vs. 154
100: 13 vs. 669
1000: 112 vs. 2600
10000: 1019 vs. 13548
不測不知道, 一測嚇一跳, 新的寫法看起來不錯, 但是性能完敗, 關鍵在于多做了兩次轉換(map , collect), 這里的測試代碼用到了 @BeforeClass, @AfterClass, @Test, @DataProvider, TestNG 還有一些不錯的功能, 比如 @threadPoolSize, @expectedExceptions, 詳情參見 http://testng.org/doc/documentation-main.html
不知道你發現沒有, 這里有個大問題, 這段測試代碼缺少 Assert, 多數情況下對于功能測試必需要有 assert , 這些 assert 就是檢查點, 沒有檢查點的測試起不到真正的作用. 你不可能去看每個測試的輸出, 當然這里說的是單元測試,而對于性能測試, 一般要出一個性能測試的報告, Assert 檢查點也不是必需的
所以我們應該這樣寫, 盡量多地加斷言, 例如我們對 google 的 libphonenumber 作一個簡單的測試
package com.github.walterfan.devaid.util;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;
@Slf4j
public class PhoneNumberUtilTest {
@Test
public void testIsNumberNsnMatch() {
String phoneNumberOne = "+86055112345678";
String phoneNumberTwo = "86055112345678";
PhoneNumberUtil.MatchType matchType = PhoneNumberUtil.getInstance().isNumberMatch(phoneNumberOne, phoneNumberTwo);
log.info("matchType is {}", matchType);
assertFalse(matchType == PhoneNumberUtil.MatchType.NO_MATCH);
assertFalse(matchType == PhoneNumberUtil.MatchType.NOT_A_NUMBER);
assertEquals(matchType , PhoneNumberUtil.MatchType.NSN_MATCH);
}
@Test
public void testIsNumberShortMatch() {
String phoneNumberOne = "+86055112345678";
String phoneNumberTwo = "086(0551)1234-5678";
PhoneNumberUtil.MatchType matchType = PhoneNumberUtil.getInstance().isNumberMatch(phoneNumberOne, phoneNumberTwo);
assertFalse(matchType == PhoneNumberUtil.MatchType.NO_MATCH);
assertFalse(matchType == PhoneNumberUtil.MatchType.NOT_A_NUMBER);
assertEquals(matchType , PhoneNumberUtil.MatchType.SHORT_NSN_MATCH);
}
@Test
public void testGetCountryCode() {
String strPhoneNumber = "+86-0551-12345678";
try {
Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(strPhoneNumber, "US");
log.info("phoneNumber.getCountryCode() is {}", phoneNumber.getCountryCode());
assertTrue(phoneNumber.getCountryCode() == 86);
} catch (NumberParseException e) {
fail(e.getMessage());
}
}
}
數據驅動測試
舉例如下, 被測試類為 HttpUtil
public class HttpUtil
{
public static boolean hasFieldValue(String httpHeader, String fieldKey, String fieldVal) {
if(null == httpHeader || null == fieldKey || null == fieldVal) {
return false;
}
String[] toggles = httpHeader.split(";");
for(String toggle: toggles) {
String[] toggleKeyVal = toggle.split("=");
if(toggleKeyVal.length > 1) {
String key = StringUtils.trim(toggleKeyVal[0]);
String val = StringUtils.trim(toggleKeyVal[1]);
if(fieldKey.equals(key) && fieldVal.equalsIgnoreCase(val)) {
return true;
}
}
}
return false;
}
}
我們會用多個不同的 HTTP 頭域來測試這個待測方法是否可正確地把相應頭域的值判斷出來, 用到的測試數據不必手工構造, 可以放在一個在 Object[][]為返回結果的方法中返回, 這些數據會逐個喂給測試方法, 決竅在于這個注解: @Test(dataProvider= "makeHttpHeadFields")
所以我們的一個測試方法最終 會生成 8 個測試用例
具體代碼如下
public class HttpUtilTest {
@DataProvider
public Object[][] makeHttpHeadFields() {
return new Object[][] {
{ "acl_enabled= true", true },
{ "acl_enabled=true; auth_type=oauth", true },
{ "acl_enabled =TRue; auth_type=basic", true },
{ "acl_enabled = false; auth_type=basic", false },
{ " acl_enabled = ; auth_type=basic", false },
{ "auth_type=basic", false },
{ "", false }
};
}
@Test(dataProvider= "makeHttpHeadFields")
public void testHasFieldValue(String toggleHeader, boolean ret) {
Assert.assertEquals(HttpUtil.hasFieldValue(toggleHeader, "acl_enabled", "true") ,ret);
}
}
運行結果如下
Test Results
對于單元測試的測試用例組織主要是要邏輯分支覆蓋, 符合 微服務實戰測試之理論篇 中所提到的三大原則
FIRST 原則
Right-BICEP
CORRECT 檢查原則
還有很多線程測試, 性能測試, 壓力測試, 異常測試, API 測試, 以及消費者契約測試,
這些測試我們后面慢慢道來, Mock 和 API 測試可參見 微服務實戰之Mock
下面我們就之前提到的測試用例的組織編寫一個 TestCase 注解和它的注解處理器, 可在很方便地生成測試用例文檔
編寫注解來自動生成測試用例
package com.github.walterfan.hello.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface TestCase {
String value();
String feature() default "";
String scenario() default "";
String given() default "";
String when() default "";
String then() default "";
//String[] checkpoints();
}
在編譯階段處理注解并生成測試用例文檔
package com.github.walterfan.hello.annotation;
import com.google.auto.service.AutoService;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.github.walterfan.hello.annotation.TestCase")
@AutoService(Processor.class)
public class TestCaseProcessor extends AbstractProcessor {
public final static String TABLE_TITLE1 = "| # | feature | case | scenario | given | when | then |\n";
public final static String TABLE_TITLE2 = "|---|---|---|---|---|---|---|\n";
public final static String TABLE_ROW = "| %d | %s | %s | %s | %s | %s | %s |\n";
private File testcaseFile = new File("./TestCases.md");
private StringBuilder testcaseBuilder = new StringBuilder();
private AtomicInteger testCaseNum = new AtomicInteger(0);
@SuppressWarnings("unchecked")
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
testcaseBuilder.append("# Testcases");
testcaseBuilder.append("\n");
testcaseBuilder.append(TABLE_TITLE1);
testcaseBuilder.append(TABLE_TITLE2);
try (BufferedWriter bw = new BufferedWriter(new FileWriter(testcaseFile))) {
bw.write(testcaseBuilder.toString());
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
StringBuilder sb = new StringBuilder();
for (TypeElement annotation : annotations) {
for (Element element : roundEnvironment.getElementsAnnotatedWith(annotation)) {
TestCase testCase = element.getAnnotation(TestCase.class);
if (testCase != null) {
String line = String.format(TABLE_ROW, testCaseNum.incrementAndGet(), testCase.feature(), testCase.value(), testCase.scenario(), testCase.given(), testCase.when(), testCase.then());
sb.append(line);
}
}
}
try (BufferedWriter bw = new BufferedWriter(new FileWriter(testcaseFile, true))) {
bw.write(sb.toString());
System.out.println("testcases:\n" + sb.toString());
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
}
假設我們有一個簡單的類 User
package com.github.walterfan.hello.annotation;
import lombok.Data;
import java.util.Calendar;
import java.util.Date;
@Data
public class User {
private String name;
private String email;
private Date birthDay;
public int age() {
Calendar now = Calendar.getInstance();
now.setTime(new Date());
Calendar birth = Calendar.getInstance();
birth.setTime(birthDay);
return Math.abs(now.get(Calendar.YEAR) - birth.get(Calendar.YEAR));
}
}
我們寫一個測試類
package com.github.walterfan.hello.annotation;
import com.github.walterfan.hello.annotation.User;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import static org.testng.Assert.assertEquals;
public class UserTest {
@Test
@TestCase(value = "testAge", feature = "UserManage", scenario = "CreateUser" ,given = "setBirthday", when="retrieveAge", then = "Age is current time minus birthday")
public void testAge() throws ParseException {
User user = new User();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd");
Date birthDay = formatter.parse("1980/02/10");
user.setBirthDay(birthDay);
Calendar birthCal = Calendar.getInstance();
birthCal.setTime(birthDay);
int diffYear = Calendar.getInstance().get(Calendar.YEAR) - birthCal.get(Calendar.YEAR);
System.out.println("diffYear: "+ diffYear);
assertEquals(user.age(), diffYear);
}
@Test
@TestCase(value = "testName", feature = "UserManage", scenario = "UpdateUser" ,given = "setName", when="retrieveName", then = "name is same")
public void testName() throws ParseException {
String name = "Walter";
User user = new User();
user.setName(name);
user.getName().equals(name);
}
}
編譯這個類會自動生成一個 TestCase.md, 內容如下
Testcases
| # | feature | case | scenario | given | when | then |
|---|---|---|---|---|---|---|
| 1 | UserManage | testAge | CreateUser | setBirthday | retrieveAge | Age is current time minus birthday |
| 2 | UserManage | testName | UpdateUser | setName | retrieveName | name is same |
也就是
#
feature
case
scenario
given
when
then
1
UserManage
testAge
CreateUser
setBirthday
retrieveAge
Age is current time minus birthday
2
UserManage
testName
UpdateUser
setName
retrieveName
name is same
參考資料
總結
以上是生活随笔為你收集整理的java测试案例_微服务实战测试之Java实例篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: arduino音乐播放器(蜂鸣器版)
- 下一篇: 微信获得用户信息