单元测试框架之Junit使用及原理分析
前言
单元测试用来保证我们的代码能够正常运行,输入一组数据,能够得到期望的结果,一般以方法作为最小单元。
简单使用
添加依赖
复制<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
简单例子
复制import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class TestJunit {
@BeforeClass
public static void beforeClass() {
System.out.println("beforeClass");
}
@Before
public void before() {
System.out.println("before");
}
@Test
public void testBase() {
//如果实际值和期望值不一致,抛出AssertionError错误
Assert.assertEquals(11, 10);
Assert.assertThat(11, Matchers.equalTo(11));
}
@Test(expected = RuntimeException.class)
public void testException() {
throw new RuntimeException();
}
@Test(timeout = 3000) //单位毫秒
public void testTimeout() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@After
public void after() {
System.out.println("after");
}
@AfterClass
public static void afterClass() {
System.out.println("afterClass");
}
}
- @Test: 标记为一个测试用例,expected参数表示方法必须抛出指定的异常,timeout参数表示方法必须在指定时间内执行完
- @BeforeClass: 必须为静态方法,在所有测试用例之前执行
- @AfterClass: 必须为静态方法,在所有测试用例之后执行
- @Before: 在每一个测试用例之前执行
- @After: 在每一个测试用例之后执行
复制import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
public class TestJunitSource {
public static void main(String[] args) {
//执行测试类,得到一个结果,包含执行错误的用例信息
Result result = JUnitCore.runClasses(TestJunit.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
//所有测试用例都是成功的
if (result.wasSuccessful()) {
System.out.println("Both Tests finished successfully...");
}
}
}
输出结果为
复制beforeClass
before
after
before
after
before
after
afterClass
testBase(com.imooc.sourcecode.java.test.junit.TestJunit): expected:<11> but was:<10>
符合每种注解的作用
原理分析
首先进入JUnitCore的runClasses()方法
复制public static Result runClasses(Class<?>... classes) {
//defaultComputer()方法返回一个Computer对象,不重要
return runClasses(defaultComputer(), classes);
}
继续跟进去
复制public static Result runClasses(Computer computer, Class<?>... classes) {
//创建JUnitCore对象并执行
return new JUnitCore().run(computer, classes);
}
public Result run(Computer computer, Class<?>... classes) {
return run(Request.classes(computer, classes));
}
核心分为两部分,根据测试类创建Runner对象,通过Runner对象执行测试方法。
创建Runner对象
复制public static Request classes(Computer computer, Class<?>... classes) {
try {
//这是一个RunnerBuilder(运行器构造器),用来创建Runner(运行器)
AllDefaultPossibilitiesBuilder builder = new AllDefaultPossibilitiesBuilder(true);
//最终执行AllDefaultPossibilitiesBuilderel的runnerForClass()方法来获取Runner对象,并封装成Suite
Runner suite = computer.getSuite(builder, classes);
//将Runner包装成一个Request对象,核心还是Runner
return runner(suite);
}
}
进入AllDefaultPossibilitiesBuilderel的runnerForClass()方法
复制@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
//内部通过其他RunnerBuilder来创建Runner
List<RunnerBuilder> builders = Arrays.asList(
ignoredBuilder(), //包含@Ignore注解的情况
annotatedBuilder(), //包含@RunWith注解的情况
suiteMethodBuilder(),//包含suite()方法
junit3Builder(),//继承TestCase类
junit4Builder());//默认BlockJUnit4ClassRunner
for (RunnerBuilder each : builders) {
//如果满足以上的任意一种情况,直接返回
Runner runner = each.safeRunnerForClass(testClass);
if (runner != null) {
return runner;
}
}
return null;
}
通过@RunWith注解,我们可以扩展使用其他Runner实现类,如SpringRunner,配合Spring使用,可以帮我们创建IOC容器并注入需要的依赖。
我们的例子中以上情况都不满足,所以最终使用BlockJUnit4ClassRunner。
通过Runner对象执行测试方法
复制public Result run(Computer computer, Class<?>... classes) {
//执行创建的Request对象,其中包含一个Runner对象
return run(Request.classes(computer, classes));
}
跟进去
复制public Result run(Request request) {
//还是通过Runner对象,这里就是Suite类型,其中包含真正工作的运行器BlockJUnit4ClassRunner对象
return run(request.getRunner());
}
执行Runner对象
复制public Result run(Runner runner) {
Result result = new Result();
//创建一个运行监听器
RunListener listener = result.createListener();
notifier.addFirstListener(listener);
try {
//运行开始,这里就是记录开始时间
notifier.fireTestRunStarted(runner.getDescription());
//真正开始运行的地方
runner.run(notifier);
//运行结束,这里就是记录结束时间
notifier.fireTestRunFinished(result);
} finally {
removeListener(listener);
}
return result;
}
进入Suite父类ParentRunner的run()方法
复制@Override
public void run(final RunNotifier notifier) {
EachTestNotifier testNotifier = new EachTestNotifier(notifier,
getDescription());
try {
//创建Statement对象
Statement statement = classBlock(notifier);
//执行Statement
statement.evaluate();
} catch (AssumptionViolatedException e) {
testNotifier.addFailedAssumption(e);
} catch (StoppedByUserException e) {
throw e;
} catch (Throwable e) {
testNotifier.addFailure(e);
}
}
看一下是如何创建Statement对象的,当前的Runner类型为Suite,其中的测试类为空,真正的测试类在BlockJUnit4ClassRunner中
复制protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
//如果测试类包含@BeforeClass的方法,使用装饰者模式创建一个装饰器(RunBefores类型),在测试方法之前执行
statement = withBeforeClasses(statement);
//如果测试类包含@AfterClass的方法,使用装饰者模式创建一个装饰器(RunAfters类型),在测试方法之后执行
statement = withAfterClasses(statement);
//处理包含@ClassRule的方法,使用RunRules包装,暂时不管
statement = withClassRules(statement);
}
return statement;
}
//创建Statement对象
protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
//执行Statement的核心
runChildren(notifier);
}
};
}
private void runChildren(final RunNotifier notifier) {
final RunnerScheduler currentScheduler = scheduler;
try {
//Suite对象的children只有BlockJUnit4ClassRunner对象
for (final T each : getFilteredChildren()) {
currentScheduler.schedule(new Runnable() {
public void run() {
//又跳转到了runChild()方法,这里的each就是BlockJUnit4ClassRunner对象
ParentRunner.this.runChild(each, notifier);
}
});
}
} finally {
currentScheduler.finished();
}
}
Suite的runChild()方法又会调用BlockJUnit4ClassRunner的run()方法,最终会执行BlockJUnit4ClassRunner的runChild()方法
复制@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
//获取方法的描述信息,不重要
Description description = describeChild(method);
//方法上是否包含@Ignore注解,表示忽略此方法
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
runLeaf(methodBlock(method), description, notifier);
}
}
//创建一个Statement对象,并进行各种装饰
protected Statement methodBlock(FrameworkMethod method) {
Object test;
try {
//创建测试类实例,这里就是TestJunit对象
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
//通过反射创建
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
//创建一个Statement对象,实际类型为InvokeMethod,通过反射执行测试方法
Statement statement = methodInvoker(method, test);
//处理@Test注解的expected参数,通过ExpectException类包装
statement = possiblyExpectingExceptions(method, test, statement);
//处理@Test注解的timeout参数,通过FailOnTimeout类包装
statement = withPotentialTimeout(method, test, statement);
//处理@Before注解,通过RunBefores类包装
statement = withBefores(method, test, statement);
//处理@After注解,通过RunAfters类包装
statement = withAfters(method, test, statement);
//处理@Rule注解,通过RunRules类包装,暂时不管
statement = withRules(method, test, statement);
return statement;
}
//真正执行Statement对象
protected final void runLeaf(Statement statement, Description description,
RunNotifier notifier) {
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
eachNotifier.fireTestStarted();
try {
statement.evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
//如果我们的测试方法抛出未被捕获的异常,就当做一次失败,主要是代码中抛出的AssertionError错误
eachNotifier.addFailure(e);
} finally {
eachNotifier.fireTestFinished();
}
}
总结
- 通过RunnerBuilder(运行器构造器)创建一个Runner(运行器),最终的Runner为Suite类型,内部包含多个实际用来工作的Runner,
默认为BlockJUnit4ClassRunner类型,我们可以通过@RunWith注解自定义Runner实现。 - BlockJUnit4ClassRunner解析测试类创建TestClass对象,并解析出其中包含@BeforeClass,@AfterClass,@Before,@After,@Test等注解的方法。
- BlockJUnit4ClassRunner对象创建一个Statement对象,使用RunBefores和RunAfters装饰此对象并执行(就是执行所有的测试方法)。
- 执行每一个测试方法,创建一个Statement对象,使用ExpectException,FailOnTimeout,RunBefores,RunAfters等类装饰此对象并执行。
参考
Java JUnit 单元测试小结
Junit源码阅读笔记一(从JunitCore开始)
Junit源码阅读笔记二(Runner构建)
Junit源码阅读笔记三(TestClass)
JUnit 5 教程 之 基础篇
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)