Unit Testing With JUnit5

Architecture

Architecture

Unlike JUnit4, JUnit5 promotes a better separation of concerns and comes with following modules.

  • JUnit Jupiter Module

  • JUnit Platform Module

  • JUnit Vintage Module

  • junit-jupiter-migration-support: - supports backward compatibility to select JUnit 4 Rules.

JUnit Jupiter Module

This Aggregator dependency(junit-jupiter) Contains Three Child Dependencies

  • API against which we write tests (junit-jupiter-api)

  • Engine that understands it.(junit-jupiter-engine)

  • Jupiter Parameters library, which helps to write parameterized tests (junit-jupiter-params)

If you are writing tests on JUnit5 this is the only dependency you need.
+--- org.junit.jupiter:junit-jupiter:5.7.0
|    +--- org.junit.jupiter:junit-jupiter-api:5.7.0  <compile-time>
|    +--- org.junit.jupiter:junit-jupiter-params:5.7.0 <compile-time>
|    \--- org.junit.jupiter:junit-jupiter-engine:5.7.0 <runtime>

JUnit Platform Module

It Contains Public APIs for configuring and launching test plans, This API typically used by IDEs and build tools.

If you are using gradle as build tool, you just need to add below line in build.gradle.

build.gradle
test {
    useJUnitPlatform()
}

If you are using maven as build tool, add below surefire plugin in pom.xml

pom.xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.2</version>
        </plugin>
    </plugins>
</build>

JUnit Vintage Module

Add this dependency only If you want run existing (junit4) tests on Junit5 platform.

JUnit Vintage itself comprised of two modules:

  • junit-vintage-engine is the test engine for running JUnit 3 and JUnit 4 tests on the JUnit Platform.

  • junit:junit is the API for JUnit 3 and JUnit 4.

when you add unit-vintage-engine as dependency , it will pull junit4 dependencies.
+--- org.junit.vintage:junit-vintage-engine:5.7.0
|    +--- org.apiguardian:apiguardian-api:1.1.0 <compile-time>
|    \--- junit:junit:4.13 <compile-time>
|         \--- org.hamcrest:hamcrest-core:1.3
|    +--- org.junit.platform:junit-platform-engine:1.7.0 <runtime>
dependencies.hierarchy
|    +--- org.junit:junit-bom:5.7.0
|    |    +--- org.junit.jupiter:junit-jupiter:5.7.0 (c) (1)
|    |    +--- org.junit.jupiter:junit-jupiter-api:5.7.0 (c) (2)
|    |    +--- org.junit.jupiter:junit-jupiter-params:5.7.0 (c) (3)
|    |    +--- org.junit.platform:junit-platform-engine:1.7.0 (c) (4)
|    |    +--- org.junit.vintage:junit-vintage-engine:5.7.0 (c) (5)
|    |    \--- org.junit.platform:junit-platform-commons:1.7.0 (c) (6)
1 Simplified Aggregator Dependency
2 JUnit Jupiter API - which contains Annotation (@Test, @BeforeEach etc) to write test cases.
3 JUnit Jupiter Params - which helps to write parameterized tests using @ParameterizedTest
4 JUnit Platform Engine - Core Runtime Test Execution Engine.
5 JUnit Vintage Engine - Engine That Execute JUnit4 Tests.
6 JUnit Platform Commons - Internal Library used by JUnit5 framework.

Artifacts

Until JUnit 5.4

If you are using JUnit5 version lower than 5.4

You need following dependencies need to be added to your application

  • junit-jupiter-api - JUnit Jupiter API for writing tests and extensions.

  • junit-jupiter-engine - JUnit Jupiter test engine implementation, only required at runtime.

  • junit-vintage-engine - JUnit Vintage test engine implementation to run JUnit4 tests (add this only if you want to run junit4 tests on junit5 platform)

pom.xml
<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.2.0</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.2.0</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.2.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>
build.gradle
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.2.0'

testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.2.0'

testImplementation group: 'org.junit.vintage', name: 'junit-vintage-engine', version: '5.2.0'

From JUnit 5.4

JUnit 5.4 Provides much simpler maven/gradle configurations.

pom.xml
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.7.0'

