Search code examples
javamultithreadingtestngjunit5parallel-testing

Parallel tests with resource-lock?


Background: Now using java JUnit4, willing to migrate to JUnit5 or TestNG.

Current state: Having 100+ Selenium tests. Most of them are repeated via @RunWith(Parameterized.class) in Junit4. (I.e. creating multiple instances of the test class based on the provided set of parameters, typically combinations of browser type + user identity.) Sharing a limited set of about 12 users.

Limitation: the tested application prevents the same user being logged in on multiple places simultaneously. So if the user logs in the application in some test running in one thread, it causes the same user being immediately logged off in another test running in another thread at the same moment.

Question: Is there any recommended way how to manage thread safety when tests executed in parallel can't share some resource? Or how to enforce those tests using the same resource are executed within the same thread?

Thanks for ideas.


Here is the simplified example of some solution I found with TestNG so far...:

public abstract class BaseTestCase {
    protected static ThreadLocal<WebDriver> threadLocalDriver = new ThreadLocal<>();
    protected String testUserName;

    private static final Set<String> inUse = new HashSet<>();

    public BaseTestCase(WebDriver driver, String testUserName) {
        threadLocalDriver.set(driver);
        this.testUserName = testUserName;
    }

    private boolean syncedAddUse(@NotNull String key){
        synchronized (inUse){
            return inUse.add(key);
        }
    }

    private boolean syncedRemoveUse(@NotNull String key){
        synchronized (inUse) {
            return inUse.remove(key);
        }
    }

    @DataProvider(parallel = true)
    public static Object[][] provideTestData() {
        //load pairs WebDriver+user from config file. E.g.:
        //Chrome + chromeUser
        //Chrome + chromeAdmin
        //Firefox + firefoxUser
        //etc...
    }

    @BeforeMethod
    public void syncPoint() throws InterruptedException {
        while( !syncedAddUse(testUserName) ){
            //Waiting due the testUserName is already in use at the moment.
            Thread.sleep(1000);
        }
    }

    @AfterMethod
    public void leaveSyncPoint(){
        syncedRemoveUse(testUserName);
    }
}

So I can have many test classes like:

public class TestA extends BaseTestCase {

    @Factory(dataProvider = "provideTestData")
    public TestA(WebDriver webDriver, String testUserName) {
        super(webDriver, testUserName);
    }

    public void someTest() {
        WebDriver driver = threadLocalDriver.get();
        threadLocalDriver.get().navigate().to("http://myPage.example.com");
        logintoMyPageWithUser(driver, testUserName);
        doSomeStuffOnPage(driver);
        logoutUserFromPage(driver);
    }
    ...
}

And all the tests are launched via testNG.xml like this:

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="sample suite" verbose="1" parallel="instances" thread-count="20" data-provider-thread-count="10">
    <test name="sample test" >
        <packages>
            <package name="com.path_to_package_with_example" />
        </packages>
    </test>
</suite>

This solution kid of works. However, I hate the Thread.sleep() there. It creates many threads and most of them keep waiting for each other. I would prefer to line up all the tests using the same user to the same thread and minimize the wait time.


Solution

  • I don't know of a way to organize tests in groups where each group runs in one thread. But you can replace the "while user busy sleep" with "try lock on user". The latter continues with execution as soon as another test is done with the user (i.e. unlocks the lock).

    The runnable example below should get you started with the "try lock on user" idea. Please keep in mind that if you get a lock ("beforeTest" in your case), you must make sure to release the lock in a "finally" block ("afterTest" in your case). Else execution can hang and never finish.

    import java.util.Map;
    import java.util.Random;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.IntStream;
    
    // https://stackoverflow.com/questions/56474713/parallel-tests-with-resource-lock
    public class NamedResourceLocks {
    
        public static void main(String[] args) {
    
            System.out.println("Starting");
            ExecutorService executor = Executors.newCachedThreadPool();
            try {
                new NamedResourceLocks().run(executor);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                executor.shutdownNow();
            }
            System.out.println("Done");
        }
    
        final static String userPrefix = "user";
        final static int maxUsers = 3;
        final static long maxWait = 10_000; // 10 seconds
        final static long startTime = System.currentTimeMillis();
    
        final Map<String, ReentrantLock> userLocks = new ConcurrentHashMap<>();
        final int maxTests = maxUsers * 10;
        final CountDownLatch allTestsDone = new CountDownLatch(maxTests);
    
        void run(ExecutorService executor) throws Exception {
    
            IntStream.range(0,  maxUsers).forEach(u -> 
                userLocks.put(userPrefix + u, new ReentrantLock(true)));
            IntStream.range(0,  maxTests).forEach(t -> 
                executor.execute(new Test(this, random.nextInt(maxUsers), t)));
            if (allTestsDone.await(maxWait, TimeUnit.MILLISECONDS)) {
                System.out.println("All tests finished");
            }
        }
    
    
        void lock(String user) throws Exception {
    
            ReentrantLock lock = userLocks.get(user);
            if (!lock.tryLock(maxWait, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Waited too long.");
            }
        }
    
        void unlock(String user) {
    
            userLocks.get(user).unlock();
        }
    
        void oneTestDone() {
    
            allTestsDone.countDown();
        }
    
        final static Random random = new Random();
    
        static class Test implements Runnable {
    
            final NamedResourceLocks locks;
            final String user;
            final int testNumber;
    
            public Test(NamedResourceLocks locks, int userNumber, int testNumber) {
                this.locks = locks;
                this.user = userPrefix + userNumber;
                this.testNumber = testNumber;
            }
    
            @Override
            public void run() {
    
                boolean haveLock = false;
                try {
                    log(this, "acquiring lock");
                    locks.lock(user);
                    haveLock = true;
                    int sleepTime = random.nextInt(maxUsers) + 1; 
                    log(this, "sleeping for " + sleepTime + " ms.");
                    Thread.sleep(sleepTime);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (haveLock) {
                        log(this, "releasing lock");
                        locks.unlock(user);
                    }
                    locks.oneTestDone();
                }
            }
    
        }
    
        static void log(Test test, String msg) {
            System.out.println((System.currentTimeMillis() - startTime) + " - " +
                    test.testNumber + " / " + test.user + " - " + msg);
        }
    }