Skip to content

Writing Tests

Estimated time to read: 15 minutes

Test Structure

Scenario Folder Structure

- Tests
 - gradle
 - src
  - main
   - java
    - io
     - entityfour
      - product
       - Product.java
      - reward
       - RewardByConversionService.java
       - RewardByDiscountService.java
       - RewardByGiftService.java
       - RewardInformation.java
       - RewardService.java
   - resources
  - test
   - java
    - io
     - entityfour
      - RewardsByDiscountService.java
 - build.gradle
 - pom.xml
 - settings.gradle

Product package

The product package contains a class which represents a product.

It has three fields, the ID, name and price of the product, as well as a constructor, x-ers and two methods, equals and hashCode.

Product.java
package io.entityfour.Product;

public class Product {
 private long id;
 private String name;
 private double price;

 public Product(long id, String name, double price) {
  this.id = id;
  this.name = name;
  this.price = price;
 }

 public long getId() {
  return id;
 }

 public void setId(long id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public double getPrice(){
  return price;
 }

 public void setPrice(double price) {
  this.price = price;
 }

 @Override
 public boolean equals(Object o) {
  if (this == o) return true;
  if (o == null || getClass() != o.getClass()) return false;

  Product product = (Product) o;

  return id == product.id;
 }

 @Override
 public int hashCode() {
  return (int) (id ^ (id >>> 32));
 }
}

Reward package

The reward package is split up into multiple components.

RewardService

The RewardService abstract class is the base class for the three types of rewards. All of them will have a field to store the points needed for the reward, the method that will decide if the reward is given, and a helped method that calculates the total of the order given a list of products.

RewardService.java
package io.entityfour.reward;

public abstract class RewardService {
 protected long neededPoints; // Store points needed for the reward

 public abstract RewardInformation applyReward (List<Product> order, long customerPoints); // The method that decides if a reward is given

 protected double calculateTotal(List<Product> order) {
  double sum = 0;

  if (order != null) {
   sum = order.stream().mapToDouble(Product::getPrice).sum();
  }

  return sum;
 } // Helper method that calculates the total of the order, given a list of products. 

 public long getNeededPoints() {
  return neededPoints;
 }

 public void setNeededPoints(long neededPoints) {
  this.neededPoints = neededPoints;
 }


}

RewardInformation

The method applyReward in the code above returns an object of type RewardInformation updates the points that were redeemed and the discount given.

RewardInformation.java
package io.entityfour.reward;

public class RewardInformation {
 private long pointsRedeemed;
 private double discount;

 public RewardInformation() {

 }

 public RewardInformation(long pointsRedeemed, double discount) {
  this.pointsRedeemed = pointsRedeemed;
  this.discount = discount;
 }

 public long getPointsRedeemed() {
  return pointsRedeemed;
 }

 public void setPointsRedeemed(long pointsRedeemed) {
  this.pointsRedeemed = pointsRedeemed;
 }

 public double getDiscount() {
  return discount;
 }

 public void setDiscount(double discount) {
  this.discount = discount;
 }
}

RewardByConversionService

If the customer points are greater than the points needed for the reward and the total of the order is greater than the reward amount, it fills a RewardInformation object and returns it.

If these conditions are not met, we just return a RewardInformation object with the default zero values for the points and the discount.

```java title="RewardByConversionSerivce.java"

public class RewardByConversionSerivce extends RewardService { private double amount;

@Override public RewardInformation applyReward(List order, long customerPoints) { RewardInformation rewardInformation = new RewardInformation();

if(customerPoints >= neededPoints) { double orderTotal = calculateTotal(order); if(orderTotal > amount) { rewardInformation = new RewardInformation(neededPoints, amount) } }

return rewardInformation; }

public double getAmount() { return amount; }

public void setAmount(double amount) { this.amount = amount; } }

#### RewardByDiscountService

If the customer points are greater than the points needed for the reward, the discount is calculated to fill the `RewardInformation` object.

```java title="RewardByDiscountService.java"
package io.entityfour.reward;

public class RewardByDiscountService extends RewardService {
 private double amount;

