codete Testing a Spring Boot Application with Different J Unit Runners 1 main 8cb5827fcb
Codete Blog

Testing Spring Boot Application with Different JUnit Runners

avatar male f667854eaa

17/03/2020 |

17 min read

Ryszard Kuśnierczyk

JUnit is probably the most popular Java testing framework. Every JUnit test is using a Runner. It's an abstract class, which is most of all responsible for invoking test methods. Runners are pretty straightforward to be customized.  

This article is not about how JUnit runners work internally – there's plenty of great articles about that on the web already. It's about a huge variety of Runners which are already implemented and ready to be used out of the box. I will focus only on Runners, which can be declared directly inside @RunWith annotation.

I've prepared a simple Spring Boot application, which provides a simple REST endpoint. It displays a number of milliseconds from the epoch of 1970-01-01T00:00:00Z to the date provided by the user. If date param is not provided it’s displaying the epoch of current time taken from external API – worldclockapi.com. This application is pretty simple, but it has some implementation pitfalls, which makes it a little bit more difficult to test in some cases. 

For testing, we will use JUnit to run tests and assertions, and Mocktito with PowerMock for mocking objects and methods. 

Now let's begin with our app

The most important classes are:
 

DateUtilsRestController

@RestController

@RequestMapping("date")

public class DateUtilsRestController {

   @Autowired

   private DateToEpochMilliConvertingService dateToEpochMilliConvertingService;


 

   /**

    * @param date date in format yyyy-MM-ddThh:mm:ss

    * @return converts input date to the number of milliseconds from the epoch of 1970-01-01T00:00:00Z.

    * If the date is not present it's taken from world clock api.

    */

   @GetMapping(value = "/epochMillis", produces = MediaType.APPLICATION_JSON_VALUE)

   public Long getEpochMilli(@RequestParam(value = "date", required = false) String date) {

       return Optional.ofNullable(date)

               .map(textDate -> dateToEpochMilliConvertingService.convertToEpochMilli(textDate))

               .orElseGet(() -> dateToEpochMilliConvertingService.convertWorldClockDateToEpochMilli());

   }


 

   @ExceptionHandler(DateTimeException.class)

   @ResponseStatus(value = HttpStatus.BAD_REQUEST)

   public String handleDateTimeException(DateTimeException exception) {

       return exception.getMessage();

   }


 

   @ExceptionHandler(RuntimeException.class)

   @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)

   public String handleAnyException(RuntimeException exception) {

       return exception.getMessage();

   }

}

 

DateToEpochMilliConvertingService:

@Service

public class DateToEpochMilliConvertingService {


 

   @Autowired

   private DateUtils dateUtils;


 

   public long convertToEpochMilli(String date) {

       if (DateValidator.isDateValid(date)) {

           LocalDateTime dateTime = dateUtils.parseLocalDateTime(date);

           return dateUtils.convertToEpochMilli(dateTime);

       }


 

       throw new DateTimeException("Date has invalid format, it should be yyyy-MM-ddThh:mm:ss");

   }


 

   public long convertWorldClockDateToEpochMilli() {

       try {

           LocalDateTime currentDateTime = WorldClockDateClient.getWorldClockDateTime();

           return dateUtils.convertToEpochMilli(currentDateTime);

       } catch (IOException e) {

           throw new RuntimeException("Could not get current date time from World Clock API");

       }

   }

}

 

DateUtils:

private DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;


 

public long convertToEpochMilli(LocalDateTime localDateTime){

   return localDateTime.atZone(ZoneId.systemDefault())

           .toInstant().toEpochMilli();

}


 

public LocalDateTime parseLocalDateTime(String date){

   return LocalDateTime.parse(date , dateFormatter);

}

 

DateValidator:

public class DateValidator {

  private static final String DATE_FORMAT_PATTERN = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}";

  public static boolean isDateValid(String date){
      return !StringUtils.isEmpty(date) && date.matches(DATE_FORMAT_PATTERN);
  }
}

 

