Search code examples
javamultithreadingcucumbertestng

How to customize Cucumber TestNG for the different threads?


I need to implement parallel execution of cucumber features and, obviously, set up them for the different threads.

So, I created two features, classes, and one TestNgRuner for the .XML execution. There is a project structure:

src/
└── test/
    └── java/
        └── com/
            └── projectname/
                └── tests/
                    ├── TestRunner.java
                    ├── suites/
                    │   ├── first/
                    │   │   └── FirstSteps.java
                    │   └── second/
                    │       └── SecondSteps.java
                    └── resources/
                        └── suites/
                            └── smoke.xml

Parallel execution ensured by smoke.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Test Debug" parallel="tests">
    <test name="Test 1">
        <parameter name="browser" value="firefox"/>
        <parameter name="cucumber.features" value="classpath:features/first"/>
        <classes>
            <class name="com.projectname.tests.TestRunner"/>
        </classes>
    </test>
    <test name="Test 2">
        <parameter name="browser" value="firefox"/>
        <parameter name="cucumber.features" value="classpath:features/second"/>
        <classes>
            <class name="com.projectname.tests.TestRunner"/>
        </classes>
    </test>
</suite>

So, you can guess, I need to establish WebDriver according to the browser parameter, and this establishment has to be executed for each test. So, as I understand, I have to use testing @BeforeTest annotation in the TestRunner class, since exactly this class managed and execute @Test methods (through AbstractTestNGCucumberTests extension):

  • TestRunner:
public class TestRunner extends AbstractTestNGCucumberTests {

    private static volatile ThreadLocal<String> threadLocal;
    private static String debug;

    @Override
    @DataProvider(parallel = true)
    public Object[][] scenarios() {
        return super.scenarios();
    }

    @BeforeClass
    @Parameters("browser")
    public void init(@Optional("chrome") String browser) {
        System.out.println("BrowserName: " + browser);
        printThread(browser);
        if (threadLocal == null) {
            synchronized (TestRunner.class) {
                if (threadLocal == null) {
                    threadLocal = new ThreadLocal<>();
                }
            }
        }
        threadPool.set(browser);
        debug = browser;
    }

    public static String getBrowserName(){
        return threadPool.get(); ------------- > line 2
    }

    public static String getDebugString(){
        return debug;// ---------------------- > line 3
    }
}

I was sure, with this solution, I will receive different threads for each test and static fields will be common for them, but it has occurred that line 1 executed 2 times. Ok, for this case I can establish browser for each thread without ThreadLocal using I thought...but the debug parameter initialized also 2 times, and in the both cases, when I called getDebugString (line 3) I receive the last value that was initialized. In this point I need to say I call getDebugString() in the FirstSteps.class and SecondSteps.class:

  • FirsSteps:

public class FirstSteps  {

    @When("First Step")
    public void firstStep() throws InterruptedException {
        TestRunner.printThread();
        System.out.println("FirstSteps::firstStep");
        System.out.println("FirstSteps thread parameter: " + TestRunner.getBrowserName()); // ------------ line 1
        System.out.println("FirstSteps Debug parameter: " + TestRunner.getDebugParameter()); // ---------- line 2
        Thread.sleep(3000);
    }

    @Then("Second Step")
    public void secondStep() {
        System.out.println("FirstSteps::secondStep");
    }
}