 @Override
 public RewardInformation applyReward(List<Product> order, long customerPoints)  {
  RewardInformation rewardInformation = new RewardInformation();

  if(customerPoints >= neededPoints) {
   double orderTotal = calculateTotal(order);
   if(orderTotal > amount) {
    rewardInformation = new RewardInformation(neededPoints, amount)
   }
  }

  return rewardInformation;
 }

 public double getPercentage() {
  return percentage;
 }

 public void setPercentage(double percentage) {
  if(percentage > 0) {
   this.percentage = percentage
  } else {
   ...
  }
 }
}

RewardByGiftService

If the customer points are greater than the points needed for the reward, it checks if the gift product is in the order to fill a RewardInformation object with the product's price as the discount.

RewardByGiftService.java
package io.entityfour.reward;

public class RewardByGiftService extends RewardService {
 private long giftProductId;

 @Override
 public RewardInformation applyReward(List<Product> order, long customerPoints)  {
  RewardInformation rewardInformation = new RewardInformation();

  if(customerPoints >= neededPoints) {
   Optional<Product> result = order.stream().filter(p -> p.getId() == giftProductId).findAny();
   if(result.isPresent()) {
    rewardInformation = new RewardInformation(neededPoints, result.get().getPrice());
   }
  }
}

 public long getGiftProductId() {
  return giftProductId;
 }

 public void setGiftProductId(long giftProductId) {
  this.giftProductId = giftProductId
 }

Tests

Test Folder Structure

 - src
  - test
   - java
    - io
     - entityfour
      - RewardsByDiscountService.java

The test directory within the project folder contains java files and packages that are used to test different elements of the application code.

The folder structure for this directory typically mirrors the structure of the source folder, this is the convention expected by Gradle, Maven and the JUnit Console Launcher by default.

Test Classes

Each method of a test class must test exactly one thing.

Methods within a test class can be named however. Though it is good practise to include the name of the method that you are testing.

Note

When declaring a test function, it must be annotated correctly by the use of the @Test annotation so that JUnit can execute it.

The example is a test to see if the setter method setNeededPoints within the class RewardByDiscountService is valid.

RewardByDiscountServiceTest.java
package io.entityfour.reward;

import ...

class RewardByDiscountServiceTest {
 @Test
 void setNeededPoints() {
  RewardByDiscountService reward = new RewardByDiscountService(); //Create an instance of the RewardByDiscountService class.

  reward.setNeededPoints(100); //Use the setter within the above instance to set the value of neededPoints to 100

  assertEquals(100, reward.getNeededPoints()); // Assert that getNeededPoints returns 100.
  // assertEquals is a static method from the Assertions class. It is a helper method that allows you to check if the behaviour that you expect from the class is correct or not. 
 }

 @Test
 void setPercentageForPoints() {
  RewardByDiscountService reward = new RewardByDiscountService();

  reward.setPercentage(0.1);

  assertEquals(0.1, reward.getPercentage());
 }
 @Test
 void zeroCustomerPoints() {
  RewardByDiscountService reward = new RewardByDiscountService();

  reward.setPercentage(0.1);
  reward.setNeededPoints(0.1);

  Product smallDecaf = new Product(1,"Small Decaf",1.99);
  List<Product> order = Collection.singletonList(smallDecaf);

  assertEquals(0, info.getDiscount());
  assertEquals(0, info.getPointsRedeemed());
 }
}

Test Order

JUnit doesn't run test methods in a pre-defined order by default. Tests should not depend on the order they are executed. However, the @TestMethodOrder(MethordOrderer.OrderAnnotation.class) annotation can be used to explicitly imply the execution order by numerical value or @TestMethodOrder(MethordOrderer.MethodName.class) can be used to explicitly imply the execution order by method name.

Test Output

Success

If tests succeeded, JUnit will return an exit code of 0.

Failure

All the test methods will be executed, regardless if one or more fail.

JUnit will report how many tests failed and also why they failed with an AssertionFailed exception, thrown by the assertion method.

JUnit also includes the expected value from the assertion in the test method, the actual value that was returned from the application code and a stack trace of the error.

Lifecycle Methods

Each test method, generally, has four phases:

  • Arrange / Test Fixture
  • Set up the object required for the test
  • Act
  • Call the appropriate function to action a test against the object
  • Assert
  • Check the result of the action and compare its expected value against the actual value
  • Annihilation
  • Return the system back into its pre-test state. This is typically done implicitly by the JVM Garbage Collection service.## Test Hierarchies

Following the above lifecycle of a test method, can help to avoid implementing too much functionality within one test.

Arrange / Test Fixture

The test fixture phase contains everything required to execute the test. This includes, but is not limited to creating objects and setting properties.

There are three different approaches for managing Test Fixtures:

  • Transient Fresh - A new fixture is set up each time a test is run
  • Persistent Fresh - These fixtures survive between tests. However, it is initialised before each test runs
  • Persistent Shared - These fixtures also survive between tests. However, it is not initialised before each test and allows states to accumulate from test to test.

Analogy

Imagine you need to write some notes on a piece of paper.

Using a transient fresh approach, you would use a new sheet of paper for every note you need to write

Using a persistent fresh approach, you would use just one sheet of paper and erase the previous note to write a new one each time.

Using a persistent shared approach, you would use just one sheet of paper without erasing them.

Lifecycle Annotations

JUnit includes annotations which can be used to specify execution of a method during the lifecycle of a test..

Once per method Description Once per class Description
@BeforeEach Executes a method before the execution of each test @BeforeAll Executes a method before all tests are executed
@AfterEach Executes a method after the execution of each test @AfterAll Executes a method after all test are executed

Lifecycle Execution

By default, JUnit creates an instance of each test class before executing each test method to run them in isolation and to avoid unexpected side-effects due to the instance state.

This behaviour can be overridden to execute all of the test methods on the same instance. This can be controlled by annotation a test class with @TestInstance() and setting instance lifecycle to either PER_METHOD or PER_CLASS...

TestInstances
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)

or by starting the JVM with the following arguments...

JVM Arguments
-Djunit.jupiter.testinstance.lifecycle.default=per_method
-Djunit.jupiter.testinstance.lifecycle.default=per_class

or within a file named junit-platform.properties...

/junit-platform.properties
junit.jupiter.testinstance.lifecycle.default=per_method
junit.jupiter.testinstance.lifecycle.default=per_class

Lifecycle Implementation

In the examples code below, each has had its TestInstance() lifecycle value set explicitly.

The following have been added to the example from earlier:

  • A Constructor
  • Methods annotated with @BeforeAll, @BeforeEach, @AfterAll and @AfterEach
  • setUp and tearDown are popular naming conventions for before and after methods respectively

Code Example

package io.entityfour.reward;

import ...

@TestInstance(TestInstance.Lifecycle.PER_METHOD)
class RewardByDiscountServiceTest {

 RewardByDiscountServiceTest() {
  System.out.println("Constructor");
 }

 @BeforeAll //Note that @BeforeAll and @AfterAll are static when using a PER_METHOD lifecycle, as these are called before the instance of the test class is created. 
 static void setUpAll() {
  System.out.println("BeforeAll");
 }

 @BeforeEach
 void setUp() {
  System.out.println("BeforeEach");
 }

 @AfterAll //Note that @BeforeAll and @AfterAll are static when using a PER_METHOD lifecycle, as these are called before the instance of the test class is created. 
 static void tearDownAll() {
  System.out.println("AfterAll")
 }

 @AfterEach
 void tearDown() {
  System.out.println("AfterEach")
 }

 @Test
 void setNeededPoints() {
  RewardByDiscountService reward = new RewardByDiscountService();

  reward.setNeededPoints(100); 

  assertEquals(100, reward.getNeededPoints()); 
 }