WorldClockDateClient:

public class WorldClockDateClient {


 

   private static final String CURRENT_DATE_TIME_ATTRIBUTE = "currentDateTime";

   private static final String WORLD_CLOCK_API_URL = "http://worldclockapi.com/api/json/utc/now";


 

   /**

    *

    * @return JSON object with Coordinated Universal Time

    * @throws IOException

    */

   public static LocalDateTime getWorldClockDateTime() throws IOException {

       HttpGet request = new HttpGet(WORLD_CLOCK_API_URL);

       CloseableHttpClient httpClient = HttpClients.createDefault();

       HttpResponse response = httpClient.execute(request);

       HttpEntity entity = response.getEntity();

       String entityContents = EntityUtils.toString(entity);

       JSONObject worldClockDate = new JSONObject(entityContents);


 

       String currentDateTime = worldClockDate.getString(CURRENT_DATE_TIME_ATTRIBUTE);

       return LocalDateTime.parse(currentDateTime, DateTimeFormatter.ISO_DATE_TIME);

   }

}

You’ve probably already noticed that WorldClockDateClient has a public static method instead of being declared as Spring bean... That’s correct – this is our pitfall. :)  

Now it’s time for some testing. Let’s start with something simple – we have our DateValidator class with two static methods to check if the date and time have the correct format. 

Let’s create a simple unit test

1. JUnit Runners: BlockJUnit4ClassRunner

public class DateValidatorTest {


 

   @Test

   public void isDateValidTest_validDate() {

       //given

       String validDate = "2020-01-01T12:01:01";


 

       //when

       boolean isDateValid = DateValidator.isDateValid(validDate);


 

       //then

       Assert.assertTrue(isDateValid);

   }


 

   @Test

   public void isDateValidTest_invalidDate() {

       //given

       String invalidDate = "01-02-2012T12:01:01";


 

       //when

       boolean isDateValid = DateValidator.isDateValid(invalidDate);


 

       //then

       Assert.assertFalse(isDateValid);

   }

}

As you can see there is no @RunWith annotation. We are implicitly using JUnit BlockJUnit4ClassRunner located in org.junit.runners package, which is a default Runner class, used for single test classes. It’s responsible for running all methods annotated with @Test, @Before, @After etc. in the correct order.

2. JUnit Runners: Parametrized

Our test is working, but there’s much more to be tested, especially in the case of negative scenarios - there may be many more cases. Adding another test method for each case doesn’t make any sense. We can create a list of inputs and run assertions in a loop - better, but JUnit has a much more elegant way to create such a test. Here comes Parameterized runner:

@RunWith(Parameterized.class)
public class DateValidatorParameterizedTest {

  private String date;
  private Boolean expectedResult;

  public DateValidatorParameterizedTest(String date, Boolean expectedResult) {
      this.date = date;
      this.expectedResult = expectedResult;
  }

  @Parameterized.Parameters
  public static Collection dates() {
      return Arrays.asList(new Object[][] {
              { "2020-01-01T12:01:01", true },
              { "01-01-2020T12:01:01", false },
              { "", false },
              { null, false }
      });
  }

  @Test
  public void isDateValidTest() {
      Assert.assertEquals(expectedResult, DateValidator.isDateValid(date));
  }
}

We can prepare a list of inputs and expected results, then mark them with @Parameterized. Parameters annotation and Parameterized Runner will execute each scenario for us. This runner is internally using BlockJUnit4ClassRunnerWithParameters, which can’t be used directly out of the box (as @RunWith) – it’s created by ParametersRunnerFactory inside Parameterized runner class. 

This Runner can do much, much more and it’s also improved in JUnit 5. It’s just a short example to keep in mind that there is such a Runner and it’s useful for tests with more cases. There’s also an alternative to Parameterized Runner – it’s an external library called JUnitParams. It has some great features and makes parameterized tests even simpler. You can find it (and manual) here: https://github.com/Pragmatists/JUnitParams 