In the line 1 I receive null value. In the line 2 I receive the same value for both cases - the value that was initialized in the first queue (if the first test was with parameter browser=chrome - it will be chrome for both the FirstSteps and SecondSteps, otherwise - firefox

  • SecondSteps:
public class SecondSteps extends BaseApi {
    @When("First Function")
    public void firstFunction() throws InterruptedException {
        TestRunner.printThread();
        Thread.sleep(1000);
        System.out.println("SecondSteps::firstFunction");
        System.out.println("SecondSteps thread parameter: " + TestRunner.getBrowserName());
        System.out.println("SecondSteps debug parameter: " + TestRunner.getDebugParameter());
    }

    @Then("Second Function")
    public void secondFunction() {
        System.out.println("SecondSteps::secondFunction");
    }
}

So, it seems the static field is common for both threads! But why is the ThreadLocal not working in this case??


Console output:

FirstSteps::firstStep
FirstSteps thread parameter: null
FirstSteps Debug parameter: chrome
SecondSteps::firstFunction
SecondSteps thread parameter: null
SecondSteps debug parameter: chrome
SecondSteps::secondFunction
FirstSteps::secondStep
  • Update

After I added printThread(); function that print {$THREAD_NAME}-{$THREAD_ID} {$ADDITION_INFO} I received following console log:

BrowserName: chrome
BrowserName: firefox
com.projectname.tests.TestRunner::init - TestNG-tests-2-18 for firefox
com.projectname.tests.TestRunner::init - TestNG-tests-1-17 for chrome
FirstSteps::secondStep
FirstSteps::firstStep
com.projectname.tests.suites.second.SecondSteps::firstFunction - TestNG-PoolService-0-20
FirstSteps thread parameter: null
FirstSteps Debug parameter: chrome
com.projectname.tests.suites.first.FirstSteps::firstStep - TestNG-PoolService-0-21
SecondSteps::firstFunction
SecondSteps thread parameter: null
SecondSteps debug parameter: chrome
SecondSteps::secondFunction

So, as I understand - the AbstractTestNGCucumberTests executes tests in a separate threads (some PoolService threads). I'll research this behavior, but anyway, I left this answer here, until I haven't solution.

PS. Still is so weird the fact that TestNG creates two new threads with the FIRST instance of the class (I mean instance with 'chrome' browser for debug field). And so weird, that cucumber works so ugly...


Solution

  • Because TestNG runs the tests from the @DataProvider method on a different thread then the @BeforeClass method, you have to pass the parameter around a bit differently. Instead of setting the value on a thread local during the before class step, it has to be set just before the scenario is executed.

    You can do this by inlining the AbstractTestNGCucumberTests class.

    import io.cucumber.testng.CucumberPropertiesProvider;
    import io.cucumber.testng.FeatureWrapper;
    import io.cucumber.testng.PickleWrapper;
    import io.cucumber.testng.TestNGCucumberRunner;
    import org.testng.ITestContext;
    import org.testng.annotations.AfterClass;
    import org.testng.annotations.BeforeClass;
    import org.testng.annotations.DataProvider;
    import org.testng.annotations.Test;
    import org.testng.xml.XmlTest;
    
    @CucumberOptions(plugin = { "html:target/results.html", "message:target/results.ndjson" }) // Not really needed you can run entirely through the XML, just for example.
    public abstract class CustomTestNGCucumberTests {
    
        private TestNGCucumberRunner testNGCucumberRunner;
        private final ThreadLocal<String> browser = new ThreadLocal<>();
        private XmlTest currentXmlTest;
    
        @BeforeClass(alwaysRun = true)
        public void setUpClass(ITestContext context) {
            // Here we put the current context in the test class. 
            // This context is distinct for each class so classes can
            // run in parallel.
            currentXmlTest = context.getCurrentXmlTest();
    
            CucumberPropertiesProvider properties = currentXmlTest::getParameter;
            testNGCucumberRunner = new TestNGCucumberRunner(this.getClass(), properties);
        }
    
        @SuppressWarnings("unused")
        @Test(groups = "cucumber", description = "Runs Cucumber Scenarios", dataProvider = "scenarios")
        public void runScenario(PickleWrapper pickleWrapper, FeatureWrapper featureWrapper) {
            // Then here we make the value of browser parameter available
            // to the tests on the same thread. Because we are picking this value
            // from the class, it should work when multiple classes run in parallel.
            browser.set(currentXmlTest.getParameter("browser"));
            testNGCucumberRunner.runScenario(pickleWrapper.getPickle());
        }
    
        @DataProvider(parallel = true) // This make Cucumber run scenarios in parallel
        public Object[][] scenarios() {
            if (testNGCucumberRunner == null) {
                return new Object[0][0];
            }
            return testNGCucumberRunner.provideScenarios();
        }
    
        @AfterClass(alwaysRun = true)
        public void tearDownClass() {
            if (testNGCucumberRunner == null) {
                return;
            }
            testNGCucumberRunner.finish();
        }
    
    }
    

    Note: I didn't test this.