- Is it right to use @SpringBootTest (or) @ContextConfiguration in Unit Test?
Starting spring for each test is a very expensive operation. It’s not a unit test anymore rather integration test. @SpringBootTest goes further and tries to mimic the processes added by Spring Boot framework for creating the context: Decides what to scan based on package structures, loads external configurations from predefined locations optionally runs autoconfiguration starters and so on and so forth. @SpringBootTest loads the necessary beans and inject into each other.
When using @ContextConfiguration you present way to filter what exactly should be run, what beans to load and to inject into each other. - How to Optimize the Unit test by selectively loading the resource?
You can selectively load the resource during unit test- To test your Respository : use @DataJpaTest annotation for test slice.
- To test your Service layer : use JUnit and Mockito. Here you will mock your Repository
- To test your Controller layer : use @WebMvcTest annotation for test slice or use JUnit and Mockito. Here you will mock your Service in both cases
- To test a Component, such as a third party library wrapper or load some specific beans: use @ExtendWith(SpringExtension.class) and @ContextConfiguration/@Import or @SpringJUnitWebConfig which is the combinaison of the both.
- To do an integration test : use @SpringBootTest
- When to use @SpringBootTest and @ExtendWith(SpringExtension.class)
Use @ExtendWith(SpringExtension.class) without loading the entire application context that is included with @SpringBootTest. As you say it’s a lighter weight approach if you just want to mock beans with @MockBean. @SpringBootTest does in fact include @ExtendWith(SpringExtension.class)Use @SpringBootTest when,
- You need to test the application as a whole, including multiple layers like the web layer, service layer, and repository layer.
- You require an embedded server to run the test, such as when testing web controllers.
- You want to verify the integration of all components in a fully loaded application context.
Use SpringRunner/@ExtendWith(SpringExtension.class) when,
- You are writing unit tests or lightweight integration tests that only need certain features of Spring, such as dependency injection or transaction management, without loading the full application context.
- You want to test specific layers (e.g., service layer) in isolation without the overhead of starting the entire Spring application.
Category Archives: Junit5
Parallel Test in Junit
BaseUnitTest.java
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import java.util.concurrent.TimeUnit; public class BaseUnitTest { long startTime; long endTime; @BeforeEach public void recordStartTime() { startTime = System.currentTimeMillis(); } @AfterEach public void recordEndAndExecutionTime() { endTime = System.currentTimeMillis(); System.out.println("Last testcase exection time in millisecond : " + TimeUnit.NANOSECONDS.toMicros((endTime - startTime)) + " Seconds"); } }
FirstTest.java
import org.junit.jupiter.api.Test; class FirstTest extends BaseUnitTest{ @Test void oneSecondTest() throws InterruptedException { System.out.println("oneSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(1000); } @Test void twoSecondTest() throws InterruptedException { System.out.println("twoSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(2000); } @Test void threeSecondTest() throws InterruptedException { System.out.println("threeSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(3000); } }
SecondTest.java
import org.junit.jupiter.api.Test; public class SecondTest extends BaseUnitTest{ @Test void oneSecondTest() throws InterruptedException { System.out.println("oneSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(1000); } @Test void twoSecondTest() throws InterruptedException { System.out.println("twoSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(2000); } @Test void threeSecondTest() throws InterruptedException { System.out.println("threeSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(3000); } }
ThirdTest.java
import org.junit.jupiter.api.Test; public class ThirdTest extends BaseUnitTest{ @Test void oneSecondTest() throws InterruptedException { System.out.println("oneSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(1000); } @Test void twoSecondTest() throws InterruptedException { System.out.println("twoSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(2000); } @Test void threeSecondTest() throws InterruptedException { System.out.println("threeSecondTest() name => " + Thread.currentThread().getName()); Thread.sleep(3000); } }
Output
Last testcase exection time in millisecond : 0 Seconds Last testcase exection time in millisecond : 2 Seconds Last testcase exection time in millisecond : 2 Seconds Last testcase exection time in millisecond : 2 Seconds Last testcase exection time in millisecond : 3 Seconds Last testcase exection time in millisecond : 3 Seconds Last testcase exection time in millisecond : 3 Seconds threeSecondTest() name => ForkJoinPool-1-worker-10 twoSecondTest() name => ForkJoinPool-1-worker-7 oneSecondTest() name => ForkJoinPool-1-worker-1 twoSecondTest() name => ForkJoinPool-1-worker-6 oneSecondTest() name => ForkJoinPool-1-worker-3 threeSecondTest() name => ForkJoinPool-1-worker-5 oneSecondTest() name => ForkJoinPool-1-worker-4 twoSecondTest() name => ForkJoinPool-1-worker-8 threeSecondTest() name => ForkJoinPool-1-worker-9
junit-platform.properties
# Enable parallelism junit.jupiter.execution.parallel.enabled = true # Enable parallelism for both test methods and classes junit.jupiter.execution.parallel.mode.default = concurrent
In Maven Command
>>mvn test -Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent
Four possibilities in junit-platform.properties config
//Only One class and One method in that class would be executed - Sequential Execution junit.jupiter.execution.parallel.mode.default = same_thread junit.jupiter.execution.parallel.mode.classes.default = same_thread //More than one class would run in parallel but only one method in each class would be executed //Test method within one class run sequentially but more than one class run in parallel junit.jupiter.execution.parallel.mode.default = same_thread junit.jupiter.execution.parallel.mode.classes.default = concurrent //Only One class and Multiple method in that class would be executed junit.jupiter.execution.parallel.mode.default = concurrent junit.jupiter.execution.parallel.mode.classes.default = same_thread //Multiple classes and Multiple method in those classes would be executed junit.jupiter.execution.parallel.mode.default = concurrent junit.jupiter.execution.parallel.mode.classes.default = concurrent
Junit5 Fundementals
Junit 5 = Platform + Jupiter + Vintage
- Platform = Engine + Runner + Launcher
- All the Class files needed for Coding Test Cases
- Provides Support for Junit3 and 4
Adding Dependency in pom.xml for Junit 5
JUnit 5 Platform
includes junit-jupiter-api + junit-platform-engine
<dependencies> [...] <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.4.0</version> <scope>test</scope> </dependency> [...] </dependencies>
If you want to write and execute JUnit 3 or 4 tests via the JUnit Platform add the Vintage Engine to the dependencies
<dependencies> [...] <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>5.4.0</version> <scope>test</scope> </dependency> [...] </dependencies>
Junit 5 Annotations
- @BeforeAll, @AfterAll – Runs Before and After Class and should be static
- @DisplayName – Displays description about the Test Method
- @Disabled – @Ignore in Junit 4. Disables the Test Method
- @Nested – Helps in grouping of similar test methods together
- @ParameterizedTest – Supplying more than one input for same method in row using array
- @ValueSource – Provides multiple paramters to same test method for ParameterizedTest
- @CsvSource – Provides multiple paramters to same test method for ParameterizedTest in Key Value Pair. Key is Input and Value is expected Output
AccountUtils.java
import java.io.IOException; public class AccountUtils { public boolean validateAccountId(String acctNo) throws IOException { if (getAccountIDLength(acctNo)) { return true; } else { throw new IOException("Account ID is Invalid"); } } public boolean getAccountIDLength(String acctNo) { if(acctNo.length() < 5) return false; if(acctNo.length() > 10) return false; if(!acctNo.contains("-")) return false; return true; } public String getFormattedAccID(String accountNo){ return accountNo.toUpperCase(); } public String[] getBankDetails(String accountNo){ String[] arrAccountDetails = accountNo.split("-"); return arrAccountDetails; } }
AccountUtilsTest.java
import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; import static org.junit.jupiter.api.Assertions.fail; public class AccountUtilsTest { AccountUtils systemUnderTest = new AccountUtils(); @BeforeAll static void init(){ System.out.println("Initilize connection to Database"); } @AfterAll static void destroy(){ System.out.println("Deallocate connection to Database"); } @Test @DisplayName("AccountID Length Validation Success") void test_getAccountIDLength_Success(){ boolean expectedOutput = true; boolean actualOutput = systemUnderTest.getAccountIDLength("SBI-104526"); Assertions.assertEquals(expectedOutput, actualOutput); Assertions.assertTrue(actualOutput); } @Test @Disabled void test_validateAccountId_Success() throws IOException { Assertions.assertTrue(systemUnderTest.validateAccountId("SBI-104526")); } @Test @DisplayName("AccountID is Invalid ") void test_validateAccountId_Exception(){ Assertions.assertThrows(IOException.class, ()->{ systemUnderTest.validateAccountId(""); }); } @Test @DisplayName("Account Bank Details Validation") void test_AccountIDForBankName_Success(){ String[] expectedOutput = new String[]{"SBI", "104526"}; String[] actualOutput = systemUnderTest.getBankDetails("SBI-104526"); Assertions.assertArrayEquals(expectedOutput, actualOutput); } @Nested class checkValidAccountId { @ParameterizedTest @ValueSource(strings={"SBI", "SBI-123456789", " ", ""}) @DisplayName("Parameterized Test AccountID Length Validation Failiure") void test_getAccountIDLength_Failiure(String accountId){ Assertions.assertFalse(systemUnderTest.getAccountIDLength(accountId)); } @ParameterizedTest(name="AccountID {0} formatted to {1}") @CsvSource(value={"sbi-123456, SBI-123456", "cbi-123456, CBI-123456"}) @DisplayName("AccountID Format Validation") void getFormattedAccID_Success(String inputString, String expectedOutput){ Assertions.assertEquals(expectedOutput, systemUnderTest.getFormattedAccID(inputString)); } } }
Using TestTraits in Mockito
TestTraits.java
public interface TestTraits { String EMPLOYEE_ID = "1"; Long EMPLOYEE_ID_LONG = 1L; default EmployeeRpy getEmployee(){ EmployeeRpy objEmpRpy = new EmployeeRpy(); objEmpRpy.setEmpName("Mugil"); objEmpRpy.setEmpAge("34"); return objEmpRpy; default EmployeeDetailsResource createMockedEmployeeDetails() throws IOException(){ EmployeeDetailsResource objEmployeeDetailsResource = new Gson.fromJson(getReader("json-data/EmployeeDetails.json"), EmployeeDetails.class); return objEmployeeDetailsResource; } default Reader getReader(String filePath) throws IOException{ return new FileReader(new ClassPathResource(filePath).getFile()); } }
EmployeeDetails.json
{ "empID" : "101", "empName" : "Mugilvannan", "empAge" : "30" }
EmployeeMgmtTest.java
@ExtendWith({MockitoExtension.class}) class EmployeeMgmtTest implements TestTraits{ . . Code for Testing Goes Here . . }