Unit testing ensures that the code functions correctly and prevents issues before deployment.
JUnit provides a structured framework for writing and running tests, while Mockito helps mock dependencies for effective isolation and verification.
This guide covers using JUnit and Mockito for unit testing, including best practices, key concepts, and implementation.
What is JUnit?
JUnit is a popular Java testing framework that helps developers write and run unit tests. It is part of the xUnit family of test frameworks and is heavily used for test-driven development (TDD).
Some key features of JUnit:
- Offers annotations such as @Test, @BeforeEach, and @AfterEach to organize test cases.
- Supports assertion statements (e.g., assertEquals, assertTrue) to confirm expected outputs.
- Enables test automation and simple integration with build tools like Maven and Gradle.
- Generates in-depth reports on test runs.
- Functions efficiently with continuous integration (CI/CD) pipelines.
JUnit guarantees Java code functionality works as expected by allowing developers to test individual pieces quickly and effectively.
Why use JUnit for Unit Testing?
JUnit makes Java applications faster, more stable, and easier to maintain by detecting problems early and encouraging good coding practices. It makes testing easier to create, run, and maintain.
JUnit 4 introduced key features like annotations (@Test, @Before, @After) and a simple test-running model, making it a popular Java testing framework.
JUnit 5 enhanced it with a modular design (JUnit Platform, Jupiter, and Vintage), better extensibility, improved lifecycle annotations (@BeforeEach, @AfterEach), stronger assertions, and seamless CI/CD integration.
Read More: How to run JUnit 4 Test Cases in JUnit 5
Here’s why JUnit is the choice of developers:
- Automation & Efficiency: JUnit minimizes effort and maximizes development speed by replacing the need for manual testing. It can easily be integrated with build tools such as Maven and Gradle for continuous testing.
- Reliable Assertions: It provides built-in assertion methods (e.g., assertEquals, assertTrue) to verify expected results, making test validation easier.
- Structured & Reusable Tests: JUnit uses annotations (@Test, @BeforeEach, @AfterEach) to organize and execute tests systematically, ensuring code remains modular and maintainable.
- Supports Test-Driven Development (TDD): JUnit encourages TDD, where tests are written before code, leading to better design, fewer bugs, and improved code quality.
- Integration with CI/CD: It integrates well with Jenkins, GitHub Actions, and other CI/CD tools to automate testing in software development pipelines.
- Detailed Reporting & Debugging: JUnit offers detailed test results and failure logs, enabling developers to identify and resolve problems quickly.
- Open-Source & Community Support: JUnit is a popular open-source framework with robust community support, so finding solutions and best practices is easy.
Must Read: Unit Testing in Java with JUnit
Features of JUnit
Various features of JUnit render it a strong, flexible, and effective unit testing tool. A few of its key features are:
1. Assertions: Assertions check expected outputs and help identify problems early. Some common assertions are:
- assertEquals(expected, actual): Tests whether two values are equal.
- assertTrue(condition): Checks if a condition is true.
- assertFalse(condition): Checks if a condition is false.
- assertThrows(Exception.class, () -> { }): Checks that an exception is thrown.
Also Read: What is Assertion Testing?
2. Test Annotations: JUnit employs annotations to specify and maintain test cases:
- @Test: Indicates that a method is a test case.
- @BeforeEach: Executes before each test (for example, setup code).
- @AfterEach: Executes after each test (for example, cleanup code).
- @BeforeAll: Executes once before all tests in a class.
- @AfterAll: Executes once after all the tests.
- @Disabled: Disables a test case.
3. Parameterized Tests: JUnit allows running the same test with different inputs using @ParameterizedTest:
- @ValueSource(ints = {1, 2, 3}): Provides multiple values for testing.
- @CsvSource({ “1, One”, “2, Two” }): Supplies multiple test cases in CSV format.
- @MethodSource: Supplies test data from a static method.
4. Exception Handling: JUnit can test if a method correctly throws an expected exception:
@Test void testException() { assertThrows(ArithmeticException.class, () -> { int result = 10 / 0; }); }
5. Timeout Handling: JUnit allows setting a time limit for test execution:
@Test @Timeout(2) // Fails if execution takes more than 2 seconds void testTimeout() { Thread.sleep(1000); }
6. Test Suites: JUnit enables grouping multiple test classes into a suite using @Suite and @SelectClasses.
7. Mocking Support: JUnit integrates with Mockito for mocking dependencies, enabling isolated testing of components.
8. CI/CD & IDE Support: JUnit works seamlessly with Jenkins, GitHub Actions, and all major IDEs like IntelliJ and Eclipse.
How to write Unit Tests with JUnit?
JUnit simplifies writing and running unit tests for Java applications. Follow these steps to create effective unit tests:
Step 1: Set Up JUnit in Your Project
Using Maven (JUnit 5)
Add the following dependency to your pom.xml:
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> </dependencies>
Using Gradle (JUnit 5)
Add this to your build.gradle:
dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' }
Step 2: Create a Java Class to Test
Example: A simple Calculator class.
public class Calculator { public int add(int a, int b) { return a + b; } public int subtract(int a, int b) { return a - b; } }
Step 3: Create a JUnit Test Class
JUnit test classes should be placed in the src/test/java directory.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { @Test void testAddition() { Calculator calculator = new Calculator(); assertEquals(5, calculator.add(2, 3)); } @Test void testSubtraction() { Calculator calculator = new Calculator(); assertEquals(2, calculator.subtract(5, 3)); } }
Step 4: Run the Tests
- In IntelliJ/Eclipse: Right-click the test class and select Run.
- Using Maven: Run mvn test in the terminal.
- Using Gradle: Run gradle test.
Step 5: Use Setup & Cleanup Methods
JUnit provides lifecycle methods for setup and cleanup.
import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { private Calculator calculator; @BeforeEach void setUp() { calculator = new Calculator(); // Runs before each test } @AfterEach void tearDown() { calculator = null; // Cleanup after each test } @Test void testAddition() { assertEquals(5, calculator.add(2, 3)); } }
Step 6: Test Exception Handling
Ensure methods throw expected exceptions.
@Test void testDivideByZero() { assertThrows(ArithmeticException.class, () -> { int result = 10 / 0; }); }
Learn More: Mockito – Throwing Exceptions in Unit Tests?
Step 7: Run Parameterized Tests (Optional)
JUnit allows testing multiple values using @ParameterizedTest.
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { @ParameterizedTest @CsvSource({ "2,3,5", "4,5,9", "10,20,30" }) void testAddition(int a, int b, int expected) { Calculator calculator = new Calculator(); assertEquals(expected, calculator.add(a, b)); } }
Step 8: Integrate with CI/CD (Optional)
JUnit works seamlessly with Jenkins, GitHub Actions, and GitLab CI for automated testing.
GitHub Actions Example
Create .github/workflows/test.yml:
name: Run JUnit Tests on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Run Tests run: mvn test
What is Mockito?
Mockito is a popular Java mocking framework used for unit testing. It helps test individual components by simulating dependencies instead of relying on actual implementations.
Mockito creates mock objects that mimic real dependencies. These objects can return predefined responses and track method calls.
Why use Mockito for Unit Testing?
Mockito is useful in the following cases when it comes to unit testing:
- Isolates the component under test by replacing dependencies with mock objects.
- Avoids database, network, or file dependencies, making tests faster and more reliable.
- Allows behavior verification (e.g., checking if a method was called).
- It supports stubbing and enables controlled responses for mocked methods.
Also Read: JavaScript Unit Testing Tutorial
Features of Mockito
Mockito provides powerful mocking capabilities for unit testing in Java. It helps isolate dependencies, control method responses, and verify interactions.
Below are its key features:
1. Mockito allows mocking objects to replace real dependencies in a test environment, preventing tests from depending on databases, APIs, or external systems. This ensures tests run in isolation, making them faster and more reliable.
For example:
UserRepository repoMock = mock(UserRepository.class);
2. Stubbing lets you define method responses for mock objects, ensuring predictable behavior in tests regardless of external factors. This allows precise control over test conditions and expected outcomes.
For example:
when(repoMock.findById(1)).thenReturn(new User(1, "Alice"));
3. Mockito allows checking whether a method was called, how many times, and with which arguments, ensuring correct interactions between components. This helps detect unnecessary or missing method calls, improving test accuracy.
For example:
verify(repoMock, times(1)).findById(1);
4. Mockito works with JUnit assertions to validate test outputs, confirming if the system produces expected values. This ensures correctness by comparing actual versus expected results.
For example:
assertEquals("Alice", user.getName()); assertNotNull(user);
5. Mockito provides @Mock, @InjectMocks, and @Spy annotations to simplify mock object setup, reduce boilerplate code, and make tests more readable and easier to maintain.
For example:
@Mock UserRepository repoMock; @InjectMocks UserService service;
6. A spy allows real method calls but enables selective method overriding, making it useful for testing specific behaviors without losing original functionality. This maintains real behavior while allowing targeted control over certain methods.
For example:
List<String> spyList = spy(new ArrayList<>()); spyList.add("Hello"); verify(spyList).add("Hello");
7. Mockito allows forcing a method to throw an exception, which helps test how the system reacts to failures, ensuring robust error handling and improving system reliability.
For example:
when(repoMock.findById(1)).thenThrow(new RuntimeException("Database error"));
8. Mockito provides argument matchers like anyInt(), anyString(), and eq(value) to handle dynamic inputs in stubbing and verification, allowing flexible and dynamic test cases.
For example:
when(repoMock.findById(anyInt())).thenReturn(new User(1, "Alice"));
How to Write Unit Tests with Mockito?
Mockito makes unit testing easier by allowing developers to mock dependencies, control method behavior, and verify interactions.
Below is a step-by-step guide to writing unit tests using Mockito.
1. Add Mockito Dependency
First, add Mockito to your project. If you are using Maven, add the following dependency in pom.xml:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.2.0</version> <!-- Use the latest version --> <scope>test</scope> </dependency>
For Gradle, add this to build.gradle:
testImplementation 'org.mockito:mockito-core:5.2.0'
Read More: A Complete Guide on Maven Lifecycle
2. Create a Sample Class to Test
Suppose a UserService class depends on a UserRepository to fetch user data.
public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User getUserById(int id) { return userRepository.findById(id); } }
The UserService relies on UserRepository, which will be mocked in the unit test.
3. Write a Mockito Test Class
Create a test class and mock dependencies.
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; class UserServiceTest { @Mock // Create a mock object private UserRepository userRepository; @InjectMocks // Inject mocks into UserService private UserService userService; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } @Test void testGetUserById() { // Arrange: Define the behavior of mock User mockUser = new User(1, "Alice"); when(userRepository.findById(1)).thenReturn(mockUser); // Act: Call method under test User result = userService.getUserById(1); // Assert: Verify the expected result assertNotNull(result); assertEquals("Alice", result.getName()); verify(userRepository, times(1)).findById(1); // Verify method was called once } }
4. Explanation of the Test
- Mock Creation: @Mock creates a fake UserRepository object.
- Dependency Injection: @InjectMocks automatically injects the mock into UserService.
- Setup Method: MockitoAnnotations.openMocks(this); initializes mocks before each test.
- Stubbing behavior: when(userRepository.findById(1)).thenReturn(mockUser); tells Mockito what to return.
- Assertions: assertEquals(“Alice”, result.getName()); ensures the correct output.
- Verification: verify(userRepository, times(1)).findById(1); ensures the method was called once.
5. Running the Test
Execute the test using JUnit in your IDE or command line. If successful, it confirms that the UserService correctly retrieves user data using a mocked UserRepository.
How to use Mockito with JUnit?
Mockito integrates seamlessly with JUnit to create effective unit tests by mocking dependencies, defining expected behavior, and verifying method calls.
Below is a step-by-step guide to using Mockito with JUnit.
1. Add Mockito and JUnit Dependencies
Ensure your project includes Mockito and JUnit 5 dependencies.
For Maven, add this to pom.xml:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.2.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.0</version> <scope>test</scope> </dependency>
For Gradle, add this to build.gradle:
testImplementation 'org.mockito:mockito-core:5.2.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0'
2. Create a Sample Service to Test
Suppose there is a UserService class that depends on UserRepository:
public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User getUserById(int id) { return userRepository.findById(id); } }
The UserService calls UserRepository, which will be mocked in the test.
3. Write a JUnit Test Using Mockito
Create a test class and use Mockito with JUnit 5.
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; class UserServiceTest { @Mock // Creates a mock of UserRepository private UserRepository userRepository; @InjectMocks // Injects the mock into UserService private UserService userService; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); // Initializes mocks } @Test void testGetUserById() { // Arrange: Define mock behavior User mockUser = new User(1, "Alice"); when(userRepository.findById(1)).thenReturn(mockUser); // Act: Call the method under test User result = userService.getUserById(1); // Assert: Verify expected behavior assertNotNull(result); assertEquals("Alice", result.getName()); verify(userRepository, times(1)).findById(1); // Ensure method was called once } }
4. Explanation of the Test
- Mock Creation: @Mock creates a fake UserRepository object.
- Dependency Injection: @InjectMocks injects the mock into UserService.
- Mock Initialization: MockitoAnnotations.openMocks(this); initializes mocks before each test.
- Defining Behavior: when(userRepository.findById(1)).thenReturn(mockUser); tells Mockito what to return.
- Assertions: assertEquals(“Alice”, result.getName()); checks the output.
- Verification: verify(userRepository, times(1)).findById(1); ensures the method was called exactly once.
5. Run the test in your IDE or with mvn test (Maven) or gradle test (Gradle). It confirms that Mockito and JUnit work together for testing if it passes.
Test-Driven Development (TDD) with JUnit and Mockito
Test-Driven Development (TDD) is a software development approach where tests are written before the actual code.
It follows a Red-Green-Refactor cycle:
- Red: Write a failing test.
- Green: Implement the minimum code to pass the test.
- Refactor: Improve the code while ensuring tests still pass.
TDD can be effectively applied using JUnit and Mockito. Below is a step-by-step guide to implementing TDD in Java.
Step 1: A UserService needs to be built to fetch user data from a UserRepository. The repository interacts with a database but will be mocked for unit testing.
Step 2: Create a JUnit test before implementing UserService, ensuring the test fails initially (Red Phase in TDD).
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; class UserServiceTest { @Mock private UserRepository userRepository; // Mock the repository @InjectMocks private UserService userService; // Inject mock into service @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } @Test void testGetUserById() { when(userRepository.findById(1)).thenReturn(new User(1, "Alice")); User user = userService.getUserById(1); assertNotNull(user); assertEquals("Alice", user.getName()); verify(userRepository, times(1)).findById(1); } }
Expected Outcome: The test fails because UserService does not exist yet.
Step 3: Write minimum code to pass the test (Green Phase)
public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User getUserById(int id) { return userRepository.findById(id); } }
Expected Outcome: The test passes because UserService now calls userRepository.findById(id).
Step 4: Optimize the code while ensuring all tests pass, including adding error handling in UserService (Refactor Phase).
public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User getUserById(int id) { User user = userRepository.findById(id); if (user == null) { throw new UserNotFoundException("User not found with ID: " + id); } return user; } }
Expected Outcome: The test still passes, confirming that the change did not break functionality.
Step 5: Add tests for exception scenarios and repeat the TDD cycle for improved coverage.
void testGetUserById_UserNotFound() { when(userRepository.findById(2)).thenReturn(null); Exception exception = assertThrows(UserNotFoundException.class, () -> { userService.getUserById(2); }); assertEquals("User not found with ID: 2", exception.getMessage()); }
Outcome: This test fails if no exception is thrown. After ensuring UserNotFoundException is handled, the test passes.
Best Practices for Unit Testing with JUnit and Mockito
Effective unit testing enhances code reliability and maintainability. JUnit and Mockito facilitate well-structured, efficient, and maintainable tests.
Here are some best practices to achieve high-quality testing.
- Use the Arrange-Act-Assert (AAA) Pattern: Organize tests into three distinct phases:
- Arrange: Initialize test data and dependencies.
- Act: Invoke the method under test.
- Assert: Verify the expected outcome.
- Provide Meaningful Test Method Names: Test names must describe the desired behavior. One proper format is “methodName_condition_expectedOutcome” to make tests self-describing.
- Tests Must Be Small and Focused: Only one behavior must be checked per test. This enhances readability, maintainability, and debugging. Having individual tests for distinct behaviors provides clarity.
- Mock Only External Dependencies: Mocks should be used only for external dependencies, such as databases, APIs, or file systems. Avoid mocking the class under test to ensure real logic is verified.
- Verify Interactions with Dependencies: Mockito allows checking whether methods were called with the right arguments. This ensures that dependencies behave as expected. Always verify critical interactions, especially when working with external systems.
- Test Edge Cases and Exceptions: Ensure tests include invalid inputs, null values, and exceptions. This avoids failures under unexpected conditions and enhances application solidity.
- Use @BeforeEach to Minimize Code Duplication: Set common objects using a setup method to reduce repeated code for various tests. This maintains clean test cases and minimizes duplication.
- Refrain from Testing Implementation Details: Emphasize testing behavior, not internal implementation. Tests must pass even if the implementation varies, but the behavior must be the same.
- Make Tests Run Independently: Every test must be independent and not depend on the result of another test. State shared among tests can give unpredictable results.
- Automate and Run Tests Regularly: Integrate tests into CI/CD pipelines so that they automatically execute with each code change. Regular test execution ensures bugs get caught early, and stability is maintained.
Why execute JUnit tests on Real Devices?
Executing JUnit tests on real devices guarantees consistent, real-world performance verification.
Although emulators and simulators are useful in initial testing, they cannot accurately simulate real user scenarios like network fluctuations, battery effects, or device-specific behaviors. Real devices deliver true test results, allowing teams to detect and resolve issues before they affect users.
Why Use BrowserStack Automate for JUnit Testing?
BrowserStack Automate offers on-demand access to 3,500+ real devices and browsers, allowing developers to execute JUnit test cases on a real device cloud under real user conditions without hosting an in-house device lab.
Key features include:
- Cross-Device Testing: Execute tests across multiple devices, OS versions, and manufacturers.
- Scalability: Automate simultaneous test runs to minimize test run time.
- Cloud-Based Access: No device purchases or hardware maintenance required—get the latest models on-demand.
- Precise Debugging: Take screenshots, logs, and network observations to identify problems quickly.
- Smooth Integration: Integrates with major CI/CD solutions to carry out constant testing.
Useful Resources for JUnit
- How to run JUnit 4 Test Cases in JUnit 5
- JUnit Testing Tutorial: JUnit in Java
- How to write JUnit test cases
- How to Ignore a Base Test Class in JUnit
- How to run JUnit Parameterized Test in Selenium
- Understanding JUnit assertions for Selenium Testing with Examples
- Test Automation using JUnit Annotations and Selenium
- How to create JUnit Test Suite? (with Examples)
- Unit Testing in Java with JUnit
- JUnit vs NUnit: Framework Comparison
- JUnit Vs TestNG
Conclusion
JUnit and Mockito offer a practical framework for creating trustworthy, maintainable, efficient unit tests.
Testing on real devices further promotes test reliability by compensating for realistic scenarios, which emulators and simulators cannot achieve.
BrowserStack Automate provides cloud access to various real devices to simplify this process, allowing easy cross-device testing, parallel testing, and simplified debugging.