 @Test
 void setPercentageForPoints() {
  RewardByDiscountService reward = new RewardByDiscountService();

  reward.setPercentage(0.1);

  assertEquals(0.1, reward.getPercentage());
 }
 @Test
 void zeroCustomerPoints() {
  RewardByDiscountService reward = new RewardByDiscountService();

  reward.setPercentage(0.1);
  reward.setNeededPoints(0.1);

  Product smallDecaf = new Product(1,"Small Decaf",1.99);
  List<Product> order = Collection.singletonList(smallDecaf);

  assertEquals(0, info.getDiscount());
  assertEquals(0, info.getPointsRedeemed());
 }
}
package io.entityfour.reward;

import ...

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RewardByDiscountServiceTest {

 RewardByDiscountServiceTest() {
  System.out.println("Constructor");
 }

 //Since the same instance is used for all the test methods and the and the instanced is created before calling the BeforeAll method, when using the PER_CLASS lifecycle, the Beforeall and AfterAll methods are not required to be static.


 @BeforeAll
 static void setUpAll() {
  System.out.println("BeforeAll");
 }

 @BeforeEach
 void setUp() {
  RewardByDiscountService reward = new RewardByDiscountService();
  System.out.println("BeforeEach");
 }

 @AfterAll
 static void tearDownAll() {
  System.out.println("AfterAll")
 }

 @AfterEach
 void tearDown() {
  System.out.println("AfterEach")
 }

 @Test
 void setNeededPoints() {
  reward.setNeededPoints(100); 
  assertEquals(100, reward.getNeededPoints()); 
 }

 @Test
 void setPercentageForPoints() {
  reward.setPercentage(0.1);
  assertEquals(0.1, reward.getPercentage());
 }
 @Test
 void zeroCustomerPoints() {
  reward.setPercentage(0.1);
  reward.setNeededPoints(0.1);

  Product smallDecaf = new Product(1,"Small Decaf",1.99);
  List<Product> order = Collection.singletonList(smallDecaf);

  assertEquals(0, info.getDiscount());
  assertEquals(0, info.getPointsRedeemed());
 }
}

Output

BeforeAll // The BeforeAll method runs before everything

Constructor // The first test initialises a new object using a constructor
BeforeEach // The BeforeEach method runs before each test
Test setPercentageForPoints // The first tests is called
AfterEach // The AfterEach method runs after each test

Constructor // The second test initialises a new object using a constructor
BeforeEach // ...
Test zeroCustomerPoints
AfterEach

Constructor
BeforeEach
Test setNeededPoints
AfterEach

AfterAll // The AfterAll method is called after all tests have been run.
Constructor // The constructor is initialised first, which is then shared between each test.

BeforeAll // The BeforeAll method runs before tests are performed

BeforeEach // The BeforeEach method runs before each test
Test setPercentageForPoints // The first tests is called
AfterEach // The AfterEach method runs after each test

BeforeEach // The BeforeEach method runs before each test
Test zeroCustomerPoints // ...
AfterEach

BeforeEach
Test setNeededPoints
AfterEach

AfterAll // The AfterAll method is called after all tests have been run.

Test Hierarchies

When setting values within a test class, there are three possible places that these values can be placed for them to be parsed.

  • Within each method
  • Within the @BeforeEach annotated setup method
  • Within non-static inner classes, annotated within the @Nested annotation to create a test hierarchy and express relationships among several groups of tests.

Nested classes

Non-Static inner classes are the preferred method for declaring these values and running tests.

@BeforeEach and @AfterEach working inside nested tests. However, @BeforeAll and @AfterAll do not work by default as static members within inner classes are not allowed, unless a class lifecycle mode is setup within the class.

Within the inner class, there can be tests that use members of the outer class, initialised in the outer setUp method, as well as members of the inner class. Nesting can be arbitrarily deep, so tests can contain tests, so long as it is annotated using @Nested.

Behaviour-Driven Development (BDD)

An application is specified and designed by describing how it should behave.

BDD aligns with the Test Phases listed earlier:

Test Phases BDD
Arrange Given
Act When
Assert Then

Example

Given an action, when an action is performed, then expect a result.

Display Names

@DisplayName annotation

@DisplayName can be used at the class and method level to change the name of the test to any unicode string. This enables a developer to create a framework based around a natural language and program accordingly. It also increases readability at test completion.

@DisplayNameGeneration annotation

@DisplayNameGeneration can be used to declare a custom DisplayName generator for the test class. These can be created by implementing the interface DisplayNameGenerator.