3. JUnit Runners: Suite

In our app, we have one more component, which can be tested separately – DateConverter. I’ve created a simple unit test for this component:

public class DateConverterTest {

  private DateConverter underTest;

  private LocalDateTime testDate;

  @Before
  public void init(){
      underTest = new DateConverter();
      testDate = LocalDateTime.of(2020, 12, 12, 12, 12, 12);
  }

  @Test
  public void convertToEpochMilliTest(){
      //given
      long expectedResult = 1607771532000L;

      //when
      long dateConvertedToEpochMilli = underTest.convertToEpochMilli(testDate);

      //then
      Assert.assertEquals(expectedResult, dateConvertedToEpochMilli);
  }

  @Test
  public void parseLocalDateTimeTest(){
      //given
      String date = "2020-12-12T12:12:12";

      //when
      LocalDateTime parsedLocalDateTime = underTest.parseLocalDateTime(date);

      //then
      Assert.assertEquals(testDate, parsedLocalDateTime);
  }
}

Now we have 2 simple unit tests: DateValidatorParameterizedTest (we don’t need DateValidatorTest anymore) and DateConverterTest. If we want to make sure that those two tests are always run together, we can use suites and SuiteRunner:

@RunWith(Suite.class)
@Suite.SuiteClasses({DateValidatorTest.class, DateConverterTest.class})
public class AllUnitTests {
  
}

The first question that comes to my mind is… WHY? Does it have any use cases? The best answer is here: https://stackoverflow.com/questions/36901680/why-use-junit-test-suites. 

The second question is: will Maven or Gradle run both single test classes and test suite, so there will be duplicated tests running? The answer is: NO, both Maven and Gradle are ignoring tests with Suite runner by default.

4. JUnit Runners: Enclosed

We have now our DateValidatorParameterizedTest and DateConverterTest, running together in-suite. Both tests are using the DateUtils service, and both may use the same date object for tests. It would be great if we could re-use our logic for both tests with inheritance.  There is also one problem: one of these tests is a basic BlockJUnit4ClassRunner test, the second one is using Parameterized runner. 

If we want to bind these tests together, Suite runner won't be helpful. We need an Enclosed runner. You can find this runner inside the org.junit.experimental.runners package. It extends Suite runner and allows running our tests inside static inner classes. The main difference between Enclosed and Suite runner is that with the second one you can extend the base class and share its members and init() method. The first one is simply binding classes together. 

Let’s put our DateValidatorParameterizedTest and DateConverterTest inside one Enclosed test class:

@RunWith(Enclosed.class)

public class AllUnitEnclosedTest {


 

   public abstract static class AbstractBaseTest {

       // must be created here, not inside init() method

       protected static LocalDateTime testDate

               = LocalDateTime.of(2020, 12, 12, 12, 12, 12);

       protected DateUtils dateUtils;


 

       @Before

       public void init() {

           dateUtils = new DateUtils();

       }

   }


 

   @RunWith(Parameterized.class)

   public static class DateValidatorParameterizedTest extends AbstractBaseTest {


 

       private String date;

       private Boolean expectedResult;


 

       public DateValidatorParameterizedTest(String date, Boolean expectedResult) {

           this.date = date;

           this.expectedResult = expectedResult;

       }


 

       @Parameterized.Parameters

       public static Collection dates() {

           return Arrays.asList(new Object[][] {

                   { testDate.toString(), true }, //date is shared between tests

                   { "01-01-2020T12:01:01", false },

                   { "", false },

                   { null, false }

           });

       }


 

       @Test

       public void isDateValidTest() {

           Assert.assertEquals(expectedResult, DateValidator.isDateValid(date));

       }

   }


 

   public static class DateUtilsTest extends AbstractBaseTest {


 

       @Test

