Ciao a tutti,
sto scrivendo dei test con il framework Jersey Test per la mia api REST e sto riscontrando un problema relativo all'iniezione di un servizio in una classe di test.
Di seguito un esempio molto semplificato… Premetto che non ha un senso funzionale ma credo che ponga il focus sul problema.
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>
Ho una risorsa dove viene iniettata la classe 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;
}
}
Riesco correttamente ad eseguire i test con questa classe
@TestInstance(Lifecycle.PER_CLASS)
public class Test_NoInjection extends JerseyTest {
@Override
protected Application configure() {
return new ResourceConfig(HelloResource.class).register(new AbstractBinder() {
@Override
protected void configure() {
bind(new MyAppProperties("charles")).to(MyAppProperties.class);
}
});
}
@BeforeAll
public void before() throws Exception {
super.setUp();
}
@AfterAll
public void after() throws Exception {
super.tearDown();
}
@Test
void doTest() {
Response response = target("hello").request().get();
assertEquals(200, response.getStatus());
assertEquals("charles", response.readEntity(String.class));
}
}
Ho però l'esigenza di eseguire dei test utilizzando l'istanza della classe MyAppProperties, quindi ho provato ad utilizzare l'annotazione @Inject direttamente nella classe di test in questo modo
@Inject
MyAppProperties myAppProperties;
ma la proprietà myAppProperties risulta essere sempre null.
Da quello che ho capito cercando in rete, l'unico modo è quello di iniettare la mia classe di test nel contenitore IoC che, nel caso di Jersey (che usa HK2 come framework DI), è ServiceLocator.
Dopo varie ricerca (e imprecazioni) ho prodotto le seguenti classi. Sono una copia esatta della classe postata sopra (Test_NoInjection), con le seguenti differenze:
Nel metodo con @BeforeAll ho aggiunto l'iniezione della classe di test nel ServiceLocator
Ho definito la proprietà di tipo MyAppProperties con l'annotazione @Inject
Uso la proprietà myAppProperties per eseguire un test (come anticipato, il test non ha nessun senso logico in questo esempio ma nel mio caso reale, si)
e tra la classe Test_InjectionInTest1 e Test_InjectionInTest2 cambia solo il parametro passato all'istanza di MyAppProperties in fase di binding
bind(new MyAppProperties("charles")).to(MyAppProperties.class);
bind(new MyAppProperties("arthur")).to(MyAppProperties.class);
Di seguito le classi complete
@TestInstance(Lifecycle.PER_CLASS)
public class Test_InjectionInTest1 extends JerseyTest {
AbstractBinder binder;
@Override
protected Application configure() {
binder = new AbstractBinder() {
@Override
protected void configure() {
bind(new MyAppProperties("charles")).to(MyAppProperties.class);
}
};
return new ResourceConfig(HelloResource.class).register(binder);
}
@BeforeAll
public void before() throws Exception {
super.setUp();
ServiceLocator serviceLocator = ServiceLocatorUtilities.bind(binder);
System.out.println("[Test_InjectionInTest1] ServiceLocator: " + serviceLocator);
serviceLocator.inject(this);
}
@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("charles", responseVal);
assertEquals(responseVal, myAppProperties.getUsername());
}
}
@TestInstance(Lifecycle.PER_CLASS)
public class Test_InjectionInTest2 extends JerseyTest {
AbstractBinder binder;
@Override
protected Application configure() {
binder = new AbstractBinder() {
@Override
protected void configure() {
bind(new MyAppProperties("arthur")).to(MyAppProperties.class);
}
};
return new ResourceConfig(HelloResource.class).register(binder);
}
@BeforeAll
public void before() throws Exception {
super.setUp();
ServiceLocator serviceLocator = ServiceLocatorUtilities.bind(binder);
System.out.println("[Test_InjectionInTest2] ServiceLocator: " + serviceLocator);
serviceLocator.inject(this);
}
@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("arthur", responseVal);
assertEquals(responseVal, myAppProperties.getUsername());
}
}
Se esegue i test singolarmente, vengono eseguiti con esito positivo ma se eseguo i test insieme (con RunAs dal package o in fase di deploy con maven) la classe che viene eseguita per seconda (qualsiasi essa sia) genera l'errore sull'ultima asserzione.
In entrambe le classi, subito dopo l'assegnazione di serviceLocator, è presente un'istruzione che stampa l'istanza di ServiceLocator e restituisce il medesimo risultato.
Questo mi fa pensare che si tratti della stessa istanza di ServiceLocator. Probabilmente sono stanco (ci sto sbattendo la tesa da diversi giorni) ma la cosa che non mi spiego è come sia possibile visto il server Grizzly viene avviato e stoppato da ogni classe (come si evince dallo stamp sotto).
Ho provato ad eseguire le due classi su due porte diverse, ma niente.
L'unica soluzione che ho trovato è stata quella di dare un nome custom al service locator in una delle due classi
ServiceLocator serviceLocator = ServiceLocatorUtilities.bind("test", binder);
non ho però capito il senso della cosa…
Qualche anima pia che può farmi capire cosa sta succedendo esattamente?
Grazie mille in anticpo a tutti.