测试分为好多种类型

测试用例的组织

单元测试

集成测试

端到端测试

消费者测试

契约测试

这是著名的测试金字塔, 也叫测试冰淇淋, 意谓我们应该从下到上地组织编写我们的测试, 大量地单元测试覆盖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实例篇相关推荐

  1. eureka多台注册中心_微服务实战系列(五)-注册中心Eureka与nacos区别

    1. 场景描述 nacos最近用的比较多,介绍下nacos及部署吧,刚看了下以前写过类似的,不过没写如何部署及与eureka区别,只展示了效果,补补吧. 2.解决方案 2.1 nacos与eureka ...

  2. springcloud feign 服务调用其他服务_微服务实战——SpringCloud与Feign集成

    上一篇集成了ZuulGateway和Eureka并进行了测试.在实际场景中,我们肯定会有很多的微服务,而他们之间可能会存在相互调用的关系,那么,如何优雅的处理服务之间的调用问题呢?接下来就是我们要解决 ...

  3. drools动态配置规则_微服务实战系列(八)-网关springcloud gateway自定义规则

    1. 场景描述 先说明下项目中使用的网关是:springcloud gateway, 因需要给各个网关服务系统提供自定义配置路由规则,实时生效,不用重启网关(重启风险大),目前已实现:动态加载自定义路 ...

  4. spring boot和spring cloud的区别_微服务实战系列(三)-cloud、boot及maven关系

    1 . 问题描述 随着springboot.springcloud的不断迭代升级,开发效率不断提升,越来越多的开发团队加入到spring的大军中,今天用通俗的语言,介绍下什么是springboot,s ...

  5. .Net微服务实战之技术选型篇

    王者荣耀 去年我有幸被老领导邀请以系统架构师的岗位带技术团队,并对公司项目以微服务进行了实施.无论是技术团队还是技术架构都是由我亲自的从0到1的选型与招聘成型的,此过程让我受益良多,因此也希望在接下来 ...

  6. .net core 微服务_.NET 微服务实战之负载均衡(上)

    (给DotNet加星标,提升.Net技能) 转自:陈珙cnblogs.com/skychen1218/p/13327965.html 系列文章 .Net微服务实战之技术选型篇 .Net微服务实战之技术 ...

  7. controller调用controller的方法_SpringCloud Alibaba微服务实战三 - 服务调用

    导读:通过前面两篇文章我们准备好了微服务的基础环境并让accout-service 和 product-service对外提供了增删改查的能力,本篇我们的内容是让order-service作为消费者远 ...

  8. 微服务实战系列之SpringCloud Alibaba学习(四)

    微服务实战系列之SpringCloud Alibaba: 微服务实战系列之SpringCloud Alibaba学习(一) 微服务实战系列之SpringCloud Alibaba学习(二) 微服务实战 ...

  9. SpringCloud Alibaba微服务实战三 - 服务调用

    SpringCloud Alibaba微服务实战三 - 服务调用 通过前面两篇文章我们准备好了微服务的基础环境并运行注册服务到nacos上了 统一接口返回结构 在开始今天的正餐之前我们先把上篇文章中那 ...

最新文章

  1. layer.tips定义弹出的宽度
  2. emeditor利用书签功能导出匹配结果到新文件
  3. 目前计算机辅助数控编程的方法,数控编程试题(附自己整理的答案)
  4. android 把assets 里面的图片文件输出到SD卡
  5. cvc降噪和主动降噪_市面上的降噪耳机,物理降噪和主动降噪,你会怎么选择?...
  6. Spring初始化:org.springframework.we...ContextLoaderListener的作用
  7. 2019年税收分类编码_通过分析112,654个编码测试,我们了解了2019年开发人员的招聘趋势...
  8. 中国数码电影摄影机行业市场供需与战略研究报告
  9. System.Data.SqlClient.SqlError: 尚未备份数据库的日志尾部
  10. android 中的 gridview 的用法
  11. PHP如何关闭notice级别的错误提示
  12. 【游戏】基于matlab GUI音乐闹钟设计【含Matlab源码 1105期】
  13. Error in loadNamespace(name) : there is no package called ‘yaml’
  14. VMware中不能使用ifconfig教程——亲测有效
  15. 应用系统设计:在线教育平台,B2C平台设计
  16. MCS-51单片机总体概述(一)
  17. Unity-lambda表达式
  18. 大数据 | 抖音,一款神奇的APP
  19. ODM、JDM、OEM、OBM
  20. springboot集成caffeine实现本地缓存

热门文章

  1. 论文投稿指南——中文核心期刊推荐(综合性经济科学 2)
  2. linux微信教程 百度云,2019-03-08 linux安装QQ,微信,百度云,迅雷
  3. python实现人脸识别系统设计_基于ROS的人脸识别系统设计与实现
  4. 『程序员必备软件』—— 短小精悍的GIF录制软件LICEcap
  5. chrome浏览器广告屏蔽插件adblock下载地址
  6. maya骨骼绑定——蒙皮中的权重问题
  7. 【小松教你手游开发】【unity实用技能】Unity图片变灰的方式
  8. 易语言 多线程 记录
  9. 大前端开箱即用的中后台管理模版,建议收藏
  10. GNSS测量与数据处理作业