blog . say magic . cn/2016/09/30/under-Junit . html # post _ _ title
JUnit是Erich Gamma和Kent Beck编写的回归测试框架。Eclipse、IDEA等Java开发环境为JUnit提供了友好的支持。Erich Gamma是著名的《设计模式:可重用面向对象软件的基础》一书的作者之一。因此,JUnit中的设计模式使用得当。所以JUnit的源代码可以说是一个优秀的武侠骗子,值得一看。本文将以JUnit4.12为基础,从JUnit的运行过程和Match验证两个方面对JUnit的源代码进行整体分析。
运行流程
启动JUnit的方法有很多。比如在Android Studio中,我们可以直接点击一个@Test标注的函数来运行:
这时候JUniteStarter就启动了,intellij为我们提供的。如果您感兴趣,可以查看其源代码:
https://github . com/JetBrains/intellij-community/blob/master/plugins/JUnit _ rt/src/com/intellij/rt/execution/JUnit/JUNitStarter . Java
如果使用gradle,可以执行gradle test来运行测试,这实际上是通过在一个线程中执行SuiteTestClassProcessor的processTestClass方法来启动的。它的源代码可以查看
https://github . com/grad le/grad le/blob/master/subjects/testing-base/src/main/Java/org/grad le/API/internal/tasks/testing/suite test class processor . Java
以上两种都是第三方工具提供的便捷方式。其实JUnit还提供了一个名为JUnitCore的类,方便我们运行测试用例。
虽然启动JUnit的方法有很多,但这些都是打开与JUnit对话的方法。最后,实现了一些在JUnit中起核心作用的类。为了让大家对这些核心boss有一个初步的了解,我画了一个类图:
上图只是JUnit中的几个核心类,也是本小节的主要分析对象。这里先给出一些对象的职责,让大家有个大概的了解。稍后,您将更清楚地知道每个对象如何通过代码来履行这些职责:
在类图的中央,有个叫做ParentRunne的对象很引人注目,它继承自Runner.Runner则表示着JUnit对整个测试的抽象Runner实现了Describable接口,Describable接口中唯一的函数getDeion()返回了Deion对象,记录着测试的信息。Statement 是一个抽象类,其 evaluate()函数代表着在测试中将被执行的方法。ParentRunner 共有两个子类,BlockJUnit4ClassRunner 用来运行单个测试类,Suite用来一起运行多个测试类RunnerBuilder 是生产Runner的策略,如使用@RunWith(Suite.class)标注的类需要使用Suite, 被@Ignore标注的类需要使用IgnoreClassRunner。TestClass是对被测试的类的封装综上所述,我们从ParentRunner开始,它的构造函数如下:
受保护的父级运行程序(类& lt?>。testClass)引发Initializati {
this . test class = CreateTestClass(TestClass);
validate();
}
This.testClass就是上面提到的testClass。我们输入createTestClass方法,看看它如何将类对象转换成测试类。
受保护的测试类创建测试类(类& lt?>。testClass) {
返回新的测试类(TestClass);
}
什么都没有,具体逻辑写在TestClass里面:
公共测试类(类& lt?>。clazz) {
this.clazz = clazz
if (clazz!= null & amp& ampclazz.getConstructors()。长度>。1) {
抛出新的IllegalArgumentException(
“测试类只能有一个构造函数”);
}
地图<。类别<。?扩展注释>,列表& lt框架工作方法>>。方法用于通知=
新建LinkedHashMap & lt类别<。?扩展注释>,列表& lt框架工作方法>>。();
地图<。类别<。?扩展注释>,列表& lt框架工作域>。>。字段存储位置=
新建LinkedHashMap & lt类别<。?扩展注释>,列表& lt框架工作域>。>。();
扫描注释成员(方法用于注释、字段或注释);
this . methods for annotations = makeDeeply不可修改(methods for annotations);
this . field sforannocations = makedeplyunmodified(field sforannocations);
}
可以看到,整个构造函数都在做一些验证和初始化工作,应该注意scanAnnotatedMembers方法:
受保护的空扫描未命名成员(映射& lt类别<。?扩展注释>,列表& lt框架工作方法>>。方法对于导航,地图& lt类别<。?扩展注释>,列表& lt框架工作域>。>。field sforannocations){
for(Class & lt;?>。每个类:getsuperclases(clazz)){
for(Method EachMethod:MethodOrter . GetDeclaredMethods(EachClass)){
添加到注释列表(新框架工作方法(每个方法),用于注释的方法);
}
//确保对字段进行排序,以确保插入条目
//并以确定的顺序从字段中读取注释
for(字段每个字段:getSortedDeclaredFields字段(每个类)){
添加到注释列表(新框架工作字段(每个字段),字段存储位置);
}
}
}
整个函数的作用是扫描类中方法和变量上的标注,根据标注的类型进行分类,缓存在methodsForAnnotations和fieldsForAnnotations中。需要注意的是,JUnit将方法和变量分别封装为FrameworkMethod和FrameworkField,它们是从FrameworkMember继承的,从而统一抽象方法和变量。
在阅读了ParentRunner的构造函数之后,让我们看看Parentrunner继承的Run方法是如何工作的:
@覆盖
公共无效运行(最终RunNotifier通知程序){
每个测试通知程序测试通知程序=新的每个测试通知程序(通知程序,
getDeion());
尝试{
Statement statement = classBlock(通知程序);
statement . evaluate();
} catch(Assumentinviolatedexception e){
test notifier . AddFailedAssessment(e);
} catch(StoppedByUserException e){
扔e;
} catch(可投掷e) {
testnotifier . addfailure(e);
}
}
关键代码之一是类块函数将通知程序转换为语句:
受语句保护的类块(最终RunNotifier通知程序){
Statement语句= childrenInvoker(通知程序);
if(!areAllChildrenIgnored忽略()){
语句= withBeforeClasses(语句);
statement = withAfterClasses(语句);
语句= withClassRules(语句);
}
返回语句;
}
在继续追赶childrenInvoker之前,请允许我现在在这里保存一个文件,并将其记录为,稍后我们将返回到classBlock
受保护的语句子调用程序(最终运行通知程序){
返回新语句(){
@覆盖
public void evaluate() {
runChildren(通知程序);
}
};
}
子调用程序返回一个语句。看看它的求值方法,它调用runChildren方法,这也是ParentRunner中一个非常重要的函数:
private void runChildren(最终运行通知程序通知程序){
final RunnerScheduler currentScheduler = scheduler;
尝试{
for(期末考试各:getFilteredChildren()) {
currentScheduler.schedule(新的Runnable() {
public void run() {
ParentRunner.this.runChild(每个,通知程序);
}
});
}
}最后{
currentscheduler . finished();
}
}
这个功能体现了抽象的重要性。注意泛型T,在ParentRunner的每个实现类中都是不一样的。在block JUnit 4类运行器中,T代表框架方法。就这个函数而言,getFilteredChildren获得了由@Test注释标记的框架方法。在套房里,t是跑者,父母是隧道者。这个。runchild(每个,通知程序);这句话中的runChild(每个,通知者)方法仍然是一个抽象方法。让我们先来看看BlockJUnit4ClassRunner中的实现:
@覆盖
受保护的void runChild(最终FrameworkMethod方法,RunNotifier通知程序){
Deion deion = describeChild(方法);
if (isIgnored(method)) {
notifier . FireTestignored(deion);
} else {
runLeaf(methodBlock(method),deion,notifier);
}
}
IsIgnored方法确定该方法是否由@Ignore注释标识,如果是,直接通知通知者触发忽略事件;否则执行runLeaf方法,runLeaf的第一个参数是Statement,所以BlockJUnit4ClassRunner通过methodBlock方法将方法转换成Statement:
受保护的语句方法块(框架方法){
实物测试;
尝试{
test = new reflectivcallable(){
@覆盖
受保护对象运行反射调用()引发可投掷{
return CreateTest();
}
}.run();
} catch(可投掷e) {
返回新的失败(e);
}
statement statement = method invoker(方法,测试);
语句=可能预期异常(方法、测试、语句);
statement = with ProfeSsionaltimeout(方法、测试、语句);
statement = withBefores(方法、测试、语句);
statement = withAfters(方法、测试、语句);
statement = withRules(方法、测试、语句);
返回语句;
}
前几行代码正在生成测试对象,测试对象的类型是我们要测试的类,然后我们追循methodInvoker方法:
受保护的语句方法声明器(框架工作方法方法,对象测试){
返回新的InvokeMethod(方法,测试);
}
可以看到,我们生成的Statement实例是InvokeMethod,那么我们来看看它的求值方法:
testMethod.invokeExplosively(目标);
invokeExplosively函数的作用是调用目标对象上的testMethod方法。前面说过,这个testMethod是BlockJUnit4ClassRunner中@Test标记的方法。这时,我们终于找到了@Test方法被调用的地方。别急,我们继续分析刚才的函数:
语句=可能预期异常(方法、测试、语句);
statement = with ProfeSsionaltimeout(方法、测试、语句);
statement = withBefores(方法、测试、语句);
statement = withAfters(方法、测试、语句);
statement = withRules(方法、测试、语句);
我们可以看到语句在不断变化,我们可以很容易地猜测函数的名称,如withBefores和withRules。这是处理诸如@Before和@Rule之类的注释的地方。让我们以withBefores为例:
带Befores的受保护语句(框架工作方法方法,对象目标,
语句语句){
列表<。框架工作方法>befores = getTestClass()。getAnnotatedMethods(
before . class);
return befores.isEmpty()?语句:新的RunBefores(语句,
befores,target);
}
这个函数首先获取所有用@Before标记的方法,并将它们封装为RunBefores。让我们看看它的构造函数
公共运行前(下一条语句,列表& lt框架工作方法>befores,Object target) {
this.next = next
this.befores = befores
this.target = target
}
public void evaluate()抛出Throwable {
for(FrameWorkMethod before:befores){
before . invoke explosive(target);
}
next . evaluate();
}
很明显,在执行求值时,首先调用所有的before方法来执行,然后调用原语句的eval方法。剩下的功能都差不多,有兴趣可以继续查。
这样,我们就理解了runLeaf方法的第一个参数Statement的由来。接下来,我们来看看这个润叶方法做了什么。runLeaf在ParentRunner中有一个默认实现:
受保护的最终无效润叶(声明声明,Deion deion,
RunNotifier通知程序){
每个测试通知程序每个通知程序=新的每个测试通知程序(通知程序,deion);
each notifier . FireTestStarted();
尝试{
statement . evaluate();
} catch(Assumentinviolatedexception e){
each notifier . addfailedSuissuen(e);
} catch(可投掷e) {
each notifier . addfailure(e);
}最后{
each notifier . FireTestfinished();
}
}
很简单,直接执行语句的求值方法。需要注意的是,这里的语句实例不一定是任何东西,可能是RunBefores或者RunAfters,与被测试类中的注释有关。
在这一点上,还记得我们前面谈到的档案a吗?让我们回到档案a:
受语句保护的类块(最终RunNotifier通知程序){
Statement语句= childrenInvoker(通知程序);
if(!areAllChildrenIgnored忽略()){
语句= withBeforeClasses(语句);
statement = withAfterClasses(语句);
语句= withClassRules(语句);
}
返回语句;
}
归档后发生的事情实际上是执行代码语句语句=子调用程序(通知程序)。换句话说,子调用器的作用是用一条语句打包所有要执行的测试用例。然后点燃这个语句,所有的测试用例都会被触发。但是我们也需要注意if语句包围的代码,我们看到了熟悉的语句。语句仍在不断地转换,但此时它处于类级别。函数的作用是:操作@BeforeClass注释:
带BeforeClasses的受保护语句(语句语句){
列表<。框架工作方法>befores = testClass
。getannotedmethods(BeforeClass . class);
return befores.isEmpty()?声明:
新的运行前(语句,前,空);
}
需要注意的是RunBefores的第三个参数为null,这意味着@BeforeClass标注的方法只能是静态的。
如上所述,我们分析了BlockJUnit4ClassRunner的运行过程,也就是JUnit在只有一个测试类的情况下是如何工作的。如前所述,ParentRunner还有一个子类Suite,表示需要运行一组测试。BlockJUnit4ClassRunner的一个运行单元是FrameworkMethod,而Suite的一个运行单元是runChild方法。让我们看看它的Runchild方法:
受保护的无效运行子级(运行程序运行程序,最终运行程序通知程序){
runner.run(通知程序);
}
很清楚,放下就好,用跑步者的跑法。这样,如果这个runner的实例仍然是Suite,它将继续向内运行,如果这个runner是BlockJUnit4ClassRunner,这将执行我们前面分析的逻辑。这里的问题是,这个runner是怎么生成的?这取决于套件的构造函数:
受保护套件(类别& lt?>。klass,Class & lt?>。[] suiteClasses)引发Initializati {
这(新的AllDefaultPro可能性构建器(true),klass,suiteClasses
}
所有默认可能性构建者的职责是为每个班级的学生找到相应的跑步者。有兴趣的话可以查查它的runnerForClass方法,很好理解,这里就不赘述了。
匹配器验证
上面我们分析了用@Test标记的函数是如何被JUnit执行的,但是用@Test标记肯定是不够的。既然是测试,我们肯定需要一些手段来验证程序的执行是否符合预期。JUnit提供了Matcher机制,可以满足我们的大部分需求。Matcher相关类主要在org.hamcrest包下。首先,看下面的类图:
上图只列出了org.hamcrest包下的一些类,它们组合在一起形成了JUnit强大的验证机制。
验证的基本写法是:
matcherassert . assert ThAT(" say magic ",CoreMatchers . ContainsStrIng(" magic "));
首先我们需要调用MatcherAssert的assertThat方法,这个方法最后变成:
公共静态<。T>。无效资产(字符串原因,实际,匹配者& lt?超级电视。matcher) {
if(!matcher.matches(实际)){
deion deion = new StringDeion();
deion.appendText(原因)
。附录文本(“预期:”)
。附件(匹配器)
。appendText("n但是:");
matcher . descriptebismatch(actual,deion);
抛出新的Asserti(deion . tostring());
}
}
这个功能的目的很明确。它直接判断匹配者是否匹配。如果不匹配,则封装描述信息,然后抛出异常。所以我们来关注一下matcher的matchs方法做了什么。核心匹配器。Containsstring ("magic ")返回一个Matcher,CoreMatchers相当于一个静态工厂,提供大量静态方法返回各种Matcher:
让我们以刚才的containsString为例来检查它的内部代码:
public static org . ham crest . Matcher & lt;java.lang.String >contains StrIng(Java . lang . StrIng substring){
return org . hamcrest . core . StrIngcontains . ContainsStrIng(substring);
}
可以看出它调用了StringContains的一个静态方法,并继续追:
@工厂
公共静态匹配器& lt字符串>包含字符串(字符串子字符串){
返回新的StringContains(子字符串);
}
这里很简单。直接新建一个StringContains实例。StringContains的继承关系如下:
首先,BaseMatcher实现了Matcher接口,TypeSafeMatcher是BaseMatcher的抽象实现,其匹配方法如下:
公共最终布尔匹配(对象项){
退货!= null
& amp& amp期望的类型。实例(项目)
& amp& ampmatchessafelly((T)项);
}
可以看出,它在验证之前已经检查了空和类型,所以子类可以实现matchesSafely方法,所以这个方法中不需要验证空和类型。
SubstringMatchers是TypeSafeMatcher的实现,是字符串类验证的抽象。它的匹配方法如下:
@覆盖
public boolean matchesSafely(字符串项){
返回evalSubstringOf(项);
}
子类需要实现evalSubstringOf方法。这样,我们可以看一下StringContains的这个方法:
@覆盖
受保护的布尔求值子字符串Of(字符串)
返回s.indexOf(子字符串)>;= 0;
}
出奇的简单,没什么好解释的。如果返回false,则意味着验证失败,并且之前的assertThat方法将引发异常。这样的话,一个JUnit测试是不会通过的。
Assert被翻译成断言,也就是说,它是用来验证对错的,但是我们也知道,不是所有的事情都是对与错的,测试也是。例如,当我们单击登录按钮时,我们可能会在通过验证后跳转到页面,而没有任何返回值。这时候我们经常验证发生了什么事情,比如登录后执行跳转方法,这就意味着测试通过了。模拟框架就是这么做的。如果有兴趣,请查看我上一篇文章Mockito源代码分析。
https://blog . say magic . tech/2016/09/17/under-mochito . html
总结
阅读JUnit的源代码并不是很难。相信是和整体架构设计得当有关,让人神清气爽。本文只是对JUnit源代码的粗略总结,更多细节需要仔细琢磨。
1.《methodinvoker JUnit 源码解析》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《methodinvoker JUnit 源码解析》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/caijing/1575671.html