       public void convertToEpochMilliTest(){

           //given

           long expectedResult = 1607771532000L;


 

           //when

           long dateConvertedToEpochMilli = dateUtils.convertToEpochMilli(testDate); //date is shared between tests


 

           //then

           Assert.assertEquals(expectedResult, dateConvertedToEpochMilli);

       }


 

       @Test

       public void parseLocalDateTimeTest(){

           //given

           String date = "2020-12-12T12:12:12";


 

           //when

           LocalDateTime parsedLocalDateTime = dateUtils.parseLocalDateTime(date);


 

           //then

           Assert.assertEquals(testDate, parsedLocalDateTime); //date is shared between tests

       }

   }

}

As you can see we have one base class called AbstractBaseTest and other test classes are extending this class. We can even use different Runner for each class, and the init() method and all members of our AbstractBaseTest class will be shared between other test classes. You must remember that static classes have some limitations, for example, I can’t create our testDate from AbstractBaseTest inside init() method, or make it nonstatic. Making it nonstatic won’t even compile, because of @Parameterized. 

The parameters method used inside DateValidatorParameterizedTest must be static. Changing our testDate to static and initializing it inside @ init() method will cause NullPointerException, because our static dates() method from DateValidatorParameterizedTest will be executed before non static init() method from AbstractBaseTest which initializes our testDate object.

5. Mockito Runners: MockitoJUnitRunner

Now it’s time to test one of our Spring beans, called DateToEpochMilliConvertingService with Mockito. MockitoJUnitRunner is the default Mockito runner, which allows us to run tests with mocked objects.

@RunWith(MockitoJUnitRunner.class)
public class DateToEpochMilliConvertingServiceMockitoJUnitRunnerTest {

  @InjectMocks
  private DateToEpochMilliConvertingService underTest;

  @Mock
  private DateUtils dateUtils;

  @Test()
  public void dateToEpochMilliConvertingServiceTest_validDate() {
      //given
      long expectedEpochMilli = 999L;
      Mockito.doReturn(expectedEpochMilli).when(dateUtils).convertToEpochMilli(any());

      //when
      long epochMilli = underTest.convertToEpochMilli("1970-01-01T00:01:01");

      //then
      Assert.assertEquals(expectedEpochMilli, epochMilli);
  }

  @Test(expected = DateTimeException.class)
  public void dateToEpochMilliConvertingServiceTest_invalidDate() {
      underTest.convertToEpochMilli("invalid_date");
  }

  @Test(expected = DateTimeParseException.class)
  public void dateToEpochMilliConvertingServiceTest_dateParsingError() {
      Mockito.doThrow(DateTimeParseException.class)
              .when(dateUtils).parseLocalDateTime(any());

      underTest.convertToEpochMilli("1970-99-01T00:01:01");
  }

  /*
  Really bad test - it's making a real call to external API and we can't assert exact result.
    */
  @Test
  public void convertWorldClockDateToEpochMilliTest() throws IOException {
      //given
      Mockito.when(dateUtils.convertToEpochMilli(any())).thenCallRealMethod();

      //when
      long epochMilli = underTest.convertWorldClockDateToEpochMilli();

      //then
      Assert.assertNotEquals(0L, epochMilli);
  }

Everything goes well, except the last test: convertWorldClockDateToEpochMilliTest(). We are testing DateToEpochMilliConvertingService, which is doing a real call to external API:

public long convertWorldClockDateToEpochMilli() throws IOException {
  JSONObject worldClockDate = WorldClockDateClient.getWorldClockDate();
  LocalDateTime currentDateTime = LocalDateTime.parse(worldClockDate.getString("currentDateTime"),
          DateTimeFormatter.ISO_DATE_TIME);
  return dateUtils.convertToEpochMilli(currentDateTime);
}

Unit tests shouldn't be dependent on external API. The second problem is that WorldClockDateClient.getWorldClockDate() is a static method, which we can’t mock with the Mockito framework. So what now?

 

6. PowerMock Runners: PowerMockRunner

PowerMock is a framework that helps with cases where other mocking frameworks like mockito fail. It mocks static methods and does much more: 

“PowerMock uses a custom classloader and bytecode manipulation to enable mocking of static methods, constructors, final classes and methods, private methods, removal of static initializers and more.” 

The first question that comes to my mind is: ok, let’s use PowerMock then, but our test is already using MockitoJUnitRunner. We can’t declare two runners at the same time, so what now? The solution is very easy: PowerMock has @PowerMockRunnerDelegate annotation. You can use it to delegate PowerMock to any other runner you want, but before that PowerMock will use its runner to do its mocking magic. 

Here’s the code:

@RunWith(PowerMockRunner.class)

@PowerMockRunnerDelegate(MockitoJUnitRunner.class)

@PrepareForTest(WorldClockDateClient.class)

public class DateToEpochMilliConvertingServicePowerMockRunnerWithDelegateTest {


 