Gradle Native Support

From Gradle4.6 onwards JUnit5 is supported Natively!

If you are using gradle as build tool , please add below gradle task into your build.gradle.

build.gradle
test {
    //Gradle native support for executing tests on the JUnit5 Platform
    useJUnitPlatform {
        includeEngines 'junit-jupiter'
        excludeEngines 'junit-vintage' //to disable lower version(<=junit4) support
    }
}

Mockito3

Mock Objects can be created Three ways

  1. Manually creating them by calling the Mockito.mock() method.

  2. Using JUnit5 @ExtendWith(MockitoExtension.class): Annotating Class with @Mock annotation, and applying the MockitoExtension extension to the test class. This helps to write clean code. UnitTest fails if it finds unnecessary Mocks.(Recommended)

  3. Using MockitoAnnotations.openMocks(this);: Annotating Class with @Mock annotation, and initializing by calling the MockitoAnnotations.openMocks() method.

Static Method Mocking

Following Test demonstrates static method mocking use case using JUnit5 with Mockito3

You need to add below to Additional Dependencies to your build.gradle file.

build.gradle
   testImplementation "org.mockito:mockito-inline:3.6.0" // for static method mocking
   testImplementation "com.ginsberg:junit5-system-exit:1.1.1" // to test System.exit() use cases
CLIToolTest.java
package com.codergists.user.service.tests;

import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;

@DisplayName("Command Line Tool")
@ExtendWith(MockitoExtension.class)
public class CLIToolTest {

    @Mock
    UserService userService;

    MockedStatic<UserServiceFactory> usMockedStatic;

    @BeforeEach
    public void setup() {

        //MockitoAnnotations.openMocks(this);

        usMockedStatic = Mockito.mockStatic(UserServiceFactory.class); (1)

        usMockedStatic.when(() -> UserServiceFactory.getUserService()).thenReturn(userService);

    }

    @DisplayName("tool should support --help option")
    @ExpectSystemExitWithStatus(0) (2)
    @Test
    public void helpOptionTest() {

        //expect: exit code 0

        //when:
        UserServiceTool.main(new String[] { "--help" });

    }

    @AfterEach
    public void cleanUp() {
        usMockedStatic.close(); (3)
    }

}
1 Method Mockito.mockStatic is used to mock static methods.
2 Annotation @ExpectSystemExitWithStatus is used to verify the System exit status code.
3 Since MockedStatic is implements AutoCloseable interface, you must call close, to release Threads and Stubs.
Refer Official Document for more use cases.

@Mock Vs @InjectMock

  • @Mock – Creates mock objects on which you can create stubs by calling Mockito.when(T.getXXX()).thenReturn(Y)

  • @InjectMocks – Creates real objects and inject mocked dependencies, you should add this annotation on the real Object, which has instance Variables of other mocked object. instead of setting mocked objects manually, you just say InjectMock which injects mocks into this class.

There are Two ways you can create StudentService Object.

  • Approach-1: Use @InjectMocks annotation on this class which will create an instance and Injects StudentRepository Mock into it.

  • Approach-2: Use StudentService studentService =new StudentService(studentRepository); manually create with new clause and pass Mocked StudentRepository

StudentServiceTest.java
package com.blogspot.codergists.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyLong;

@ExtendWith(MockitoExtension.class)
public class StudentServiceTest {

  @Mock
  StudentRepository studentRepository; // interface

  @InjectMocks
  StudentService studentService;// StudentService service is real object, which has Parameterize Constructor i.e StudentService(StudentRepository studentRepository)
  // Two ways you can create StudentService object
  // Approach-1:  Use @InjectMocks annotation on this class which will create instance and Injects StudentRepository Mock into it
  // Approach-2: Use StudentService studentService =new StudentService(studentRepository);  manually create with new clause and pass Mocked studentRepository

  @Spy
  Student studentSpy=new Student();// create real object, want stub partially

