cucumber-jvm icon indicating copy to clipboard operation
cucumber-jvm copied to clipboard

hooks should not require public visibility -- at least should log an error/warning

Open elonderin opened this issue 3 years ago • 15 comments

Is your feature request related to a problem? Please describe. I was frustrated when i tried to figure out why my tests would fail. Until i notices that my setup was not public. From the old days in junit i know that they used to require that too, but no more and nowadays it's so common to have all that test stuff with package visibility (to avoid cluttering autocompletion in IDEs i guess) I just did it here too w/o even giving it a 2nd though.

So now my code looks like so to make my team mates (we are all new CC) aware of the fact.

   @Before //method must be public!! otherwise it's silently ignored!
    public void setUp() {
        msUniteTestHarness.reset();
    }

Describe the solution you'd like Also allow package visibility but by all means don't just silently drop the execution

Describe alternatives you've considered Throwing an error would be OK too but behavior is not anymore in alignment with junit.

elonderin avatar Aug 31 '21 19:08 elonderin

This is an acceptable suggestion, but:

  1. This would be a breaking change. So it would have to go into the next major version. We are currently in the process of releasing v7 RC1 so this would have to be resolved in relatively short order. Otherwise the next opportunity would be v8. Possibly next year but impossible to say as this is an open source project.

  2. The motivation behind JUnit 5 not requiring methods to be public was to reduce the amount of ceremony needed to write tests. Specifically test classes and methods are can be package-private, projected or public. Merely allowing methods to not be public but classes would only lead to more confusion. So any pull requests that address this issue would have to be equally motivated to reduce ceremony and address both method and class visibility.

    Fortunately both are covered by the same section of code:

    https://github.com/cucumber/cucumber-jvm/blob/6a4567a7a32d904debbfda2d77feab0a9569e547/java/src/main/java/io/cucumber/java/MethodScanner.java#L23-L45

Would you be interested in creating a pull request?

mpkorstanje avatar Aug 31 '21 20:08 mpkorstanje

I wish i had the time then i would. Not seeing this though...

This would be a breaking change.

May i ask why ? After all, the methods which are now public would still work or is there some impl. detail i dont understand?

elonderin avatar Sep 01 '21 12:09 elonderin

Step definitions and hooks that would previously be ignored would now be discovered. Even though this may not have been intentional, it would change an existing setup.

mpkorstanje avatar Sep 01 '21 16:09 mpkorstanje

is there a timeline on v8 ? I guess it's still long way out/unknown, or ?

maybe have an opt-in property for v7?

elonderin avatar Sep 06 '21 14:09 elonderin

We don't use timelines or ETAs since contributions are mostly made on a voluntary basis.

aslakhellesoy avatar Sep 06 '21 14:09 aslakhellesoy

maybe have an opt-in property for v7?

Currently there is no system to provide properties to the BackendSupplier so that would also be a breaking change. Not to mention that cucumber would be carrying the complexity cost.

mpkorstanje avatar Sep 09 '21 22:09 mpkorstanje

I was considering working on this issue. So, I took a look at the code of the MethodScanner mentioned above and it only requires the test class to be public not the methods annotated with the hook annotations in the test class. Maybe JUnit's @Before annotation was accidentally used or the problem with the Cucmber's @Before annotation processing lies elsewhere.

rozenshteyn avatar Nov 01 '21 04:11 rozenshteyn

it only requires the test class to be public not the methods annotated with the hook annotations in the test class.

I don't believe that is correct. https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getMethods--

mpkorstanje avatar Nov 01 '21 08:11 mpkorstanje

Oops. You're right. I will test out what happens if getDeclaredMethods() is used instead of getMethods().

rozenshteyn avatar Nov 02 '21 01:11 rozenshteyn

I've made progress in implementing a solution for this request. I do have a question about some functionality though. There is a test in MethodScannerTest and JavaBackendTest that tests the following scenario. When there is a class

package io.cucumber.java.incorrectlysubclassedsteps;

import io.cucumber.java.steps.Steps;

public class SubclassesSteps extends Steps {

}

that extends

package io.cucumber.java.steps;

import io.cucumber.java.en.Given;

public class Steps {

    @Given("test")
    public void test() {

    }

}

an io.cucumber.java.InvalidMethodException is expected to be thrown by the MethodScanner class. Since my implementation of the requested feature involves changing from using Class.getMethods() to Class.getDeclaredMethods(), methods of the parent class are no longer being considered. Is such a test still valid? I think for the child class like the one above my change should be ok (the child class has no annotations of its own so none of its methods would be in play). However, what about the following child classes?:

package io.cucumber.java.incorrectlysubclassedsteps;

import io.cucumber.java.steps.Steps;

public class SubclassesSteps extends Steps {

    @Before
    public void before() {

    }
}

or

package io.cucumber.java.incorrectlysubclassedsteps;

import io.cucumber.java.steps.Steps;

public class SubclassesSteps extends Steps {

    @Given("test")
    public void test() {

    }
}

I assume that Cucumber tries to invoke the collected annotated methods, but in what order? Is it ok if parent and child classes have non-overlapping annotations? Or, should an io.cucumber.java.InvalidMethodException still be thrown if class that extends another class with Cucumber annotations is encountered?

Thanks.

rozenshteyn avatar Nov 03 '21 05:11 rozenshteyn

Cucumber does not allow step definitions on super classes. It results in duplicate step definitions.

class A {
   @Given("^hello .*$")
   public void example(){
   }
}

class B extends A {

}

class C extends A {

}

For example when classes B and C (and A) are scanned for glue this always results in a duplicate step definition because both B and C (and A) have the example method which would register a step definition with the same regex. Because the regex is the same, both step definitions would match the same step and Cucumber would not have a way to disambiguate. For consistency (and implementation convenience) this rule is also applied to other methods such as hooks.

mpkorstanje avatar Nov 03 '21 10:11 mpkorstanje

Though to resolve this, I don't think you have to calculate the actual methods of a class.

abstract class A {
   @Given("^hello .*$")
   public void example(){
   }
}

class B extends A {
   @Override
   public void example(){
   }
}

In pseudo code:

class to scan = B
current class = class to scan
while current class != null {
  scan methods on current class
  current class = parent class of current class
}
reject any methods found not from current class

mpkorstanje avatar Nov 03 '21 10:11 mpkorstanje

In the implementation that I'm trying to test I'm using Class.getDeclaredMethods() which:

public Method[] getDeclaredMethods()
                            throws SecurityException

Returns an array containing Method objects reflecting all the declared methods of the class or interface represented by this Class object, including public, protected, default (package) access, and private methods, but excluding inherited methods.

So, there should not be ambiguity as to which class a method came from - it would only be from the actual class being scanned. If the getDeclaredMethods() method is used, then in the following scenario:

class A {
   @Given("^hello .*$")
   public void example(){
   }
}

class B extends A {
   @Override
   public void example(){
   }
}

the scan result would only contain class A with its own example() method. On the other hand in the following scenario:

class A {
   @Given("^hello .*$")
   public void example(){
   }
}

class B extends A {
   @Override
   @Given("^hello .*$")
   public void example(){
   }
}

the scan result would contain both classes A and B, each with its own example method.

rozenshteyn avatar Nov 04 '21 06:11 rozenshteyn

We would still have to scan the super class. Just to let users know they made a mistake.

JUnit for example does allow inheritance -it makes sense there- and that assumption is carried over to Cucumber where it does not make sense.

mpkorstanje avatar Nov 04 '21 07:11 mpkorstanje

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in two months if no further activity occurs.

stale[bot] avatar Apr 14 '23 08:04 stale[bot]