How to test thread safety in Java with TestNG

Writing thread-safe code in Java can be a difficult task. Even very simple code, when executed concurrently, can have complex behavior. Additionally, the race conditions that can cause the code to fail, are difficult to reproduce in a testing environment, due to their probabilistic nature.

Fortunately, modern testing frameworks like TestNG have gone a long way and addressed some of these concerns.

In this comprehensive tutorial, you will learn how to use the TestNG framework to test concurrency and write thread-safe code.

The tools you will use are:

Gradle: For building and executing the project.
TestNG: As a testing framework.
AssertJ: For setting assertions.

Here is an outline of the various steps you will follow in this tutorial:

Before you start, you need to make sure you have JDK 1.8, or later, installed on your system.

You also need to have Gradle already installed on your system. You can find the installation manual here.

You can get the complete source code for this tutorial from this git repository:
git clone https://bitbucket.org/apostolosbenisis/thread-safety-with-testng.git

Create the Gradle project

First, create the root directory for the project:

mkdir thread-safety-with-testng

Note: In this tutorial, shell commands are for the Windows operating system. For Unix-based systems it should be easy to find the analogous commands

Using your favorite editor, create a new build.gradle file in the project root directory (thread-safety-with-testng\build.gradle) and add the following code:

buildscript {
  repositories {
    mavenCentral()
  }
}

apply plugin: 'java'

jar {
  baseName = 'multithreading'
  version =  '0.1.0'
}

repositories {
  mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
  testCompile group: 'org.testng', name:'testng', version:'6.11'
  testCompile group: 'org.assertj', name:'assertj-core', version:'3.8.0'
}

/**
 * Tests
 */
test {
  useTestNG ()
  testLogging {
    events "PASSED", "FAILED"
  }
}

In the dependencies section you declared that your project depends on TestNG and AssertJ. The dependencies are declared for the testCompile configuration, as these are relevant only for testing.

You also setup the main test task to use the TestNG framework (useTestNG()) and also configured the logging of the tests to output information about the tests that passed and failed (events "PASSED", "FAILED").

Next, you will work on the java part of the project.

Write the Java thread-safe code

Start by creating a typical directory structure for the java project:

cd thread-safety-with-testng
mkdir src\main\java\threadsafe
mkdir src\test\java\threadsafe

Your directory structure should look like this:

thread-safety-with-testng
└──src
   ├── main
   |   └── java
   |       └── threadsafe
   └── test
       └── java
           └── threadsafe

You are now ready to work on the Java code.

Create the FooThreadsafeService class

To demonstrate thread safety in Java, you will create a class named FooThreadsafeService.
This class does the following:

• It maintains an internal list of integer numbers. Initially the list will be empty.
• It offers a public method (increment()) that when called, gets the number at the end of the list, increments it by 1 and appends the new number at the end of the list. If the list is empty, it adds 0 as the first element. After calling this function N times, the list is expected to have all the numbers from 0 to N-1 in ascending order, every number in the list should appear only once and the list does not contain any null entries.
• It offers a public method (getArray()) that returns an array with the entries of the list.
• Both increment() and getArray() are thread-safe. This means that they can be called concurrently from multiple threads in any order, without causing the code to fail and without violating the expected integrity of the number list, as described above.

Using your favorite editor create the file FooThreadsafeService.java in the src\main\java\threadsafe directory (src\main\java\threadsafe\FooThreadsafeService.java) and add the following code:

package threadsafe;

import java.util.LinkedList;

public class FooThreadsafeService {
    /**
     * Use a linked list to store the integer numbers.
     */
    private LinkedList<Integer> list = new LinkedList<>();

    /**
     * Get the last number from the list, increment it by 1 and add the new number at the end of the list.
     * If the list is empty add 0 as the first element. After calling this method N times,
     * the list is expected to have all the numbers from 0 to N-1 in ascending order, every number in
     * the list should appear only once and the list should not not contain any <tt>null</tt> entries.
     */
    public synchronized void increment() {
        int nextNumber = 0;
        int size = list.size();
        if (size > 0) {
            nextNumber = list.getLast() + 1;
        }
        Integer nextInteger = new Integer(nextNumber);
        list.addLast(nextInteger);

    }