  • Simple = The default behaviour, removes trailing parentheses for methods with no parameters.
  • ReplaceUnderscores - replaces underscores in method names with spaces.
  • IndicativeSentences, generates complete sentences by concatenating the names of the test methods and the enclosing classes.

Assertions

A test is a boolean operation, it only returns true or false.

A good test should only have one act and assert phase per operation.

A test should have one action followed by one or more physical assertions that form a single logical assertions about the action.

JUnit includes many other methods to evaluate if an expected outcome has been achieved.

JUnit Jupiter Assertions

External assertion libraries can also be used, such as AssertJ and Hamcrest.

Assertion Usage

In most cases, assertion methods take an expected outcome, the actual outcome, and optionally a message string, as inputs.

assertEquals

When using multiple assertions within a test, if one assertions fails, the rest within that test will not be executed, to indicate a failure.

assertNotNull(info);
assertEquals(2, info.getDiscount());
assertEqual(10, info.getPointsRedeemed());

Alternatively, a lambda expression can be used with assertAll to wrap assertions, in this instance, even if an assertion fails the others will continue to be executed.

assertAll("Heading Text",
    () -> assertNotNull(info),
    () -> assertEquals(2, info.getDiscount()),
    () -> assertEquals(10, info.getPointsRedeemed())
   );

assertThrows

It it possible to test for invalid / illegal values via the usage of assertThrows. assertThrows can be used to assert the execution of a supplied lambda and that the exception is of the expected type. If no exception or an exception of a different type is thrown, this method will fail.

assertThrows(RuntimeException.class, () -> {
 reward.setGiftProductId(ProductId);
});

assertDoesNotThrow

To test if no exception is thrown, use assertDoesNotThrow

assertTimeout

assertTimeout is used to test if a supplied lambda expression completes before the specified timeout duration is reached.

RewardInformation info = assertTimeout (
 Duration.ofMillis(4),
 () ->
    reward.applyReward(
     buildSampleOrder(numberOfProducts),
     200)
);

If a timeout is exceeded, the test operation still completes. In order to timeout the test operation in full if the duration is exceeded then assertTimeoutPreemptively can be used. This will also run the lambda in a different thread.

Disabling Tests

To disable individual methods and classes fully within JUnit 5, use the @Disabled annotation.

Annotation-Based Conditions

However, JUnit 5 also provides annotations that can be used to target specific conditions. For example

Enable Disable Description
@EnableOnOS( { LINUX, MAC }) @DisableOnOS(WINDOWS ) Enable / Disable test depdendant on the OS.
@EnableOnJre(JAVA_8 ) @DisableOnJre({ JAVA_9, JAVA_10 }) Enable / Disable test depending on the version of the JRE.
@EnableForJreRange(max=JAVA_10 ) @DisableForJreRange(min=JAVA_11,max=JAVA_14 ) Enable / Disable test for a given range of JREs.
@EnableIfSystemProperty(named="version, matches=1.4 ") @DisableIfSystemProperty(named="dev", matches="true ") Enable / Disable test based upon the value of a JVM system property.
@EnabledIfEnvironmentVariable(named="ENV", matches="*server") @DisabledIfEnvironmentVariable(named="ENV", matches="QA" ) Enable / Disable test based upon the value of an e nvironment variable.
@EnabledIf("methodName" ) @DisabledIf("com.example.MyClass#myMethod ") Enable / Disable test based on the boolean return value of a method specified with its name of its FQN, if it is defined outside of the test class.

Example

Disabled Tests
@Test
@DisplayName("Should not exceed timeout")
@Disabled("Optimisation not yet implemented")
void timeoutNotExceeded() {
 int numberOfProducts = 50_000;
 reward.setGiftProductId(numberOfProducts - 1);

 RewardInformation info = assertTimeoutPreemptively(
  Duration.ofMillis(4),
  () -> 
     reward.applyReward(
         buildSampleOrder(numberOfProducts),
         200
     )
 );

 assertEquals(2..99, info.getDiscount());
}

Assumptions

Assumptions are an easy way to conditionally stop the execution of the test. For example, if the test depends on something that doesn't exist in the current environment.

Failed assumptions do not result in a test failure. A failed assumption simply aborts the test.

Types of assumptions

assumeTrue

assumeTrue - Validates if the given assumption evaluates to true to allow the execution of the test.

The assumption can be a a boolean expression of a lambda expression that represents the functional interface BooleanSupplier

assumeTrue(boolean assumption)
assumeTrue(boolean assumption, String message)
assumeTrue(BooleanSupplier assumptionSupplier)
assumeTrue(boolean assumption, Supplier<String> message)
assumeTrue(BooleanSupplier assumptionSupplier, String message)
assumeTrue(BooleanSupplier assumptionSupplier, Supplier<String> message)

assumeFalse

assumeFalse validates if the given assumption evaluates to false to allow the execution of the test.

assumeFalse(boolean assumption)
assumeFalse(boolean assumption, String message)
assumeFalse(BooleanSupplier assumptionSupplier)
assumeFalse(boolean assumption, Supplier<String> message)
assumeFalse(BooleanSupplier assumptionSupplier, String message)
assumeFalse(BooleanSupplier assumptionSupplier, Supplier<String> message)  

assumingThat

Executes the supplied lambda expression that represents the functional interface Executable only if the given assumption evaluates to true.

assumingThat(boolean assumption, Executable executable)
assumingThat(BooleanSupplier assumptionSupplier, Executable executable)

Example

@Test
@DisplayName("When empty order and enough points no rewards should be applied")
void emptyOrderEnoughPoints() {
 RewardInformation info = reward.applyReward(getEmptyOrder(), 200);

 assertEquals(0, info.getDiscount());

 assumeTrue("1".equals(System.getenv("TEST_POINTS")));
 // Only execute the next assert if the above assumption is valid
 assertEquals(0, info.getPointsRedeemed());
}
@BeforeEach
void setUp() {
 reward = new RewardByConversionService();
 reward.setAmount(10);
 reward.setNeededPoints(100);
 assumeTrue("1".equals(System.getenv("TEST_POINTS")))
}
@Test
@DisplayName("When empty order and enough points no rewards should be applied")
void emptyOrderEnoughPoints() {
 RewardInformation info = reward.applyReward(getEmptyOrder(), 200);

 assertEquals(0, info.getDiscount());

 assumingThat("1".equals(System.getenv("TEST_POINTS")),
    () -> {
     assertEquals(10, info.getPointsRedeemed());
    });
}

Test Interfaces and Default Methods

With JUnit 5, methods annotated with @Test, @BeforeEach and @AfterEach can be moved to an interface and then have the test class implemented to us those methods.

Other annotations such as @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @ExtendWith, @Tag, and many others, can also be implemented within an interface.

As a special case, methods annotated with @BeforeAll and @AfterAll must be static.

Anything that can be used within a test class can also be moved to an interface.

Repeating Tests

JUnit provides the @RepeatedTest annotation to repeat a test. The number of repetitions is fixed, it can not be changed at runtime. Each invocation of a repeated test behaves like a regular test method with full lifecycle support.

Note

@RepeatedTest substitutes the @Test annotation. If @Test is included alongside @RepeatedTest, JUnit will generate an error at runtime.

Custom Display Names

A custom display name can be assigned to each repetition of the test.

Placeholders

