Guide for writing unit and integration tests with JUnit 5 and the ONTIC test base framework.
Guide: Writing Unit & Integration Tests with JUnit 5 and Ontic Test Base
1. Unit Tests vs Integration Tests — What & Why
| Aspect | Unit Tests | Integration Tests |
|---|---|---|
| Scope | A single class or method in isolation | Multiple modules or components working together, like DB, messaging, or external systems |
| Dependencies | Mocked or stubbed, in-memory or fake | Real dependencies, via containers, embedded DBs, and so on |
| Speed | Very fast, milliseconds to tens of milliseconds | Slower, hundreds of milliseconds to seconds |
| Purpose | Test internal logic, edge cases, and algorithm correctness | Validate integration, configuration, and boundary behavior |
| Flakiness risk | Lower if well isolated | Higher because of network, startup, and external state |
| Ideal usage ratio | Many | Some, but fewer than unit tests |
When to choose which
Unit Test- The point of unit testing is
logic correctness - It will not tell you how dependencies are interacting or how your object is getting saved
- So if your goal is
testing business logic, unit testing is best - But if your code also involves I/O-bound behavior like:
- how an object is getting saved
- database indexing behavior
- aspect-based proxies
- then mocking can become misleading
- If you mock a service that has an aspect-based proxy, you bypass the proxy entirely
- The point of unit testing is
Integration Testing- For
system testing, integration tests are best - They are slower, but they usually give a much more accurate depiction of how the code actually behaves
- Of course you can rely only on integration tests, but sometimes you just want to test business logic, and in that case mocks are useful
- For
2. Test Framework — Overview & Concepts
Architecture
Base test infrastructure:
- Container support for MongoDB, Elasticsearch, Redis, and planned Kafka
- Reusable containers to avoid repeated setup per test run
- Scoped setup tasks via
TestSetupTask, global, suite, and test - Annotation-driven config through
@UseContainersand@WithSetup(...) - Local DB support via
@UseLocal - Presets to choose between single-node or cluster topologies to better mirror production
- Gradle plugin
ontic.test-basefortestandintegrationTesttasks
Scoped Setup Details
- Global
- Setup tasks for container bootstrapping, for example index templates
- Suite
- Per-class setup data
- Test
- Specific to the individual test method
@WithSetup allows linking setup logic with Spring-managed services. The framework handles lifecycle, wiring, and cleanup.
Container Reuse
- The framework caches and reuses containers and global setups
- Unless containers are explicitly killed, restarts are fast
Local Mode
- Use annotations like
@UseLocalto skip containers and run against a local DB - This improves speed for local development workflows
3. Guide: Writing Test Cases & Framework Usage
3.1 Unit Tests (JUnit 5)
These live under src/test/java and are executed via the test task.
Best Practices
- Use descriptive names, for example
shouldCalculateDiscount_whenCustomerIsPreferred() - Follow Arrange–Act–Assert
- Collect — Test — Verify
- Avoid external dependencies
- Use mocks and fakes for isolation
- Make tests deterministic and readable
class PromoServiceTest {
@Mock
private CustomerRepository customerRepo;
@InjectMocks
private PromoService promoService;
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
void shouldApplyDiscount_whenCustomerHasLoyalty() {
Customer c = new Customer("Alice", 3);
when(customerRepo.findById("id1")).thenReturn(Optional.of(c));
double discount = promoService.calculateDiscount("id1", 100.0);
assertEquals(10.0, discount);
}
@Test
void shouldThrowException_whenCustomerNotFound() {
when(customerRepo.findById("id2")).thenReturn(Optional.empty());
assertThrows(NotFoundException.class,
() -> promoService.calculateDiscount("id2", 50.0));
}
}3.2 Integration Tests (Using Framework)
These live under src/test/java and are executed via integrationTest.
Setup
@UseContainers
@WithSetup({ OrderSetupTask.class})
class OrderProcessingIntegrationTest extends ModuleBaseIntegrationTest {
// Spring context loaded, containers running, setup executed
}Local Mode
@UseLocal
class LocalDbOrderIntegrationTest extends IntegrationBaseTest {
// No container startup
}Example Test
@Test
@WithSetup(RequiresNewUserSetup.class)
@Tag("integration")
void testOrderLifecycle_fullFlow() {
CreateOrderRequest req = new CreateOrderRequest(...);
OrderResponse resp = restTemplate.postForObject("/api/orders", req, OrderResponse.class);
assertNotNull(resp.getOrderId());
await().untilAsserted(() -> {
Order order = orderRepository.findById(resp.getOrderId()).orElseThrow();
assertEquals(OrderStatus.CONFIRMED, order.getStatus());
});
List<OrderEvent> events = kafkaConsumer.fetchEvents(...);
assertTrue(events.stream().anyMatch(e -> e.getType() == ORDER_CONFIRMED));
}3.3 Reusable and Utility Classes
- These live under
src/testFixtures/java - All
TestSetupTaskclasses should reside intestFixturesif you think they are going to be used in other modules
Sample Module Base Integration Test
@SpringJUnitConfig(classes = {CoreTestsConfig.class})
public class CoreNewIntegrationTest extends FwkBaseIntegrationTest {
}Sample TestSetupTask
@Service
public class RedisDefaultConfigTestSetup implements TestSetupTask {
private final ResourceConfigService resourceConfigService;
public RedisDefaultConfigTestSetup(ResourceConfigService resourceConfigService) {
this.resourceConfigService = resourceConfigService;
}
@Override
public TestSetupTaskType type() {
return CoreTestSetupTaskType.REDIS_SETUP;
}
@Override
public SetupScope scope() {
return SetupScope.GLOBAL;
}
@Override
public void setup(TestSetupExecutionContext executionContext) {
RunningContainer<?> redisContainer = executionContext.getRunningContainerMap().get(ResourceCategory.REDIS.name());
TestSetupUtils.setupIfChanged(type().name(), ResourceCategory.REDIS.name(), redisContainer, this::ensureRedisConfigs);
}
private void ensureRedisConfigs(RunningContainer<?> runningContainer) {
final List<RedisContainer> redisContainers = (List<RedisContainer>) runningContainer.containers();
RedisContainer redisContainer = OnticCollectionUtils.firstElement(redisContainers);
ensureGenericTenantConfig(FwkConstants.Redis.BROADCAST, new Host(redisContainer.getHost(), redisContainer.getFirstMappedPort()));
ensureGenericTenantConfig(FwkConstants.Redis.CACHE, new Host(redisContainer.getHost(), redisContainer.getFirstMappedPort()));
ensureGenericTenantConfig(FwkConstants.Redis.KEYSTORE, new Host(redisContainer.getHost(), redisContainer.getFirstMappedPort()));
}
private void ensureGenericTenantConfig(String resourceType, Host host) {
RedisConfig redisConfig1 = new RedisConfig();
redisConfig1.setOrgId(FwkConstants.GLOBAL_ORG_ID);
redisConfig1.setResourceType(resourceType);
redisConfig1.setResourceCategory(ResourceCategory.REDIS.name());
redisConfig1.setSpaceId(FwkConstants.GLOBAL_SPACE_ID);
redisConfig1.setClusterMode(false);
List<Host> hostList = new ArrayList<>();
hostList = new ArrayList<>();
hostList.add(host);
redisConfig1.setHosts(hostList);
resourceConfigService.createConfig(redisConfig1);
}
}3.4 Gradle Configuration
- Consume fixtures from other modules using the
testFixturesconfiguration to avoid duplication and keep test code DRY - Use other modules' test fixtures based on your need
- Do not copy-paste other Gradle files
- Consumer module must contain the
ontic.test-baseplugin in the Gradle file to include standard Gradle tasks - Use:
./gradlew testfor unit tests./gradlew integrationTestfor integration tests
- All required dependencies will be included by default by adding other modules' test fixtures dependency
- No need to add every library explicitly
Gradle
plugins {
id 'ontic.java-base'
id 'ontic.test-base'
}
dependencies {
testFixturesApi(testFixtures(project(':core')))
}4. Best Practices & Suggestions
- Use meaningful test names
- Group related tests into suites
- Clean up test data properly
- Avoid global state unless isolated by container instance
- Prefer singleton containers for reuse across suites
- For new containers, implement
ContainerProvider