项目-Java-Junit-MockMvc-Jmeter
主要介绍java测试框架JUnit
文章目录
参考内容
testNG教程:
https://zhuanlan.zhihu.com/p/623245201
maven项目创建教程:
https://blog.csdn.net/m0_68857031/article/details/129842135
junit5官方文档:
https://doczhcn.gitbook.io/junit5/index
接口的本质
基础
接口是服务,是代码实现的一段业务逻辑,如:登录接口、认证接口;
服务可以是任意协议(https、dubbo、webService),业务逻辑实现方式为任意语言(java、python、C++)
接口测试只需要关注对应协议,无需关注后台实现语言。
HTTP接口
一般接口协议大都数为HTTP协议;
HTTP协议的特点:
分为客户端和服务端,客户端发送请求,服务端返回响应结果;
客户端发送的请求包括:URL = 接口地址,请求方式 = psot/get/delete/put/patch,请求参数 = 根据接口文档填写
服务器返回的响应包括:响应头, 响应体(json格式或其他格式);
Junit5
基础内容
Junit5是什么?
Junit5由JUnit Platform + JUnit Jupiter + JUnit Vintage3部分构成。
JUnit Platform:其主要作用是在 JVM 上启动测试框架,是运行在平台上的测试框架,其他自动化测试引擎以及开发人员定制的测试引擎可以接入JUnit5实现对接与执行;
JUnit Jupiter:提供了JUnit5 新的编程模型,是JUnit5新特性的核心,内部包含了一个测试引擎;
JUnit Vintage3:提供了兼容JUnit4.x,Junit3.x的测试引擎。
springboot结合JUnit5测试?
在pom.xml中引入依赖;创建测试类,添加@SpringbootTest注解以及@Test注解;
依赖项是:org.springframework.boot:spring-boot-starter-test
当前项目使用的测试代码不是采用Springboot实现的测试,而是直接导入JUnit测试包实现的。
测试注解
@Test : 标识测试方法;
@DisplayName : 为测试类或者测试方法 设置展示的名字
@BeforeEach :每个单元测试前执行
@AfterEach:每个单元测试后执行
@BeforeAll :所有单元测试前执行
@AfterAll :所有单元测试后执行
@Tag :单元测试的级别
@Disabled :测试类或测试方法不执行
@Timeout :测试方法运行超过制定时间返回错误
@ExtendWith :给测试类或测试方法提供扩展类引用
@ParamterizedTest :方法是参数化测试
@RepeatedTest :方法可重复执行
注意:
-
xxxEach和xxxAll区别
前者表示每个测试用例开始和结束位置需要执行的内容;
后者表示整个测试用例组执行前或执行后需要展示的内容; -
xxxAll使用
由于该注解修饰内容仅会被调用一次,如果不加限制则会报错;
解决:被标注为静态方法,即需要存在static关键词。 -
Timeout使用
需要添加参数value = 超时的数值设定,unit = 超时单位设设定;
超时会报错误,返回TestTimeout() timed out after 500 microseconds -
ExtendWith使用
使用场景:使用springboot的容器功能,使用@SpringbootTest注解,它是组合注解,包括
@ExtendWith(SpringExtension.class)
@BootstrapWith(SpringBootTestContextBootstrapper.class)
那么整个过程就是采用Springboot的测试驱动来测试。如果想对接别的平台,就需要对ExtendWith(SpringExtension.class)做定制开发; -
RepeatedTest使用
当和Test注解同时存在时,会报注解竞争异常。
断言
用于检测测试方法返回结果是否满足预取设置结果。
所有测试方法执行结束之后会产生测试详细的测试报告来检查当前测试的情况。
断言分为:简单断言、数组断言、组合断言、异常断言、超时断言、快速失败;
- 整体特性
测试方法中存在多个断言,当某个断言运行异常时,则后续代码均不执行;
断言中两个参数分别是:期望值,真实值;且都可以加三个值,则第三个为自定义错误信息;
简单断言
assertEquals : 判断两个对象或原始类型是否相等
assertNotEquals : 判断两个对象或原始类型是否不相等
assertSame :判断两个对象引用是否指向同一个对象
assertNotSame : 判断两个对象引用是否只想不同的对象
assertTrue :判断给定的布尔值是否为true
assertFalse :判断给定的布尔值是否为false
assertNull :判断给定的对象引用是否为null
assertNotNull :判断给定的对象引用是否不为null
- assetEquals使用
调用采用Assertion.assertEquals 方式,因为该方法是静态方法;
方法参数有两个,分别是(期待值,真实值);
默认异常提示是:org.opentest4j.AssertionFailedError:
该方法支持三个参数时,最后一个参数是 自定义错误提示信息;
数组断言
assertArrayEquals : 判断两个数组是否相等,针对每个位置进行匹配;
new int[]{1,2} new int[]{2,1} 这两个数组就不相等
组合断言
assertAll :可以添加多个断言,全部通过之后才成功;多个断言采用lamda表达式实现;
assertAll("给该断言起个名字",
() -> assertxxx(),
() -> assertxxx());
多个断言都成功,组合断言才会成功;
组合断言没有自定义异常描述,可以对其中的多个断言分别加描述信息。
异常断言
assertThrows : 断定测试方法一定会出现某异常。
Assertions.assertThrows(异常类,() -> 具体内容, "自定异常信息");
如果出现异常,则正常执行;反之,则报错,
org.opentest4j.AssertionFailedError: 正常执行? ==> Expected java.lang.ArithmeticException to be thrown, but nothing was thrown.
超时异常
assertTimeout :程序超过设定时间,则出现异常;
参数可以有三个:1 = 设定超时阈值时间;Duration.ofMillis(1000) 2 = 具体执行操作 3 = 自定义返回异常信息
快速失败
fail :直接将测试方法置于失败,参数为“表示该错误展示信息”
该语句运行,则直接抛出:org.opentest4j.AssertionFailedError: 快速失败
前置条件
assumption:假设,不满足前置条件会使得测试方法的执行终止,
不满足于断言会使得测试方法失败;
方法调用为:Assumption.assumeTrue 和 Assumption.assumeFalse
Assumptions.assumeTrue(Boolean.FALSE);
// org.opentest4j.TestAbortedException: Assumption failed: assumption is not true
嵌套测试
@Nested : 测试类中还有测试类 则需要写这个参数;该注解修饰内层测试类;
-
注意:
嵌套测试中,外层的test不能驱动内层测试类的beforeEach/All方法;
内层的test可以驱动外层测试类的beforeEach/All方法; -
案例:
@DisplayName("嵌套测试")
public class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
assertNull(stack);
// org.opentest4j.AssertionFailedError: expected: not <null>
// 外层测试类执行 并会不驱动内层测试类的BeforeEach方法
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, () -> stack.pop()); // 栈中弹出一个元素
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, () -> stack.peek()); // 返回栈顶元素,但不移除它
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
// 内层测试类的执行 可以驱动外层测试类的BeforeEach方法
// 该测试方法显示成功,在于 外层测试类的BeforeEach方法创建了一个Stack
// 而 本层类的BeforeEach方法 存入一个元素 所以stack不为空
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
// 该测试方法显示成功,在于 外层测试类的BeforeEach方法创建了一个Stack
// 而 本层类的BeforeEach方法 存入一个元素 又将stack中元素弹出,则stack为空;
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
// 该测试方法显示成功,在于 外层测试类的BeforeEach方法创建了一个Stack
// 而 本层类的BeforeEach方法 存入一个元素 将stack中第一个元素展示但不取出,则stack不为空;
}
}
}
参数化测试
关键注解:ParameterizedTest 表示当前测试方法不是普通测试方法,是参数化测试方法;
实现不同的参数,多次运行测试方法;
ValueSource : 为参数化测试制定入参来源,支持八大基础类以及String类型、Class类型;
NullSource :提供一个null的参数
EnumSource :提供一个枚举入参
CsvFileSource :读取CSV文件内容为参数化测试入参
MethodSource :读取指定方法的返回值作为参数化测试入参
- 案例:
@ParameterizedTest
@ValueSource(ints = {1,2,3})
public void testparameterized(int i){
System.out.println(i);
}
// 运行一次 直接输出 1 2 3
遇见问题
1. 使用@ParameterizedTest出现:
java.lang.Exception: No tests found matching Method testRiskControl(com.unicom.qh.common.redis.RedisApplicationTests) from org.junit.vintage.engine.descriptor.RunnerRequest@1b1426f4
分析:
表明JUnit运行器没有找到与@ParamemberizedTest注解匹配的测试方法。
已使用:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
依赖:
导入spring-boot-starter-test
导包:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
这个问题没有解决,望看到的网友如果知道怎么解决,请留言告知,感恩。
项目学习
1
@RunWith(SpringRunner.class)
// 指定了测试运行器,
// SpringRunner是Spring框架提供的一个测试运行器,它扩展了JUnit的SpringJUnit4ClassRunner,这使得Spring框架的测试能够以Spring应用上下文的形式进行初始化。可以使用依赖注入和事务管理;
@SpringBootTest(classes = {Application.class})
// 指示测试类是一个Spring Boot测试类,它将使用Spring Boot的测试框架进行测试;
// 会加载所有spring boot的bean,创建应用上下文,从而使得测试环境与生产环境尽可能一致。
与JUnit4的显著区别
- @Test注解
JUnit4 引入包是 org.Junit
JUnit5 引入包是 org.Junit.jupiter.Test
MockMvc
基础
基础概念
定义:
是Spring框架提供的一种用于测试Spring MVC控制器的工具,它允许开发者在不启动完整的web服务器的情况下,模拟HTTP请求并验证响应。
优点:
执行速度快 --》 不需要启动web服务器;
便于集成 --》 可以与Junit、TestNG等测试框架无缝衔接;
强大的功能 --》 对HTTP请求的详细配置和响应的全面验证;
依赖配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Mockxxx
MockMvc:
来自于:
import org.springframework.test.web.servlet.MockMvc;
定义:
是Spring Test模块的一部分,它允许开发者对Spring MVC控制器进行单元测试而无需启动完整的Web服务器;通过MockMvc,可以模拟HTTP请求并验证响应,使得测试执行速度更快,同时便于与JUnit、TestNG等测试框架集成。
使用:
使用mockMvc。perform()模拟HTTP请求,使用.andExpect()和.andReturn()等方法进行响应验证。
Mockito:
来自于:
import org.mockito.Mockito;
定义:
流行的Java单元测试框架,专门用于创建和验证模拟对象的行为。它允许开发者在编写测试时模拟外部依赖,从而使得测试更便捷,减少对外部类、系统和依赖给单元测试带来的耦合。
特点:
行为验证、测试桩、参数匹配器、注册支持、监控真实对象、重置mock对象;
MvcResult:
来自于:
import org.springframework.test.web.servlet.MvcResult;
定义:
是在执行模拟HTTP请求以测试控制器(Controller)层功能时的重要概念,它代表一个完整的HTTP响应结果,包括响应的状态码、头信息、响应体以及任何可能产生的错误等。
作用:
封装HTTP响应 -》封装由模拟的HTTP产生的完整响应信息;
获取响应细节 -》包括响应的状态码、头信息、响应体等内容;
断言测试 -》 使用MvcResult中的方法进行断言,确保控制器返回正确的HTTP状态码和数据;
一般实现
// 类注解,用于配置 JUnit 5 测试类以使用 Spring 的测试支持。
@ExtendWith(SpringExtension.class)
// 是一个 Spring Boot 提供的注解,用于对 Web 层(即控制器层)进行测试。如果使用该注解,那么Spring Boot 会自动配置一个模拟的 Spring MVC 环境,这样就可以在不启动完整应用的情况下测试控制器的行为。
@WebMvcTest(value = {Controller.class, Handler.class})
// 完成bean自动装配
@Autowired
private MockMvc mockMvc;
// 标识测试方法
@Test
MockHttpServletRequestBuilder requestBuilder = get("控制类路径/xxx/xxx");
// MockHttpServletRequestBuilder 是一个用于构建模拟HTTP请求的工具类。
// 主要在单元测试和集成测试中使用,构造出特定的HTTP请求来测试控制器(Controller)或端点(Endpoint)。
requestBuilder.param("参数名", 参数值);
// 给已创建的mockhttpservletrequestbuilder实例添加查询参数;
// 将参数通过键值对的形式填入MockHttpServletRequestBuilder对象中;
mockMvc.perform(requestBuilder) // mockMvc对象执行,之前定义的requestbuiler对象
.andExpect(status().isOk()) // 该调用方式是链式调用;--》 用于检查HTTP响应的状态码是否是200
.andDo(new ResultHandler() { // 自定义处理MvcResult对象,即模拟请求后的结果
@Override
public void handle(MvcResult mvcResult) throws Exception {
// 获取响应体并将其转化为字符串类型;
String content = mvcResult.getResponse().getContentAsString();
// 检查响应体是否为空,以确保有数据返回;
assertTrue(StringUtils.isNotBlank(content));
// 将响应体内容解析成map集合
Map<String, String> resp = JSONUtils.toMap(content);
// 验证code/message 两个属性的值是否等于1/success;
assertEquals("1", resp.get("code"));
assertEquals("success", resp.get("message"));
// 获取data属性对应的值
String data = resp.get("data");
// base64解码
byte[] encryptedData = Base64.decode(data);
// aes密钥生成
AES aes = genAES(keyIv);
// 解密data对应的值
String json = new String(aes.decrypt(encryptedData), StandardCharsets.UTF_8);
// json format is ResponseKeyCollection 自定义的一个类
ResponseKeyCollection collection = JSONUtils.toObject(json, ResponseKeyCollection.class);
// 将json转成 ResponseKeyCollection对象
assertEquals(10, collection.getResponseKeys().size());
// 验证对象中getResponseKeys方法返回的集合大小是否为10
}
});
@BeforeEach
ResponseKey responseKey1 = COLLECTION.getResponseKeys().stream().filter(key -> key.getIndex() == 1).findFirst().orElse(null);
// COLLECTION获取ResponseKeys属性--》转换为流--》过滤出index=1的responseKey对象
// --》查找第一个满足条件的--》有则返回,无则返回null;
// COLLECTION 是自定义的对象
Mockito.when(keyStoreService.getResponseKey(1)).thenReturn(responseKey1);
// 模拟该方法,当此入参为1时,则返回上述找到的对象;
Mockito.when(keyStoreService.generateResponseKeyCollection(10)).thenReturn(COLLECTION);
// 模拟该方法,当此入参为10时,则返回Collecion对象;
Mockito.when(keyHelper.isExpiredKeyIndex(anyInt())).thenReturn(false);
// 模拟此方法,不管该方法入参为谁,都返回false --》 即keyIndex永不过期
文件位置
与项目目录保持一致;
假设项目文件为
src/main/java/xxx/controller/xxxController
测试类项目文件为
src/test/java/xxx/controller/xxxTest
实战
使用1:MockMvc与Test注解不兼容
参考博客:https://segmentfault.com/q/1010000042943340
描述:
使用
@Autowired
private MockMvc mockMvc;
总是导致注入的mockMvc失败;
原因是:
MockMvc与@Test不兼容的问题;
原本依赖库为:org.junit.Test
改成org.junit.jupiter.api.Test 就可以了;
两个依赖库之间的关系为:
org.junit.Test --》 JUnit4
org.junit.jupiter.api.Test --》 JUnit5
使用2:@RequestParams参数
MockHttpServletRequestBuilder requestBuilder = get("/decrypt");
requestBuilder.param("id1", id1);
requestBuilder.param("id2", id2);
mockMvc.perform(requestBuilder)
.andExpect(status().isOk())
.andDo(mvcResult -> {
String content = mvcResult.getResponse().getContentAsString();
});
使用3:@RequestBody参数
接口请求参数:
@RequestMapping("/getfunction1")
public ResultDtoRisk riskGetTokenByph(HttpServletRequest request, @RequestBody NumReq numReq){}
测试代码:
@Autowired
pivate MockMvc mockMvc;
// import org.springframework.test.web.servlet.MockMvc;
NumReq numReq = new NumReq();
numReq.setId1(id1);
numReq.setId2(id2);
MockHttpServletRequestBuilder requestBuilder = get("/getfunction1");
// import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
String requestBodyContent = objectMapper.writeValueAsString(numReq);
requestBuilder.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBodyContent);
mockMvc.perform(requestBuilder)
.andExpect(status().isOk())
.andDo(print());
jmeter
基础
案例1-使用BeanShell
参数配置:使用变量${xxx}
消息体参数:
案例2-http请中body data不能选中
jmeter默认 参数与消息体参数两者只能存在其一,前者有参数,后者就不能使用;
参数案例:
消息体案例:
案例3-使用HTTP信息头管理器
可以设置http信息头内容,比如Content-Type的值,来设定http请求的参数传递方式。
案例4-本机jmeter加密方式
sm3:
import cn.hutool.crypto.SmUtil;
String[] appIdList = new String[]{"1c7aad9cd3abc01268c4ecfdac248a3b912073d6"};
String[] reqIdList = new String[]{"1234567"};
String[] secretList = new String[]{"806ec0000170a169"};
String ct = String.valueOf(System.currentTimeMillis());
String appId = appIdlist[0];
String reqId = reqIdList[0];
String secret = secretList[0];
String sceneId = "0001";
String ss1 = String.join("&","appId=" + appId, "ct=" + ct, "reqId=" + reqId);
log.info(ss1);
String sign = SmUtil.sm3WithSalt(secretList[0].getBytes()).digestHex(ss1);
vars.put("reqId",repId);
vars.put("appId",appId);
vars.put("sceneId",sceneId);
vars.put("ct",ct);
vars.put("sign",sign);
//vars.get("reqId");
//vars.get("appId");
//vars.get("sceneId");
//vars.get("sign");
//vars.get("ct");
md5:
import org.apache.commons.codec.digest.DigestUtils;
String apiKey = "wuxi";
String transId = "20240412150606100335423";
String code = "28f09d127a745d97b82b8d9259ce3a79";
// String ip = "127.0.0.1"; // 异常ip
String ip = "127.2.2.12";
String apiSecret = "bv78g4f38ofb";
String checkParam = "apiKey"+ apiKey +"apiSecret" + apiSecret + "transId" + transId;
String token = DigestUtils.md5Hex(checkParam);
vars.put("apiKey",apiKey);
vars.put("transId",transId);
vars.put("code",code);
vars.put("ip",ip);
vars.put("token",token);
案例5-重定向
概念:
自动重定向(状态码一般是200、20X):当重定向时,自动跳转时,只针对GET和Head请求,自动重定向可以自动跳转到最终目标页面,但是jmeter不记录重定向过程内容【在查看结果树中只能看到重定向后的响应内容】
跟随重定向(状态码一般是302、30X):当重定向时,自动跳转时,自动重定向可以自动跳转到最终目标页面,但是jmeter记录重定向过程内容【在查看结果树中既能看到重定向后的响应内容,也能看到重定向前的响应内容】
更多推荐
所有评论(0)