Search code examples
seleniumcucumbergherkin

How to return the current method that is going to get executed from step definition file


I need this so that i can pass the required arguments to the class before executing that method for reporting, logging,etc., so that test cases can be executed in parallel. I am in the process of converting my keyword driven selenium framework to BDD Gherkin selenium framework .In Keyword driven framework, i get the keywords from Excel , pass the required parameters to the class through Java reflection before executing the method

    String [] argv = new String[]{ 
            "-g"
            ,"businesscomponents"
            ,"./Features"
            //,"-t","@P1,@P2"
            ,"-n","Validate_login_search_using_sku_id"
            };
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    byte exitstatus = Main.run(argv, contextClassLoader);

Solution

  • This was much easier to do than I thought. This just required modifying couple of lines of one class (Runtime.java) to figure out all the hook and step methods. All it then needed was to run in dryrun mode set to true to get all the details.

    Override Runtime.java behaviour -

    1. Download the 1.2.5 class from this link https://github.com/cucumber/cucumber-jvm/blob/v1.2.5/core/src/main/java/cucumber/runtime/Runtime.java

    2. Create a package cucumber.runtime in the classpath and copy the Runtime class inside it.

    3.In private method runHooks(line 209) comment out lines 210 and 214.

        private void runHooks(List<HookDefinition> hooks, Reporter reporter, Set<Tag> tags, boolean isBefore) {   
            //if (!runtimeOptions.isDryRun()) {
                for (HookDefinition hook : hooks) {
                    runHookIfTagsMatch(hook, reporter, tags, isBefore);
                }
            //}
        }
    
    1. In private method runHookIfTagsMatch(line 217) add the line 210 before existing line 224. Basically if in dryrun mode do not execute the hook. The try block contains this code. if(!runtimeOptions.isDryRun()) hook.execute(scenarioResult);

    CustomFormatter - This will now store all the details in a ThreadLocal variable.

    public class CustomFormatter implements Reporter, Formatter {
    
        private Map<String, Map<Scenario, List<StepDefinitionMatch>>> featureDetails;
    
        private Map<Scenario, List<StepDefinitionMatch>> scenarioDetails;
    
        private List<StepDefinitionMatch> stepDetails;
    
        private String featureUri = "";
    
        private Scenario scenario;
    
        private Map<Scenario, List<Match>> beforeHooks;
    
        private Map<Scenario, List<Match>> afterHooks;
    
        private List<Match> beforeMatches;
    
        private List<Match> afterMatches;
    
    
        public CustomFormatter() {
            featureDetails = new LinkedHashMap<>(); 
    
            beforeHooks = new LinkedHashMap<>();
            afterHooks = new LinkedHashMap<>();
        }
    
        @Override
        public void before(Match match, Result result) {
            beforeMatches.add(match);
        }
    
        @Override
        public void result(Result result) {
    
        }
    
        @Override
        public void after(Match match, Result result) {
            afterMatches.add(match);
        }
    
        @Override
        public void match(Match match) {
            //ThreadLocalStepDefinitionMatch.set((StepDefinitionMatch) match);
            stepDetails.add((StepDefinitionMatch) match);
        }
    
        @Override
        public void embedding(String mimeType, byte[] data) {
        }
    
        @Override
        public void write(String text) {
        }
    
        @Override
        public void syntaxError(String state, String event,
                List<String> legalEvents, String uri, Integer line) {
        }
    
        @Override
        public void uri(String uri) {
            featureUri = uri;
        }
    
        @Override
        public void feature(Feature feature) {
            scenarioDetails = new LinkedHashMap<>();
        }
    
        @Override
        public void scenarioOutline(ScenarioOutline scenarioOutline) {
        }
    
        @Override
        public void examples(Examples examples) {
        }
    
        @Override
        public void startOfScenarioLifeCycle(Scenario scenario) {
    
            this.scenario = scenario;
    
            stepDetails = new ArrayList<>();
            beforeMatches = new ArrayList<>();
            afterMatches = new ArrayList<>();
        }
    
        @Override
        public void background(Background background) {
        }
    
        @Override
        public void scenario(Scenario scenario) {
        }
    
        @Override
        public void step(Step step) {
        }
    
        @Override
        public void endOfScenarioLifeCycle(Scenario scenario) {
    
            scenarioDetails.put(this.scenario, stepDetails);
            beforeHooks.put(this.scenario, beforeMatches);
            afterHooks.put(this.scenario, afterMatches);
        }
    
        @Override
        public void done() {
        }
    
        @Override
        public void close() {
            /*System.out.println(featureDetails);
            System.out.println(beforeHooks);
            System.out.println(afterHooks);*/
    
            StepHookDetails shd = new StepHookDetails(featureDetails, beforeHooks, afterHooks);
            ThreadLocalStepHookDetails.set(shd);
        }
    
        @Override
        public void eof() {
            featureDetails.put(featureUri, scenarioDetails);
        }
    }
    

    If you uncomment the print lines in the close() method you can see the output as something like this.

    Step Methods Match - Corresponds to Map - Map<String, Map<Scenario, List<StepDefinitionMatch>>> featureDetails in CustomFormatter class

    {src/test/resources/features/format2.feature={gherkin.formatter.model.Scenario@38d8f54a=[cucumber.runtime.StepDefinitionMatch@6236eb5f, cucumber.runtime.StepDefinitionMatch@7c1e2a9e], gherkin.formatter.model.Scenario@49e53c76=[cucumber.runtime.StepDefinitionMatch@fa36558, cucumber.runtime.StepDefinitionMatch@672872e1]}}

    Before Hook Matches -Corresponds to Map - Map<Scenario, List<Match>> beforeHooks in CustomFormatter class

    {gherkin.formatter.model.Scenario@38d8f54a=[gherkin.formatter.model.Match@32910148], gherkin.formatter.model.Scenario@49e53c76=[gherkin.formatter.model.Match@3f56875e]}

    After Hook Matches - -Corresponds to Map - Map<Scenario, List<Match>> afterHooks in CustomFormatter class

    {gherkin.formatter.model.Scenario@38d8f54a=[gherkin.formatter.model.Match@2b4bac49], gherkin.formatter.model.Scenario@49e53c76=[gherkin.formatter.model.Match@fd07cbb]}

    This is for a run of a single feature file as below. If you have multiple feature files you will have multiple feature keys in featureDetails map. The hooks will be added to the hook maps.

    Feature: Validating sample Two
        Background:
           Given user gets count from "Car0"
    
      @Format
      Scenario: Scenario Two
        And user gets count from "Car1"
    
      @Format
      Scenario: Scenario Two
        And user gets count from "Car1"
    

    Important - The key which binds the three together is the Scenario object. Pretty easy to iterate the featureDetails map and get the before hook if available, then available steps and then after hook. For getting hook method names use

    ThreadLocal Class -

    public class ThreadLocalStepHookDetails {
    
        private static final ThreadLocal<StepHookDetails> threadStepHookDetails = new InheritableThreadLocal<StepHookDetails>();
    
        private ThreadLocalStepHookDetails() {
        }
    
        public static StepHookDetails get() {
            return threadStepHookDetails.get();
        }
    
        public static void set(StepHookDetails match) {
            threadStepHookDetails.set(match);
        }
    
        public static void remove() {
            threadStepHookDetails.remove();
        }
    }
    

    StepHookDetails class -

    public class StepHookDetails {
    
        private Map<String, Map<Scenario, List<StepDefinitionMatch>>> featureDetails;
    
        private Map<Scenario, List<Match>> beforeHooks;
    
        private Map<Scenario, List<Match>> afterHooks;
    
        public StepHookDetails() {}
    
        public StepHookDetails(
                Map<String, Map<Scenario, List<StepDefinitionMatch>>> featureDetails,
                Map<Scenario, List<Match>> beforeHooks,
                Map<Scenario, List<Match>> afterHooks) {
            this.featureDetails = featureDetails;
            this.beforeHooks = beforeHooks;
            this.afterHooks = afterHooks;
        }
    
        public Map<String, Map<Scenario, List<StepDefinitionMatch>>> getFeatureDetails() {
            return featureDetails;
        }
    
        public void setFeatureDetails(
                Map<String, Map<Scenario, List<StepDefinitionMatch>>> featureDetails) {
            this.featureDetails = featureDetails;
        }
    
        public Map<Scenario, List<Match>> getBeforeHooks() {
            return beforeHooks;
        }
    
        public void setBeforeHooks(Map<Scenario, List<Match>> beforeHooks) {
            this.beforeHooks = beforeHooks;
        }
    
        public Map<Scenario, List<Match>> getAfterHooks() {
            return afterHooks;
        }
    
        public void setAfterHooks(Map<Scenario, List<Match>> afterHooks) {
            this.afterHooks = afterHooks;
        }
    }
    

    Check details in store -

    You can use the below code as a sample to see the details inside the maps and in order. ie before steps after.

    StepHookDetails sd = ThreadLocalStepHookDetails.get();
    
            sd.getFeatureDetails().entrySet().stream().forEach(
                    e -> e.getValue().entrySet().stream().forEach(
                            ee -> {
                                System.out.println(ee.getKey().getLine());
                                sd.getBeforeHooks().get(ee.getKey()).stream().forEach(bh -> System.out.println(bh.getLocation()));
                                ee.getValue().stream().forEach(s -> System.out.println(s.getLocation() + " " + s.getArguments()));
                                sd.getAfterHooks().get(ee.getKey()).stream().forEach(ah -> System.out.println(ah.getLocation()));
                            }));
    

    For the above feature file got this output

    Scenario Line Number 8 Before Hook SampleSteps.before() Steps SampleSteps.userGetsCountFromAndStores(String) [Car0] Steps SampleSteps.userGetsCountFromAndStores(String) [Car1] After Hook SampleSteps.afterOne(Scenario) Scenario Line Number 15 Before Hook SampleSteps.before() Steps SampleSteps.userGetsCountFromAndStores(String) [Car0] Steps SampleSteps.userGetsCountFromAndStores(String) [Car1] After Hook SampleSteps.afterOne(Scenario)

    This will also work for scenariooutlines.


    UPDATED -

    Feature: UI Automation
    
    @P1 
    Scenario: Validate_Home_page 
    Given the customer opens launches the homepage
    

    @Given("^the customer opens launches the homepage$") 
        public void openbrowser() { }
    

    public static void main (String args[]) throws IOException {
    
            String[] argv = new String[] { "-g", "cucumpar.test.stepdefs", "src/test/resources/features/features.feature", 
                    "-t", "@P1" , "-p", "cusform.CustomFormatter","-d"}; 
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 
            byte exitstatus = Main.run(argv, contextClassLoader);
            ----------------
            StepHookDetails sd = ThreadLocalStepHookDetails.get();
            ----------------
        }
    

    Results

    {src/test/resources/features/features.feature={gherkin.formatter.model.Scenario@6771beb3=[cucumber.runtime.StepDefinitionMatch@51399530]}} {gherkin.formatter.model.Scenario@6771beb3=[]} {gherkin.formatter.model.Scenario@6771beb3=[]} Scenario Line Number 4 Steps SampleSteps.openbrowser() []


    UPDATED

    Need to add some code to the StepDefinitionMatch class. Similar to what u had done for Runtime.java download the 1.2.5 class at - https://github.com/cucumber/cucumber-jvm/blob/v1.2.5/core/src/main/java/cucumber/runtime/StepDefinitionMatch.java. Copy this class to the existing cucumber.runtime package. Then add the following method --

    public List<List<String>> getDataTableData() {
    
            List<List<String>> data = new ArrayList<>();
            if(step.getRows()!=null)
                step.getRows().forEach(row -> data.add(row.getCells()));
            return data;
        }
    

    That is it. Call this method in the existing print method like below.

    ee.getValue().stream().forEach(s -> System.out.println("Steps " + s.getLocation() + " " + s.getArguments() + " " + s.getDataTableData()));
    

    For this feature file

     Scenario: Scenario Four
        And car gets count
          | car  | owner      |
          | Car1 | OwnerOne   |
          | Car2 | OwnerTwo   |
          | Car3 | OwnerThree |
    

    Got this output - The first line is the header, customize as required.

    SampleSteps.carGetsCount(DataTable) [] [[car, owner], [Car1, OwnerOne], [Car2, OwnerTwo], [Car3, OwnerThree]]


    UPDATE

    public Map<String, List<String>> getDataTableData() {
    
            Map<String, List<String>> mapData = new HashMap<>();
            if(step.getRows()!=null) {
                List<List<String>> data = new ArrayList<>();
                step.getRows().forEach(row -> data.add(row.getCells()));
                List<String> keys = data.remove(0);         
                data.stream().forEach(row -> IntStream.range(0, keys.size()).forEach(index -> {
                    List<String> tempList = new ArrayList<>();
                    tempList.add(row.get(index));
                    mapData.compute(keys.get(index), (key, val) -> {
                        if(val==null)
                            return tempList;
                        val.addAll(tempList);
                        return val;
                    });             
                }));            
            }
            return mapData;
        }