我正在使用 Jersey Test 框架为 REST API 编写测试,但在将服务注入测试类时遇到问题。
这是一个非常简化的示例...首先我会说它没有任何功能意义,但我认为它重点关注问题。
pom.xml
<properties>
<jersey.version>2.25.1</jersey.version>
<junit.version>5.11.2</junit.version>
</properties>
<dependencies>
<!-- Jersey -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Jersey Test -->
<dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-core</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
我有一个资源,其中注入了
MyAppProperties
类
@Path("/hello")
public class HelloResource {
@Inject
MyAppProperties myAppProperties;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayJsonHello() {
return myAppProperties.getUsername();
}
}
public class MyAppProperties {
private final String username;
public String getUsername() {return username;}
public MyAppProperties(String username) {
this.username = username;
}
}
我的目标是使用不同的用户名测试
HelloResource
并注入 MyAppProperties
来运行其他测试,但使用 @Inject MyAppProperties myAppProperties
,myAppProperties
始终为空。
@TestInstance(Lifecycle.PER_CLASS)
public abstract class AbstractInjectionInTest extends JerseyTest {
AbstractBinder binder;
public AbstractInjectionInTest() {
ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
ServiceLocator locator = factory.create(null);
ServiceLocatorUtilities.bind(locator, binder);
}
protected abstract String getUsername();
@Override
protected Application configure() {
binder = new AbstractBinder() {
@Override
protected void configure() {
bind(new MyAppProperties(getUsername())).to(MyAppProperties.class);
}
};
return new ResourceConfig(HelloResource.class).register(binder);
}
@BeforeAll
public void before() throws Exception {
super.setUp();
}
@AfterAll
public void after() throws Exception {
super.tearDown();
}
@Inject
MyAppProperties myAppProperties;
@Test
void doTest() {
Response response = target("hello").request().get();
assertEquals(200, response.getStatus());
String responseVal = response.readEntity(String.class);
assertEquals(getUsername(), responseVal);
assertEquals(responseVal, myAppProperties.getUsername()); // myAppProperties is null
}
}
public class InjectInTest1 extends AbstractInjectInTest {
@Override
protected String getUsername() {
return "charles";
}
}
根据我对这个答案和这个的理解,唯一的方法是将我的测试类注入到IoC容器中,对于Jersey(使用HK2作为DI框架)来说,是
ServiceLocator
.
然后我对
AbstractInjectInTest
类进行了这些更改:
AbstractBinder binder
字段configure
方法(设置我也将在服务定位器绑定中使用的 binder
字段)public abstract class AbstractInjectInTest extends JerseyTest {
private AbstractBinder binder;
// ...
public AbstractInjectInTest() {
ServiceLocator serviceLocator = ServiceLocatorUtilities.bind(this.binder);
System.out.println("[" + this.getClass().getSimpleName() + "] ServiceLocator: " + serviceLocator);
serviceLocator.inject(this);
}
// ...
@Override
protected Application configure() {
this.binder = new AbstractBinder() {
@Override
protected void configure() {
bind(new MyAppProperties(getUsername())).to(MyAppProperties.class);
}
};
return new ResourceConfig(HelloResource.class).register(this.binder);
}
}
我还添加了第二个测试类来测试不同的用户名
public class InjectInTest2 extends AbstractInjectInTest {
@Override
protected String getUsername() {
return "arthur";
}
}
这两类测试在单独运行时都可以工作,但是如果我一起运行测试(使用包中的 RunAs 或在使用 maven 安装期间),第二个执行的类(无论是哪个)都会在最后一个断言上抛出错误:
assertEquals(responseVal, myAppProperties.getUsername())
在图像中,您可以看到
InjectInTest2
类最后执行,并且断言失败,因为 myAppProperties.getUsername()
返回“charles”而不是“arthur”(“charles”是在 InjectInTest1
类中绑定的用户名)。
问题是由于两个类使用相同的 ServiceLocator 实例造成的。
我可能很累(我已经为此绞尽脑汁好几天了),但我不明白的是,既然 Grizzly 是由每个类启动和停止的,那么他们怎么可能共享服务定位器的实例呢? ?
我也找到了解决方案,就是给服务定位器绑定自定义名称
public abstract class AbstractInjectInTest extends JerseyTest {
// ...
public AbstractInjectInTest() {
ServiceLocator serviceLocator = ServiceLocatorUtilities.bind(this.getClass().getSimpleName(), this.binder);
System.out.println("[" + this.getClass().getSimpleName() + "] ServiceLocator: " + serviceLocator);
serviceLocator.inject(this);
}
// ...
}
或使用
ServiceLocator
获得 ServiceLocatorFactory.getInstance().create(null)
(而不是 ServiceLocatorUtilities.bind
)
public abstract class AbstractInjectInTest extends JerseyTest {
// ...
public AbstractInjectInTest() {
ServiceLocator serviceLocator = ServiceLocatorFactory.getInstance().create(null);
ServiceLocatorUtilities.bind(serviceLocator, binder);
System.out.println("[" + this.getClass().getSimpleName() + "] ServiceLocator: " + serviceLocator);
serviceLocator.inject(this);
}
// ...
}
但我想了解为什么在第一种情况下两个类指向服务定位器的同一个实例。
提前感谢大家
出现此问题的原因是 Jersey 使用的 HK2 默认情况下在同一 JVM 会话中重用 ServiceLocator 实例。顺序运行测试时,ServiceLocatorFactory.getInstance().create(null) 将返回共享实例,除非您指定唯一标识符。当不同的测试类尝试将配置绑定到同一个 ServiceLocator 时,这可能会导致绑定重叠。
解决方案
为了确保每个测试都有一个独立的 ServiceLocator,您有两个选择:
1. Use a Unique Name for Each Test Class: This forces HK2 to create a new ServiceLocator for each test class, preventing conflicts.
公共抽象类 AbstractInjectInTest 扩展 JerseyTest { 私有 AbstractBinder 活页夹;
public AbstractInjectInTest() {
ServiceLocator serviceLocator = ServiceLocatorUtilities.bind(this.getClass().getSimpleName(), this.binder); // Unique name
serviceLocator.inject(this);
}
@Override
protected Application configure() {
this.binder = new AbstractBinder() {
@Override
protected void configure() {
bind(new MyAppProperties(getUsername())).to(MyAppProperties.class);
}
};
return new ResourceConfig(HelloResource.class).register(this.binder);
}
protected abstract String getUsername();
}
2. Create a New ServiceLocator Instance Directly: If you don’t need the name, you can ensure a unique instance with ServiceLocatorFactory.getInstance().create(null).
公共抽象类 AbstractInjectInTest 扩展 JerseyTest { 私有 AbstractBinder 活页夹;
public AbstractInjectInTest() {
ServiceLocator serviceLocator = ServiceLocatorFactory.getInstance().create(null); // Ensures a new instance
ServiceLocatorUtilities.bind(serviceLocator, binder);
serviceLocator.inject(this);
}
@Override
protected Application configure() {
this.binder = new AbstractBinder() {
@Override
protected void configure() {
bind(new MyAppProperties(getUsername())).to(MyAppProperties.class);
}
};
return new ResourceConfig(HelloResource.class).register(this.binder);
}
protected abstract String getUsername();
}
说明
使用唯一名称或显式请求新的 ServiceLocator 实例可防止 HK2 的缓存机制重用 ServiceLocator 实例,从而避免不同测试类之间共享状态并允许每个测试拥有其独立的配置。
此解释解决了问题的核心,并提供了具体的代码示例以清晰起见。如果您需要进一步的帮助,请告诉我!