    /**
     * Returns an int[] array with the entries of the list.
     **/
    public synchronized int[] getArray() {
        int[] intArray = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            intArray[i] = list.get(i);
        }
        return intArray;
    }
}

Concurrency in detail

To understand how concurrency and thread synchronization work, consider the case, where two threads concurrently call the increment() method of the same FooThreadsafeService instance. Suppose also, that the increment() method is not synchronized. The image bellow shows how the timing of the code execution, which is probabilistic and unpredictable, leads to double entries being added in the list.

As you can imagine, the race conditions that actually occur when the code is executed, are even more complex. One additional reason for this complexity is the LinkedList used to manage the internal list of numbers. According to the LinkedList API Specification, it’s implementation is not synchronized. Therefore, if multiple threads access a LinkedList instance concurrently and at least one of the threads modifies the list structurally (add or delete one or more elements), it must be synchronized externally. If the list is not synchronized it will also exhibit an unpredictable behavior, due to the probabilistic timing of the code execution.

To address the concurrency problem, you may have considered using a synchronized list with Collections.synchronizedList(new LinkedList(...)). However, such an approach would not have solved the concurrency problem, because, as demonstrated in the example above, both increment() and getArray() functions, need to be treated as atomic operations.

The concurrency problem is solved by using the synchronized keyword at the declaration of each of the increment() and getArray() functions:

public synchronized void increment(){...}
public synchronized int[] getArray(){...}

By doing so, the code within these functions is guaranteed to be executed in a linear fashion, avoiding any race conditions that could have otherwise occurred. You can see how synchronization affects the timing of code execution in the image bellow:

Note: You can read more about synchronization in Java in the concurrency lessons of the official Java™ Tutorials. You can also read another great article on java concurency here.

Build the project

Now, you can build the project by running gradle build from the project root directory thread-safety-with-testng, where the build.gradle file is located. Gradle will download any relevant dependencies, compile the code and assemble the jar artifacts:

C:\thread-safety-with-testng>gradle build
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Total time: 40.905 secs

Note: All Gradle commands need to be run from the directory where the build.gradle file is located.

The next step is to write a test for the class code.

Write the TestNG tests

Now, you can write a test to ensure that the class behaves as expected.

The test scenario you will consider, will call the increment() and getArray() functions N times concurrently and will verify that:

• calling these functions does not cause the code to throw any exception
• the list intergity is never violated, i.e.:
– the list contains all the numbers from 0 to N-1 once and in an ascending order
– it does not contain any null entries

Using your favorite editor create the file FooThreadsafeServiceUnitTest.java in the src\test\java\threadsafe directory (src\test\java\threadsafe\FooThreadsafeServiceUnitTest.java) and add the following code:

package threadsafe;

import org.testng.annotations.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class FooThreadsafeServiceUnitTest {
    //The instance of the service used in the tests.
    FooThreadsafeService fooService = new FooThreadsafeService();

    public final static int INVOCATION_COUNT = 10;

    @Test(threadPoolSize = 2, invocationCount = INVOCATION_COUNT)
    public void ConcurrentlyInvokingMethodsIsSuccessfull() throws Exception {
        //Calling the increment() or getArray() method, while the list is being concurrently modified by other threads,
        //must not throw any exceptions and the list's integrity must not be compromised.
        fooService.increment();
        listIntegrityNotCompromised(fooService.getArray());
    }

    private void listIntegrityNotCompromised(int[] intArray) {
        //The list is not empty.
        assertThat(intArray).isNotEmpty();
        //Elements of the list are in an continuous ordered sequence and it does not contain any nulls.
        for (int i = 0; i < intArray.length; i++) {
            assertThat(intArray[i]).isEqualTo(i);
        }
    }

    @Test(alwaysRun = true, dependsOnMethods = {"ConcurrentlyInvokingMethodsIsSuccessfull"})
    public void IncrementMethodInvocationCountEqualsToListSize() {
        int[] intArray = fooService.getArray();
        //Verify the integrity of the list.
        listIntegrityNotCompromised(intArray);
        //The list contains as many elements as the number of times the increment method was called.
        assertThat(fooService.getArray().length).isEqualTo(INVOCATION_COUNT);
    }
}