  @BeforeEach
   void setUp(){
    //Spy on POJOs/ Implementation classes(if they available to your module on compile time),
    // if want to do partial stubbing and partial real calls
    // Try to avoid Spy if Possible, unless you really want to test time behavior in unitTests
    //(RealTime behaviour such as like DB Calls, /real API which takes long time / that works only on real environment)

    //Spy-Stubbing
    Mockito.when(studentSpy.getName()).thenReturn("Thiru");

    //Mock Stubbing, if your module depends on interfaces(API), implementation classes(external libraries) available runtime.
    Mockito.when(studentRepository.getById(anyLong())).thenReturn(studentSpy);
  }

  @Test
  void getStudentByIdTest() {

    //given: a service instance
    //StudentService studentService =new StudentService(studentRepository); Approach2: to inject mocks into real Objects.

    //when: invoking getStudentById
    Student student = studentService.getStudentById(1000L);

    //then: expect response from partially stubbed (Spy)
    assertEquals("Thiru", student.getName());// student is Spy ,  getName() value returned from Stubbing
    assertEquals(38, student.getAge());// student is Spy , getAge()  value returned from real method

  }
}

@Mock Vs @Spy

Mock Spy

@Mock Creates Mocked Object (Entire Object and its all methods are Fake).

@Spy Creates Real Object, Same as creating object with new clause.

You must to define method behaviour explicitly by stubbing.

You can define method behaviour by stubbing, if not it executes real method.

(COMMON POINT) You MUST Create Stubs on Mock Object using Mockito.when(T).thenReturn() or similar method.

(COMMON POINT) You MAY Create Stubs on Spy Object using Mockito.when(T).thenReturn() or similar method.

If Stubs are not Created on Mock Object, that method return null on invocation.

If Stubs are not Created on Spy Object, it invokes real method.

@Mock Annotation can be added to interface or class as well.

@Spy Annotation must be added to class with default constructor.

Without using @Mock Annotation You can use Mockito.mock(StudentRepository.class);

Without using @Spy Annotation, You can Use Mockito.spy(new Student(id, name)); you can use parameterized constructor with this approach

Use @Mock, When you want to test your module code, and mocking the external/downstream/other module functionality (especially UnitTests)

Use @Spy, When you want to test your module along with some of downstream APIs real functionality.(same as using real object)

When you use @Mock, all methods in this object you must create stubs.

When you use @Spy, you can test real functionality for some methods, at the same time, you can also mock some of the methods

You can’t test real behavior of downstream/external API, when you Mock

You can partially/fully test real behavior downstream/external API, when you Spy

works on interface driven modules. (prefer to use Mocks)

works if implementation classes available at compile time.

StudentService.java
package com.blogspot.codergists.service;

public class StudentService {

  StudentRepository studentRepository;

  public StudentService(StudentRepository studentRepository) {
    this.studentRepository = studentRepository;
  }

  public Student getStudentById(Long id) {
    return studentRepository.getById(id);
  }

}
StudentRepository.java
package com.blogspot.codergists.service;

public interface StudentRepository {

  Student getById(Long studentId);

}
Student.java
package com.blogspot.codergists.service;

public class Student {

  Long id;

  String name;

  Integer age=38;


  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 Integer getAge() {
    return age;
  }

  public void setAge(Integer age) {
    this.age = age;
  }
}

Source Code

This source contains

  1. Writing Tests Using Junit5

  2. Writing Legacy(Junit4) Tests on JUnit5 Platform

  3. Static Method Mocking using Mockito3

Complete source code available at https://github.com/tvajjala/jupiter-mockito.git

Additional Scenarios

Static Method Mocking Using PowerMockito and JUnit4

Following code snippet demonstrate static method mocking using PowerMockito and JUnit4

PowerMockStaticMethodTest.java
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest(StringUtil.class)
public class PowerMockStaticMethodTest
{
	@Test
	public void testMyStaticMethod() {

	    //given:
	    boolean expectation=true; // 1
	    PowerMockito.mockStatic(StringUtil.class); //2
	    PowerMockito.when(StringUtil.checkSubString("CodeGists","blog")).thenReturn(true); //3

        //when:
	    boolean actual = StringUtil.checkSubString("CodeGists","blog"); //4

        //then:
        Assert.assertEquals(expectation, actual); //5
	}
}

Comments

Popular posts from this blog

IBM Datapower GatewayScript

Spring boot Kafka Integration

Spring boot SOAP Web Service Performance