   @InjectMocks

   private DateToEpochMilliConvertingService underTest;


 

   @Mock

   private DateUtils dateUtils;


 

   @Test()

   public void dateToEpochMilliConvertingServiceTest_validDate() {

       //given

       long expectedEpochMilli = 999L;

       Mockito.doReturn(expectedEpochMilli).when(dateUtils).convertToEpochMilli(any());


 

       //when

       long epochMilli = underTest.convertToEpochMilli("1970-01-01T00:01:01");


 

       //then

       Assert.assertEquals(expectedEpochMilli, epochMilli);

   }


 

   @Test(expected = DateTimeException.class)

   public void dateToEpochMilliConvertingServiceTest_invalidDate() {

       underTest.convertToEpochMilli("invalid_date");

   }


 

   @Test(expected = DateTimeParseException.class)

   public void dateToEpochMilliConvertingServiceTest_dateParsingError() {

       Mockito.doThrow(DateTimeParseException.class)

               .when(dateUtils).parseLocalDateTime(any());


 

       underTest.convertToEpochMilli("1970-99-01T00:01:01");

   }


 

   @Test

   public void convertWorldClockDateToEpochMilliTest() throws IOException {

       //given

       LocalDateTime testDate = LocalDateTime.of(2020, 12, 12, 12, 12, 12);

       long expectedResult = 1607771532000L;

       JSONObject worldClockApiResponse = new JSONObject();

       worldClockApiResponse.put("currentDateTime", testDate.format(DateTimeFormatter.ISO_DATE_TIME));


 

       Mockito.when(dateUtils.convertToEpochMilli(any())).thenCallRealMethod();

       PowerMockito.mockStatic(WorldClockDateClient.class);

       PowerMockito.when(WorldClockDateClient.getWorldClockDate()).thenReturn(worldClockApiResponse);


 

       //when

       long epochMilli = underTest.convertWorldClockDateToEpochMilli();


 

       //then

       Assert.assertEquals(expectedResult, epochMilli);

   }


 

   @Test(expected = UnknownHostException.class)

   public void convertWorldClockDateToEpochMilliTest_worldClockAPIisOffline() throws IOException {

       PowerMockito.mockStatic(WorldClockDateClient.class);

       PowerMockito.when(WorldClockDateClient.getWorldClockDate()).thenThrow(UnknownHostException.class);

       underTest.convertWorldClockDateToEpochMilli();

   }

}

As you can see, we’ve got rid of the call to external API from convertWorldClockDateToEpochMilliTest by mocking static method:

PowerMockito.mockStatic(WorldClockDateClient.class);
      PowerMockito.when(WorldClockDateClient.getWorldClockDate()).thenReturn(worldClockApiResponse);

The rest of the tests remain the same. We can also test what happens with our service when external API is offline by mocking our WorldClockDateClient to throw an exception, just like with Mockito. The only problem that you will probably face is that PowerMock combined with Mockito sometimes has compatibility issues. It may throw some exceptions and report missing internal classes etc. You need to test which version of Mockito is compatible with which version of PowerMock. 

7. Spring Runners: SpringJUnit4ClassRunner aka SpringRunner

Our application is Spring Boot based, so let’s use Spring Runners for our tests. SpringRunner is the base Spring framework Runner. It extends SpringJUnit4ClassRunner, but that’s all – it’s just an alias:

public final class SpringRunner extends SpringJUnit4ClassRunner {
  public SpringRunner(Class<?> clazz) throws InitializationError {
      super(clazz);
  }
}

Spring framework developers try to make things as simple as possible, so probably that was the reason to change the name of the Runner from SpringJUnit4ClassRunner to SpringRunner with backward compatibility. Here’s our test: 

@RunWith(SpringRunner.class)
@SpringBootTest
public class DateToEpochMilliConvertingServiceSpringRunnerTest {