The first test method ConcurrentlyInvokingMethodsIsSuccessfull() will verify that:

• Calling the increment() and getArray() functions concurrently does not throw any exceptions. If an exception is thrown the test method will fail.
• While calling the above functions concurrently, the integrity of the list is not violated, i.e.:
– The list is not empty.
– Elements of the list are in an continuous ordered sequence starting from 0 and it does not contain any null values.

The constant INVOCATION_COUNT = 10 is used to declare how many times the test will run. To execute the method concurrently, the @Test annotation attributes invocationCount and threadPoolSize are used. The method will run 10 times (invocationCount = INVOCATION_COUNT) in total and 2 threads (threadPoolSize = 2) will be used to concurrently execute it.

The second test method IncrementMethodInvocationCountEqualsToListSize() will verify that:

• The integrity of the list is not violated, as described above.
• The list contains as many elements as the number of times the increment() function was called.

This test method will be executed only after all concurrent executions of the ConcurrentlyInvokingMethodsIsSuccessfull() method have completed. This is achieved by declaring an execution order dependency, with the use of the dependsOnMethods attribute (dependsOnMethods = {"ConcurrentlyInvokingMethodsIsSuccessfull"}).

You are now ready to run the tests.

Run the tests

From the project root directory enter the command gradle test. Gradle will now download the dependencies, compile all the code, assemble the jar files and finally run the test:

C:\thread-safety-with-testng>gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava
Download https://repo1.maven.org/maven2/org/testng/testng/6.11/testng-6.11.pom
Download https://repo1.maven.org/maven2/org/assertj/assertj-core/3.8.0/assertj-core-3.8.0.pom
Download https://repo1.maven.org/maven2/org/assertj/assertj-parent-pom/2.1.6/assertj-parent-pom-2.1.6.pom
Download https://repo1.maven.org/maven2/com/beust/jcommander/1.64/jcommander-1.64.pom
Download https://repo1.maven.org/maven2/org/testng/testng/6.11/testng-6.11.jar
Download https://repo1.maven.org/maven2/org/assertj/assertj-core/3.8.0/assertj-core-3.8.0.jar
Download https://repo1.maven.org/maven2/com/beust/jcommander/1.64/jcommander-1.64.jar
:processTestResources UP-TO-DATE
:testClasses
:test
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.ConcurrentlyInvokingMethodsIsSuccessfull PASSED
Gradle suite > Gradle test > threadsafe.FooThreadsafeServiceUnitTest.IncrementMethodInvocationCountEqualsToListSize PASSED
BUILD SUCCESSFUL

Total time: 9.241 secs

Congratulations, all tests passed! You have successfully tested the thread safety of your code. You can view the test results by opening the thread-safety-with-testng\build\reports\tests\build\reports\tests\index.html file in you browser.

If you want you can change the code to make the test fail. Remove the synchronized keyword from the increment() and the getArray() functions and run the test again by executing gradle test. Depending on your environment, all, some or none of the tests will fail. As you learned in a previous chapter, which test condition will fail is unpredictable and depends on the exact state of your test environment. If your tests don’t fail, try changing the values for the threadPoolSize and invocationCount attributes, to increase to chance of creating a race condition that will cause the code to fail.

Epilogue

Congratulations! You have successfully reached the end of this tutorial.

You gained some insights in writing thread-safe code in java and also learned how to use the TestNG framework to test your code. During this process, you also gained some insights on how to use Gradle and AssertJ.

You can get the complete source code from the git repository:
git clone https://bitbucket.org/apostolosbenisis/thread-safety-with-testng.git