unit-testing
Unit Testing Guidelines
General Principles
1. Test Independence
- Each test must be independent and self-contained
- Tests must not rely on execution order
- Use local variables within test methods instead of shared instance variables when possible
2. Clear Test Names
Follow the pattern: methodName_scenario_expectedBehavior
Examples: tableExists_positive_tableIsPresent, deleteTimer_negative_timerDescriptorIdIsNull
3. Test Organization
Structure every test with: Arrange → Act → Assert
Mockito Best Practices
1. Never Use Lenient Mode
❌ Never add @MockitoSettings(strictness = Strictness.LENIENT) — write precise tests instead.
2. Avoid Unnecessary Stubbing
Only stub (when()) what is actually used in the test. Use helper methods called only by tests that need them:
private void setupContextMocks() {
when(context.getFolioModuleMetadata()).thenReturn(moduleMetadata);
when(context.getTenantId()).thenReturn(TENANT_ID);
when(moduleMetadata.getDBSchemaName(TENANT_ID)).thenReturn(SCHEMA_NAME);
}
3. Remove Redundant Verify Statements
Only verify interactions that are not mocked with when():
verify(connection).close(); // ✅ not mocked
verify(dataSource).getConnection(); // ❌ redundant — already mocked
4. Prefer Specific Object Stubs Over any() Matchers
Use specific objects when you control test data:
when(mapper.convert(input)).thenReturn(output); // ✅
when(mapper.convert(any(InputType.class))).thenReturn(output); // ❌
Use any() only when: the service mutates the object unpredictably, multiple different objects may be passed, or when using argument captors.
5. Use Simple when().thenReturn() for Basic Returns
when(service.process(data)).thenReturn(true); // ✅
doAnswer(inv -> true).when(service).process(data); // ❌
Use thenAnswer() only for computed return values, conditional logic, or side effects.
6. Stub the Complete Service Flow
Stub every external call in the code path being tested:
when(repository.findByKey(key)).thenReturn(Optional.empty());
when(repository.save(entity)).thenReturn(entity);
when(repository.findById(id)).thenReturn(Optional.of(entity));
7. Use verifyNoMoreInteractions Carefully
Only include mocks that should be fully accounted for:
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dataSource, connection, databaseMetaData, resultSet);
}
Test Structure
1. Class Setup
@UnitTest
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
private static final String CONSTANT_VALUE = "test-value";
@Mock
private Dependency1 dependency1;
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dependency1);
}
}
2. Individual Test
@Test
void methodName_scenario_expectedBehavior() throws Exception {
// Arrange
setupCommonMocks();
var service = new MyService(dependency1);
when(dependency1.doSomething()).thenReturn(expectedValue);
// Act
var result = service.methodUnderTest();
// Assert
assertThat(result).isEqualTo(expectedValue);
verify(dependency1).close();
}
Verification Patterns
1. Successful Operations
@Test
void operation_positive_success() throws Exception {
setupRequiredMocks();
when(repository.find()).thenReturn(entity);
var result = service.operation();
assertThat(result).isNotNull();
verify(connection).close();
}
2. Exception Scenarios
@Test
void operation_negative_throwsException() throws Exception {
var expectedException = new SQLException("Connection failed");
when(dataSource.getConnection()).thenThrow(expectedException);
assertThatThrownBy(() -> service.operation())
.isInstanceOf(DataRetrievalFailureException.class)
.hasMessageContaining("Failed to perform operation")
.hasCause(expectedException);
}
Common Patterns
1. Constants for Test Data
Define at class level for reuse across tests:
private static final String MODULE_ID = "mod-foo-1.0.0";
private static final String TENANT_ID = "test-tenant";
2. Helper Methods
Extract private static methods for object creation, assertions, finding objects, and mock setup when the same code appears in 2+ tests.
For advanced patterns (parameterized tests, deep copy stubbing, fluent API, exact matching, explicit lambda types), see references/patterns.md.
For complete test class examples, see references/examples.md.
Checklist
- No
@MockitoSettings(strictness = Strictness.LENIENT) - No unnecessary stubbing — all
when()statements are used - Only verify methods that are NOT mocked
- Specific object stubs over
any()when test data is controlled - Simple
when().thenReturn()for basic returns - All repository/service interactions in the code path are stubbed
- Copy operations stubbed when service creates internal copies
- Test names follow
methodName_scenario_expectedBehavior - Each test is independent
- Resources (connections, streams) verified to be closed
- Exception tests verify type, message, and cause
- Constants used for reusable test data
- Helper methods eliminate code duplication
- Clear Arrange-Act-Assert sections
- Fluent API used for test data creation
- Parameterized tests use
Stream<Type>for single parameters - Explicit type declarations for lambdas when var inference fails
- Exact matching tests include similar non-matching values
- All error paths from utility methods covered
- Unused imports removed
More from folio-org/folio-eureka-ai-dev
write-user-story
Use when creating, writing, or refining a user story or ticket. Produces structured stories with purpose/overview, functional requirements, Given-When-Then acceptance criteria, and manual testing guidance. Also use when asked to define acceptance criteria, scope a feature, or prepare a story for development.
21document-feature
Use when the user asks to document an implemented feature. Analyze the diff from the base branch, infer the feature boundary and name, and generate behavioral feature documentation under docs/features/.
19code-review
Use when the user asks to perform a code review, review code changes, analyze a diff, or audit code quality. Runs a structured review of git diff output covering security, correctness, performance, maintainability, and style. Produces a markdown report saved as a .md file named after the current branch.
12skill-feedback
Use when a user has finished using one installed skill and wants to preserve actionable feedback about that skill while the session context is still fresh
4write-bug
Use when creating, writing, or refining a bug report for a FOLIO project. Produces structured bug tickets with a clear summary, preconditions, numbered steps to reproduce, expected vs. actual results, and supporting evidence (logs, stack traces, screenshots). Also use when asked to file a defect, triage an issue, or prepare a bug for Jira. Optionally interacts with the user to gather missing context and can create the ticket via the Jira MCP integration.
4liquibase-migration
>-
4