  @Autowired
  private DateToEpochMilliConvertingService underTest;

  @MockBean
  private DateUtils dateUtils;

  @Test()
  public void dateToEpochMilliConvertingServiceTest_validDate() {
      //given
      long expectedEpochMilli = 999L;
      Mockito.doReturn(expectedEpochMilli).when(dateUtils).convertToEpochMilli(any());

      //when
      long epochMilli = underTest.convertToEpochMilli("1970-01-01T00:01:01");

      //then
      Assert.assertEquals(expectedEpochMilli, epochMilli);
  }

  @Test(expected = DateTimeException.class)
  public void dateToEpochMilliConvertingServiceTest_invalidDate() {
      underTest.convertToEpochMilli("invalid_date");
  }

We already know how to handle static methods, so I’ve simplified our test, and it has only two simple tests just to explain how SpringRunner works. @SpringBootTest annotation is used to start Spring ApplicationContext, and @MockBean annotation is used to create Mockito mocks and put them into a Spring ApplicationContext container before our test starts. We can also use @Autowire to inject beans. 

Now our DateUtils class is a bean located in Spring ApplicationContext and it’s injected into our DateToEpochMilliConvertingService bean just like it would happen when we run our application. Now we can test our components as a part of the Spring Boot application.  This kind of Runner is better for integration tests than unit tests, because it’s running the whole Spring container, with all other beans, so it’s more memory-consuming and takes much more time to launch the test. 

Spring provides one SpringRunner, which can be combined with a few more annotations, which will automatically run different test scenarios, depending on what we want to test: @SpringBootTest, @JsonTest, @WebMvcTest, @DataJpaTest, etc. You can read more about it here: https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/html/boot-features-testing.html

 

8. Let’s combine our runners

We have tested most of the common runners provided by JUnit, Mockito, PowerMock, and Spring framework. Now it's time for a final “end to end” test. We will test the whole application, by making requests directly to our REST API. The idea is to create one Enclosed test with two static test classes:

  • EndToEndSpringRunnerWithParameterizedRunnerTest: it will test our API with the “date” parameter as a parameterized test. It’s using a Parameterized runner combined with SpringRunner.
  • EndToEndSPowerMockRunnerWithSpringRunnerTest: it will call our API without the “date” parameter, which results in calling external world clock API. This test will have two methods: one will let our API call external API, the second one will mock it. Here we have PowerMockRunner, delegated to SpringRunner.

Now we have a huge mix of runners: main EndToEndTest class annotated with @RunWith(Enclosed.class), which contains an abstract class AbstractBaseTest annotated with @SpringBootTest. Then we have two classes extending AbstractBaseTest using PowerMockRunner and Parameterized runner. So here’s the code:

@RunWith(Enclosed.class)
public class EndToEndTest {

