- TOC
JUnit ToDo
- What and Why
- ArgumentMatchers – anyString(), any(XYZ.class) from journaldev
- @Mock
- @InjectMocks – main class where mocks needs to be injected
- @MockMvc
Introduction
Why write Tests
The point of writing automated tests is not so much to verify that the code works now, but to verify on an ongoing basis that it continues to work in the future.
Why use testing Framework?
JUnit 4
- 10 years old
- Not up to date with new testing patterns
- Not up to date with java language features
- Monolithic architecture
- Bugs and feature requests piled up
JUnit 5
- JUnit 5 aims to adapt java 8 style of coding and several other features as well.
- Java 8 is required to create and execute tests in JUnit 5 (though tests written with JUnit 3 or JUnit 4 can be exceued as well for backward compatibility).
Setup
How to include JUnit 5 to your project ?
<!-- pom.xml -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
// build.gradle
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0-M4")
testRuntime("org.junit.platform:junit-platform-runner:1.0.0-M4")
scope = test
, means that the dependency jar will not be included in the final distributable.
JUnit 4 vs JUnit 5
JUnit 5 Architecture
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform
- To be able to launch junit tests, IDEs, build tools or plugins need to include and extend platform APIs.
- It defines the TestEngine API for developing new testing frameworks that runs on the platform.
- It also provides a Console Launcher to launch the platform from the command line and build plugins for Gradle/Maven.
JUnit Jupiter
- It includes new programming and extension models for writing tests.
- It has all new junit annotations and TestEngineimplementation to run tests written with these annotations.
JUnit Vintage
- It primary purpose is to support running JUnit 3 and JUnit 4 written tests on the JUnit 5 platform. It’s there are backward compatibility.
JUnit 5 Annotations
Annotaions | |
---|---|
@BeforeEach | The annotated method will be run before each test method in the test class. |
@AfterEach | The annotated method will be run after each test method in the test class. |
@BeforeAll | The annotated method will be run before all test methods in the test class. This method must be static. |
@AfterAll | The annotated method will be run after all test methods in the test class. This method must be static. |
@Test | It is used to mark a method as junit test |
@DisplayName | Used to provide any custom display name for a test class or test method |
@Disable | It is used to disable or ignore a test class or method from test suite. |
@Nested | Used to create nested test classes |
@Tag | Mark test methods or test classes with tags for test discovering and filtering |
@TestFactory | Mark a method is a test factory for dynamic tests |
@Test(timeout = time)
@Test(expected = exception.class)
@RepeatedTest(3)
void testCircleArea(RepetitionInfo repetitionInfo){
if(repetitionInfo.getCurrentRepetition == 1){...}
...
// Here JUnit 5 gives RepetitionInfo by Dependency Injection
}
public class AppTest {
@BeforeAll
static void setup() {
System.out.println("@BeforeAll executed");
}
@BeforeEach
void setupThis() {
System.out.println("@BeforeEach executed");
}
@Tag("DEV")
@Test
void testCalcOne() {
System.out.println("======TEST ONE EXECUTED=======");
Assertions.assertEquals(4, Calculator.add(2, 2));
}
@Tag("PROD")
@Disabled
@Test
void testCalcTwo() {
System.out.println("======TEST TWO EXECUTED=======");
Assertions.assertEquals(6, Calculator.add(2, 4));
}
@AfterEach
void tearThis() {
System.out.println("@AfterEach executed");
}
@AfterAll
static void tear() {
System.out.println("@AfterAll executed");
}
}
Conditional Execution
@EnabledOnOS(OS.LINUX)
@EnabledOnJre(JRE.JAVA_11)
@EnabledIf
@EnabledIfSystemProperty
@EnabledIfEnvironmentVariable
Other way for conditional execution is to use assumptions.
JUnit 5 - Assertions and Assumptions
Assertions
- Assertions help in validating the expected output with actual output of a testcase.
- To keep things simple, all JUnit Jupiter assertions are static methods in the
org.junit.jupiter.Assertions
class e.g.assertEquals()
,assertNotEquals()
.
assertEquals(expected, actual)
assertArrayEquals(expectedArray, actualArray)
assertIterableEquals(expectedIterable, actualIterable)
void testCase(){
//Test will pass
Assertions.assertNotEquals(3, Calculator.add(2, 2));
//Test will fail
Assertions.assertNotEquals(4, Calculator.add(2, 2), "Calculator.add(2, 2) test failed");
}
//Test will fail - Lazy Assert Messages
Supplier<String> messageSupplier = ()-> "Calculator.add(2, 2) test failed";
Assertions.assertNotEquals(4, Calculator.add(2, 2), messageSupplier);
//Assert Exception Throw
Assertions.assertThrows(ArithmeticException.class, () -> Calculator.divide(2, 0), "Divide by zero should throw.");
@Test
void assertAllProperties_fail() {
Address address = new Address("New City", "Some Street", "No");
assertAll("address",
() - > assertEquals("Neustadt", address.city),
() - > assertEquals("Irgendeinestraße", address.street),
() - > assertEquals("Nr", address.number)
);
}
// Output
org.opentest4j.MultipleFailuresError: address (3 failures)
expected: <Neustadt> but was: <New City>
expected: <Irgendeinestraße> but was: <Some Street>
expected: <Nr> but was: <No>
Assumptions
- Assumptions class provides static methods to support conditional test execution based on assumptions.
- A failed assumption results in a test being aborted.
- Assumptions are typically used whenever it does not make sense to continue execution of a given test method. In test report, these test will be marked as passed.
- JUnit jupiter Assumptions class has two such methods:
assumeFalse()
,assumeTrue()
.
public class AppTest {
@Test
void testOnDev() {
System.setProperty("ENV", "DEV");
Assumptions.assumeTrue("DEV".equals(System.getProperty("ENV")), AppTest::message);
}
@Test
void testOnProd() {
System.setProperty("ENV", "PROD");
Assumptions.assumeFalse("DEV".equals(System.getProperty("ENV")));
}
private static String message () {
return "TEST Execution Failed :: ";
}
}
TestInfo and TestReporter
Test Suites
- Using JUnit 5 test suites, you can run tests spread into multiple test classes and different packages.
-
JUnit 5 provides two annotations to create test suites.
- To execute the suite, you will use @RunWith(JUnitPlatform.class).
-
Additionally, you can use following annotations for filtering test packages, classes or even test methods.
- @IncludePackages and @ExcludePackages to filter packages
- @IncludeClassNamePatterns and @ExcludeClassNamePatterns to filter test classes
- @IncludeTags and @ExcludeTags to filter test methods
@RunWith(JUnitPlatform.class)
@SelectPackages("com.howtodoinjava.junit5.examples")
@IncludePackages("com.howtodoinjava.junit5.examples.packageC")
@ExcludeTags("PROD")
public class JUnit5TestSuiteExample{ }
JUnit Lifecycle
JUnit 5 creates new instance of test class for every method run.
- @BeforeAll needs to be executed before creating an instance of the test class hence it has to static only.
@BeforeAll
static void beforeAllInit(){
System.out.println("Before All executed.");
}
- To change this behavior, use
@TestInstance
annotation on the test class.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MathUtilTest{...}
Maven Surefire Plugin
- To run unit test using maven command, such as in jenkins pipeline.
- Run maven build as goal test.
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
</plugins>
</build>
FixMethodOrder
How to test methods in specific order using JUnit4?
- JUnit currently allows test methods run ordering using class annotations:
- @FixMethodOrder(MethodSorters.NAME_ASCENDING)
- @FixMethodOrder(MethodSorters.JVM)
- @FixMethodOrder(MethodSorters.DEFAULT)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SampleTest {
@Test
public void testAcreate() {
System.out.println("first");
}
@Test
public void testBupdate() {
System.out.println("second");
}
@Test
public void testCdelete() {
System.out.println("third");
}
}
@Test
public void testOrder1() { test1(); test3(); }
Testing Spring Boot
Mocking with @MockBean
For Unit Testing, Service layer needs to be independent of repository layer implementation.
@MockBean
annotation
- It creates a Mock for the EmployeeRepository which can be used to bypass the call to the actual EmployeeRepository.
@TestConfiguration
annotation
- To check the Service class, instance needs to be created and available as a @Bean in order to @Autowire it.
- Spring Boot annotation that can be used on classes in src/test/java to indicate that they should not be picked up by component scanning.
@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest {
@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeServiceImpl();
}
}
@Autowired
private EmployeeService employeeService;
@MockBean
private EmployeeRepository employeeRepository;
@Before
public void setUp() {
Employee alex = new Employee("alex");
Mockito.when(employeeRepository.findByName(alex.getName()))
.thenReturn(alex);
}
@Test
public void whenValidName_thenEmployeeShouldBeFound() {
String name = "alex";
Employee found = employeeService.getEmployeeByName(name);
assertThat(found.getName()).isEqualTo(name);
}
}
Unit Testing with @WebMvcTest
Mock the Service layer code for our unit tests of @Controller
.
@WebMvcTest
annotation
- auto-configure the Spring MVC infrastructure for our unit tests.
- Mostly @WebMvcTest will be limited to bootstrap a single controller.
- used along with @MockBean to provide mock implementations for required dependencies.
- also auto-configures
MockMvc
which offers a powerful way of easy testing MVC controllers without starting a full HTTP server.
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeRestController.class)
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@MockBean
private EmployeeService service;
@Test
public void givenEmployees_whenGetEmployees_thenReturnJsonArray() throws Exception {
Employee alex = new Employee("alex");
List < Employee > allEmployees = Arrays.asList(alex);
given(service.getAllEmployees()).willReturn(allEmployees);
mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].name", is(alex.getName())));
}
}
Integration Testing with @SpringBootTest
Integration Tests
- focus on integrating different layers of the application.
- That also means no mocking is involved.
- Ideally, we should keep the integration tests separated from the unit tests and should not run along with the unit tests.
- Can be done by using a different profile to only run the integration tests.
- Reason : Integration tests are time-consuming and might need an actual database to execute.
Integration tests need to start up a container to execute the test cases, which needs extra setup.
@SpringBootTest
annotation
- used when we need to bootstrap the entire container
- creating the ApplicationContext that will be utilized in our tests.
webEnvironment
attribute configures runtime environment- we’re using WebEnvironment.MOCK - container will operate in a mock servlet environment.
@TestPropertySource
annotation
- configure locations of properties files specific to our tests.
- property file loaded with
@TestPropertySource
will override the existing application.properties file.
# application-integrationtest.properties
spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
@RunWith(SpringRunner.class)
@SpringBootTest(SpringBootTest.WebEnvironment.MOCK, classes = Application.class)
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-integrationtest.properties")
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@Autowired
private EmployeeRepository repository;
@Test
public void givenEmployees_whenGetEmployees_thenStatus200() throws Exception {
createTestEmployee("bob");
mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].name", is("bob")));
}
}
Test cases for the integration tests might look similar to the Controller layer unit tests, but for IntegrationTest nothing is mocked and end-to-end scenarios will be executed.
Auto-Configured Tests
One of the amazing features of Spring Boot’s auto-configured annotations is that it helps to load parts of the complete application and test specific layers of the codebase.
Few widely used annotations
@WebFluxTest | Used to test Spring Webflux controllers. It’s often used along with @MockBean to provide mock implementations for required dependencies. |
@JdbcTest | Used to test JPA applications but it’s for tests that only require a DataSource. The annotation configures an in-memory embedded database and a JdbcTemplate. |
@JooqTest | To test jOOQ-related tests, this annotation configures a DSLContext. |
@DataMongoTest | To test MongoDB applications. By default, it configures an in-memory embedded MongoDB if the driver is available through dependencies, configures a MongoTemplate, scans for @Document classes, and configures Spring Data MongoDB repositories. |
@DataRedisTest | Used to test Redis applications. It scans for @RedisHash classes and configures Spring Data Redis repositories by default. |
@DataLdapTest | configures an in-memory embedded LDAP (if available), configures a LdapTemplate, scans for @Entry classes, and configures Spring Data LDAP repositories by default. |
@RestClientTest | Used to test REST clients. It auto-configures different dependencies like Jackson, GSON, and Jsonb support, configures a RestTemplateBuilder, and adds support for MockRestServiceServer by default. |
The complete source code of this article can be found over on GitHub. And, if you want to keep learning about testing – we have separate articles related to integration tests and unit tests in JUnit 5.
Example
spring-boot-starter-test dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.0.4.RELEASE</version>
</dependency>
for practice add h2 DB (in-memory DB)
Entity Definition
@Entity
@Table(name = "person")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Size(min = 3, max = 20)
private String name;
// standard getters and setters, constructors
}
Repository Definition
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
public Employee findByName(String name);
}
Service Implementation Definition
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Override
public Employee getEmployeeByName(String name) {
return employeeRepository.findByName(name);
}
}
Controller Definition
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employees")
public List<Employee> getAllEmployees() {
return employeeService.getAllEmployees();
}
}
Integration Testing with @DataJpaTest
@RunWith(SpringRunner.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
@Test
public void whenFindByName_thenReturnEmployee() {
// given
Employee alex = new Employee("alex");
entityManager.persist(alex);
entityManager.flush();
// when
Employee found = employeeRepository.findByName(alex.getName());
// then
assertThat(found.getName()).isEqualTo(alex.getName());
}
}
@RunWith(SpringRunner.class)
is used to provide a bridge between Spring Boot test features and JUnit.@DataJpaTest
provides some standard setup needed for testing the persistence layer:- configuring H2, an in-memory database
- setting Hibernate, Spring Data, and the DataSource
- performing an
@EntityScan
- turning on SQL logging
TestEntityManager
provided by Spring Boot- to setup data in our database
- alternative to the standard
JPAEntityManager
that provides methods commonly used when writing tests.
MockMvc - Unit Testing Controller
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(MockitoJUnitRunner.class)
public class MyControllerTest {
private MockMvc mockmvc;
@InjectMocks MyController controller;
@Mock MyService myService;
@Before
public void setup() {
this.mockmvc = MockMvcBuilders.standaloneSetup(verifyController).build();
}
@Test
public void method_success() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/v1/users"))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].date").exists())
.andExpect(jsonPath("$[0].type").value("1"))
.andExpect(jsonPath("$[0].element.list").value(new ArrayList < > ()))
.andExpect(jsonPath("$[0].element.id").value("42"))
.andExpect(jsonPath("$[0].element.*", hasSize(2)))
.andExpect(jsonPath("$[0].*", hasSize(3)))
.andExpect(jsonPath("$.*", hasSize(1)))
.andExpect(jsonPath("$.graph_data.length()", is(equalTo(1))))
.andExpect(jsonPath("$.graph_data[0][1]", is(equalTo(123))))
.andExpect(jsonPath("$.table_data.length()", is(equalTo(1))));
MvcResult result = mockMvc.perform(
post("/api/users")
.header("Authorization", base64ForTestUser)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"userName\":\"testUserDetails\",\"firstName\":\"xxx\",\"lastName\":\"xxx\",\"password\":\"xxx\"}")
)
.andExpect(status().isBadRequest())
.andReturn();
String content = result.getResponse().getContentAsString();
// do what you will
}
}