Charles
Charles

Reputation: 65

Inject dependencies into unit test with ServiceLocator

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:

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()) enter image description here

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. enter image description here

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

Answers (0)

Related Questions