  @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  public abstract static class AbstractBaseTest {

      static {
          TomcatURLStreamHandlerFactory.disable();
      }

      @LocalServerPort
      protected int port;

      @Autowired
      protected TestRestTemplate restTemplate;

      protected static final String API_STATUS_SUCCESS = "success";
      protected static final String API_STATUS_ERROR = "error";
      protected static final LocalDateTime TEST_DATE
              = LocalDateTime.of(2020, 12, 12, 12, 12, 12);

      protected String getBaseApiUrl(){
          return String.format("http://localhost:%d/date/convertToEpochMilli", port);
      }

  }

  @RunWith(Parameterized.class)
  public static class EndToEndSpringRunnerWithParameterizedRunnerTest extends AbstractBaseTest {

      private String inputDate;
      private String expectedStatus;
      private String expectedResult;

      public EndToEndSpringRunnerWithParameterizedRunnerTest(String inputDate, String expectedStatus, String expectedResult) {
          this.inputDate = inputDate;
          this.expectedStatus = expectedStatus;
          this.expectedResult = expectedResult;
      }

      @Parameterized.Parameters
      public static Collection data() {
          return Arrays.asList(new String[][]{
                  {TEST_DATE.format(DateTimeFormatter.ISO_DATE_TIME), API_STATUS_SUCCESS, "1607771532000"},
                  {null, API_STATUS_ERROR, "Date has invalid format, it should be yyyy-MM-ddThh:mm:ss"},
                  {"", API_STATUS_ERROR, "Date has invalid format, it should be yyyy-MM-ddThh:mm:ss"},
                  {"wrong_input", API_STATUS_ERROR, "Date has invalid format, it should be yyyy-MM-ddThh:mm:ss"},
                  {"2020-99-12T12:12:12", API_STATUS_ERROR, "Text '2020-99-12T12:12:12' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 99"}
          });
      }

      @ClassRule
      public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();

      @Rule
      public final SpringMethodRule springMethodRule = new SpringMethodRule();

      @Test
      public void apiTestWithDifferentDateParameters() throws JSONException, URISyntaxException {
          String apiUrl = UriComponentsBuilder
                  .fromUriString(getBaseApiUrl())
                  .queryParam("date", inputDate)
                  .build()
                  .toUriString();
          JSONObject apiResponse = new JSONObject(restTemplate.getForObject(apiUrl,
                  String.class));
          String status = apiResponse.getString("status");
          Assert.assertEquals(status, expectedStatus);
          if (API_STATUS_SUCCESS.equals(status)) {
              Assert.assertEquals(expectedResult, apiResponse.getString("epochMilli"));
          } else {
              Assert.assertEquals(expectedResult, apiResponse.getString("cause"));
          }
      }
  }

  @RunWith(PowerMockRunner.class)
  @PowerMockRunnerDelegate(SpringRunner.class)
  @PrepareForTest(WorldClockDateClient.class)
  @PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "javax.xml.transform.*", "org.xml.*",
          "javax.management.*", "javax.net.ssl.*", "com.sun.org.apache.xalan.internal.xsltc.trax.*"})
  public static class EndToEndSPowerMockRunnerWithSpringRunnerTest extends AbstractBaseTest {

      @Test
      public void apiTestWithNoParametersAndMockedWorldClockApiResponse() throws IOException, JSONException {
          //given
          String expectedResult = "1607771532000";
          JSONObject worldClockApiResponse = new JSONObject();
          worldClockApiResponse.put("currentDateTime", TEST_DATE.format(DateTimeFormatter.ISO_DATE_TIME));

          PowerMockito.mockStatic(WorldClockDateClient.class);
          PowerMockito.when(WorldClockDateClient.getWorldClockDate()).thenReturn(worldClockApiResponse);

          //when
          JSONObject apiResponse = new JSONObject(this.restTemplate.getForObject(getBaseApiUrl(),
                  String.class));

          //then
          String status = apiResponse.getString("status");
          API_STATUS_SUCCESS.equals(status);
          Assert.assertEquals(expectedResult, apiResponse.getString("epochMilli"));
      }

