实现junit4原型
您是否听说过JUnit Lambda ? 我希望如此,因为这些家伙正在塑造JVM上测试的未来。 太夸张了吗? 也许吧,但不是很多。 这是我们最受欢迎的JUnit的下一个版本,这是迄今为止使用最广泛的Java库 。
我尝试了全新的原型,并在此介绍了我的发现。 该项目目前正在收集反馈,因此这是我们参与的机会。
总览
快速浏览该项目的背景之后,我将介绍已记录的功能。 每篇文章都会以我个人对该主题的几行结尾。 随意大声同意或不同意。
我创建了一个小的演示项目 ,其中的大多数代码示例都来自于此。 每个呈现的功能都有一个演示类,并且标头链接到这些功能。
请注意,原型尚在大量开发中,因此它是一个不断发展的目标。 因此,所有指向我的演示代码以及项目代码的链接都固定为其当前版本。 在我撰写本文和阅读本文之间,事情可能已经发生了很大变化,而这些版本可能已经过时了。 注意警告。
JUnit Lambda是由许多Java测试爱好者(包括JUnit核心提交者)组成的项目。
目标是为JVM上的开发人员端测试创建最新的基础。 这包括关注Java 8及更高版本,以及启用许多不同的测试样式。
他们从7月到10月募集了资金 ,在10月20日至22日的一次会议上开始了他们的专职工作,并于 11月18日发布了其原型 。
该项目正在收集反馈,直到11月30日,然后才能开始使用Alpha版本。 这也意味着当前版本甚至不是alpha。 在形成您的意见时,请记住这一点。
功能:
JUnit API的最重要部分是@Test
注释,这里没有任何变化:仅将带有注释的方法视为测试。
用于设置和拆除测试的久经考验的注释几乎保持不变,但具有新名称:
- 在每个测试方法之前和之后运行的
@Before
和@After
现在称为@BeforeEach
和@AfterEach
-
@BeforeClass
和@AfterClass
在类的第 一次测试之前和最后一次测试之后运行,现在称为@BeforeAll
和@AfterAll
我喜欢新名字。 它们更具意图性,因此更易于理解-特别是对于初学者。
然后是新的@Name
,可用于提供更多人类可读的名称来测试类和方法。 文档中的示例:
@Name("A special test case")
class CanHaveAnyNameTest {
@Test
@Name("A nice name, isn't it?")
void testWithANiceName() {}
}
我的测试方法名遵循的模式unitOfWork_stateUnderTest_expectedBehavior如由Roy Osherove提出的 ,我不是任何重复的那其他地方规划。 我的未经教育的猜测是,大多数关心他们的测试方法名称的开发人员的想法是相似的,而那些根本不会使用它的人。 因此,从我的角度来看,这并没有增加太多价值。
如果@Before...
, @After...
,和@Test
是一个测试套件的骨架,断言是它的心脏。 原型在这里进行了仔细的改进。
断言消息现在排在最后,可以延迟创建。 这assertTrue
和assertFalse
可以直接评估BooleanSupplier
是一个不错的好办法。
@Test
void interestingAssertions() {
String mango = "Mango";
// message comes last
assertEquals("Mango", mango, "Y U no equal?!");
// message can be created lazily
assertEquals("Mango", mango,
() -> "Expensive string, creation deferred until needed.");
// for 'assert[True|False]' it is possible
// to directly test a supplier that exists somewhere in the code
BooleanSupplier existingBooleanSupplier = () -> true;
assertTrue(existingBooleanSupplier);
}
更有趣的是捕获异常的附加功能…
@Test
void exceptionAssertions() {
IOException exception = expectThrows(
IOException.class,
() -> { throw new IOException("Something bad happened"); });
assertTrue(exception.getMessage().contains("Something bad"));
}
…并且可以将断言分组以一次测试它们。
@Test
void groupedAssertions() {
assertAll("Multiplication",
() -> assertEquals(15, 3 * 5, "3 x 5 = 15"),
// this fails on purpose to see what the message looks like
() -> assertEquals(15, 5 + 3, "5 x 3 = 15")
);
}
请注意该组如何具有名称(在本例中为“乘法”),并且所包含的断言以lambda形式给出,以延迟执行。
一般而言,当谈到断言时,我总是重视JUnit的可扩展性。 这使我可以忽略内置断言和隐秘的杰作,即Hamcrest赞成AssertJ 。 因此,我对此没有任何实际意见,只是这些更改似乎会稍微改善一下情况。
您可能已经发现了它:测试类和方法不再必须公开。 我认为这是个好消息! 一个没用的关键字少了。
尽管程序包可见性足以运行,但私有方法仍将被忽略。 这是一个非常明智的决定,与最终采用可视性的方式一致。
JUnit 4始终为每个测试方法创建测试类的新实例。 这样可以最大程度地减少各个测试彼此巧妙地交互以及彼此不必要地相互依赖的机会。
该原型包含一个新的注释@TestInstance
,它指定测试类实例的生命周期。 可以按照测试方法(默认行为)创建它们,也可以针对所有测试创建一次:
@TestInstance(Lifecycle.PER_CLASS)
class _2_PerClassLifecycle {
/** There are two test methods, so the value is 2. */
private static final int EXPECTED_TEST_METHOD_COUNT = 2;
/** Is incremented by every test method
AND THE STATE IS KEPT ACROSS METHODS! */
private int executedTestMethodCount = Integer.MIN_VALUE;
// Note that the following @[Before|After]All methods are _not_ static!
// They don't have to be because this test class has a lifecycle PER_CLASS.
@BeforeAll
void initializeCounter() {
executedTestMethodCount = 0;
}
@AfterAll
void assertAllMethodsExecuted() {
assertEquals(EXPECTED_TEST_METHOD_COUNT, executedTestMethodCount);
}
@Test
void oneMethod() { executedTestMethodCount++; }
@Test
void otherMethod() { executedTestMethodCount++; }
}
我认为这是功能的典型案例,在99%的案例中是有害的,而在其他1%的案例中则是必不可少的。 老实说,我担心没有经验的开发人员会在测试套件中散布这种测试间的依赖关系,因此它看起来会是什么样。 但是让这样的开发人员在没有检查和平衡的情况下做他们想做的事情,例如结对编程和代码审查本身就是一个问题,因此拥有每个类的生命周期不会使情况变得更糟。
你怎么看? 运送还是报废?
有些人在他们的测试套件中使用内部类。 我这样做是为了继承接口测试 ,其他人则使他们的测试类保持较小 。 为了使它们在JUnit 4中运行,您必须使用JUnit的@Suite
或NitorCreations更为优雅的NestedRunner
。 仍然你必须做点什么 。
使用JUnit Lambda不再需要! 在以下示例中,将执行所有打印方法:
class _4_InnerClasses {
@Nested
class InnerClass {
@Test
void someTestMethod() { print("Greetings!"); }
}
@Nested
static class StaticClass {
@Test
void someTestMethod() { print("Greetings!"); }
}
class UnannotatedInnerClass {
@Test
void someTestMethod() { throw new AssertionError(); }
}
static class UnannotatedStaticClass {
@Test
void someTestMethod() { print("Greetings!"); }
}
}
新的注解@Nested
指导JUnit和读者将测试理解为更大套件的一部分。 原型代码库的一个示例很好地说明了这一点。
在当前版本中,还需要在非静态内部类中触发测试的执行,但这似乎是巧合 。 尽管文档不鼓励在静态类上使用,但我猜是因为它与每个类的生命周期交互不良,但这不会导致异常。
假设利用lambda表达式的功能得到了很好的补充:
@Test
void assumeThat_trueAndFalse() {
assumingThat(true, () -> executedTestMethodCount++);
assumingThat(false, () -> {
String message = "If you can see this, 'assumeFalse(true)' passed, "
+ "which it obviously shouldn't.";
throw new AssertionError(message);
});
}
我想我从来没有使用过假设,那么我能说什么呢? 看起来不错。 :)
当JUnit Lambda检查类或方法(或其他任何东西)是否有注释时,它还会查看注释的注释等。 然后,它将在搜索过程中发现的所有注释视为直接位于所检查的类或方法上。 这是模拟注释继承的艰巨但常见的方法, Java不直接支持这种方法 。
我们可以使用它轻松创建自定义注释:
/**
* We define a custom annotation that:
* - stands in for '@Test' so that the method gets executed
* - gives it a default name
* - has the tag "integration" so we can filter by that,
* e.g. when running tests from the command line
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Test
@Name("Evil integration test! ")
@Tag("integration")
public @interface IntegrationTest { }
然后我们可以像这样使用它:
@IntegrationTest
void runsWithCustomAnnotation() {
// this is run even though 'IntegrationTest' is not defined by JUnit
}
这真是整洁! 具有重要意义的简单功能。
现在变得非常有趣! JUnit Lambda引入了conditions(条件)的概念,该条件允许创建定制批注,以决定是否应跳过测试。 决定是否运行特定的测试方法如下:
- 运行程序在测试方法上查找本身以
@Conditional (Class<? extends Condition> condition)
进行注释的任何注释@Conditional (Class<? extends Condition> condition)
- 它创建一个
condition
实例和一个TestExecutionContext
,其中包含有关当前测试的大量信息 - 它使用上下文调用条件的
evaluate
方法 - 根据调用的返回值,它决定是否运行测试
JUnit附带的一个条件是@Disabled
,它代替了@Ignore
。 它的DisabledCondition
仅检查方法或类上是否存在注释,并创建匹配的消息。
让我们做一些更有趣,更有用的事情:如果星期五星期五,我们将创建一个注释,跳过测试。 对于那些威胁您周末的棘手问题。
让我们从日期检查开始:
static boolean itsFridayAfternoon() {
LocalDateTime now = LocalDateTime.now();
return now.getDayOfWeek() == DayOfWeek.FRIDAY
&& 13 <= now.getHour() && now.getHour() <= 18;
}
现在,我们创建评估测试的Condition
实现:
class NotFridayCondition implements Condition {
@Override
public Result evaluate(TestExecutionContext testExecutionContext) {
return itsFridayAfternoon()
? Result.failure("It's Friday afternoon!")
: Result.success("Just a regular day...");
}
}
我们可以看到我们根本不需要TestExecutionContext
,只需检查它是否是星期五下午。 如果是,则结果为失败(命名错误),因此将跳过测试。
现在,我们可以将此类传递给@Conditional
并定义我们的注释:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional({NotFridayCondition.class})
public @interface DontRunOnFridayAfternoon { }
为了摆脱那讨厌的考验,骑上星期五下午的日落:
@Test
@DontRunOnFridayAfternoon
void neverRunsOnFridayAfternoon() {
assertFalse(itsFridayAfternoon());
}
非常好。 很棒的功能!
最后但并非最不重要的一点是,它支持将实例注入测试。
这是通过简单地将所需实例声明为测试方法参数来完成的。 对于每个参数,JUnit然后将查找支持其类型的MethodParameterResolver
。 此类解析器可以与JUnit一起提供,也可以在测试类的新@ExtendWith
批注中列出。
如果JUnit找到合适的解析器,它将使用它来创建参数的实例。 否则,测试将失败。
一个简单的示例是@TestName
,如果在字符串上使用,它将注入测试的名称:
@Test
void injectsTestName(@TestName String testName) {
// '@TestName' comes with JUnit.
assertEquals("injectsTestName", testName);
}
创建解析器很容易。 假设有一个Server
类,我们需要为不同的测试进行预配置。 这样做就像编写此类一样简单:
public class ServerParameterResolver implements MethodParameterResolver {
@Override
public boolean supports(Parameter parameter) {
// support all parameters of type 'Server'
return parameter.getType().equals(Server.class);
}
@Override
public Object resolve(Parameter parameter, TestExecutionContext context)
throws ParameterResolutionException {
return new Server("http://codefx.org");
}
}
假设测试类使用@ExtendWith ( {ServerParameterResolver.class} )
注释,则解析器初始化Server
类型的任何参数:
@Test
void injectsServer(Server server) {
int statusCode = server.sendRequest("gimme!");
assertEquals(200, statusCode);
}
让我们看一个稍微复杂的例子。
假设我们要为应该包含电子邮件地址的字符串提供一个解析器。 现在,与上述类似,我们可以编写一个StringParameterResolver
但是它将用于所有我们不想要的字符串。 我们需要一种方法来识别那些应该包含地址的字符串。 为此,我们引入了一个注释…
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface EMailParameter { }
…我们用来限制支持的参数。
public class Resolver implements MethodParameterResolver {
@Override
public boolean supports(Parameter parameter) {
// support strings annotated with '@EmailParameter'
return parameter.getType().equals(String.class)
&& isAnnotated(parameter, EMailParameter.class);
}
@Override
public Object resolve(Parameter parameter, TestExecutionContext context)
throws ParameterResolutionException {
return "nipa@codefx.org";
}
}
有道理吧? 现在我们可以像@TestName
一样使用它:
@Test
void injectsEMailAddress(@EMailParameter String eMail) {
assertTrue(eMail.contains("@"));
}
我是说我喜欢这个功能吗? 我做。 实际上,我认为它很棒! 我确信第三方测试库可以充分利用这一点。
但是我不知道supports
人员是否也应该收到TestExecutionContext
以进行更细粒度的决策。
可扩展性
该项目列出了几个核心原则 ,其中之一是“优先于功能而不是扩展点”。 这是一个很棒的原则,我认为尤其是我们讨论的最后一个功能很好地遵循了这一原则。
创建自定义批注,条件和进样的能力以及将它们视为与库一起提供的功能一样,确实很棒。 我确信这将导致第三方测试库中有趣的创新。
这仍然有点不清楚(至少对我而言)。 尽管该API支持lambda,但似乎大多数情况下不需要Java 8的类型或功能就可以做到这一点。 还考虑避免在JUnit中使用Java 8功能,以便可以针对较早版本编译该项目。 如果是这样,则可以在未升级或无法升级的环境(例如Android)中使用。
该项目专用于一个单独的页面来解决此重要主题。
取而代之的是,JUnit 5通过JUnit 4测试引擎提供了平缓的迁移路径,该引擎允许使用JUnit 5基础结构执行基于JUnit 4的现有测试。 由于特定于JUnit 5的所有类和注释都位于新的org.junit.gen5基本包下,因此在类路径中同时包含JUnit 4和JUnit 5不会导致任何冲突。 因此,与JUnit 5测试一起维护现有的JUnit 4测试是安全的。
我们已经看到了原型的基本特征:
- 设置,测试,拆卸:更好的命名
- 断言:略有改进
- 公开程度:不再
public
! - 生命周期:每个类的生命周期保持测试之间的状态
- 内部类:直接支持内部类中的测试
- 假设:略有改善
- 自定义注释:启用完全兼容的自定义注释
- 条件:可以跳过测试
- 注入:支持通过测试参数注入实例
特别是最后一项显示了可扩展性的核心原理。 我们还讨论了对迁移兼容性的关注,这将允许项目使用JUnit 4和JUnit 5执行测试。
通过所有这些,您准备好对细节发表意见,并(重要的一步!)向为我们做这件事的伟大人士提供反馈:
顺便说一句,如果您要在Twitter上关注我 ,那么自上周五以来您将了解其中的大部分内容。
是否希望看到@junitlambda原型的实际效果? 我做了一个演示项目,展示了所有新(旧)功能: https : //t.co/Csyx1GLY5J
— Nicolai Parlog(@nipafx) 2015年11月20日
只是说...
该帖子最初发布在codefx.org上 。
翻译自: https://jaxenter.com/junit-lambda-the-prototype-122583.html
实现junit4原型