Java测试
# 1. 为什么需要测试
# 1.1 软件缺陷
任何事物都是有缺陷的,软件也不例外。
计算机软件是人类发明的,计算机软件也是由程序员编写的,是人都会犯错,所以计算机软件必定存在各种各样的问题。
# 1.2 软件缺陷案例
- 2018年10月29日,印度尼西亚狮航一架波音737MAX8从首都雅加达起飞13分钟后,在附近海域坠毁,机上189人无一幸免。2019年3月10日,埃塞俄比亚航空一架波音737MAX8从首都亚的斯亚贝巴起飞后约6分钟,飞机坠落,8名机组人员和149名乘客无人生还,两起空难与飞机自动防失速系统“机动特性增强系统”(MCAS)有关;
- 爱国者导弹防御系统时钟的一个很小的计时错误积累起来到14小时后,跟踪系统不再准确,导致28名士兵死亡;
- 千年虫问题导致损失已达数千亿美元;
- 闰年虫导致闰年2月29日1500多辆出租车时间性锁表故障;
- 12306春运崩溃······
这几个案例都是由软件缺陷引起的案例,除了这些还有很多,随着软件越来越复杂,支持的功能越来越多,软件的问题也会越来越多,这个很好理解,比如写了1000行代码可能由一个bug,而写了一万行代码可能有十个bug,这就是经常说的做的多,错的多
# 1.3 软件测试的重要性
错误从来不分大小,只是错误所带来的影响大小,有些影响很小,可能微乎其微,有些错误影响很大,世人皆知,我们没有办法准确预测问题的影响面有多大,我们只能尽可能的在软件面向用户的的时候减少软件的问题,这个就是测试人员所要完成的工作,尽可能的发现软件的bug,并推动解决问题。
任何的软件产品,都不希望出现质量问题,如何发现软件缺陷,是一个合格的软件测试的评判标准,如何发现潜在的软件缺陷,是一个优秀的软件测试的职责,也是每一个软件测试从业人员的目标。
# 1.4 开发自测代码
程序员的职责就是开发自认为完美的程序,我们自己写的程序是否要自己重新测试一遍呢?
程序员劈里啪啦的疯狂输出代码,很多小伙伴对自己的代码很有信心,认为经手的代码不会出错,肯定是测试在坑我,但是奈何自己没有测试帮手,唯有自己的代码自己测试,一边开发一边测试
很多大公司都会有自己的测试团队,他们会对代码进行完整的测试之后才会上线,但是很多人都会觉得我自己写的代码,有必要自己再测试一遍吗?而且不还有测试工程师吗?
这里有一个很搞笑的问题,万一系统上线之后,出现问题该是谁来背锅?开发人员说都是测试的责任,没有测试出来错误;但是测试人员就说都是开发,代码写得不好。所以这里到底是谁的锅我们就有点难判断了。
所以为了避免这种事情发生,我们开发人员退一步,自己开发的代码先自己测试一遍,看功能是否正常,逻辑是否有问题,然后我们再交给测试,这样的愉快过程对大家都好。
# 1.4.1 如何自测代码
我们到底该如何测试自己的代码呢?换句话来说我们测试应该要遵循什么样的原则呢?
# 1.4.1.1 单一职责
我们测试某一个代码一定是从某一个功能的最小功能测起,测试是否有问题,就像函数一样,只测试一个功能,一旦我们代码比较多的时候,测试就难以进行了,特别如何代码耦合性很高,很难进行测试,如果多个功能之间互相引用,加入某个环节出现问题也不知道从哪里推测。
# 1.4.1.2 用例完整
按照正常逻辑来说,我们的程序大部分都不会有问题,一般出现问题的都是一些边缘情况,我们称为特殊值或者边界情况,比如下标越界就是这种情况,这种问题可大可小,程序有可能就会因为一个小小的下标越界而崩溃
我们一定要保证测试用例在我们的可控范围之内,否则就会出现问题,数据的完整性是指数据既不能多也不能少,通过不断的添加测试用例,将不断覆盖代码分支的不同情况
# 2. 单元测试Junit
# 2.1 什么是单元测试
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作,程序单元是应用的最小可测试部件,简单来说,就是测试数据的稳定性是否达到程序的预期
# 2.2 单元测试的重要性
一般公司都会要求使用单元测试,那么单元测试的意义是什么呢,为什么这么重要
首先,我们每个人都会犯错误,在程序中犯错误就像生活中犯错一样,错误不是一天两天而形成的,当需要改的时候,也不是能花少的时间而改掉的,这里我谈到的程序中的错误,就是著名的Bug。
我们可能在不经意间写错,如果你到了最后阶段去检验项目成果时,发现会有错误,这时候我们很难找到Bug的源头在哪里,我们都知道,有可能一处出错会导致步步错的情况。
然而,测试就在我们的上述说法中,显得尤为重要,有了测试的概念,这时候当我们做完项目的一个小模块,我们先去测试一下这个小模块是否正确或达到预期,如果错误或者没有达到预期就需要反复修改,直到正确或达到预期,这里所说的也就是使用了单元测试。
当我们一块一块的做完并一块一块的测试后OK后,这时候你会发现项目像拼图一样拼在了一起,简单来说,这就是单元测试存在的重要意义!
# 2.3 测试类型
# 2.3.1 黑盒测试
黑盒测试又称功能测试,它通过测试来检验程序是否能正常使用
在测试过程中,我们把程序看作为一个打不开的盒子,黑黑的什么也看不见,内部代码怎么写的也不知道。也就是说完全不考虑任何内部结构和性能的情况下为程序传入(Input)参数,然后去查看程序输出(Output)是否在正常范围内,通常这时候我们需要多此测试才得出结论
# 2.3.2 白盒测试
白盒测试又称结构测试,单元测试就是白盒测试的一种
在这里白盒测试与黑盒测试不同,在测试过程中,我们可以把程序看作为一个可以看到的白色透明盒子,我们清楚盒子内部的代码和结构,我们在使用白盒测试的时候,测试人员必须检查程序的内部结构,而要从检查程序的逻辑开始,一步一步的检查传入参数(Input)并查看程序的运行过程和输出(Output)结果,最终得出测试数据
这也就是“白盒测试”为什么叫穷举路径测试的原因,再次强调,是因为我们清楚程序的内部结构和代码,从而检查所有结构的正确与否和预期值。
# 2.4 普通测试缺点
在这里我们忘掉单元测试,使用平时我们自己测试的方式来测试数据,看看它有什么缺点
# 2.4.1 创建程序代码
首先,我先创建在一个计算器类,在其中随便创建两个运算方法,供我们模拟测试
/**
* 计算器
*/
public class Calculator {
/**
* 加法
*/
public int add(int num1, int num2) {
return num1 + num2;
}
/**
* 减法
*/
public int cut(int num1, int num2) {
return num1 - num2;
}
}
# 2.4.2 编写测试类
然后我们再去编写测试类,创建对象,先去测试加法。
public class CalculatorTest {
public static void main(String[] args) {
Calculator calculator = new Calculator();
//测试加法
System.out.println(calculator.add(10, 10)); //20 正确
}
}
测试后,我们查看结果为正确的,然后进行下一步测试
# 2.4.3 后续测试
因为我们有两条数据需要测试,平时在测试完一条数据后需要把测试过的数据注释掉,再进行接下来的测试
public class CalculatorTest {
public static void main(String[] args) {
Calculator calculator = new Calculator();
//测试加法
// System.out.println(calculator.add(10, 10)); //20 正确
//测试减法
System.out.println(calculator.cut(10, 10)); //0 正确
}
}
测试完两条数据后,再去继续编写我们的项目代码
# 2.4.4 缺点
我们发现我们通过注释代码进行测试非常麻烦,并且很容翻译出现问题,并且不能够很好的复用这些测试用例,而且在测试的过程序,数据与数据之间是有关联是互相影响的,这就会造成我们的测试不准确从而影响后续编码进度和项目准确性
# 2.5 单元测试规范
了解了上述测试的缺点,我们也需要了解单元测试的思想了,单元测试需要拥有什么样的特点才能解决掉上述测试的麻烦呢?其实我们的单元测试也是通过编码规范来约束的
# 2.5.1 规范定义
单元测试的编码规范有这几条
# 2.5.1.1 类名
定义测试类
类名是由被测试类名Test
构成,例如:CalculatorTest
# 2.5.1.2 包名
定义的测试类需要放在
xxx.xxx.xxx.test
包中
例如:package com.heima.test;
# 2.5.1.3 方法名
一般以test开头或者直接就是方法名
测试方法的方法名有两种定义方式test测试方法
和测试方法
,例如:testAdd
和add
# 2.5.1.4 返回值
返回值为空
因为我们的方法只是在类中测试,可以独立运行,所以不需要处理任何返回值,所以这里使用void
,例如:public void testAdd();
# 2.5.1.5 参数列表
参数列表为空
因为我们的方法是用来测试的,至于参数列表的传入是没有必要的,我们在测试的时候自行传入需要的参数测试即可,所以在此参数列表为空
,例如:例如:public void testAdd();
# 2.5.1.6 IDEA快捷
使用IDEA编辑器可以快速生成测试用例
我们可以先创建测试类和方法,然后在测试方法上方加入@Test
注解,此时IDEA显示的@Test注解是飘红的,这时候我们使用Alt + Enter
组合键来打开导入Junit单元测试列表,然后再选择Junit4或者Junit5确定即可导入成功!这时候再查看注解就没有飘红了!
# 2.5.2 注解定义
Junit是由一系列注解进行支撑来完成的,下面我们看下相关的注解
# 2.5.2.1 @Test
@Test注解的
public void
方法将会被当做测试用例
JUnit每次都会创建一个新的测试实例,然后调用@Test注解方法,任何异常的抛出都会认为测试失败,当以一个类为测试单元时,所有测试用例(测试方法)共属同一个测试实例(具有同一个环境)
当以一个方法为测试单元时,JUnit每次都会创建一个新的测试实例
参数
@Test注解提供2个参数
- expected: 定义测试方法应该抛出的异常,如果测试方法没有抛出异常或者抛出了一个不同的异常,测试失败;
- timeout:如果测试运行时间长于该定义时间,测试失败(单位为毫秒)
# 2.5.2.2 @Before
会在每一个测试方法被运行前执行一次
当编写测试方法时,经常会发现一些方法在执行前需要创建相同的对象,使用@Before注解一个public void 方法会使该方法在每个@Test注解方法被执行前执行(那么就可以在该方法中创建相同的对象),同时父类的@Before注解方法会在子类的@Before注解方法执行前执行
# 2.5.2.3 @After
会在每一个测试方法运行后被执行一次
使用@After注解一个public void
方法会使该方法在每个@Test
注解方法执行后被执行,如果在@Before注解方法中分配了额外的资源,那么在测试执行完后,需要释放分配的资源,这个释放资源的操作可以在After中完成
即使在@Before注解方法,@Test注解方法中抛出了异常,所有的@After注解方法依然会被执行,同时父类中的@After注解方法会在子类@After注解方法执行后被执行
# 2.5.2.4 @BeforeClass
会在所有的方法执行前被执行,static 方法 (全局只会执行一次,而且是第一个运行)
有些时候,一些测试需要共享代价高昂的步骤(如数据库登录),这会破坏测试独立性,通常是需要优化的,使用@BeforeClass注解一个public static void 方法,并且该方法不带任何参数,会使该方法在所有测试方法被执行前执行一次,并且只执行一次,同时父类的@BeforeClass注解方法会在子类的@BeforeClass注解方法执行前执行
# 2.5.2.5 @AfterClass
会在所有的方法执行之后进行执行,static 方法 (全局只会执行一次,而且是最后一个运行)
如果在@BeforeClass注解方法中分配了代价高昂的额外的资源,那么在测试类中的所有测试方法执行完后,需要释放分配的资源,使用@AfterClass注解一个public static void方法会使该方法在测试类中的所有测试方法执行完后被执行
即使在@BeforeClass注解方法中抛出了异常,所有的@AfterClass注解方法依然会被执行,同时父类中的@AfterClass注解方法会在子类@AfterClass注解方法执行后被执行
# 2.5.2.6 @Ignore
所修饰的测试方法会被测试运行器忽略
对包含测试类的类或@Test注解方法使用@Ignore注解将使被注解的类或方法不会被当做测试执行,JUnit执行结果中会报告被忽略的测试数
# 2.5.2.7 @RunWith
可以更改测试运行器 org.junit.runner.Runner
比如和其他的框架集成的时候,比如spring或者springboot中运行测试用例的时候可以使用@RunWith
指定相应框架的测试运行器
# 2.5.3 执行顺序注解
Junit 4.11里增加了指定测试方法执行顺序的特性,测试类的执行顺序可通过对测试类添加注解
@FixMethodOrder(value)
来指定,其中value 为执行顺序
# 2.5.3.1 MethodSorters.DEFAULT(默认)
默认顺序由方法名hashcode值来决定,如果hash值大小一致,则按名字的字典顺序确定
由于hashcode的生成和操作系统相关(以native修饰),所以对于不同操作系统,可能会出现不一样的执行顺序,在某一操作系统上,多次执行的顺序不变
# 2.5.3.2 MethodSorters.NAME_ASCENDING(推荐)
按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致
不过这种方式需要对测试方法有一定的命名规则,如 测试方法均以testNNN开头(NNN表示测试方法序列号 001-999)
# 2.5.3.3 MethodSorters.JVM(不推荐)
按JVM返回的方法名的顺序执行,此种方式下测试方法的执行顺序是不可预测的,即每次运行的顺序可能都不一样(JDK7里尤其如此)
# 2.5.4 测试单元测试
# 2.5.4.1 改写测试用例
我们就可以使用我们的规范来改写我们上面的测试用例
/**
* 指定测试方法执行顺序
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class CalculatorTest {
private static Calculator calculator;
/**
* 最开始就执行,并且只执行一次
*/
@BeforeClass
public static void testBeforeClass() {
System.out.println("执行方法--BeforeClass");
}
/**
* 在测试方法执行前执行
*/
@Before
public void testBefore() {
System.out.println("执行方法--Before");
calculator = new Calculator();
}
/**
* 测试方法
*/
@Test
public void testAdd2() {
System.out.println("执行测试--testAdd2");
int result = calculator.add(10, 10);
}
/**
* 定义测试超时时间
*/
@Test(timeout = 1000)
public void testCut1() {
System.out.println("执行测试--testCut1");
int result = calculator.cut(10, 10);
}
/**
* 方法忽略
*/
@Test
@Ignore
public void testIgnore() {
System.out.println("执行方法--Ignore");
}
/**
* 测试方法执行完成后执行
*/
@After
public void testAfter() {
System.out.println("执行方法--After");
}
/**
* 最后完成测试后执行,并且只会执行一次
*/
@AfterClass
public static void testAfterClass() {
System.out.println("执行方法--AfterClass");
}
}
# 2.5.4.2 执行测试用例
执行完成后,我们看下执行后的打印效果
执行方法--BeforeClass
执行方法--Before
执行测试--testAdd2
执行方法--After
执行方法--Before
执行测试--testCut1
执行方法--After
Test ignored.
执行方法--AfterClass
# 2.5.5 不足之处
如果我们需要一个预期值呢,比如上面错误的测试用例就是我们期望的结果
有的小伙伴会说,我们已经查看了打印控制台的信息,打印结果不是预期值就说明程序有问题,需要去修改呗。对,其实这样说是没有任何毛病的,但是,我们在开发中,如果由于你的疏忽或者疲劳看到了绿色就觉得程序没有问题怎么办呢?所以面对这个问题,我们在单元测试的时候,尽量不要去打印预期值,需要注重观察是绿色和红色比较好,它可以直观的反映程序的是否准确性和达到预期值。
这时候,我们就需要引入一个对象的静态方法来断言是否为预期值
# 2.6 断言
# 2.6.1 什么是断言
可以理解为断定一个表达式结果为真,不为真就通过抛异常或者其他方式使这个测试用例失败。
断言一词来自逻辑学,在逻辑学中,“断言”是“断定一个特定前提为真的陈述”,在软件测试中也是类似的含义,测试中断言语句的一般形式为“assert 表达式”,其中的“表达式”就是逻辑学中的“陈述”,表达式的值为真(true)的时候该断言才能通过,否则就断言失败
# 2.6.2 断言语法
在junit中提供了很多的断言支持,下面是一些常用的断言语法
断言方法 | 描述 |
---|---|
assertNull(java.lang.Object object) | 检查对象是否为空 |
assertNotNull(java.lang.Object object) | 检查对象是否不为空 |
assertEquals(long expected, long actual) | 检查long类型的值是否相等 |
assertEquals(double expected, double actual, double delta) | 检查指定精度的double值是否相等 |
assertFalse(boolean condition) | 检查条件是否为假 |
assertTrue(boolean condition) | 检查条件是否为真 |
assertSame(java.lang.Object expected, java.lang.Object actual) | 检查两个对象引用是否引用同一对象(即对象是否相等) |
assertNotSame(java.lang.Object unexpected, java.lang.Object actual) | 检查两个对象引用是否不引用统一对象(即对象不等) |
# 2.6.3 断言测试用例
# 2.6.3.1 改写测试用例
我们使用学会的断言来改写我们的测试用例
/**
* 指定测试方法执行顺序
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class CalculatorTest {
private static Calculator calculator;
/**
* 最开始就执行,并且只执行一次
*/
@BeforeClass
public static void testBeforeClass() {
System.out.println("执行方法--BeforeClass");
}
/**
* 在测试方法执行前执行
*/
@Before
public void testBefore() {
System.out.println("执行方法--Before");
calculator = new Calculator();
}
/**
* 测试方法
*/
@Test
public void testAdd2() {
System.out.println("执行测试--testAdd2");
int result = calculator.add(10, 10);
//断言结果是20
Assert.assertEquals(20,result);
}
/**
* 定义测试超时时间
*/
@Test(timeout = 1000)
public void testCut1() {
System.out.println("执行测试--testCut1");
int result = calculator.cut(10, 10);
//断言结果是20
Assert.assertEquals(20,result);
}
/**
* 方法忽略
*/
@Test
@Ignore
public void testIgnore() {
System.out.println("执行方法--Ignore");
}
/**
* 测试方法执行完成后执行
*/
@After
public void testAfter() {
System.out.println("执行方法--After");
}
/**
* 最后完成测试后执行,并且只会执行一次
*/
@AfterClass
public static void testAfterClass() {
System.out.println("执行方法--AfterClass");
}
}
# 2.6.3.2 执行测试用例
我们可以执行测试用例看下输出结果,我们就可以看到断言来打印出来的结果
执行方法--BeforeClass
执行方法--Before
执行测试--testAdd2
执行方法--After
执行方法--Before
执行测试--testCut1
执行方法--After
java.lang.AssertionError:
Expected :20
Actual :0
<Click to see difference>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:645)
at org.junit.Assert.assertEquals(Assert.java:631)
at com.heima.junitTest.CalculatorTest.testCut1(CalculatorTest.java:50)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:298)
at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:292)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.lang.Thread.run(Thread.java:748)
Test ignored.
执行方法--AfterClass
# 2.7 与Spring集成
只是用Junit测试Spring项目的话会比较麻烦,注入Bean启动容器等都需要考虑,这种情况下我们可以使用Spring的测试套件SpringTest和Junit集成来实现对spring的测试
# 2.7.1 SpringTest
SpringTest是Spring提供的一套测试Spring的环境,可以和Junit无缝的整合在一起,让对Spring框架的整合就和本地测试测试一样简单
# 2.7.2 引入依赖包
和Spring整合需要引入相关的依赖包
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
# 2.7.3 编写测试用例
可以使用模板模式来对Spring通用配置进行简化
# 2.7.3.1 编写测试用例基类
编写测试用例需要是引入Spring的的启动运行器
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)//表示整合JUnit进行测试
@ContextConfiguration(locations={"classpath:spring-config.xml"})//加载spring配置文件
//------------如果加入以下代码,所有继承该类的测试类都会遵循该配置,也可以不加,在测试类的方法上///控制事务,参见下一个实例
//这个非常关键,如果不加入这个注解配置,事务控制就会完全失效!
//@Transactional
//这里的事务关联到配置文件中的事务控制器(transactionManager = "transactionManager"),同时//指定自动回滚(defaultRollback = true)。这样做操作的数据才不会污染数据库!
//@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
//------------
public class BaseJunitTest {
}
# 2.7.3.2 编写测试用例
继承父类来编写相关测试用例
public class SpringJdbcTest extends BaseJunitTest {
@Autowired
private UserDao userDao;
@Test
public void test01() {
User user = new User();
user.setId("1");
user.setName("qq");
userDao.add(user);
User user2 = userDao.get("1");
Assert.assertNotNull(user2);
}
}
# 2.8 与SpringBoot集成
与和Spring整合差不多,只是更加的简单,只要引入测试依赖就可以
# 2.8.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
# 2.8.2 创建测试类
这里也是基于模式模式来简化通用配置
# 2.8.2.1 编写测试用例基类
只有在基类里面使用的测试环境不一样
@RunWith(SpringRunner.class)//表示整合JUnit进行测试
@SpringBootTest(classes = Application.class) //设置SpringBoot的启动类
public class BaseJunitTest {
}
# 2.8.2.2 编写测试用例
其他的就和正常使用Junit一样了
public class SpringBootTest extends BaseJunitTest {
@Autowired
private UserDao userDao;
@Test
public void test01() {
User user = new User();
user.setId("1");
user.setName("qq");
userDao.add(user);
User user2 = userDao.get("1");
Assert.assertNotNull(user2);
}
}
# 3. 压力测试Jmeter
# 3.1 Jmeter简介
Jmeter是由Apache公司开发的一个纯Java的开源项目,即可以用于做接口测试也可以用于做性能测试
Apache JMeter是100%纯JAVA桌面应用程序,被设计为用于测试客户端/服务端结构的软件(例如web应用程序),它可以用来测试静态和动态资源的性能,例如:静态文件,Java Servlet,CGI Scripts,Java Object,数据库和FTP服务器等等,JMeter可用于模拟大量负载来测试一台服务器,网络或者对象的健壮性或者分析不同负载下的整体性能
同时,JMeter可以帮助你对你的应用程序进行回归测试,通过你创建的测试脚本和assertions来验证你的程序返回了所期待的值。为了更高的适应性,JMeter允许你使用正则表达式来创建这些assertions
# 3.1.1 Jmeter 特点
- Jmeter具备高移植性,可以实现跨平台运行
- Jmeter可以实现分布式负载
- Jmeter采用多线程,允许通过多个线程并发取样或通过独立的线程对不同的功能同时取样。
- Jmeter具有较高扩展性。
# 3.1.2 JMeter与LoadRunner比较
JMeter 是一款开源(有着典型开源工具特点:界面不美观)测试工具,虽然与LoadRunner相比有很多不足,比如:它结果分析能力没有LoadRunner详细;很它的优点也有很多:
- 开源,他是一款开源的免费软件,使用它你不需要支付任何费用,
- 小巧,相比LR的庞大(最新LR11将近4GB),它非常小巧,不需要安装,但需要JDK环境,因为它是使用java开发的工具。
- 功能强大,jmeter设计之初只是一个简单的web性能测试工具,但经过不段的更新扩展,现在可以完成数据库、FTP、LDAP、WebService等方面的测试,因为它的开源性,当然你也可以根据自己的需求扩展它的功能。
# 3.1.3 JMeter缺点
使用JMeter无法验证JS程序,也无法验证页面UI,所以要须要和Selenium配合来完成Web2.0应用的测试
# 3.2 Jmeter使用
使用参考 https://baiyp.ren/Jmeter%E4%BD%BF%E7%94%A8.html (opens new window)
# 4. 自动化测试
# 4.1 什么是自动化测试
一般是指软件测试的自动化,软件测试就是在预设条件下运行系统或者应用程序,评估运行结果,预先应包括正常条件和异常条件。
# 4.2 优缺点
# 4.2.1 优点
- 节省人力,只要代码维护得好,不需要那么多人就可以完成测试。
- 节省时间,测试脚本可以晚上或者周末跑。
- 优化资源分配,在运行测试脚本的同时,QA可以做其他事情,比如设计新的测试用例。
- 方便回归测试,极大提高了工作效率。
- 增加软件的可信度,测试是机器执行的,一定程度上排除了因人工测试所造成的忽略性错误,测试结果更加可信。
- 能完成手工不容易控制的测试工作,比如对于大量用户的测试,不可能同时让足够多的测试人员同时进行测试,但是却能通过自动化测试模拟同时有很多用户操作,从而达到测试的目的。
# 4.2.2 缺点
- 脚本维护成本高,尤其是版本变动比较大的,对于项目来说是潜在的风险
- 不能在发现新的bug
# 4.3 场景说明
我们的测试是通过城市名称获取城市编码,然后根据城市编码再来获取对应城市的天气数据的一个接口自动化测试
# 4.3.1 获取城市编码
根据API接口可以通过城市名称来获取对应的城市编码,接口地址
http://toy1.weather.com.cn/search?cityname=上海
# 4.3.2 获取城市天气
我们还需要根据获取的城市编码来获取天气信息,接口地址:
http://t.weather.sojson.com/api/weather/city/101020100
# 4.4 公用组件介绍
# 4.4.1 HTTP默认值
该组件可以为我们的http请求设置默认的值
我们需要访问多个不同的接口,并且接口协议是一致的,我们可以把这些公共的参数配置在HTTP请求默认值里面,然后在具体的HTTP请求的时候旧不需要在进行重复的填写,可以简化自动化测试的重复配置
# 4.4.2 用户定义的变量
用户定义的变量可以帮用户将后面测试中需要的一些变量配置出来,进行统一管理
假设你有一个场景是这样的,你有很多个接口都要用到同一个参数,并且这个参数它的值不是固定不变的。在没有使用用户定义的变量这个元件时,每一次这个参数改变了值,你都必须到用到它的每一个http请求去修改值,如果此时你有一百个这样的接口,显然这样的修改方式也是一件艰巨的任务。
jmeter允许你定义一个参数,接口用到它只需要通过EL表达式来获取定义的值,这样一来,当参数的值变化时,你只需要改动一个地方,即可完成这上百个接口的修改。
# 4.4.3 CSV Data Set Config
如果大量参数配置可以使用外部CVS方式进行数据的配置
JMeter支持使用外部文件来保存接口测试时所要使用的参数,当我们配置了CSV Data Set Config这个元件后,JMeter会根据我们的配置使用指定的文件中的值作为参数。
# 4.4.3.1 参数说明
- 文件名:所要引用的外部文件路径,若该文件是存放在JMeter的bin目录下,那么这里只需要文件名。但是一般是不建议放在bin目录下的,推荐在存放接口测试脚本目录下创建一个data文件夹用来存放测试的数据。
- 文件编码:使用什么编码读取文件,一般我们是使用UTF-8。
- 变量名称:这里表示的是存放数据的文件中列对应的参数名,这个参数名的设置是便于我们使用EL表达式来获取文件中列所对应的值。
- 分隔符:文件分隔符,这里默认是使用
,
(注意是英文逗号)作为列之间的分隔符。在这里也是比较推荐使用英文逗号作为分隔符。
# 4.4.4 线程组
这里需要添加一个线程组进行统一的测试管理,可以控制测试的线程数、启动时间等
# 4.4.5 BeanShell
BeanShell是一种脚本语言,一种完全符合java语法的java脚本语言,并且又拥有自己的一些语法和方法,BeanShell是一种松散类型的脚本语言(这点和JS类似),我们完全可以在BeanShell中编写java脚本
# 4.4.5.1 BeanShell组件
- 定时器: BeanShell Timer
- 前置处理器:BeanShell PreProcessor
- 采样器: BeanShell Sampler
- 后置处理器:BeanShell PostProcessor
- 断言: BeanShell断言
- 监听器: BeanShell Listener
# 4.4.5.2 内置变量
vars
这个变量是所有内置变量中最有用的,它的本事是Map,Map是用键值对形式来存取变量的,常用方法:
vars.put(String key,String value) //通过键值对来存变量
vars.get(String keys) //获取key为keys的变量值
vars.putObject(String key,new Object()) //通过键值对来存一个对象
vars.getObject(key) //获取key为keys的对象
prev
一般用于前置/后置处理器中,获取前面sample返回的信息,常用方法:
prev.getResponseDataAsString() //获取前面sample的响应信息
prev.getResponseCode() //获取前面sample的响应code
log
看到log就知道是日志打印,常用方法:
log.info("要打印的信息");
log.error("出现错误");
# 4.4.6 JSON提取器
如果返回的数据是JSON格式的,我们可以用JSON提取器来提取需要的字段
# 4.4.6.1 使用界面
# 4.4.6.2 参数说明
- Variable names:保存的变量名,后面使用${Variable names}引用
- JSON Path expressions:调试通过的json path表达式
- Match Numbers:匹配数字(0代表随机,1代表第一个,-1代表所有)
- Default Values:找不到时默认值,一般设置为NOT FOUND
- Compute concatenation var(suffix_ALL):是否统计所有,即将匹配到的所有值保存,名为“变量名_ALL”
# 4.4.6.3 json path 语法
使用json提取器之前,可以使用在线工具测试
json path
- JsonPath语法地址:https://blog.csdn.net/roc1010/article/details/90812130
- JSONPath测试地址:http://www.atoolbox.net/Tool.php?Id=792
# 4.4.6.4 JSON提取器测试
测试JSON提取器的表达式是否正确可以在
查看结果树
中选择JSON Path Tester
进行测试
# 4.5 获取地区编码
# 4.5.1 添加HTTP请求
因为我们已经知道获取地区编码的请求地址,我们就可以添加一个HTTP请求
这里面我们可以输入相关的服务器IP以及访问路径,对于请求参数我们可以引用用户自定义变量
的配置
# 4.5.1.1 动态参数
获取地区编码需要地区的名称,因为我们已经将地区名称配置化,我们在这里可以使用EL表达式来进行配置使用,因为API地址是http://toy1.weather.com.cn/search?cityname=上海
我们可以在参数一项中添加参数,参数名称cityname
,参数值使用EL表达式${city_name}
# 4.5.2 响应数据转码
响应数据处理
因为返回的响应报文不是标准的JSON格式,并且返回的数据也是乱码,我们需要先将数据改为UTF-8,然后将响应报文的括号去掉,这里我们使用BeanShell后置处理器
//设置响应报文编码是用户自定义的UTF-8
prev.setDataEncoding(vars.get("city_resp_charset"));
//获取响应的报文
String result = prev.getResponseDataAsString();
//字符串截取并且去掉第一个和最后一个括号
result = result.substring(1,result.length()-1);
//设置响应报文编码是用户自定义的UTF-8
prev.setResponseData(result,"UTF-8");
# 4.5.3 提取城市信息
我们需要从一堆的JSON中提取到对应的城市编码
经过后置处理器处理过的数据如下
[{"ref":"101180101~henan~郑州~Zhengzhou~郑州~Zhengzhou~371~450000~ZZ~河南"},{"ref":"10118010104A~henan~郑州城隍庙~City God Temple of Zhengzhou~郑州城隍庙~City God Temple of Zhengzhou~null~450000~null~河南省景点"},{"ref":"10118010105A~henan~郑州文庙~Confucian Temple of Zhengzhou~郑州文庙~Confucian Temple of Zhengzhou~null~450000~null~河南省景点"},{"ref":"10118010107A~henan~郑州黄河风景名胜区~Yellow River Scenic Attraction of Zhengzhou~郑州黄河风景名胜区~Yellow River Scenic Attraction of Zhengzhou~null~450000~null~河南省景点"},{"ref":"101180105019~henan~郑州曲梁产业集聚区管理委员会~zhengzhouquliangchanyejijuquguanliweiyuanhui~新密~xinmi~0371~452300~henan~河南"},{"ref":"101180901019~henan~郑州路街道~zhengzhoulujiedaobanshichu~涧西~jianxi~0379~471000~henan~河南"},{"ref":"10118010401B~henan~郑州嵩山滑雪滑草场~zhengzhousongshanhuaxuehuacaochang~郑州嵩山滑雪滑草场~zhengzhousongshanhuaxuehuacaochang~371~452400~ZZSIXICC~河南"},{"ref":"10118010301B~henan~郑州桃花峪生态滑雪场~zhengzhoutaohuayushengtaihuaxuechang~郑州桃花峪生态滑雪场~zhengzhoutaohuayushengtaihuaxuechang~371~450100~ZZTIYSTIXC~河南"}]
我们可以使用JSON提取器提取数据,我们只需要提取第一项的内容即可即该JSON数组的第0个元组,获取ref的值,JSON的提取表达式是$.[0].ref
,我们将取到的值放进city_message
的变量中。
# 4.5.4 获取城市编码
因为上面已经通过JSON提取器将地区数据提取到了
city_message
中了,但是数据格式还不是我们想要的
101180101~henan~郑州~Zhengzhou~郑州~Zhengzhou~371~450000~ZZ~河南
上面的数据我们需要的是101180101
这个地区编码,我们需要将这个数据进行分割并且提取到第一个城市编码,我们可以添加BeanShell
后置处理器进行处理,我们将符合条件的地区编码假如到vars
中,供后续的测试使用
String city_message = vars.get("city_message");
if(null == city_message || "null".equals(city_message)) {
return;
}
String[] cityArray = city_message.split("~");
String city_name = vars.get("city_name");
if(city_name.equals(cityArray[2])) {
log.info("设置城市编码成功");
vars.put("city_code",cityArray[0]);
}
# 4.5.5 城市编码断言
断言是用来判断返回的数据是否正确,我们还需要校验我们获取的城市编码是否正确,我们需要对参数进行断言,我们可以添加
BeanShell
断言,我们可以获取上面加入到vars
中的城市编码
String city_code = vars.get("city_code");
if (null == city_code) {
Failure = true;
FailureMessage = "城市编码断言失败";
return;
}
log.info("城市编码:"+city_code);
FailureMessage = "规则解析成功";
使用BeanShell
断言可以使用内置的Failure
判断是否断言失败,使用FailureMessage
断言失败原因
# 4.6 获取天气
# 4.6.1 添加HTTP请求
我们已经获取到了城市编码,我们就可以添加后续的天气查询的接口,接口地址
http://t.weather.sojson.com/api/weather/city/101020100
因为最后的编码就是城市编码,因为已经加入到了vars
中,可以直接使用EL
表达式获取,这里使用${city_code}
来表示城市编码
# 4.6.1.1 返回结果
这里我们标红的就是我们查询天气的城市编码,后面data是相应天气信息
# 4.6.2 天气信息断言
我们还需要校验天气信息是否正确获取,我们可以根据上面获取的城市编码和刚刚天气接口返回的城市编码进行对比,如果发现一致则说明正确,我们可以添加JSON断言来进行判断
# 4.7 自动化测试
到现在位置我们已经将两个接口都进行关联,并且可以完成相应的自动化测试,我们可以点击线程组进行运行测试
我们发现结果都已经正常显示,说明测试成功,我们可以在上线之前运行这些测试用来测试接口是否正常,还可以和Jenkins集成,在每次构建的是否进行功能测试
# 5. 安全测试Burpsuite
Burp Suite 是用于攻击web 应用程序的集成平台
Burp Suite是一个集成化的渗透测试工具,它集合了多种渗透测试组件,使我们自动化地或手工地能更好的完成对web应用的渗透测试和攻击,官方提供免费版和专业版,但是免费版没有主动扫描功能,可用于手动挖掘。
# 5.1 Burpsuite安装
# 5.1.1 下载安装Burpsuite
Burp Suite工具安装下载地址:https://portswigger.net/burp/版本区分为社区版本和专业版本以及企业版
一般使用选择社区版本即可
点击后跳转到下载页面,选择对应的操作系统下载即可
下载完成后下一步安装即可,安装完成后打开后出现如下界面
选择默认的临时项目,或者新建一个项目,点击下一步最终出现如下界面
# 5.2 搭建BWAPP靶场
buggy web Application 这是一个集成了各种常见漏洞和最新漏洞的开源Web应用程序,目的是帮助网络安全爱好者、开发人员和学生发现并防止网络漏洞,包含了超过100种漏洞,涵盖了所有主要的已知Web漏洞,包括OWASP Top10安全风险,最重要的是已经包含了OpenSSL和ShellShock漏洞。
# 5.2.1 查找bwapp镜像
docker search bwapp,一般选择官方、starts最多的镜像
# 5.2.2 启动容器
运行以下命令启动bwapp容器
docker run --name bwapp -d -p 80:80 raesene/bwapp
docker ps
# 5.2.3 登录访问
访问
http://xxxxxxxx/login.php
后会出现数据库未初始化的提示
# 5.2.4 初始化数据库
这个时候需要访问
http://xxxxxxxx/install.php
做数据库初始化操作,进入页面点击页面的here
出现如下界面代表数据库已经初始化成功
# 5.2.5 重新登录
再次访问
http://xxxxxxxx/login.php
后出现登录页面
输入Bwapp默认账密登入:bee/bug,成功登入后就可以搞起破坏了
# 5.3 安全测试
# 5.3.1 Cookie跨站漏洞
Cookie跨站攻击是一种很危险的漏洞,这里面我们来模拟下用户
提权
没有登录但是可以通过Cookie登录
# 5.3.1.1 查找Cookie
可以在chrome浏览器中查找网站的Cookie值,并将其复制到一个文本中
# 5.3.1.2 打开新的浏览器
这里我们使用
Burpsuite
进行攻击,打开Burpsuite
浏览器,选择porxy -> Intercept -> Open Borwser
打开浏览器后就可以在这个浏览器进行访问
# 5.3.1.3 正常访问内容页面
直接不登录访问内容页面
http://xxxxxx/portal.php
发现直接回到了登录页面
# 5.3.1.4 Burpsuite 拦截请求
在Burpsuite 的拦截器模块开启请求拦截并阻断拦截,这设置
Intercept is on
是开启阻断请求
打开Proxy功能中的Intercept选项卡,确认拦截功能为“Interception is on”状态,如果显示为“Intercept is off”则点击它,设置为“Interception is on”,是用来针对某个请求或接口进行分析的,相当于程序中的断点。
# 5.3.1.5 篡改请求报文
这里我们已经阻断请求后直接访问内容页面发现页面已经被阻断
在Burpsuite中可以看到请求的报文
这我们对请求的报文进行修改并点击
Foward
并进行内容转发
我们再看浏览器我们发现已经能够访问到的内容页面,也就是我们越权了
# 5.3.2 XSS(跨站脚本)漏洞
跨站脚本攻击XSS(Cross Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的,XSS攻击针对的是用户层面的攻击!
# 5.3.2.1 GET请求测试
选择HTML Injection - Reflected(GET)选择,选择back显示页面
正常输入用户名是可以正常显示的
在文本中输入HTML脚本,我们发现出现了注入漏洞
# 5.3.2.2 POST请求测试
访问并登录,选择HTML Injection - Reflected (POST),选择back显示页面
# 5.3.2.3 篡改报文
阻断请求并且篡改报文内容
将报文内容篡改为
<script>alert("XSS漏洞");</script>
点击Forward后查看浏览器
# 5.3.3 SQL注入漏洞
SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。
具体来说,它是利用现有应用程序,将(恶意)的SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。
# 5.3.3.1 测试分析
在输入框内输入一些数字,点击搜索,发现什么都没有出来
输入字母
e
试试,发现搜索出来一堆数据,并且URL会显示参数
# 5.3.3.2 猜测分析
通过上面的测试我们可以大概猜测出来相关的SQL如下
SELECT * FROM movies WHERE title LIKE '%"+title+"%'
现在需要进行验证以下,可以通过
'
将sql的LIKE给闭合掉,输入'
后sql入下
SELECT * FROM movies WHERE title LIKE '%'%'
这种sql在执行的时候会出现错误,我们输入以下执行试试,果然出现了错误,基本上可以确定有SQL注入漏洞
# 5.3.3.3 显示列数
就是爆字段,显示具体的SQL的数据列的数量,我们可以输入以下参数
' ORDER BY 50#
这里
order by num
是用列数来代替排序的字段,我们可以通过二分查找来确定列的数量,经过以上SQL的注入后,我们的SQL会变成如下形式
SELECT * FROM movies WHERE title LIKE '%' ORDER BY 50#%'
#
号代表SQL的注释,这里的SQL就可以通过order by
来确定列的数量了,我们尝试以下
发现报错了,说明列数大于50,我们可以通过二分方式减少,找到一个不报错的值,最终我们发现是7,我们输入
' ORDER BY 7#
就可以查询出来数据,说明数据表有7列
# 5.3.3.4 确定列字段
我们可以通过
select 数字,数字
的形式来测试表的列,有多少列就写多少数字,可以重复,我们通过union来进行注入可以使用如下的字符串
xxxx' union select 1,2,3,4,5,6,7#
反应到SQL中是这样的,这里的数字的数量就是列的数量
SELECT * FROM movies WHERE title LIKE '%xxxx' union select 1,2,3,4,5,6,7#%'
我们尝试输入以下参数,点击搜索
我们发现显示了2,3,4,5这样我们就可以确定页面显示字段是数据库的第2,3,4,5列了
# 5.3.3.5 显示数据库信息
经过上面的操作我们已经确定了需要那些字段是在页面显示的,我们可以将显示的数字替换为数据库函数来显示数据库的相关信息
相关函数
MySql有以下的一些相关函数
system_user() 系统用户名
user() 用户名
current_user 当前用户名
session_user() 连接数据库的用户名
database() 数据库名
version() MYSQL数据库版本
load_file() MYSQL读取本地文件的函数
@@datadir 读取数据库路径
@@basedir MYSQL 安装路径
@@version_compile_os 操作系统
这些函数,你们自己都尝试去替换里面的数字,一次性替换几个也行,只要这些数字都是我们利用联合查询查到的数字就行
获取信息
我们输入以下信息来获取数据库名称,版本号以及操作系统信息
xxxx' union select 1,database(),version(),@@version_compile_os,5,6,7#
输入信息后搜索查看结果,我们就获取到了数据库以及操作系统的相关信息
# 5.3.3.6 获取表信息
上面我们通过相关函数获取到了数据库信息,我们发现是
MySQL5.5.47
,我们继续使用MySQL的information_schema.tables
来根据数据库名称查询表数据信息
xxxx' union select 1,table_name,3,4,5,6,7 from information_schema.tables where table_schema=database()#
执行后我们已经可以获取到表的相关信息了
可以看到,爆出了5个表,其中users表是一个比较重要的表,也是值得我们注意的表
# 5.3.3.7 获取表字段信息
因为users表看起来是用户表,我们先获取以下users表的列信息,我们使用MySQL的
information_schema.columns
来获取表的列信息
xxxx' union select 1,column_name,3,4,5,6,7 from information_schema.columns where table_name='users'#
这样我们就已经获取到了相关的用户列的信息
# 5.3.3.8 获取用户信息
继续使用SQL注入使用union的方式来获取用户的信息
xxxx' union select 1,login,password,4,5,6,7 from users#
这样我们就获取到了用户名以及密码信息
在Title和Release栏把它们对应的账号和密码爆出了,其中密码是经过md5加密,我们需要去解密