1. 单元测试核心原则
- 自动化:单元测试应该是全自动执行的,并且非交互式的。利用断言Assert进行结果验证。
- 独立性:保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。 单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。
- 可重复:单元测试是可以重复执行的,不能受到外界环境的影响。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
- 全面性:除了正确的输入得到预期的结果,还需要强制错误信息输入得到预期的结果,为了系统的鲁棒性,应加入边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
- 细粒度:保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。
2. 测试框架
JUnit和TestNG是最受欢迎的两个单元测试框架
JUnit
默认情况下,JUnit的运行顺序无规律
JUnit3:
//JUnit3的测试类必须继承TestCase
public class JUnit3Test extends TestCase {
//JUnit3的测试方法必须test开头
public void testName() {
}
}
JUnit4;
//JUnit使用注解来运行,无需继承TestCase,也不需要测试驱动以test开头
public class JUnit4Test {
@Test
public void testName() {
}
}
Test设置
-
JUnit:@BeforeClass和**@AfterClass的方法需要加*static***
常用:
- @BeforeClass:在所有测试方法前执行一次,一般在其中写上整体初始化的代码。
- @AfterClass:在所有测试方法后执行一次,一般在其中写上销毁和释放资源的代码。
- @Before:在每个测试方法前执行,有多少个测试方法就会运行多少次。
- @After:在每个测试方法后执行,有多少个测试方法就会运行多少次。
- @Test:具体的测试方法
- @Ignore:执行测试时将忽略掉此方法,如果用于修饰类,则忽略整个类。
- @Rule:定义一些规则,如超时时间,异常捕获
不常用:
- @Test(timeout = 1000):指明要被测试的方法(测试方法执行超过1000毫秒后算超时,测试将失败)。
- @Test(expected = Exception.class):测试方法期望得到的异常类,如果方法执行没有抛出指定的异常,则测试失败。
- @FixMethodOrder:可以指定按方法名的升序或是降序排序
- @RunWith:指定用什么方式策略去运行这些测试集、类、方法。
- @ActiveProfiles(“xxx”):在测试的时候启用某些Profile的Bean。
-
TestNG:
-
方法级别:@BeforeMethod:测试方法运行前执行的方法注解
@AfterMethod:测试方法运行后执行的方法注解
-
类级别:@BeforeClass
@AfterClass
-
套件:@BeforeSuite、@AfterSuite
-
组级别:@BeforeGroup、@AfterGroup
-
@Test
- alwaysRun : 如果=
true
,表示即使该测试方法所依赖的前置测试有失败的情况,也要执行 - dataProvider : 选定传入参数的构造器。(
@DataProvider
注解将在后面章节介绍) - dataProviderClass : 确定参数构造器的Class类。(参数构造器首先会在当前测试类里面查找,如果参数构造器不在当前测试类定义,那么必须使用该属性来执行它所在的Class类)
- dependsOnGroups : 确定依赖的前置测试组别。
- dependsOnMethods : 确定依赖的前置测试方法。
- description : 测试方法描述信息。(建议为每个测试方法添加有意义的描述信息,这将会在最后的报告中展示出来)
- enabled : 默认为
true
,如果指定为false
,表示不执行该测试方法。 - expectedExceptions : 指定期待测试方法抛出的异常,多个异常以逗号(,)隔开。
- groups : 指定该测试方法所属的组,可以指定多个组,以逗号隔开。组测试的用法将在后面文章单独介绍。
- invocationCount : 指定测试方法需要被调用的次数。
- invocationTimeOut: 每一次调用的超时时间,如果
invocationCount
没有指定,该参数会被忽略。应用场景可以为测试获取数据库连接,超时就认定为失败。单位是毫秒。 - priority : 指定测试方法的优先级,数值越低,优先级越高,将会优先与其他数值高的测试方法被调用。(注意是针对一个测试类的优先级)
- timeout : 指定整个测试方法的超时时间。单位是毫秒
- alwaysRun : 如果=
-
多线程:
threadPoolSize = 3,invocationCount = 6,timeOut = 1000
-
停用测试
- JUnit:@Ignore
- TestNG:@Test后面加入enable=false
@Test(enable=false)
套件/分组测试
-
JUnit利用@RunWith、@SelectPackages、@SelectClasses注解来组合测试用例
@RunWith(Suite.class) @Suite.SuiteClasses({ JUnit3Test.class, JUnit4Test.class }) public class JUnitTest { }
-
TestNG
@Test(groups = {"group2","group1"})
<test name="test-2"> <groups> <run> <include name="group2"/> <include name="group1"/> </run> </groups> <packages> <package name="com.cxl.testng"/> </packages> </test>
参数化测试
-
JUnit
@RunWith(value = Parameterized.class) public class JUnitTest { private int number; public JUnitTest06(int number) { this.number = number; } @Parameterized.Parameters public static Collection<Object[]> data() { Object[][] data = new Object[][]{{1},{2},{3},{4}} ; return Arrays.asList(data); } @Test public void pushTest() { System.out.println("Parameterized Number is : " + number); } }
-
TestNG
@Parameters({"param1"}) @Test public void paramterTest(String param1){ System.out.println("\n---------------"+param1); }
<parameter name="param1" value="http://127.0.0.1:4723/wd/hub" /> <test name="testDemo1"> <classes> <class name="com.cxl.testng.TestNG01"></class> </classes> </test>
@DataProvider 注解做参数化测试
-
TestNG:
@Test(dataProvider = "userData") public void test(Class clazz, String[] str) { System.out.println(clazz + "-------------" + str[0]); System.out.println(clazz + "-------------" + str[1]); } @DataProvider(name = "userData") public Object[][] data() { Object[][] objects = new Object[][]{ {Vector.class, new String[]{"java.util.Arrays", "java.util.List"}}, {String.class, new String[]{"this is my str", "this is my pp"}}, {Integer.class, new String[]{"123", "345"}}, {Float.class, new String[]{"12.45f", "33.11f"}}}; return objects; }
依赖测试
JUnit4 框架主要聚焦于测试的隔离,暂时还不支持这个特性。
TestNG使用dependOnMethods、dependsOnGroups 来实现了依赖测试的功能。
@Test
public void method1() {
System.out.println("This is method 1");
}
@Test(dependsOnMethods={"method1"})
public void method2() {
System.out.println("This is method 2");
}
如果method1()成功执行,那么method2()也将被执行,否则method2()将会被忽略。
@Test(groups = { "init.1" })
public void test1() {
}
@Test(groups = { "init.2" })
public void test2() {
}
@Test(dependsOnGroups = { "init.*" })
public void test3() {
}
并发测试
Junit单元测试不支持多线程测试,TestNg使用threadPoolSize用来指明线程池的大小。
public class TestNgThreadPoolSize {
@Test(threadPoolSize = 3,invocationCount = 5)
public void threadPool(){
System.out.println("Thread ----------"+Thread.currentThread().getName());
}
}
总结:
建议使用TestNG作为Java项目的核心单元测试框架,因为TestNG在参数化测试,依赖测试和套件测试(分组概念)方面更加突出。 TestNG用于高级测试和复杂集成测试。 它的灵活性对于大型测试套件尤其有用。 此外,TestNG还涵盖了整个核心的JUnit4功能。
3. Mock
在单元测试中模拟一个被调用方法,进而隔离测试环境,其实现原理是利用接口的多态性。
通常意义的mock指的就是mock server,模拟服务端返回的接口数据,用于前端开发
- 为什么要用mock?
- 前后端开发速度不一致,前端开发速度快于后端,需要一个假的接口用于模拟后端返回。
- 项目需要用到第三方接口,如果第三方接口未开发好,或者第三方接口没有测试环境,为了保证进度,所以需要模拟接口用于调试。
@Mock
private IExecuteSqlManage iExecuteSqlManage;
@BeforeMethod
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(AopTargetUtils.getTarget(espJfxxInfoFileToDb), "iExecuteSqlManage", iExecuteSqlManage);
}
@AfterMethod
public void tearDown() throws Exception {
ReflectionTestUtils.setField(AopTargetUtils.getTarget(espJfxxInfoFileToDb), "iExecuteSqlManage", null);
}
4.PowerMock
1、类上加注解
@PowerMockIgnore("javax.management.*,org.apache.log4j.*")
@PrepareForTest({AcctFileProcessUtil.class, FileTool.class, GlobalConf.class}) //需要Mock的静态类
@PowerMockIgnore({"javax.xml.*", "org.xml.sax.*", "org.w3c.dom.*", "org.springframework.context.*", "org.apache.log4j.*"})
2、初始化
@ObjectFactory
public ITestObjectFactory getObjectFactory() {
return new PowerMockObjectFactory();
}
3、自定义返回值
PowerMockito.mockStatic(GlobalConf.class);
PowerMockito.when(GlobalConf.getString(anyString(), any())).thenReturn("path_in");
4、检验次数
PowerMockito.verifyStatic(times(1));
5.断言
验证结果
@Test
void getPeopleById() {
People people = peopleService.getPeopleById(1);
//System.out.println(people);
Assert.assertNotNull(people);
Assert.assertEquals(people.getAge(), 11);
Assert.assertEquals(people.getName(),"cxl");
}
try {
agetAgentSeqImpl.getAgetAgentSeq(req);
} catch (Exception e) {
Assert.assertEquals(e.getMessage(),"未取到代理商充值流水");
}
agetAgentSeqRes = agetAgentSeqImpl.getAgetAgentSeq(req);
Assert.assertEquals(agetAgentSeqRes.getXtableAgentSeq(), "null~0~-1~null~-1~1~0~-1~null~-1~0~20170301000000~null~0~0~0~-1~0~1~;");
验证方法是否被调用
@Test
public void testVerify() {
// 创建并配置 mock 对象
MyClass test = Mockito.mock(MyClass.class);
when(test.getUniqueId()).thenReturn(43);
// 调用mock对象里面的方法并传入参数为12
test.testing(12);
test.getUniqueId();
test.getUniqueId();
// 查看在传入参数为12的时候方法是否被调用
verify(test).testing(Matchers.eq(12));
// 方法是否被调用两次
verify(test, times(2)).getUniqueId();
// 其他用来验证函数是否被调用的方法
verify(mock, never()).someMethod("never called");
verify(mock, atLeastOnce()).someMethod("called at least once");
verify(mock, atLeast(2)).someMethod("called at least twice");
verify(mock, times(5)).someMethod("called five times");
verify(mock, atMost(3)).someMethod("called at most 3 times");
}
断言错误
@Test(expectedExceptions = com.newland.common.NLException.class)
public void queryUserUnionPayUserIdMinusOne() throws Exception {
try {
qryUserUnionPayImpl.qryUserUnionPay(getReq("-1"));
} catch (NLException e) {
//断言结果是否与预期相同
Assert.assertEquals(e.getResCode(), CloudErrorCode.ERR_PAKERR);
Assert.assertEquals(e.getMessage(), "帐户userid节点没找到或值不对");
throw e;
}
}