当前位置: 首页 > 科技观察

JUnit5系列基础介绍

时间:2023-03-14 17:05:13 科技观察

我们上周刚刚搭建了JUnit5环境,现在可以写测试了。让我们在本节中写下其中的一些!概述本文是本JUnit5系列的一部分:环境搭建基础入门架构系统扩展模型(ExtensionModel)条件断言注入动态测试...(不喜欢看文章的可以点这里看我的演讲,或者看看最近的AvJUGlecture,或者我的PowerPointonDevoxxPL。这个系列的文章是基于Junit5的Milestone2的预发布版本。它可能会改变。如果有新的里程碑版本发布,或者当试用版正式发布后,我会再次更新这篇文章。这里要介绍的大部分知识都可以在JUnit5用户指南中找到(这个链接指向Milestone2的第一版,如果你想看最新版本文档,请点击这里),指南还有更多内容等你发现。下面所有代码都可以在我的Github上找到。目录设计哲学JUnit5Prep:.包可见性。测试生命周期#@Test#之前和#DisabledTest#之后测试类的生命周期。断言#一般断言#扩展断言。假设/判断(Assumptions)。测试嵌套。测试命名回顾Sharing&attentiontothedesignphilosophy新的架构设计(这个我们后面会讲到),其重点在高扩展性方面。如果有一些神一样的测试技术(至少对于我们的Java?神),也有可能是在JUnit5的框架下实现的。不过就目前而言,所涉及的基础知识与JUnit4是非常相似的。JUnit5的变化并不激进,而是它的优化过程是细心的、小步迭代的。因此,开发人员应该对新API感到非常熟悉。至少我是这样的,相信你不会觉得奇怪:...");}@TestvoidsomeTest(){System.out.println("Runningsometest...");assertTrue(true);}@TestvoidotherTest(){assumeTrue(true);System.out.println("Runninganothertest...");assertNotEquals(1,42,"为什么这些不一样?");}@Test@DisabledvoiddisabledTest(){System.exit(1);}@AfterEachvoidtearDown(){System.out.println("拆解。..");}@AfterAllstaticvoidfreeExternalResources(){System.out.println("Freeingexternalresources...");}}对吗?这里没有太大的变化。JUnit5PreparatoryPackageVisibility也许JUnit5中最明显的变化是不再需要手动公开测试类和测试方法。包可见的访问级别就足够了。当然,私有(private)访问还是不行的。我认为这种变化是有道理的,符合我们对可见性的一般直觉。那挺好的!至少少输入几个字母。但是,我敢肯定您不会每次都手打这些字母,对吗?尽管如此,在查看测试时使用更少的关键字和更少的切换还是很好的。@TestJUnit中最基本的注解是@Test。它将方法标记为测试方法,以便构建工具和IDE可以识别和执行它们。它的API和作用没有改变,只是不再接受任何参数。测试是否有异常抛出,可以通过新的断言API进行;但是,据我所知,目前没有超时选项超时的替代品。与JUnit4一样,JUnit5为每个测试方法创建一个新实例。BeforeandAfter您可能需要在测试执行前后执行一些代码来完成一些初始化或销毁操作。在JUnit5中,有4个注释可以用于此工作:@BeforeAll只执行一次,在所有测试和@BeforeEach注释方法之前。@BeforeEach在每次测试执行之前执行。@AfterEach在每次测试执行后执行。@AfterAll仅在所有测试和@AfterEach注释方法之后执行一次。因为框架为每个测试创建一个单独的实例,所以在执行@BeforeAll/@AfterAll方法时不会创建测试实例。因此,这两个方法必须定义为静态方法。使用相同注释注释的不同方法的执行顺序是不可预测的,包括继承方法。这是开发团队经过深思熟虑后做出的决定,即将单元测试和集成测试的关注点分开。集成测试可能需要方法之间更紧密的协调,但单元测试不应依赖于其他单元测试。对集成测试(也称为场景测试)的支持也在团队的计划中。除了名称的不同,这些注解的工作方式与JUnit4中的注解完全相同。巧合的是,与主流观点一致,我也觉得这种新命名并不能使我相信它的必要性。在这个问题下有更多的讨论。残疾人考试是周五,一抬头就已经是4点30分了。你不想工作,想回家?我完全理解,只需在测试上打一个@Disabled注释即可。有良心的话,写一个忽略测试的理由是极好的,不过你也可以留下这个参数。@Test@Disabled("你就是故意跑不了?!")voidfailingTest(){assertTrue(false);}测试类的生命周期JUnit团队发布的最新版原型包含一对tests类生命周期的描述。有趣的是,此功能并未添加到alpha版本中。这种生命周期模型建议在被测类的多个测试方法中使用同一个实例,因为这样我们就可以通过改变对象的状态来实现多个测试方法中的交互。(我再说一遍,更像是场景测试。)正如我在最新版本的测试版中所说,这样的特性在99%的场景中是有害的,而在另外1%的场景中是有害的。有实际用途。只能说幸好这个功能被放弃了。想想你的单元测试,如果它们必须通过维护方法之间的状态来工作,那画面是不是太美了?断言如果@Test、@Before...、@After...等注释是测试套件的骨架,那么断言就是它的核心。在准备好测试实例并执行被测类的方法后,断言可确保您获得所需的结果。否则,当前测试失败。常规断言一般断言无非就是检查一个实例的属性(比如判断空和非空等),或者比较两个实例(比如检查两个实例对象是否相等)等等。检查时,断言方法可以接受一个字符串作为可选的最后一个参数,它将在断言失败时提供必要的描述信息。如果提供错误消息的过程很复杂,也可以将其包装在lambda表达式中,以便在真正出现故障时才真正构建消息。@TestvoidaassertWithBoolean(){assertTrue(true);assertTrue(this::truism);assertFalse(false,()->"真的"+"贵"+"消息"+".");}boolantruism(){returntrue;}@TestvoidassertWithComparison(){Listexpected=asList("element");Listactual=newLinkedList<>(expected);assertEquals(expected,actual);assertEquals(expected,actual,"Shouldbeequal.");assertEquals(expected,actual,()->"Should"+"be"+"equal.");assertNotSame(expected,actual,"显然不是同一个实例。");}如您所见,JUnit5API变化不是很多。断言方法的命名是一样的,该方法也接受两个参数,一个期望值和一个实际值。期望值和实际值的传入顺序很重要,无论是为了理解测试内容,还是失败时的错误信息,但有时候很容易出错,这是个坑.但是仔细想想,也没有更好的办法,除非自己新建一个断言框架。既然市面上有相应的产品Hamcrest(呃!)和AssertJ(耶!译者说:我不知道加油的梗在哪里)等等,浪费有限的时间造轮子显然不值得.毕竟,最重要的是确保你的断言库专注于一件事,借鉴现有的实现可以节省成本。哦,对了,失败信息现在是作为***的参数传入的。我喜欢这个细节,因为它可以让你专注于真正重要的事情——需要断言的两个价值观。作为拥抱Java8的结果,真值断言方法现在也接受供应商参数,这是另一个令人心动的小细节。扩展断言除了检查特定实例或属性的一般断言之外,还有一些其他类型的断言。这里的第一个甚至不是实际的断言,它所做的只是强制测试失败并提供失败消息。@TestvoidfailTheTest(){fail("epicly");}还有一个assertAll方法,它接受可变数量的断言作为参数,确保它们都被执行,然后报告错误信息(如果有的话)。@TestvoidassertAllProperties(){Addressaddress=newAddress("NewCity","SomeStreet","No");assertAll("地址",()->assertEquals("Neustadt",address.city),()->assertEquals("Irgendeinestra?e",address.street),()->assertEquals("Nr",address.number));}org.opentest4j.MultipleFailuresError:address(3failures)预期:但为:预期:butwas:expected:butwas:这个特性在检查一个对象的多个属性值时很有用。按照一般惯例,测试会在第一个断言失败时挂掉。这个时候只会提示第一个错误,无法知道其他值的断言是否成功,所以要重新跑测试。***,我们终于有了assertThrows和expectThrows方法。当被测方法没有抛出预期的异常时,两者都会失败。后者还会返回抛出的异常实例,以供后续验证,例如断言异常信息包含正确信息等。class,this::throwing);assertEquals("BecauseIcan!",exception.getMessage());}假设/判断(Assumptions)假设/判断允许您仅在满足某些条件时运行测试。此功能可以减少测试套件的运行时间和代码重复,尤其是在没有满足任何假设的情况下。@TestvoidexitIfFalseIsTrue(){assumeTrue(false);{assumingThat("null".equals(null),()->System.exit(1));}假设/判断适用于两种情况,要么你想在某些条件不满足时中止测试,要么是您希望仅在满足特定条件时才执行(部分)测试。主要区别在于中止的测试被报告为已禁用,因为不满足条件而没有进行任何测试。测试嵌套在JUnit5中,嵌套测试几乎毫不费力。只需要在嵌套类上加上@Nested注解,类中的所有方法都会被引擎执行:packageorg.codefx.demo.junit5;importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiterapi){count=0;}@TestvoidcountIsZero(){assertEquals(0,count);}@NestedclassCountGreaterZero{@BeforeEachvoidincreaseCount(){count++;}@TestvoidcountIsGreaterZero(){assertTrue(count>0);}@NestedclassCountMuchGreaterZero{@BeforeEachvoidincreaseCount(){count+=Integer.MAX_VALUE/2;}@TestvoidcountIsLarge(){assertTrue(count>Integer.MAX_VALUE/2);}}}}如您所见,嵌套类中的@BeforeEach(和@AfterEach)注释也可以正常工作.但是,构造顺序似乎还没有记录,它们的初始化顺序是从外到内。这也允许您额外地为内部类准备测试数据。如果嵌套内部测试想要访问外部测试类的字段,则嵌套类本身不应该是静态的。但是这样一来,静态方法的使用就被禁止了,所以@BeforeAll和@AfterAll方法在这种场景下是不能使用的(或者有没有别的方法实现?)你??可能会有疑惑,嵌套的内部测试类有什么用.就个人而言,我使用内部类来增量测试接口,其他人更多地使用它来保持测试类的简短和重点。后者也通过JUnit团队提供的一个经典示例进行了说明,该示例测试了一个堆栈:classTestingAStack{Stackstack;booleanisRun=false;@TestvoidisInstantiatedWithNew(){newStack();}@NestedclassWhenNew{@BeforeEachvoidinit(){stack=newStack();}//sometestson'stack',为空@NestedclassAfterPushing{StringanElement="anelement";@BeforeEachvoidinit(){stack.push(anElement);}//sometestson'stack',whichhasoneelement...}}}上面的例子中,stack的状态变化会反映到内部测试类中,内部类根据自己的场景执行一些测试。测试命名JUnit5提供了一个注解@DisplayName,用于为开发者提供更具可读性的测试类和测试方法信息。上面的stack测试例子加上这个注解后变成了这样:@DisplayName("Astack")classTestingAStack{@Test@DisplayName("isinstantiatedwithnewStack()")voidisInstantiatedWithNew(){/*...*/}@Nested@DisplayName("whennew")classWhenNew{@Test@DisplayName("isempty")voidisEmpty(){/*...*/}@Test@DisplayName("throwsEmptyStackExceptionwhenpopped")voidthrowsExceptionWhenPopped(){/*...*/}@Test@DisplayName("throwsEmptyStackExceptionwhenpeeked")voidthrowsExceptionWhenPeeked(){/*...*/}@Nested@DisplayName("afterpushinganelement")classAfterPushing{@Test@DisplayName("itisnolongerempty")voidisEmpty(){/*...*/}@Test@DisplayName("returnstheelementwhenpoppedandisempty")voidreturnElementWhenPopped(){/*...*/}@Test@DisplayName("returnstheelementwhenpeekedbutremainsnotempty")voidreturnElementWhenPeeked(){/*...*/}}}}这是一个TDDer看完会感动,一个BDDer看完测试结果输出会流泪。评论就到这里了,恭喜你终于看完了。我们已经快速了解了JUnit5的基本特性。现在,您应该了解编写测试所需的所有知识:包括如何为方法添加生命周期注释(@[Before|After][All|Each],如何注释测试方法本身(@Test),如何嵌套测试(@Nested),如何给测试一个好的消息(@DisplayName),你也应该能够理解断言和假设是如何工作的(与之前的版本基本相同)。但是这还没完呢!我们还没有讲到测试方法的条件执行,非常酷的参数注入,还有JUnit5的扩展机制和架构。别着急,这真的很***,这些话题我们一个月后再聊,现在可以歇歇了,敬请期待下一集!