      @Test
      public void apiTestWithNoParametersAndWorldClockApiCall() throws IOException, JSONException {
          //given
          PowerMockito.mockStatic(WorldClockDateClient.class);
          PowerMockito.when(WorldClockDateClient.getWorldClockDate()).thenCallRealMethod();

          //when
          JSONObject apiResponse = new JSONObject(this.restTemplate.getForObject(getBaseApiUrl(),
                  String.class));

          //then
          Assert.assertEquals(API_STATUS_SUCCESS, apiResponse.getString("status"));
      }
  }
}

When I was writing this test I was pretty sure it wouldn't work like this. There are many different frameworks combined, we also have inheritance, but… it worked :) There were some problems, but all of them turned out to be solvable.



The first test class was executed successfully, but then the second one was failing, because TomcatURLStreamHandlerFactory has a method setURLStreamHandlerFactory, which can be executed only once. Otherwise, it will throw an exception. This can be easily fixed, by disabling the default TomcatURLStreamHandlerFactory before starting the Tomcat server instance:

static {
          TomcatURLStreamHandlerFactory.disable();
      }

 

Another problem was with EndToEndSpringRunnerWithParameterizedRunnerTest. Enclosed runner test, with the SpringBootTest and Parameterized, combined all together was too much. We can use only one @RunWith annotation and here we had to use SpringRunner and Parameterized runner together. The solution was simple – we can use both @ClassRule and @Rule to use SpringRunner inside Parameterized runner.

  @ClassRule
  public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();

  @Rule
  public final SpringMethodRule springMethodRule = new SpringMethodRule();

The last problem was with EndToEndSPowerMockRunnerWithSpringRunnerTest. PowerMockRunner delegated to SpringRunner caused Spring ApplicationContext fail to load, because of:

java.lang.LinkageError: loader constraint violation: when resolving method 'javax.management.MBeanServer java.lang.management.ManagementFactory.getPlatformMBeanServer()' the class loader org.powermock.core.classloader.javassist.JavassistMockClassLoader @4148db48 of the current class, org/apache/tomcat/util/modeler/Registry, and the class loader 'bootstrap' for the method's defining class, java/lang/management/ManagementFactory, have different Class objects for the type javax/management/MBeanServer used in the signature (org.apache.tomcat.util.modeler.Registry is in unnamed module of loader org.powermock.core.classloader.javassist.JavassistMockClassLoader @4148db48, parent loader 'app'; java.lang.management.ManagementFactory is in module java.management of loader 'bootstrap')

Sounds like a huge problem, but it turned out to be easy to fix. There was some mismatch between PowerMock and Spring dependencies. We can simply tell PowerMock to ignore these dependencies with this annotation:

@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "javax.xml.transform.*", "org.xml.*",
      "javax.management.*", "javax.net.ssl.*", "com.sun.org.apache.xalan.internal.xsltc.trax.*"})

After fixing all these issues I could run all my tests together, with the use of all runners that could be helpful in this case. 

Mixing runners turned out to be possible, even with inheritance from annotated classes. The only problem you may face in the future is a mismatch between dependencies used in different versions of testing frameworks. 

You can find the whole project here: https://gitlab.codete.com/ryszard.kusnierczyk/junit-runners-test 

Rated: 5.0 / 1 opinions
avatar male f667854eaa

Ryszard Kuśnierczyk

Senior Software Engineer

Our mission is to accelerate your growth through technology

Contact us

Codete Global
Spółka z ograniczoną odpowiedzialnością

Na Zjeździe 11
30-527 Kraków

NIP (VAT-ID): PL6762460401
REGON: 122745429
KRS: 0000983688

Get in Touch
  • icon facebook
  • icon linkedin
  • icon instagram
  • icon youtube
Offices
  • Kraków

    Na Zjeździe 11
    30-527 Kraków
    Poland

  • Lublin

    Wojciechowska 7E
    20-704 Lublin
    Poland

  • Berlin

    Bouchéstraße 12
    12435 Berlin
    Germany