Athena — mahmoud-consultancy/setup.md

Testing Guide - Recruitment Backend

Project: InterimPlaza Recruitment Platform Developed by: GloryLabs for InterimPlaza Last Updated: October 10, 2025


Table of Contents

  1. Overview
  2. Test Structure
  3. Running Tests
  4. Writing Tests
  5. Test Coverage
  6. Best Practices
  7. Troubleshooting

Overview

This project uses a comprehensive testing strategy including:

  • Unit Tests - Test individual components in isolation
  • Integration Tests - Test component interactions
  • BDD Tests - Behavior-driven development with Cucumber
  • Security Tests - Test authentication and authorization

Testing Framework

  • JUnit 5 - Primary test framework
  • Spring Boot Test - Spring context for tests
  • Mockito - Mocking framework
  • MockMvc - HTTP request testing
  • Cucumber - BDD testing
  • Testcontainers - Integration testing with Docker

Test Structure

backend/src/test/
├── java/nl/glorylabs/
│   ├── controller/          # Controller layer tests
│   │   ├── JobControllerTest.java              ✅ 39 tests
│   │   ├── ApplicationControllerTest.java      ✅ 34 tests
│   │   └── AuthControllerSecurityTest.java     ✅ Security tests
│   │
│   ├── service/             # Service layer tests
│   │   ├── JobServiceTest.java                 ✅ 30+ tests
│   │   ├── ApplicationServiceTest.java         ✅ 25+ tests
│   │   ├── AuthServiceTest.java                ✅ 20+ tests
│   │   └── EmailServiceTest.java               ✅ 15+ tests
│   │
│   ├── security/            # Security tests
│   │   ├── JwtTokenSecurityTest.java           ✅ JWT validation
│   │   ├── RateLimitingSecurityTest.java       ✅ Rate limiting
│   │   ├── SecurityHeadersTest.java            ✅ Security headers
│   │   └── PasswordSecurityTest.java           ✅ Password policies
│   │
│   ├── mapper/              # Mapper tests
│   │   ├── JobMapperTest.java                  ✅ DTO mapping
│   │   └── ApplicationMapperTest.java          ✅ DTO mapping
│   │
│   ├── integration/         # Integration tests
│   │   ├── JobControllerIT.java                ✅ End-to-end
│   │   ├── ApplicationControllerIT.java        ✅ End-to-end
│   │   └── CucumberIT.java                     ✅ BDD runner
│   │
│   └── cv/                  # CV module tests
│       └── service/CVProfileServiceTest.java   ✅ CV tests
│
└── features/                # BDD feature files
    ├── authentication/
    │   └── 01-user-authentication.feature
    └── cv-management/
        ├── 01-cv-profile-management.feature
        └── 02-cv-pdf-generation.feature

Test Statistics

  • Total Test Files: 15
  • Total Test Cases: ~470
  • Controller Tests: 73 test cases
  • Service Tests: 90+ test cases
  • Security Tests: 40+ test cases
  • Integration Tests: 3 test suites
  • BDD Scenarios: 3 feature files

Running Tests

All Tests

# Maven
./mvnw test

# Make
make test

# With coverage report
./mvnw clean test jacoco:report

Specific Test Categories

# Unit tests only
./mvnw test -Dgroups=unit

# Integration tests only
./mvnw test -Dgroups=integration

# Controller tests
./mvnw test -Dtest="*ControllerTest"

# Service tests
./mvnw test -Dtest="*ServiceTest"

# Security tests
./mvnw test -Dtest="*SecurityTest"

Specific Test Classes

# Single test class
./mvnw test -Dtest=JobControllerTest

# Single test method
./mvnw test -Dtest=JobControllerTest#getAllJobs_Success_ReturnsPageOfJobs

# Multiple test classes
./mvnw test -Dtest=JobControllerTest,ApplicationControllerTest

BDD Tests

# Run Cucumber tests
./mvnw test -Dtest=CucumberIT

# Run specific feature
./mvnw test -Dcucumber.features=src/test/features/authentication/

With Docker

# Run tests in Docker
docker-compose run --rm backend mvn test

# With coverage
docker-compose run --rm backend mvn clean test jacoco:report

Writing Tests

Unit Test Template

package nl.glorylabs.recruitment.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * Unit tests for YourService
 *
 * Tests cover:
 * - Feature 1
 * - Feature 2
 * - Error handling
 *
 * InterimPlaza Recruitment Platform - Developed by GloryLabs for InterimPlaza
 */
