uthomas
uthomas

Reputation: 754

CDI instantiate and inject parent-child type beans is either ambigious or child types is not found

I have an interface like:

public interface DateTimeService {
    ZonedDateTime now();

    void fixTo(ZonedDateTime date);

    void forget();
}

and I have two implementations of it. One for production where fixTo and forget throws exceptions and one for testing where we control time. Then I have a CDI config that depending on a flag instantiate the right type.

@ApplicationScoped
public class Configuration {
    @Produces
    @ApplicationScoped
    public DateTimeService dateTimeService(Configuration config) {
        if (config.isFakeDateTimeServiceEnabled()) {
            return new FakeDateTimeService();
        } else {
            return new DefaultDateTimeService();
        }
    }
}

However, I wanted to remove fixTo and forget from DateTimeService as they are only there so we can control time in tests. I made a new interface like:

public interface FakeDateTimeService extends DateTimeService {
    // fixto and forget is moved from DateTimeService to here
}

I have a few injection points. Some is in production code, some is in test code. In production code I would like to only be able to access DateTimeSerice, in test code I want be able to get a handle on the extended service.

In prod code:

    @Inject
    private DateTimeService dateTimeService;

In test code:

    @Inject
    private FakeDateTimeService dateTimeService;

If I leave the config unchanged, then the test code will never find my extended service (as CDI seems to ignore the run-time type of the instance produced by the producer method).

If I update the config to instantiate both (in this case I can even inject the genuine service into the fake one), then production cannot wire together as fake also implements the DateTimeService interface and it causes ambiguity. At this point I could probably just use a qualifier, but I neither want to change my production code because of this nor have to have a fake date time service exist in production context.

I tried to veto bean creation but I failed to do so.

What I thought would/should work is instantiate the right type and programmatically add it to the bean context but examples I found was for CDI 2.0 while for now, relevant part of my code is stuck on CDI 1.2.

Probably at this point you can tell, that I'm not a CDI expert. So I'm opened for suggestion on good CDI read materials as well as concrete suggestions on this problem.

Otherwise, I'm about to give up and just live with a DateTimeService that have fixTo and forget methods.

Upvotes: 0

Views: 445

Answers (1)

Thomas Herzog
Thomas Herzog

Reputation: 506

Depending on how you are doing it, you could use the following approaches, whereby some of them have already be mentioned in the comments.

With alternatives

For testing you provide an own beans.xml, which defines the alternative for testing, which replaces the production one. You will have to keep them in sync, whereby the test beans.xml contains changes necessary for testing. Depending on the CDI implementation the beans.xml can be left out and @Alternative will be enough.

https://docs.oracle.com/javaee/6/tutorial/doc/gjsdf.html

interface DateTimeService  { ... }

// Active during production, beans.xml does not define alternative
class DefaultDateTimeService implements DateTimeService  { ... }

// Active during test, beans.xml defines alternative
@Alternative
class FakeDateTimeService implements DateTimeService { ... }

// Injection point stays the same.
@Inject
DateTimeService dateTimeService;

// Used in test environment
<beans ... >
    <alternatives>
        <class>FakeDateTimeService</class>
    </alternatives>
</beans>

With Arquillian

With Aarquillian you can assemble the deployment yourself and exclude the DefaultDateTimeService implementation and replace it with the FakeDateTimeService implementation. With this approach you don't need to hack anything because during test, only FakeDateTimeService implementation will be visible to the CDI container.

http://arquillian.org/guides/getting_started/

With CDI Extension

For testing you could provide an CDI extension and the FakeDateTimeService implementation in the test code, whereby the CDI Extension does veto the DefaultDateTimeService implementation.

package test.extension

public class VetoExtension implements Extension {

    public <T> void disableBeans(@Observes ProcessAnnotatedType<T> pat) {
        // type matching
        if (DefaultDateTimeService.class.isAssignableFrom(pat.getAnnotatedType().getJavaClass())) {
            pat.veto();
        }
    }
}

// Define SPI provider file, assuming test/resources is on CDI classpath
src/test/resources/META-INF/services/javax.enterprise.inject.spi.Extension
// Single line you put in there
test.extension.VetoExtension 

Deltaspike @Exclude

Deltaspike is a CDI extension library which provides some useful utilities for CDI development and testing, also a CDITestRunner is provided. The @Exclude annotation is used by an extension which does the same as the example above but already implemented for you.

https://deltaspike.apache.org/

https://deltaspike.apache.org/documentation/test-control.html

https://deltaspike.apache.org/documentation/projectstage.html

You should choose an approach which doesn't pollute your source code and forces you to hack to much to get it running for testing.

I would prefer Arquillian because here you have full control about what is in the deployment and nothing in your production code has to change or be specially implemented so its testable.

With Deltaspike you can exclude production code for testing and replace them with test implementations.

If no additional library or framework can be used I would prefer the alternative approach, because its the least complicated one to use.

Upvotes: 0

Related Questions