Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340
1. 使用JUnit进行单元测试
以下针对使用JUnit作为单元测试框架进行说明。
1.1. JUnit4
在示例代码中,使用JUnit4,版本为4.13。
1.1.1. 添加引用
参考“Gradle Dependency”( https://github.com/junit-team/junit4/wiki/Use-with-Gradle#gradle-dependency ),可通过以下方式在Gradle中引用JUnit 4.13。
'junit:junit:4.13'
1.1.2. @RunWith配置
参考“@RunWith annotation”( https://github.com/junit-team/junit4/wiki/Test-runners#runwith-annotation )。
当类使用@RunWith注解或者扩展使用@RunWith注解的类时,JUnit将调用它引用的类来运行该类中的测试,而不是JUnit中内置的运行器( runner )。
默认的运行器是BlockJUnit4ClassRunner,它取代了旧的JUnit4ClassRunner。
使用@RunWith(JUnit4.class)对类进行注解,将始终在当前版本的JUnit中调用默认的JUnit4运行器,此类的别名为当前默认的JUnit4类运行器。
可参考示例TestJUnit1类,未通过@RunWith指定运行器类;示例TestJUnit2类,通过@RunWith指定运行器类为BlockJUnit4ClassRunner,均能够正常执行。
1.1.3. 注解对应方法执行顺序
参考“Getting started”( https://github.com/junit-team/junit4/wiki/Getting-started ),需要在测试方法使用@Test注解。
参考“Test fixtures”( https://github.com/junit-team/junit4/wiki/test-fixtures )。
JUnit有四个固定注解:两个用于类级别,两个用于方法级别。在类级别,有@BeforeClass和@AfterClass,在方法( 或测试 )级别,有@Before和@After。
JUnit4中的常用注解包含@BeforeClass、@Before、@Test、@After、@AfterClass等,API文档地址如下:
注解 | 文档地址 |
---|---|
@BeforeClass | https://junit.org/junit4/javadoc/4.12/org/junit/BeforeClass.html |
@Before | https://junit.org/junit4/javadoc/4.12/org/junit/Before.html |
@Test | https://junit.org/junit4/javadoc/4.12/org/junit/Test.html |
@After | https://junit.org/junit4/javadoc/4.12/org/junit/After.html |
@AfterClass | https://junit.org/junit4/javadoc/4.12/org/junit/AfterClass.html |
以上API文档中关于注解的执行顺序的说明如下:
- @BeforeClass
使用@BeforeClass注解的public static void无参数方法会使它在类中的任意测试方法之前运行一次。
- @Before
使用@Before注解的public void方法使该方法在每个@Test方法之前运行。
- @Test
@Test注解告诉JUnit其附加的public void方法可以作为测试用例运行。
- @After
使用@After注解public void方法会使该方法在每个@Test方法之后运行。即使@Before或@Test方法抛出异常,所有的@After方法都会运行。
- @AfterClass
使用@AfterClass注解public static void方法,会使当前类中的所有测试结束之后运行该方法。即使@BeforeClass方法抛出异常,所有@AfterClass方法都会运行。
通常可以在@BeforeClass、@Before注解对应方法中进行测试准备工作;在@Test对应方法进行测试操作;在@After、@AfterClass对应方法中进行检查或清理等操作。
上述注解对应方法的执行顺序为:@BeforeClass->@Before->@Test->@After->@AfterClass,可参考示例TestJUnitAnnotationOrder类。
1.1.4. 注解对应方法执行次数
JUnit4文档对于@BeforeClass、@Before、@After、@AfterClass对应方法的执行次数的说明如下:
- @BeforeClass对应方法在对应的测试类执行时,会执行一次,且在其他方法前执行;
- @AfterClass对应方法在对应的测试类执行时,会执行一次,且在其他方法后执行;
- @Before对应方法在每个@Test方法执行前都会执行一次;
- @After对应方法在每个@Test方法执行后都会执行一次。
可参考示例TestJUnitAnnotationTimes类。
1.1.5. 注解对应方法在子类及超类中的执行顺序
在@BeforeClass、@Before、@After、@AfterClass的API文档中,关于使用继承时的执行顺序说明如下:
- @BeforeClass
超类的@BeforeClass方法会在当前类的方法之前运行;
- @Before
超类的@Before方法会在当前类的方法之前运行;
- @After
在超类中声明的@After方法将在当前类的方法之后运行;
- @AfterClass
在超类中声明的@AfterClass方法将在当前类的方法之后运行。
可参考示例TestJUnitChild1、TestJUnitParent1类。
1.1.6. 子类覆盖超类的注解对应方法
在@BeforeClass、@Before、@After、@AfterClass的API文档中,说明当子类覆盖超类的以上注解对应方法时,超类中的方法不会执行。
例如超类中存在指定了@Before注解的before()方法,在子类中也存在同名的指定了@Before注解的方法,则执行测试时,子类中的before()方法会执行,但超类中的不会执行。为了使超类中的方法也执行,应使子类与超类中使用了相同注解的方法不同名。
可参考示例TestJUnitChild2、TestJUnitParent2类。
1.1.7. @Test注解的位置
@Test注解可以只出现在超类中,不出现在子类中,适用于准备工作不同,但测试执行操作及结果相同的情况。可参考示例TestJUnitChild3、TestJUnitParent3类。
1.1.8. @Test方法执行顺序
参考“Test execution order”( https://github.com/junit-team/junit4/wiki/Test-execution-order )。
从设计上,JUnit不指定测试方法调用的执行顺序。到目前为止,这些方法只是按反射API返回的顺序调用。但是,使用JVM顺序是不明智的,因为Java平台没有指定任何特定顺序,实际上JDK7返回的顺序会有或多或少的随机。当然,编写良好的测试代码不应对执行顺序进行任何假设,但有时可能需要这样做,并且可预测的失败优于某些平台上的随机失败。
JUnit从版本4.11开始,将默认使用确定性但不可预测的顺序(MethodSorters.DEFAULT)。若需要更改测试执行顺序,可在测试类指定@FixMethodOrder注解,并指定一个可用的MethodSorters:
-
@FixMethodOrder(MethodSorters.JVM):按JVM返回的顺序对测试方法进行排序,顺序在执行时可能会出现差异。
-
@FixMethodOrder(MethodSorters.NAME_ASCENDING):按方法名称的字典顺序对测试方法进行排序。
@FixMethodOrder注解应添加在类级别,使用@FixMethodOrder(MethodSorters.NAME_ASCENDING)可以指定一个测试类中的多个测试方法以字典顺序执行。可参考示例TestJUnitOrder1类。
1.1.9. @Test方法与测试类实例
JUnit在执行测试类的每个@Test方法前,都会执行测试类的无参数构造函数,创建新的实例。可参考示例TestJUnitMultiTest类。
1.1.10. 断言
参考“Assertions”( https://github.com/junit-team/junit4/wiki/Assertions )。
JUnit为所有基本类型和对象和数组(基本类型或对象的数组)提供重载的断言方法。 断言方法的参数顺序是预期值在前,实际值在后。 可选地,第一个参数可以是断言判断不通过时输出的String类型的消息。
参考Assert类的API文档( https://junit.org/junit4/javadoc/4.12/org/junit/Assert.html ),支持的方法包括assertArrayEquals()、assertEquals()、assertFalse()、assertNotEquals()、assertNotNull()、assertNotSame()、assertNull()、assertSame()、assertThat()、assertTrue()、fail()等,以上方法的使用根据方法名、参数或文档均容易理解。若使用以上方法对应的参数1为“String message”的方法,当断言检查不通过时,提示信息为message变量值。
Assert类中的方法可以使用静态导入,在调用时可以省略“Assert.”,例如“assertEquals("…", str)”。
1.1.10.1. 抛出异常
当断言检查实际值与预期值不符时,会调用Assert.fail()方法,抛出AssertionError异常,父类为Error。
Assert.fail()方法可以在单元测试代码中调用,会抛出异常,可以通过String message参数指定异常信息。
可参考示例TestAssertFail类。
1.1.10.2. 比较值是否相同/是否为同一个对象
用于比较是否相同的方法有两组,Assert.assertEquals()/Assert.assertNotEquals()与Assert.assertSame()/Assert.assertNotSame():
-
Assert.assertEquals()/Assert.assertNotEquals()方法,用于判断对象的值是否相同/不同;
-
Assert.assertSame()/Assert.assertNotSame()方法,用于判断是/不是同一个对象。
例如存在变量“Integer integer1 = Integer.valueOf(10000)”与“Integer integer2 = Integer.valueOf(10000)”,integer1与integer2满足Assert.assertEquals()方法的检查,但不满足Assert.assertSame()方法的检查(Integer的缓存范围使用默认的-128~127)。
可参考示例TestAssertEqualsSame类。
当需要判断是否为同一个对象时,也可以使用System.identityHashCode()方法获取对象的hashCode进行比较。
1.1.10.3. 使用匹配器Matcher
Assert.assertThat()方法定义为“public static <T> void assertThat(T actual, Matcher<? super T> matcher)”,T actual参数为需要检查的对象,Matcher<? super T> matcher参数为Matcher接口实现类,可在其中对参数值进行自定义检查操作,例如对对象的各个属性依次进行检查等。
Matcher接口实现类需要实现org.hamcrest.Matcher接口的方法,包括matches()、describeMismatch()、_dont_implement_Matcher___instead_extend_BaseMatcher_()、describeTo()等方法,可以不指定范型类型。
matches()方法用于判断被检查对象是否符合预期,传入的Object item参数为被检查对象,当返回true时代表检查通过,false代表不通过。
describeMismatch()方法当matches()方法检查不通过时会被调用,参数中的Object item为被检查的对象。
Matcher接口实现类可以通过构造函数的参数接收预期的参数值等。
Matcher接口实现类示例如下:
private String id;
private String flag;
public TestMatcherTestTableEntity(String id, String flag) {
this.id = id;
this.flag = flag;
}
@Override
public boolean matches(Object item) {
if (!(item instanceof TestTableEntity)) {
logger.error("not instanceof TestTableEntity {}", item);
return false;
}
TestTableEntity testTableEntity = (TestTableEntity) item;
return id.equals(testTableEntity.getId()) && flag.equals(testTableEntity.getFlag());
}
可参考示例TestMatcherTestTableEntity类。
调用Assert.assertThat()方法示例如下:
assertThat(testTableEntity, new TestMatcherTestTableEntity(TestConstants.FLAG1, TestConstants.FLAG2));
可参考示例TestAssertThat类test1方法。
Matcher接口实现类可以使用匿名类,示例如下:
assertThat(testTableEntity, new Matcher<TestTableEntity>() {
@Override
public boolean matches(Object item) {
if (!(item instanceof TestTableEntity)) {
logger.error("not instanceof TestTableEntity {}", item);
return false;
}
TestTableEntity testTableEntity = (TestTableEntity) item;
return TestConstants.FLAG1.equals(testTableEntity.getId()) && TestConstants.FLAG2.equals
(testTableEntity.getFlag());
}
@Override
public void describeMismatch(Object item, Description mismatchDescription) {
logger.error("{} mismatch!", item);
}
@Override
public void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
}
@Override
public void describeTo(Description description) {
}
});
可参考示例TestAssertThatAnonymous类test1方法。
1.1.10.3.1. 使用简化的Matcher实现类
Matcher接口提供了4个方法需要实现,比较繁琐,可以使用实现Matcher接口的抽象自定义类,仅提供类似matches()方法的抽象方法,供实现类实现。可参考示例中实现Matcher接口的抽象类TestMatcherSimple,如下所示。在matches方法中对参数进行了强制类型转换,并调用matches2方法处理,在使用时继承该类时只需要实现matches2方法即可。
public abstract class TestMatcherSimple<T> implements Matcher<T> {
private static final Logger logger = LoggerFactory.getLogger(TestMatcherSimple.class);
@Override
public boolean matches(Object item) {
if (item == null) {
return false;
}
T item2 = (T) item;
return matches2(item2);
}
public abstract boolean matches2(T item);
@Override
public void describeMismatch(Object item, Description mismatchDescription) {
logger.error("{} mismatch!", item);
}
@Override
public void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
}
@Override
public void describeTo(Description description) {
}
}
继承TestMatcherSimple的自定义类示例如下:
public class TestMatcherTestTableEntity2 extends TestMatcherSimple<TestTableEntity> {
private String id;
private String flag;
public TestMatcherTestTableEntity2(String id, String flag) {
this.id = id;
this.flag = flag;
}
@Override
public boolean matches2(TestTableEntity item) {
return id.equals(item.getId()) && flag.equals(item.getFlag());
}
}
在Assert.assertThat()方法中使用继承TestMatcherSimple的自定义类示例如下:
assertThat(testTableEntity, new TestMatcherTestTableEntity(TestConstants.FLAG1, TestConstants.FLAG2));
可参考示例TestAssertThat类test2方法。
使用继承TestMatcherSimple的匿名类示例如下:
assertThat(testTableEntity, new TestMatcherSimple<TestTableEntity>() {
@Override
public boolean matches2(TestTableEntity item) {
return TestConstants.FLAG1.equals(item.getId()) && TestConstants.FLAG2.equals
(item.getFlag());
}
});
可参考示例TestAssertThatAnonymous类test2方法。
1.1.11. 异常测试
参考“Exception testing”( https://github.com/junit-team/junit4/wiki/Exception-testing )。
对于预期会出现指定异常的情况,JUnit提供了比较方便的检测方法,如使用@Test注解的expected属性,或使用ExpectedException类。当未出现异常,或出现的异常不符合预期时,会抛出AssertionError异常。
1.1.11.1. 使用@Test注解的expected属性
可以使用@Test注解的expected属性,检查是否出现指定的异常,使用示例如下:
@Test(expected = RuntimeException.class)
expected属性只能检查异常类,无法检查异常信息。当需要检查异常信息时,可在代码中通过try-catch对预期会出现异常的代码进行捕获,检查异常信息后再抛出异常。示例如下。
@Test(expected = RuntimeException.class)
public void test2() {
try {
testException();
} catch (RuntimeException e) {
logger.info("error: {}", e);
assertEquals(TestConstants.MOCKED, e.getMessage());
throw e;
}
}
当测试代码中出现异常后,后续代码不会继续执行。假如需要在出现异常后,继续执行后续代码时,可在代码中通过try-catch对预期会出现异常的代码进行捕获,进行后续处理后再抛出异常。
可参考示例TestJUnitException1类。
1.1.11.2. 使用ExpectedException类
ExpectedException支持对异常类及异常信息等进行检查,使用示例如下:
@Rule
public ExpectedException expectedException = ExpectedException.none();
expectedException.expect(RuntimeException.class);
expectedException.expectMessage("...");
//出现异常的代码
ExpectedException类的expect()方法参数类型为Class,可以检查异常类是否符合预期;expectMessage()方法参数类型为String,可以检查异常信息是否符合预期。
当测试代码中出现异常后,后续代码不会继续执行。假如需要在出现异常后,继续执行后续代码时,可在代码中通过try-catch对预期会出现异常的代码进行捕获,进行后续处理后再抛出异常。示例如下。
expectedException.expect(RuntimeException.class);
expectedException.expectMessage(TestConstants.MOCKED);
try {
testException();
} catch (Exception e) {
flag2a = true;
throw e;
}
可参考示例TestJUnitException2类。
1.1.11.3. 使用Matcher接口实现类检查异常信息
ExpectedException类的expect、expectMessage方法返回值均为void,无法直接返回异常信息。expect与expectMessage方法的参数支持org.hamcrest.Matcher,可以使用Matcher接口的实现类,进行自定义的检查操作,例如使用比较复杂的规则检查异常类与异常信息,打印异常类名、调用堆栈、异常信息等。
matches()方法传入的Object item参数为异常对象或异常信息。
describeMismatch()方法传入的Object item参数为异常对象或异常信息。
Matcher接口实现类的使用可参考前文Assert.assertThat()方法对应的内容,Matcher接口实现类可以通过构造函数的参数接收预期的异常类名或异常信息等。
示例中的TestMatcherExpClassEquals类可以检查异常类是否为指定的类,并打印异常类名、调用堆栈;TestMatcherExpClassIsInstance类可以检查异常类是否继承自指定的类,并打印类名、异常堆栈;TestMatcherExpMsgContains类可以检查异常信息是否包含指定字符串,并打印异常信息。
使用Matcher接口实现类示例如下:
expectedException.expect(new TestMatcherExpClassEquals(RuntimeException.class));
expectedException.expect(new TestMatcherExpClassIsInstance(RuntimeException.class));
expectedException.expectMessage(new TestMatcherExpMsgContains("..."));
可参考示例TestJUnitException3类。
1.1.11.4. 使用Assert.assertThrows()方法检查异常信息
参考 https://github.com/junit-team/junit4/blob/master/doc/ReleaseNotes4.13.md#pull-request-1633-deprecate-expectedexceptionnone 。
在JUnit 4.13中,ExpectedException.none()方法已被标记为过时,建议使用Assert.assertThrows()方法替代(在JUnit 4.12及之前的版本中,没有Assert.assertThrows()方法)。
Assert.assertThrows()方法使用可参考 https://github.com/junit-team/junit4/wiki/Exception-testing#using-assertthrows-method 。
Assert.assertThrows()方法使用示例如下:
RuntimeException exception = assertThrows(RuntimeException.class, () -> throw1());
assertEquals(TestConstants.FLAG1, exception.getMessage());
private void throw1() {
throw new RuntimeException(TestConstants.FLAG1);
}
Assert.assertThrows()方法返回值为指定方法出现的异常,可用于比较或打印日志。
当Assert.assertThrows()方法中执行的方法出现异常时,Assert.assertThrows()方法之后的代码也会执行。
Assert.assertThrows()方法的参数1支持指定预期异常的父类。
在使用Assert.assertThrows()时,若未出现异常,或若实际出现的异常与预期不符时,会抛出AssertionError异常。
推荐使用Assert.assertThrows()方法检查异常信息,可参考示例TestJUnitException4类。
1.1.12. 使用Suite
参考“Aggregating tests in suites”( https://github.com/junit-team/junit4/wiki/Aggregating-tests-in-suites )。
使用Suite作为runner,可以手动构建包含多个测试类的套件。在使用Suite时,需要使用@RunWith(Suite.class)和@SuiteClasses(TestClass1.class,…)注解类。当运行Suite类时,将运行所有套件类中的所有测试类的方法。
Suite的使用示例如下:
@RunWith(Suite.class)
@Suite.SuiteClasses(value = {
xxx1.class,
xxx2.class
})
public class xxx {
}
可参考示例TestSuiteFrameworkJUnit类。
1.2. JUnit5
JUnit5与JUnit4相比,有一些新增的功能,及发生改变的功能。
1.2.1. 添加引用
“Example Projects”( https://junit.org/junit5/docs/current/user-guide/#overview-getting-started-example-projects )中说明,对于Gradle与Java,可以参考“junit5-jupiter-starter-gradle”工程,在该工程的build.gradle文件( https://github.com/junit-team/junit5-samples/blob/r5.5.2/junit5-jupiter-starter-gradle/build.gradle )中,通过以下方式引用JUnit5:
'org.junit.jupiter:junit-jupiter:5.5.2'
1.2.2. 对于IDE的支持
参考“IDE Support”( https://junit.org/junit5/docs/current/user-guide/#running-tests-ide )。
- IntelliJ IDEA
IntelliJ IDEA自2016.2版以来支持在JUnit平台上运行测试。建议使用IDEA 2017.3或更新版本。
IntelliJ IDEA中捆绑了JUnit5,可能会由于版本冲突导致测试执行失败。为了使用不同的JUnit5版本( 如5.5.2 ),需要通过以下方式引用相应版本的junit-platform-launcher,junit-jupiter-engine和junit-vintage-engine:
"org.junit.platform:junit-platform-launcher:1.5.2"
"org.junit.jupiter:junit-jupiter-engine:5.5.2"
"org.junit.vintage:junit-vintage-engine:5.5.2"
- Eclipse
自Eclipse Oxygen.1a( 4.7.1a )版本开始,Eclipse IDE为JUnit平台了提供支持。
经测试,引用上述版本的组件后,在Eclipse中也可以正常使用JUnit5。
1.2.3. 注解
JUnit5的注解相对JUnit4有一些变化,可以参考“Annotations”( https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations )。
JUnit5的注解的包变为org.junit.jupiter.api,@Test注解类名未变化,其他部分常用注解类名出现变化,如下所示:
JUnit4的注解 | JUnit5的注解 |
---|---|
@BeforeClass | @BeforeAll |
@Before | @BeforeEach |
@After | @AfterEach |
@AfterClass | @AfterAll |
新增注解@DisplayName,可以指定测试类或测试方法在测试结果中的显示名称,参考 https://junit.org/junit5/docs/current/user-guide/#writing-tests-display-names 。
可参考示例TestJUnit1类。
1.2.4. 从JUnit4迁移到JUnit5
参考“Migrating from JUnit 4”( https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4 )。
其中说明了@RunWith不再存在,被@ExtendWith取代。
JUnit5有很多新的特性,在此不进行说明。
1.2.5. Mock框架支持
1.2.5.1. Mockito支持
Mockito支持JUnit5。参考“45. New JUnit Jupiter (JUnit5+) extension”( https://static.javadoc.io/org.mockito/mockito-core/3.1.0/org/mockito/Mockito.html#junit5_mockito ),“MockitoExtension”( https://static.javadoc.io/org.mockito/mockito-junit-jupiter/3.1.0/org/mockito/junit/jupiter/MockitoExtension.html )。
需要添加对于org.mockito:mockito-junit-jupiter的引用,如下所示:
"org.mockito:mockito-junit-jupiter:3.1.0"
在测试类中使用注解“@ExtendWith(MockitoExtension.class)”,而不是“@RunWith(MockitoJUnitRunner.class)”。
可参考示例TestMockitoNonSt1类。
1.2.5.2. PowerMock不支持JUnit5
参考github的以下issue,关于PowerMock不支持JUnit5,作者有答复。
https://github.com/powermock/powermock/issues/830
https://github.com/powermock/powermock/issues/906
https://github.com/powermock/powermock/issues/929
可参考示例TestPowerMockSt1类。
2. 单元测试代码需要注意的其他问题
2.1. 等待异步线程
当在被测试代码中使用线程执行操作时,可能出现测试代码执行完毕时,被测试代码中的线程还未结束的情况,此时被测试代码还未执行完毕,检查操作可能会不通过。可在测试代码中等待被测试代码执行完毕,再执行后续的检查操作。
例如在测试代码中获得线程池对象后,获取当前活跃线程数,等待直到变为0,如下所示:
while (threadPoolTaskExecutor.getActiveCount() > 0) {
Thread.sleep(200L);
}
可参考示例TestThread类。
2.2. 基类设置为抽象类
在测试的基类中有时不会设置@Test方法,当JUnit尝试执行此类测试基类时会出现异常。可以将测试的基类设置为抽奖类,JUnit不会尝试执行,可避免以上问题。
2.3. 使用MockDriver类作为数据源驱动
当使用Druid作为数据源时,在程序启动时若连接数据库失败,会导致数据源初始化失败,程序也无法正常启动。
对于执行单元测试不需要执行数据库操作,但启动时需要连接数据库的情况,可将数据源的“driverClassName”参数使用“com.alibaba.druid.mock.MockDriver”,且“url”参数需以“jdbc:fake:”或“jdbc:mock:”开头。当满足以上情况时,Druid数据源在初始化时,不会实际连接数据库,即使没有可以访问的数据库,单元测试也能正常执行。示例如下:
<property name="driverClassName" value="com.alibaba.druid.mock.MockDriver"/>
<property name="url" value="jdbc:mock://"/>