  • {displayName} - The displayName of the current test method
  • {currentRepetition} - The iteration of the current test being run
  • {totalRepetitions} - The total iterations of tests to be run

Long Display Name

RepeatedTest.LONG_DISPLAY_NAME
{displayName} :: repetition {currentRepetition} of {totalRepetitions}
// My Test :: repetition 1 of 10

Short Display Name (Default)

RepeatedTest.SHORT_DISPALY_NAME
repetition {currentRepetition} of {totalRepetitions}
// repetition 1 of 10

RepetitionInfo Interface

@RepeatedTest, @BeforeEach and @AfterEach can be passed an object of type RepetitionInfo as a parameter.

RepetitionInfo is an interface with the method int getCurrentRepetition(); and int getTotalRepetitions();

Example

@RepeatedTest(5) // Annotated to repeat the test 5 times
void methodOne(){
 ...
}


@RepeatedTest(value = 5, name = "{displayName} - > {currentRepetition}/{totalRepetitions}") // Repeats five times and outputs the name of each iteration.
// Note that the value and the name have to be specified using attributes 
@DispalyName("Test Two")
void methodTwo() {
 ...
}

@RepeatedTest(value = 5, name = "{displayName} - > {currentRepetition}/{totalRepetitions}")
@DisplayName("Test Three")
void methodThree(RepetitionInfo repetitionInfo) { // Inject type RepetitionInfo from JUnit. This can be used within the test method. In the example below, it is used to set the productId + 1000.
 long productId = repetitionInfo.getCurrentRepetition() + 1000;
 System.out.println(productId);
 reward.setGiftProductId(productId);

 ...

 private long getRandomProductIdNotInOrder() {
  Random r = new Random();
  return r.longs(1000,2000).findFirst().getAsLong();
 }
}