Skip to content

Phase 2: Refactor Service Tests to Use Testcontainers #529

@robfrank

Description

@robfrank

Overview

Refactor BackfillEmbeddingsServiceTest and SearchContentServiceTest to use Testcontainers with real production code instead of Mockito mocks.

Priority: HIGH
Estimated Time: Week 2
Depends On: #528 (Phase 1)

Context

Replace mock-based testing with integration tests using:

  • REAL ContentPersistenceAdapter and ArcadeContentRepository
  • REAL ArcadeDB with actual vector index
  • FakeEmbeddingGenerator for deterministic embeddings
  • State-based assertions instead of mock verify() statements

Benefits

  • ✅ Tests validate actual database behavior, not mock interactions
  • ✅ Catches SQL errors, schema issues, and mapping bugs
  • ✅ Tests survive refactoring (no brittle mock setups)
  • ✅ More readable with Given/When/Then structure
  • ✅ Validates real vector search with LSM_TREE index

Tasks

2.1 Refactor BackfillEmbeddingsServiceTest

File: src/test/java/it/robfrank/linklift/application/domain/service/BackfillEmbeddingsServiceTest.java

Changes:

  • Extend ArcadeDbTestBase instead of using @ExtendWith(MockitoExtension.class)
  • Replace mocked LoadContentPort and SaveContentPort with real repository
  • Replace mocked EmbeddingGenerator with FakeEmbeddingGenerator
  • Update all 11 tests to:
    • Use repository.saveContent() to set up test data
    • Verify actual database state with repository.findContentById()
    • Remove all verify() statements
    • Use Awaitility instead of Thread.sleep() where possible

Example transformation:

// Before (with mocks)
when(loadContentPort.findContentsWithoutEmbeddings(100))
    .thenReturn(List.of(content))
    .thenReturn(List.of());
verify(saveContentPort, times(1)).updateContent(argThat(c -> c.embedding() != null));

// After (with Testcontainers)
repository.saveContent(content); // Setup test data in REAL database
backfillEmbeddingsService.backfill();
Thread.sleep(1000);
Content updated = repository.findContentById("id-1").orElseThrow();
assertThat(updated.embedding()).isNotNull().hasSize(384);

2.2 Refactor SearchContentServiceTest

File: src/test/java/it/robfrank/linklift/application/domain/service/SearchContentServiceTest.java

Changes:

  • Extend ArcadeDbTestBase
  • Replace mocked LoadContentPort with real repository
  • Replace mocked EmbeddingGenerator with FakeEmbeddingGenerator
  • Update all 21 tests to:
    • Save content with embeddings to real database
    • Verify actual vector search results using ArcadeDB's LSM_TREE index
    • Remove all verify() statements
    • Test actual cosine similarity ranking

Example transformation:

// Before (with mocks)
when(embeddingGenerator.generateEmbedding(query)).thenReturn(queryVector);
when(loadContentPort.findSimilar(queryVector, 10)).thenReturn(List.of(resultContent));

// After (with Testcontainers)
Content ml1 = content.withEmbedding(embeddingGenerator.generateEmbedding("machine learning"));
repository.saveContent(ml1);
List<Content> results = searchContentService.search("machine learning basics", 2);
assertThat(results).extracting(Content::id).containsExactlyInAnyOrder("id-1", "id-2");

2.3 Update Test Structure

For both test classes:

  • Add @AfterEach to call super.tearDownDatabase() and clean up ExecutorService
  • Use Given/When/Then comments for clarity
  • Replace Thread.sleep() with Awaitility where feasible
  • Ensure all tests use FIXED_TEST_TIME for deterministic dates

2.4 Validation

  • All 32 tests pass (11 from BackfillEmbeddings + 21 from SearchContent)
  • No Mockito verify() statements remain in service tests
  • Tests use real database state assertions
  • Measure total execution time (target: under 40 seconds)
  • Verify tests catch SQL/schema errors (try intentional typo)

Success Criteria

  • Both test classes extend ArcadeDbTestBase
  • All mocks replaced with real implementations
  • All 32 tests pass with Testcontainers
  • Tests validate actual database state
  • Execution time under 40 seconds
  • Code coverage maintained or improved

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions