TNG / JGiven

Behavior-Driven Development in plain Java
http://jgiven.org
Apache License 2.0
439 stars 98 forks source link

intro words with dynamically added stages #1469

Closed adrian-herscu closed 11 months ago

adrian-herscu commented 11 months ago
public class JGivenMultipleStages extends
    ScenarioTest<JGivenMultipleStages.MFixtures<?>, JGivenMultipleStages.MActions<?>, JGivenMultipleStages.MVerifications<?>> {
    public static class MActions<SELF extends MActions<SELF>>
        extends Stage<SELF> {

        public SELF m_action() {
            return self();
        }
    }

    public static class MFixtures<SELF extends MFixtures<SELF>>
        extends Stage<SELF> {
        // ISSUE super given(), and(), all intro words do not return this type
        // @Override
        // @IntroWord
        // public SELF and() {
        // return super.and();
        // }

        public SELF m_fixture() {
            return self();
        }
    }

    public static class MVerifications<SELF extends MVerifications<SELF>>
        extends Stage<SELF> {

        public SELF m_verification() {
            log.info(">>> m_verification");
            return self();
        }
    }

    public static class NActions<SELF extends NActions<SELF>>
        extends Stage<SELF> {

        public SELF n_action() {
            return self();
        }
    }

    public static class NFixtures<SELF extends NFixtures<SELF>>
        extends Stage<SELF> {
        public SELF n_fixture() {
            return self();
        }
    }

    public static class NVerifications<SELF extends NVerifications<SELF>>
        extends Stage<SELF> {

        public SELF n_verification() {
            return self();
        }
    }

    @Test
    public void shouldWorkWithMultipleStages() {
        given().m_fixture();
        addStage(NFixtures.class)
            // shouldn't it work? if uncommented, it fails to compile
            /* .and() */.n_fixture();

        when().m_action();
        addStage(NActions.class).n_action();

        then().m_verification();
        addStage(NVerifications.class).n_verification();
    }
}
l-1squared commented 11 months ago

Hi @adrian-herscu
I think this problem is more a limitation of the Java language rather than JGiven (Type erasure). usually Stage classes are declared as MStage extends Stage<MStage> now, the generic type is always known and the compiler can infer the return type.

You however told the compile that MStage is MStage<?> so the return type is an unspecifed capture of which the compiler can make no assumption. likewise with addStage(NFixtrues.class) you pass the type-erased class literal for NFixtures<Stage> i.e. NFixtures<?>

Using them as generic types should not be necessary, if you don't intend to subclass them further. If you need to keep the generics, I suggest you try to cast to a more specific generic type.

l-1squared commented 11 months ago

Feel free, to close. If I hear nothing from you, I'll close it on Friday

adrian-herscu commented 11 months ago

Using them as generic types should not be necessary, if you don't intend to subclass them further. If you need to keep the generics, I suggest you try to cast to a more specific generic type.

I must reuse them somehow... I am developing system tests, and these always require working with multiple interfaces, like Selenium, JDBC, ElasticSearch, RabbitMQ, REST... So my initial tought was to build generic steps for these, hold them in some separate JAR artifacts and then reuse them by subclassing. My current scheme looks like this:

classDiagram
    Stage <|-- StageEx
    StageEx <|-- GenericFixtures
    StageEx <|-- GenericActions
    StageEx <|-- GenericVerifications
    GenericFixtures <|-- WebDriverFixtures
    GenericActions <|-- WebDriverActions
    GenericVerifications <|-- WebDriverVerifications
    GenericFixtures <|-- RestFixtures
    GenericActions <|-- RestActions
    GenericVerifications <|-- RestVerifications
    GenericFixtures <|-- RabbitMQFixtures
    GenericActions <|-- RabbitMQActions
    GenericVerifications <|-- RabbitMQVerifications

Further, each application has its specific steps derived from these, adding an additional layer of subclasses for each relevant interface. A scenario may look like this:

given a REST interface XXX
and a JDBC interface YYY
and an ElasticSearch interface ZZZ

when calling some REST method

then calling other REST method returns something
and the DB contains something
and the ElasticSearch query returns something

All my trouble appeared later when I discovered that additional stages do not play well with TestNG's parallel modes. Any other suggestion(s)?

l-1squared commented 11 months ago

Ok, so I manage to reproduce your issue:

    @Test
    public void test() {
        given().a_thing_doer();
        when().do_thing().and().move_result();
        then().thing_doer_should_say_hello_world();

        addStage(ExtraStage.class).smile().and().wave().and().hope_nobody_saw_that();
        //                                       ^we fail here
    }

    static class ExtraStage<T extends ExtraStage<T>> extends Stage<T> {
        @ProvidedScenarioState
        private String result;

        public T smile(){
            return self();
        }

        public T wave(){
            return self();
        }

        public T hope_nobody_saw_that(){
            return self();
        }
    }

But I am still conviced that this is a java, and not a JGiven limitation... I actually noticed that we kap of the generic chain at Stage -> Stage<T extends Stage<?>> but even after replacing that with T the error persisted. Thankfully, I could resolve the issue with ((ExtraStage<?>)addStage(ExtraStage.class)).smile().and().wave().and().hope_nobody_saw_that(); I suggest you follow that example, I cannot see a way to fix that issue on my end.

adrian-herscu commented 11 months ago

Thankfully, I could resolve the issue with ((ExtraStage<?>)addStage(ExtraStage.class)).smile().and().wave().and().hope_nobody_saw_that(); I suggest you follow that example, I cannot see a way to fix that issue on my end.

Instead of ((ExtraStage<?>)addStage(ExtraStage.class)) we can use:

    public ExtraStage<?> extraStage() {
        return addStage(NFixtures.class);
    }

This is what I did originally. Then I tried adding @IntroWord -- but it has no effect. So instead of just writting extraStage().smile(), I have to write extraStage().given().smile(). Any possible cure for that?

l-1squared commented 11 months ago

I would have to look deeper into what "has no effect" actually means here, before setting out for a cure

adrian-herscu commented 11 months ago

I would have to look deeper into what "has no effect" actually means here, before setting out for a cure

Well... It seems that @IntroWord has an effect only on Stage methods -- hence adding it to a Scenario method has no effect. Am I wrong? The end-result is that the scenario writer has to explicitly add given() or other intro word on each invocation of each additional stage. Let's remember that all this begun with JGiven not supporting injected stages in parallel TestNG modes.