Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cdi2,Jakarta Cdi] Add step definitions as beans when not discovered #2248

Merged
merged 34 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
377f828
[Cdi2] Do not add step definitions to the synthetic bean archive
mpkorstanje Feb 27, 2021
179c3fd
Revert version number
mpkorstanje Feb 27, 2021
f2fa54a
Bump weld version
mpkorstanje Feb 27, 2021
6263146
[Jakarta Cdi] Correctly cast the UnmanagedInstance values
mpkorstanje Feb 27, 2021
3c792fb
Merge branch 'jakarta-cdi-unmanaged' into cdi2-weld
mpkorstanje Feb 27, 2021
61a2df4
Port stuff to jakarta
mpkorstanje Feb 27, 2021
0754285
[Jakarta Cdi] Correctly cast the UnmanagedInstance values
mpkorstanje Feb 27, 2021
80ea1fb
Merge branch 'jakarta-cdi-unmanaged' into cdi2-weld
mpkorstanje Feb 27, 2021
785ec1f
Add to CI
mpkorstanje Feb 27, 2021
31395a7
Naming stuff
mpkorstanje Feb 27, 2021
2c6fc42
fix typo, removed unnecessary dependency, renamed test property file
dcendents Feb 28, 2021
f6ed48b
Merge remote-tracking branch 'origin/main' into cdi2-weld
mpkorstanje Feb 28, 2021
79efff1
Remove unnecessary dependency
mpkorstanje Feb 28, 2021
8892693
Merge branch 'cdi2-weld' of github.com:cucumber/cucumber-jvm into cdi…
mpkorstanje Feb 28, 2021
949c39a
Add junit-platform.properties
mpkorstanje Feb 28, 2021
40ef3f0
run some tests without a local beans.xml file
dcendents Mar 3, 2021
bbaa990
run features without local beans.xml file
dcendents Mar 3, 2021
6f3b33e
default to weld as it is the cdi reference implementation
dcendents Mar 3, 2021
da701f8
fix build
dcendents Mar 3, 2021
cac3566
add unmanaged beans after bean discovery using CDI extension
dcendents Mar 3, 2021
d11bf3e
apply changes to jakarta-cdi
dcendents Mar 4, 2021
3e9b424
[Cdi2/Jakarta Cdi] Run tests for OpenWebBeans and Weld in CI (#2251)
dcendents Mar 6, 2021
c0b2c48
Merge remote-tracking branch 'origin/main' into cdi2-weld
mpkorstanje Mar 6, 2021
d6e4840
Nits
mpkorstanje Mar 6, 2021
9605331
Warn when unmanaged bean could not be added
mpkorstanje Mar 6, 2021
e0b7e7d
Spotless
mpkorstanje Mar 6, 2021
f9ea8e2
Suggest solution in log message
mpkorstanje Mar 6, 2021
d5266e7
support unmanaged parameterized beans
dcendents Mar 6, 2021
dfbe663
support multiple parameterized types for the same raw type bean
dcendents Mar 6, 2021
5aed18f
revert processing of injection points
dcendents Mar 7, 2021
f6f4c7d
Update documentation
mpkorstanje Mar 7, 2021
01e200a
Update changelog
mpkorstanje Mar 7, 2021
cc676c4
Update changelog
mpkorstanje Mar 7, 2021
c040705
Update readme
mpkorstanje Mar 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
run: mvn install -DskipTests=true -DskipITs=true -Darchetype.test.skip=true -Dmaven.javadoc.skip=true -B -V --toolchains .github/workflows/.toolchains.xml
- name: test
run: mvn verify -P-spotless-apply --toolchains .github/workflows/.toolchains.xml
- name: test cdi2-weld
run: cd cdi2 && mvn verify -Pcdi2-weld --toolchains ../.github/workflows/.toolchains.xml

javadoc:
name: 'Javadoc'
Expand Down
2 changes: 1 addition & 1 deletion cdi2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ And for Weld it is:
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>3.1.1.Final</version>
<version>4.0.0.Final</version>
mpkorstanje marked this conversation as resolved.
Show resolved Hide resolved
<scope>test</scope>
</dependency>
```
Expand Down
21 changes: 7 additions & 14 deletions cdi2/src/main/java/io/cucumber/cdi2/Cdi2Factory.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,41 @@
public final class Cdi2Factory implements ObjectFactory {

private final Map<Class<?>, Unmanaged.UnmanagedInstance<?>> standaloneInstances = new HashMap<>();
private SeContainerInitializer initializer;
private SeContainer container;

@Override
public void start() {
container = getInitializer().initialize();
if (container == null) {
SeContainerInitializer initializer = SeContainerInitializer.newInstance();
container = initializer.initialize();
}
}

@Override
public void stop() {
if (container != null) {
container.close();
container = null;
initializer = null;
}
for (final Unmanaged.UnmanagedInstance<?> unmanaged : standaloneInstances.values()) {
for (Unmanaged.UnmanagedInstance<?> unmanaged : standaloneInstances.values()) {
unmanaged.preDestroy();
unmanaged.dispose();
}
standaloneInstances.clear();
}

private SeContainerInitializer getInitializer() {
if (initializer == null) {
initializer = SeContainerInitializer.newInstance();
}
return initializer;
}

@Override
public boolean addClass(final Class<?> clazz) {
getInitializer().addBeanClasses(clazz);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropping this line can break some step definition (anyone used as injection in other steps or using cdi some extensions for their injections) so not sure it is desired

Copy link
Contributor Author

@mpkorstanje mpkorstanje Feb 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a test for this. I could only break it when I don't include beans.xml. Do you know other ways to break it?

@Test
void shouldInjectStepDefinitions() {
factory.addClass(OtherStepDefinitions.class);
factory.addClass(StepDefinitions.class);
factory.start();
StepDefinitions stepDefinitions = factory.getInstance(StepDefinitions.class);
assertThat(stepDefinitions.injected, is(notNullValue()));
factory.stop();
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A way to ensure it is as expected is to write a CDI extension which veto all undesired managed types and add the desired ones (bm.addAnnotatedType(bm.createAnnotatedType(MyType.class))), this way whatever the scanning is, the test is "as expected".

Copy link
Contributor

@dcendents dcendents Feb 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The steps inject the local test bean Belly so a beans.xml will always be required.
To properly test it we would need a project that injects beans from the main code or from libraries only.

Here is a quick test project without a test beans.xml: https://github.com/dcendents/cucumber-cdi2-test
It works fine, getInstance creates UnmanagedInstances, beans are injected.

Copy link
Contributor

@dcendents dcendents Feb 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought of something, what about calling clazz.getClassLoader().getResourceAsStream("META-INF/beans.xml"); and add the class only if the file is not there?

I think this way we have the best of both worlds. (And the classes need to be cached and added to every cdi container).

Forget it, this is more complex than that, looking at how openwebbeans find beans, we need to scan the classpath for all beans.xml files, then resolve the jar (or directory) for each and load classes inside that specific jar.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is part of @Inject spec which is used by CDI but also most of other IoC and this is how some compatibilty test suites are written.

But it is required by the CDI spec: http://www.cdi-spec.org/faq/
Look at: What is beans.xml and why do I need it?

Also: https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html

What you call @Inject spec does not exist, it might be called that by some but CDI is a Java spec.
@Inject is just a java annotation that CDI recognises, and other frameworks can use the same annotation but that doesn't make them a CDI implementation.

https://weld.cdi-spec.org/
"Weld is the reference implementation of CDI: Contexts and Dependency Injection for the Java EE Platform - a JCP standard for dependency injection and contextual lifecycle management and one of the most important and popular parts of the Java EE"

https://openwebbeans.apache.org/
"Apache OpenWebBeans delivers an implementation of the Contexts and Dependency injection for Java EE (CDI) 2.0 Specification (JSR-365)."

As a user, if I want to use CDI, at a minimum I need:

  • beans.xml file in every lib part of the CDI
  • write @Inject on beans I want injected

If I deploy code in a web or application container that might be it. Nothing else is required.
e.g.: Deploy JAX-WS service on Wildfly and inject beans in it.

If I run in a plain JavaSE environment then I also need to:

  • include a CDI implementation on my classpath
  • Initialize the container and get beans from it

So as a user, there is no way around having a beans.xml file if I want to use CDI.

At this point I hope we can agree CDI is a spec and beans.xml is required.

Now let's talk about the tests:

I've used cdi-unit in the past and it is true I did not need a beans.xml file in my test resources. Why? Because with plain junit tests I only need to inject production code into my class. I also could easily declare mocks as alternative beans and get them injected into the code to test. All in all, everything about my tests was contained in a single class as is usual with junit tests.

With cucumber it makes sense to split step classes around features and break the 1 test class for 1 production class paradigm. Then we have to inject a shared state object between those steps.

Imagine the following very simple test structure:

  • io.cucumber.StepClass1
  • io.cucumber.StepClass2
  • io.cucumber.PojoSharedState (injected into the 2 teps classes)

With the current cucumber-cdi2 version 6.10.0, is it possible currently to run those tests without having a beans.xml file: NO it is not possible.

StepClass1 and StepClass2 are added as synthetic beans, but not PojoSharedState, cucumber does not know anything about that class, it does not have cucumber annotations in it.

So great if cucumber-cdi2 says, "You can start using CDI in your tests and we'll do some automatic wiring for you and it will work even if you don't follow the spec. As long as you don't inject test POJO though."

But where I don't agree (and is why I opened #2241), is that if you want to use Weld (the reference CDI implementation) it will not work because our automatic wiring broke it. You will have to start editing that beans.xml file, figure out what cucumber-cdi2 does under the hood and explicitely ignore these classes from the discovery process. And everytime you add a step class don't forget to go and ignore it in beans.xml.

Now to recap different @Injection scenarios:

  • Should a beans.xml file be required in src/test/resources?
    • Step classes that @Inject classes from other libs (and transitive injection):
      • currently (6.10.0): NO (but it is required in src/main/resources and every jar part of the injection)
      • proposed (7.0.0): NO (but it is required in src/main/resources and every jar part of the injection)
    • Step classes that @Inject local pojo test classes (and transitive injection):
      • currently (6.10.0): YES
      • proposed (7.0.0): YES
    • Step classes that @Inject other step classes (and transitive injection):
      • currently (6.10.0): NO
      • proposed (7.0.0): ? This is what all this is all about

I've asked the question a few times before: Why would I ever need to inject a cucumber step class into another cucumber step class? In my opinion this is a very bad design and should be refactored, test state should not be saved in step classes.

Now you both @mpkorstanje and @rmannibucau disagree, I'm very new to cucumber so I don't know how this should be resolved.

My vote is to keep the code lean and stop messing with synthetic beans.
It is already great that step classes are loaded as UnmanagedInstances and allow injection of classes outside of the local test scope.

Is it reasonable to spend so much time and effort to allow injection of step classes into other step classes (bad design), decide to use CDI (and not PicoContainer or other alternatives) but don't want to add the required beans.xml as per the CDI spec?

P.S.: Should the discussion moved to Slack?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Inject is NOT CDI but CDI uses @Inject, don't reverse or merge it. This is also why some steps are written portably and tested against CDI, Spring, Guice and a few other IoC. You have CDI so you have @Inject but you can miss all other parts of CDI like its activation (beans.xml).

What you call @Inject spec does not exist, it might be called that by some but CDI is a Java spec.

It does, it is the JSR330: https://javax-inject.github.io/javax-inject/.

As a user, if I want to use CDI, at a minimum I need:

beans.xml file in every lib part of the CDI
write @Inject on beans I want injected

and

So as a user, there is no way around having a beans.xml file if I want to use CDI.

No again, CDI 2 enables to no more write/create beans.xml and you don't have to use @Inject as this cucumber integration shows - steps are looked up without @Inject generally.

AFAIK with the addClass the case I described works as of today and is broken by removing addClass.

Why would I ever need to inject a cucumber step class into another cucumber step class?

This is a fair question and it can happen to reuse some steps (aliases/rephrasing for a better human writing experience) but you also have the case where you have a transversal bean checking data or resetting in steps with a scope (or not) which is purely technical but requires the step to be made a bean.

My vote is to keep the code lean and stop messing with synthetic beans.

Agree, this is why I proposed to enable archives with "just" replacing the addClass by a addPackage(true, xxx) so adding a config to the integration instead of trying to be clever.

It is already great that step classes are loaded as UnmanagedInstances and allow injection of classes outside of the local test scope.

This is true but on the other side, if you do what you describe, ie make them actual bean, it is not what you want, you want to lookup the bean properly, this is why I proposed to use a try to lookup and if not possible use unmanaged instance.

Is it reasonable to spend so much time and effort to allow injection of step classes into other step classes (bad design), decide to use CDI (and not PicoContainer or other alternatives) but don't want to add the required beans.xml as per the CDI spec?

Well it is not only about steps into steps - even if this case makes sense from time to time I could live without it - but also being able to use step as beans which is what the addClass enables (but not yet the lookup which I lacked in a few projects already).
And once again CDI does NOT need a beans.xml as per spec but has some funny scanning rules. That said the point is to be able to use steps "as this", don't assume you own all the code you run, it only happens when you are lucky but it seems I'm not as lucky as you on this point ;).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then my bad to think that cucumber-cdi2 was a CDI 2.0 injection framework for cucumber.
I suggest the module be renamed to cucumber-javax-inject. ;-)

Seriously I'm not an expert on the matter, but javax.inject is JSR-330, CDI 2.0 is JSR-365 which includes (and respects) JSR-330: https://javaee.github.io/tutorial/cdi-basic002.html

Now unless I'm mistaken javax.enterprise.inject.se.SeContainerInitializer.SeContainerInitializer.newInstance() is part of the CDI and not the javax.inject spec.

javax.enterprise.inject.se.SeContainerInitializer is from cdi-api-2.0.SP1.jar
@javax.inject.Inject comes from javax.inject-1.jar

So by using SeContainerInitializer this is a CDI 2.0 module and therefore must respect the spec, at least not break it (again issue #2241)

I think part of the problem is that openwebbeans seems very lax about the CDI spec compared to Weld. Weld is the reference implementation and is used in most application containers so it makes sense that we test properly against it. If enabling a feature breaks Weld then in my opinion this is not acceptable.

Weld 1.0 was following javax.inject spec (I never used Weld 1 so wasn't even aware of that spec)
Weld 2.0 is CDI 1.1
Weld 3.0 is CDI 2.0
Weld 4.0 is CDI 3.0

cucumber-cdi2 project builds with Weld 3.X, jakarta-cdi (CDI 3.0) builds with Weld 4.X

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then my bad to think that cucumber-cdi2 was a CDI 2.0 injection framework for cucumber.
I suggest the module be renamed to cucumber-javax-inject. ;-)

I can't understand what you mean there. cucumber-cdi2 is about running in a CDI container with the CDI 2 SE API configuration support. This means it must also support @Inject spec since CDI includes it (and once again not the opposite).

So by using SeContainerInitializer this is a CDI 2.0 module and therefore must respect the spec, at least not break it (again issue #2241)

I see where you come from but to be a CDI container you must pass CDI validations (TCK) but also @Inject validation. This means you can reuse @Inject modules in CDI features without having to have these steps CDI steps. Hope it is clearer written this way.

I think part of the problem is that openwebbeans seems very lax about the CDI spec compared to Weld.

Hmm, it is not, both pass the same test kit but weld tend to be broken in several environments and more restrictive (but it is not in the spec). I agree we must run with weld - and we were at some point, didn't check recently. We can refine the error you get if you want but in current form it should be portable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rmannibucau well I suppose we disagree on what cucumber-cdi2 should do because I have no idea what is the contract between these extensions and cucumber-core. I'm very new to the project.

But I think I nailed the solution this time. Not only does my latest commit fix #2241, it doesn't break any functionality of 6.1.0, it also makes it possible to inject most POJO objects without a beans.xml file.

I register a CDI Extension and test if all the step classes can be resolved, if not I add them as a bean on the BeanManager. So this doesn't break Weld and it means addClasses does what it should. This is also a supported way to work with CDI so no voodoo magic here.

Now the cherry on top: for each class added manually, I introspect the injection points and test them as well, so now we can @Inject POJO classes without a beans.xml file, as long as the injected types are valid.
i.e.: if it works with a beans.xml file, it should also work without. if it is broken with a beans.xml file, then it will be broken without it, I'm not doing any magic.

return true;
}

@Override
public <T> T getInstance(final Class<T> type) {
final Unmanaged.UnmanagedInstance<?> instance = standaloneInstances.get(type);
Unmanaged.UnmanagedInstance<?> instance = standaloneInstances.get(type);
if (instance != null) {
return type.cast(instance.get());
}
final Instance<T> selected = container.select(type);
Instance<T> selected = container.select(type);
if (selected.isUnsatisfied()) {
BeanManager beanManager = container.getBeanManager();
Unmanaged<T> unmanaged = new Unmanaged<>(beanManager, type);
Expand Down
56 changes: 44 additions & 12 deletions cdi2/src/test/java/io/cucumber/cdi2/Cdi2FactoryTest.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,46 @@
package io.cucumber.cdi2;

import io.cucumber.core.backend.ObjectFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Vetoed;
import javax.inject.Inject;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

class Cdi2FactoryTest {

final ObjectFactory factory = new Cdi2Factory();

@AfterEach
void stop(){
factory.stop();
}

@Test
void lifecycleIsIdempotent(){
assertDoesNotThrow(factory::stop);
factory.start();
assertDoesNotThrow(factory::start);
factory.stop();
assertDoesNotThrow(factory::stop);
}

@Vetoed
static class VetoedBean {

}

@Test
void shouldCreateNewInstancesForEachScenario() {
factory.addClass(VetoedBean.class);

// Scenario 1
factory.start();
VetoedBean a1 = factory.getInstance(VetoedBean.class);
Expand Down Expand Up @@ -54,28 +68,46 @@ static class ApplicationScopedBean {

@Test
void shouldCreateApplicationScopedInstance() {
factory.addClass(ApplicationScopedBean.class);
factory.start();
ApplicationScopedBean cdiStep = factory.getInstance(ApplicationScopedBean.class);
ApplicationScopedBean bean = factory.getInstance(ApplicationScopedBean.class);
assertAll(
// assert that it is is a CDI proxy
() -> assertThat(cdiStep.getClass(), not(is(ApplicationScopedBean.class))),
() -> assertThat(cdiStep.getClass().getSuperclass(), is(ApplicationScopedBean.class)));
() -> assertThat(bean.getClass(), not(is(ApplicationScopedBean.class))),
() -> assertThat(bean.getClass().getSuperclass(), is(ApplicationScopedBean.class)));
factory.stop();
}

static class UnmanagedBean {

}

@Test
void shouldCreateUnmanagedInstance() {
factory.addClass(UnmanagedBean.class);
factory.start();
assertNotNull(factory.getInstance(UnmanagedBean.class));
UnmanagedBean cdiStep = factory.getInstance(UnmanagedBean.class);
assertThat(cdiStep.getClass(), is(UnmanagedBean.class));
UnmanagedBean bean = factory.getInstance(UnmanagedBean.class);
assertThat(bean.getClass(), is(UnmanagedBean.class));
factory.stop();
}

static class UnmanagedBean {
static class OtherStepDefinitions {

}

static class StepDefinitions {

@Inject
OtherStepDefinitions injected;

}

@Test
void shouldInjectStepDefinitions() {
factory.addClass(OtherStepDefinitions.class);
factory.addClass(StepDefinitions.class);
factory.start();
StepDefinitions stepDefinitions = factory.getInstance(StepDefinitions.class);
assertThat(stepDefinitions.injected, is(notNullValue()));
factory.stop();
}

}