@ExtendWith(MockitoExtension.class)
class YourServiceTest {

    @Mock
    private YourRepository repository;

    @InjectMocks
    private YourService service;

    private YourEntity testEntity;

    @BeforeEach
    void setUp() {
        // Setup test data
        testEntity = YourEntity.builder()
                .id(1L)
                .name("Test")
                .build();
    }

    @Test
    void methodName_scenario_expectedResult() {
        // Given (Arrange)
        when(repository.findById(1L)).thenReturn(Optional.of(testEntity));

        // When (Act)
        YourDto result = service.getById(1L);

        // Then (Assert)
        assertNotNull(result);
        assertEquals("Test", result.getName());
        verify(repository).findById(1L);
    }
}

Controller Test Template

package nl.glorylabs.recruitment.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Controller tests for YourController
 *
 * InterimPlaza Recruitment Platform - Developed by GloryLabs for InterimPlaza
 */
@WebMvcTest(YourController.class)
class YourControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private YourService service;

    private YourDto testDto;

    @BeforeEach
    void setUp() {
        testDto = new YourDto();
        testDto.setId(1L);
        testDto.setName("Test");
    }

    @Test
    void getAll_Success_ReturnsData() throws Exception {
        // Given
        when(service.getAll()).thenReturn(List.of(testDto));

        // When & Then
        mockMvc.perform(get("/your-endpoint"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].name").value("Test"));
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void create_Success_AsAdmin() throws Exception {
        // Given
        when(service.create(any(YourDto.class))).thenReturn(testDto);

        // When & Then
        mockMvc.perform(post("/your-endpoint")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(testDto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.name").value("Test"));
    }
}

Integration Test Template

package nl.glorylabs.recruitment.integration;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Integration test for YourController
 *
 * InterimPlaza Recruitment Platform - Developed by GloryLabs for InterimPlaza
 */
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class YourControllerIT {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void endToEndFlow_Success() throws Exception {
        // Test the full flow with real database
        mockMvc.perform(get("/your-endpoint"))
                .andExpect(status().isOk());
    }
}

BDD Feature File Template

Feature: Your Feature Name
  As a user role
  I want to perform an action
  So that I can achieve a goal

  Background:
    Given the system is running
    And test data is loaded

  Scenario: Happy path scenario
    Given I am authenticated as a "USER"
    When I request "/your-endpoint"
    Then the response status should be 200
    And the response should contain "expected data"

  Scenario: Error scenario
    Given I am not authenticated
    When I request "/your-endpoint"
    Then the response status should be 401

Test Coverage

Current Coverage (October 10, 2025)

| Layer | Coverage | Status | |-------|----------|--------| | Controllers | 75% | 🟢 Good | | Services | 60% | 🟡 Moderate | | Repositories | 40% | 🟡 Moderate | | Security | 90% | 🟢 Excellent | | Mappers | 100% | 🟢 Excellent | | Overall | 25% | 🟡 Moderate |

Coverage Goals

  • Target Overall: 80%
  • Minimum Per Class: 70%
  • Critical Paths: 100%

Generate Coverage Report

# Generate JaCoCo report
./mvnw clean test jacoco:report

# View report
open target/site/jacoco/index.html

# Check coverage threshold (configured in pom.xml)
./mvnw jacoco:check

Coverage Thresholds (pom.xml)

<jacoco.minimum.coverage>0.70</jacoco.minimum.coverage>

Best Practices

1. Test Naming

Pattern: methodName_scenario_expectedResult()

Good Examples:

✅ getAllJobs_Success_ReturnsPageOfJobs()
✅ createJob_Unauthorized_AsUser()
✅ getJobById_NotFound_ThrowsResourceNotFoundException()

Bad Examples:

❌ test1()
❌ testGetJobs()
❌ shouldReturnJobs()

2. Test Structure (AAA Pattern)

@Test
void testMethod() {
    // Given (Arrange) - Setup test data and mocks
    when(repository.findById(1L)).thenReturn(Optional.of(entity));

    // When (Act) - Execute the method being tested
    YourDto result = service.getById(1L);

    // Then (Assert) - Verify the results
    assertNotNull(result);
    assertEquals("expected", result.getValue());
}

3. Mock Strategy

Do Mock:

  • External services (APIs, databases)
  • Dependencies of the class under test
  • Time-dependent code (Clock)

Don't Mock:

  • The system under test
  • Value objects (DTOs, entities)
  • Simple data structures (List, Map)

4. Assertion Guidelines

// Good - Multiple assertions for related properties
assertNotNull(result);
assertEquals(1L, result.getId());
assertEquals("Test", result.getName());

// Good - Specific error messages
assertThrows(ResourceNotFoundException.class,
    () -> service.getById(999L),
    "Should throw when resource not found");

// Bad - Single assertion in test
assertNotNull(result);  // Test more!

// Bad - No verification of mocks
when(repository.save(any())).thenReturn(entity);
service.create(dto);
// Missing: verify(repository).save(any());

5. Test Data Management

@BeforeEach
void setUp() {
    // Create reusable test data
    testEntity = Entity.builder()
            .id(1L)
            .name("Test")
            .active(true)
            .build();
}

// Use builders for flexibility
Entity.builder()
    .id(1L)
    .name("Custom Name")
    .build();

6. Security Testing

// Test authenticated access
@Test
@WithMockUser(roles = "USER")
void testAsUser() { }

// Test admin access
@Test
@WithMockUser(roles = "ADMIN")
void testAsAdmin() { }

// Test unauthenticated access
@Test
void testUnauthenticated() { }

// Test with specific user
@Test
@WithMockUser(username = "john@example.com", roles = "USER")
void testAsSpecificUser() { }

7. Controller Testing

// Test GET endpoint
mockMvc.perform(get("/jobs"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.content").isArray());

// Test POST endpoint with CSRF
mockMvc.perform(post("/jobs")
        .with(csrf())
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(dto)))
    .andExpect(status().isCreated());

// Test PUT endpoint
mockMvc.perform(put("/jobs/1")
        .with(csrf())
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(dto)))
    .andExpect(status().isOk());

// Test DELETE endpoint
mockMvc.perform(delete("/jobs/1")
        .with(csrf()))
    .andExpect(status().isNoContent());

8. Test Organization

Group tests by functionality:

class JobServiceTest {

    // ========== GET ALL JOBS TESTS ==========

    @Test
    void getAllJobs_Success() { }

    @Test
    void getAllJobs_EmptyList() { }

    // ========== CREATE JOB TESTS ==========

    @Test
    void createJob_Success() { }

    @Test
    void createJob_Unauthorized() { }

    // ========== ERROR HANDLING TESTS ==========

    @Test
    void createJob_InvalidInput() { }
}

Troubleshooting

Common Issues

1. Tests Pass Locally but Fail in CI

Cause: Environment differences Solution:

# Use test profile
./mvnw test -Dspring.profiles.active=test

# Check environment variables
env | grep TEST

2. Flaky Tests (Intermittent Failures)

Cause: Race conditions, timing issues Solution:

// Bad - Time-dependent
Thread.sleep(1000);

// Good - Use awaitility
await().atMost(5, SECONDS)
    .until(() -> repository.findById(1L).isPresent());

3. Mock Not Working

Cause: Wrong matcher or mock setup Solution:

// Bad - Exact match required
when(repository.findById(1L)).thenReturn(Optional.of(entity));
service.getById(2L); // Returns empty!

// Good - Use any() or verify argument
when(repository.findById(anyLong())).thenReturn(Optional.of(entity));

// Debug - Print actual arguments
verify(repository).findById(argThat(id -> {
    System.out.println("Called with: " + id);
    return true;
}));

4. CSRF Token Missing

Cause: Security enabled, no CSRF token Solution:

// Add .with(csrf()) to all mutations
mockMvc.perform(post("/jobs")
        .with(csrf())  // Add this!
        .contentType(MediaType.APPLICATION_JSON)
        .content(json))
    .andExpect(status().isCreated());

5. JSON Serialization Errors

Cause: Missing ObjectMapper configuration Solution:

@Autowired
private ObjectMapper objectMapper;

String json = objectMapper.writeValueAsString(dto);

Resources

Documentation

Project-Specific


Contact & Support

Project: InterimPlaza Recruitment Platform Developed by: GloryLabs for InterimPlaza Company: Mahmoud Consultancy B.V.

For questions or issues with tests:

  1. Check this guide
  2. Review existing test examples
  3. Ask the team on Slack
  4. Create an issue on GitHub

Last Updated: October 10, 2025 Testing Guide v1.0

Reacties

Nog geen reacties