在自动化测试中使用 JFR 事件流 API - Sip of Java

JDK Flight Recorder (JFR) 是一种性能分析和诊断工具,长期以来一直是 JDK 的一部分。在大部分历史中,JFR 只能用于部署的应用程序,以提供对其性能的洞察。随着 JDK 14(JEP 349)的发布,添加了 JFR 事件流 API,它提供了一种实时查看 Java 应用程序内部发生情况的方法。本文将探讨如何在自动化测试中利用 JFR 事件流 API。

在自动化测试中使用事件流 API

以下是使用 JFR 事件流在自动化测试中的完整代码示例。我们将在下面逐步介绍此代码示例的关键部分。

import jdk.jfr.Event;
import jdk.jfr.Name;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordingStream;

public class TestSampleApplication {

	@Name("TestEvent")
	class TestEvent extends Event {

	}

	@Test
	public void testDoStuff() throws Exception {
		SampleApplication sampleApplication = new SampleApplication();
		List<RecordedEvent> recordedEvents = new ArrayList<>();
		try (RecordingStream rs = new RecordingStream();) {
			rs.enable("jdk.FileRead");
			rs.onEvent("TestEvent", e -> rs.close());
			rs.onEvent("jdk.FileRead", recordedEvents::add);
			rs.startAsync();
			TestEvent testEvent = new TestEvent();
			sampleApplication.doStuff();
			testEvent.commit();
			rs.awaitTermination();
		}
		
		assertFalse(recordedEvents.isEmpty());
		for (RecordedEvent recordedEvent : recordedEvents) {
			if(recordedEvent.getValue("path").equals("/path/to/your/file")) {
				return;
			}
		}
		fail("A readFile event wasn't found!");
	}
}

初始化事件流

为了配置和捕获在自动化测试期间触发的事件,我将使用 RecordingStreamRecordingStream 实现 AutoCloseable,因此我们可以在 try-with-resources 块中声明它,并允许 JVM 确保在完成时关闭和清理流。

try (RecordingStream rs = new RecordingStream();) {
	...
}

启用事件

默认情况下不会启用内置 JFR 事件,例如 jdk.FileRead,因此需要使用 rs.enable(String) 手动启用它们。自定义事件,例如 TestEvent,默认情况下是启用的,不需要手动启用。

@Name("TestEvent")
class TestEvent extends Event {

}
...
try (RecordingStream rs = new RecordingStream();) {
	...
	rs.enable("jdk.FileRead");
	...
}

还可以通过在初始化时将设置文件传递给 RecordingStream 来配置事件,并在其中启用相应的事件,如下面的示例所示

Configuration c = Configuration.create(Path.of("/my/config/file"));
try (RecordingStream rs = new RecordingStream(c);) { 
	...
}

配置事件行为

可以使用 rs.onEvent(String, Runnable) 配置触发事件时应发生的操作。在下面的代码中,TestEvent 用于关闭流,稍后会详细介绍,当 jdk.FileRead 事件发生时,它将被写入 List 以供稍后检查。

rs.onEvent("TestEvent", e -> rs.close());
rs.onEvent("jdk.FileRead", recordedEvents::add);

启动流、运行测试和关闭流

rs.startAsync() 用于在单独的线程中启动 JFR 事件流。要测试的代码(从文件进行简单读取)使用 sampleApplication.doStuff(); 执行。

代码示例中最奇怪的部分是处理流的关闭。这是必要的,因为事件大约每秒刷新到 JFR 流一次,这通常意味着某些或所有事件没有写入流,导致测试结果不准确或不一致。可以使用 Thread.sleep(long) 来强制执行此操作,但在大型测试套件中,添加大量等待可能会开始对执行测试套件所需的时间产生重大影响。

TestEventrs.onEvent("TestEvent", e -> rs.close());testEvent.commit(); 以及 rs.awaitTermination() 提供了一种方法,可以在所有相关事件刷新到流中后立即关闭流。 testEvent.commit(); 将在测试代码执行完毕后触发 rs.onEvent("TestEvent", e -> rs.close());,而 rs.awaitTermination() 会阻止线程执行,直到线程关闭。这最大限度地减少了测试中的任何额外等待时间。或者,可以使用 JFRUnit 等库,请务必查看“其他阅读材料”部分以获取链接。

断言行为

当然,自动化测试如何处理断言被测试代码将高度依赖于上下文。此代码示例演示了如何将事件捕获到 List 中以供稍后检查。这只是一个示例,但还有许多其他有效的方法可以使用 JFR 编写自动化测试。

assertFalse(recordedEvents.isEmpty());
for (RecordedEvent recordedEvent : recordedEvents) {
	if(recordedEvent.getValue("path").equals("/path/to/your/file")) {
		return;
	}
}
fail("A readFile event wasn't found!");

其他阅读材料

使用 Flight Recorder 事件流 API 监控事件

JEP 349

JFRUnit

祝您编码愉快!