Testing / QA¶
Test pyramid¶
- Unit (most) — fast, isolated, mock dependencies.
- Integration — components together (DB, external services / virtualization).
- End-to-end (fewest) — full system through the UI/API; slow, brittle.
Black box vs white box¶
-
Black Box Testing The technique of testing in which the tester doesn’t have access to the source code of the software and is conducted at the software interface without any concern with the internal logical structure of the software is known as black-box testing.
-
White-Box Testing The technique of testing in which the tester is aware of the internal workings of the product, has access to its source code, and is conducted by making sure that all internal operations are performed according to the specifications is known as white box testing.
Unit Testing A level of the software testing process where individual units/components of a software/system are tested. The purpose is to validate that each unit of the software performs as designed.
Integration Testing A level of the software testing process where individual units are combined and tested as a group. The purpose of this level of testing is to expose faults in the interaction between integrated units
System Testing A level of the software testing process where a complete, integrated system/software is tested. The purpose of this test is to evaluate the system’s compliance with the specified requirements.
Acceptance Testing A level of the software testing process where a system is tested for acceptability. The purpose of this test is to evaluate the system’s compliance with the business requirements and assess whether it is acceptable for delivery.
Regression testing Checking whether new features break or degrade functionality
Smoke tests check the basic functionality of an application, check ready for other tests, tests simple and basic functionalities, such as if the user is able to log in or log out.
Performance tests Test the run-time performance of software - load testing
Stress tests check how the system works under unfavorable conditions
TDD & BDD¶
See SDLC → BDD & TDD for the driven-development approaches.
Principles & tooling¶
- AAA — Arrange, Act, Assert. Test behavior, not implementation. Deterministic, independent, fast.
- Java: JUnit 5 + Mockito;
@SpringBootTest/@WebMvcTest(MockMvc) for Spring; Testcontainers for real deps. - Test doubles: dummy · stub · spy · mock · fake. Coverage (JaCoCo) is a guide, not a goal.
JUnit 5 Basics¶
JUnit 5 is the testing framework for Java. Structure: @Test → Arrange → Act → Assert.
Maven Dependency¶
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
Basic Test Structure¶
public class CalculatorTest {
private Calculator calculator;
// Run before each test
@BeforeEach
public void setup() {
calculator = new Calculator();
}
// Run after each test (cleanup)
@AfterEach
public void tearDown() {
calculator = null;
}
// Actual test method
@Test
public void testAddition() {
// Arrange: set up test data
int a = 5;
int b = 3;
// Act: perform the action
int result = calculator.add(a, b);
// Assert: verify the result
assertEquals(8, result);
}
// Skip this test
@Disabled("Skipping for now")
@Test
public void testDivisionByZero() {
assertThrows(ArithmeticException.class, () -> {
calculator.divide(10, 0);
});
}
}
Lifecycle Annotations¶
| Annotation | Runs | Usage |
|---|---|---|
@BeforeEach |
Before each test | Initialize test fixtures |
@AfterEach |
After each test | Cleanup resources |
@BeforeAll |
Once before all tests (static) | Expensive setup |
@AfterAll |
Once after all tests (static) | Resource cleanup |
@Disabled |
Skips test | Temporarily disable |
public class DatabaseTest {
private static Database db;
// Run once at the beginning (static method)
@BeforeAll
static void setupDatabase() {
db = new Database("jdbc:mysql://localhost:3306/testdb");
db.connect();
}
// Run once at the end (static method)
@AfterAll
static void tearDownDatabase() {
db.disconnect();
}
}
Display Names & Tags¶
@DisplayName("Calculator Tests")
public class CalculatorTest {
@DisplayName("Should add two numbers correctly")
@Test
@Tag("math")
public void testAdd() {
assertEquals(8, calculator.add(5, 3));
}
@DisplayName("Should throw exception on division by zero")
@Test
@Tag("exception")
public void testDivisionError() {
assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0));
}
}
Run specific tag: mvn test -Dgroups=math
JUnit Assertions¶
Common assertions for verifying test outcomes.
Basic Assertions¶
// Equality
assertEquals(expected, actual);
assertEquals(8, calculator.add(5, 3));
// Inequality
assertNotEquals(5, calculator.add(2, 2));
// Null checks
assertNull(nullValue);
assertNotNull(nonNullValue);
// Boolean
assertTrue(condition);
assertFalse(!condition);
Exception Assertions¶
// Verify exception is thrown
@Test
public void testException() {
ArithmeticException ex = assertThrows(
ArithmeticException.class,
() -> calculator.divide(10, 0)
);
assertEquals("Cannot divide by zero", ex.getMessage());
}
// Verify no exception
@Test
public void testNoException() {
assertDoesNotThrow(() -> {
calculator.divide(10, 2);
});
}
Collection Assertions¶
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// Contains
assertTrue(list.contains(3));
assertAll(
() -> assertTrue(list.contains(1)),
() -> assertTrue(list.contains(5))
);
// Size
assertEquals(5, list.size());
// Is empty
assertTrue(emptyList.isEmpty());
Grouped Assertions¶
@Test
public void testUser() {
User user = userService.createUser("John", "Doe");
// All assertions must pass; failures reported together
assertAll("User validation",
() -> assertEquals("John", user.getFirstName()),
() -> assertEquals("Doe", user.getLastName()),
() -> assertNotNull(user.getId())
);
}
Timeout Assertions¶
@Test
public void testPerformance() {
// Code must complete within 100ms
assertTimeout(Duration.ofMillis(100), () -> {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}
});
// Aborts if exceeds timeout
assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
Thread.sleep(2000); // Aborted
});
}
Custom Messages¶
@Test
public void testWithMessage() {
assertEquals(
8,
calculator.add(5, 3),
"Addition should equal 8" // Message on failure
);
assertEquals(
8,
calculator.add(5, 3),
() -> "Addition of 5 + 3 should equal 8" // Lazy evaluation (only if fails)
);
}
Mockito - Mocking Framework¶
Mockito creates mock objects to isolate units under test. Mock external dependencies instead of real ones.
Maven Dependency¶
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<!-- For Spring Boot tests -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Creating Mocks¶
public class OrderServiceTest {
private OrderService orderService;
private PaymentGateway paymentGateway; // Mock
private UserRepository userRepository; // Mock
@BeforeEach
public void setup() {
// Method 1: Create mock explicitly
paymentGateway = mock(PaymentGateway.class);
userRepository = mock(UserRepository.class);
// Inject mocks into service under test
orderService = new OrderService(paymentGateway, userRepository);
}
// OR use @Mock annotation with MockitoAnnotations.openMocks()
@Mock
private PaymentGateway paymentGateway;
@BeforeEach
public void init() {
MockitoAnnotations.openMocks(this); // Initializes @Mock fields
}
}
Stubbing (Configure Mock Behavior)¶
@Test
public void testOrderProcessing() {
// Arrange: configure mock to return specific value
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "John")));
when(paymentGateway.charge(100.0))
.thenReturn(true);
// Act
Order order = orderService.createOrder(1L, 100.0);
// Assert
assertTrue(order.isPaid());
}
Stubbing Different Scenarios¶
// Return value on each call
when(mock.method())
.thenReturn(1)
.thenReturn(2)
.thenReturn(3);
// Throw exception
when(userRepository.findById(999L))
.thenThrow(new UserNotFoundException("Not found"));
// Return based on argument
when(userRepository.findById(1L))
.thenReturn(Optional.of(user1));
when(userRepository.findById(2L))
.thenReturn(Optional.of(user2));
// Matcher-based stubbing (any, eq, argThat)
when(userRepository.findById(any(Long.class)))
.thenReturn(Optional.of(anyUser));
when(paymentGateway.charge(anyDouble()))
.thenReturn(true);
// Custom matcher
when(userRepository.findByEmail(argThat(email -> email.endsWith("@example.com"))))
.thenReturn(Optional.of(user));
// Do-nothing for void methods
doNothing().when(notificationService).sendEmail(any());
// Do-something for void methods
doAnswer(invocation -> {
String email = invocation.getArgument(0);
System.out.println("Sending email to: " + email);
return null;
}).when(notificationService).sendEmail(any());
Verifying Mock Interactions¶
@Test
public void testOrderPayment() {
orderService.createOrder(1L, 100.0);
// Verify method was called at least once
verify(paymentGateway).charge(100.0);
// Verify called exactly N times
verify(paymentGateway, times(1)).charge(100.0);
// Verify called with specific arguments
verify(userRepository).findById(1L);
// Verify never called
verify(notificationService, never()).sendEmail(any());
// Verify call order
InOrder inOrder = inOrder(userRepository, paymentGateway);
inOrder.verify(userRepository).findById(1L);
inOrder.verify(paymentGateway).charge(100.0);
}
Spies (Partial Mocks)¶
Wrap real object; mock specific methods, call real methods for others:
@Test
public void testWithSpy() {
List<String> list = new ArrayList<>();
List<String> spyList = spy(list); // Spy on real ArrayList
// Stub one method
when(spyList.size()).thenReturn(100);
spyList.add("item");
// size() returns stubbed value
assertEquals(100, spyList.size());
// add() executes real method
assertEquals(1, list.size()); // Real list has 1 item
verify(spyList).add("item");
}
Spring + Mockito Integration¶
@SpringBootTest
public class UserServiceIntegrationTest {
@MockBean // Mock bean in Spring context
private UserRepository userRepository;
@Autowired
private UserService userService; // Real service with mocked repo
@Test
public void testGetUser() {
// Arrange
User user = new User(1L, "John");
when(userRepository.findById(1L))
.thenReturn(Optional.of(user));
// Act
User result = userService.getUser(1L);
// Assert
assertEquals("John", result.getName());
verify(userRepository).findById(1L);
}
}
Best Practices¶
✅ Do's
- Mock external dependencies (DB, HTTP, APIs)
- Use verify() to confirm interactions
- Keep tests focused and independent
- Use argument matchers for flexibility
- Test behavior, not implementation
❌ Don'ts - Don't mock the class under test - Don't over-mock (keep tests readable) - Don't verify details that shouldn't matter - Don't use real objects when mocks make sense - Don't forget to verify void method calls