Unit Testing With JUnit5
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
.
test {
useJUnitPlatform()
}
If you are using maven as build tool, add below surefire plugin in 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>
| +--- 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)
<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>
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.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
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
.
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
-
Manually creating them by calling the
Mockito.mock()
method. -
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) -
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.
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
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.
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 |
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 |
(COMMON POINT) You |
If Stubs are not Created on Mock Object, that method return |
If Stubs are not Created on Spy Object, it invokes |
|
|
Without using @Mock Annotation You can use |
Without using @Spy Annotation, You can Use |
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. |
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);
}
}
package com.blogspot.codergists.service;
public interface StudentRepository {
Student getById(Long studentId);
}
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
-
Writing Tests Using Junit5
-
Writing Legacy(Junit4) Tests on JUnit5 Platform
-
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
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
Post a Comment