Reputation: 65
I'm writing tests with the Jersey Test framework for my REST API and I'm having a problem with injecting a service into a test class.
Here's a very simplified example... I'll start by saying that it doesn't make any functional sense but I think it focuses on the problem.
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>
I have a resource where the MyAppProperties
class is injected
@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;
}
}
My goal is to test the HelloResource
with different usernames and inject MyAppProperties
to run additional tests, but using the @Inject MyAppProperties myAppProperties
, myAppProperties
is always null.
@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";
}
}
From what I understand from this answer and this one, the only way is to inject my test class into the IoC container which, in the case of Jersey (which uses HK2 as a DI framework), is ServiceLocator
.
I then made these changes to the AbstractInjectInTest
class:
AbstractBinder binder
fieldconfigure
method (set the binder
field that I will also use in the service locator binding)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);
}
}
I also added a second test class to test a different username
public class InjectInTest2 extends AbstractInjectInTest {
@Override
protected String getUsername() {
return "arthur";
}
}
Both classes of tests work when run individually, but if I run the tests together (with RunAs from the package or during install with maven) the class that is executed second (whichever it is) throws the error on the last assertion: assertEquals(responseVal, myAppProperties.getUsername())
In the image you can see that the InjectInTest2
class was executed last and the assertion fails because myAppProperties.getUsername()
returns "charles" instead of "arthur" ("charles" is the username binded in the InjectInTest1
class).
The problem is due to the fact that both classes use the same instance of ServiceLocator.
I'm probably tired (I've been banging my head against this for several days) but the thing I don't understand is how is it possible that they share the instance of service locator since Grizzly is started and stopped by each class?
I also found the solution, which is to bind the service locator with a custom name
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);
}
// ...
}
or get ServiceLocator
with ServiceLocatorFactory.getInstance().create(null)
(instead of 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);
}
// ...
}
but I would like to understand why in the first case the two classes pointed to the same instance of service locator.
Thanks to everyone in advance
Upvotes: 0
Views: 29