Part 3: Testing AdventureTube Microservices

🧭 Series Navigation:

⬅️ Introduce AdventureTube Microservice Hub
⬅️ Part 1: Architecture Overview & Design Patterns
⬅️ Part 2: Development Environment & Debugging
Part 3: Testing Strategies (Current)
▶️ Part 4: CI/CD Deployment with Jenkins


Comprehensive Microservice Testing: Unit, Integration & API Testing Strategies

Testing microservices isn’t just unit tests – here’s the complete strategy

After setting up our efficient development environment in Part 2, we need to ensure our AdventureTube microservices are bulletproof. Testing distributed systems presents unique challenges that go far beyond traditional unit testing. Today I’ll show you my comprehensive testing approach that catches bugs before they reach production.

The Microservice Testing Challenge

Traditional testing approaches fall short with microservices because:

  • Service Dependencies: Your service depends on config servers, databases, and other services
  • Network Communication: HTTP calls, service discovery, and load balancing add complexity
  • Configuration Management: Different environments and profiles affect behavior
  • Data Consistency: Distributed transactions and eventual consistency

My solution? A multi-layered testing pyramid specifically designed for microservice architectures.

AdventureTube Testing Strategy Overview

I organize my tests into five distinct levels, each serving a specific purpose:

Testing Pyramid Structure

  1. Unit Tests (70%): Fast, isolated component testing
  2. Integration Tests (20%): Database and external service integration
  3. API Tests (7%): HTTP endpoint and contract validation
  4. Contract Tests (2%): Service interaction agreements
  5. End-to-End Tests (1%): Complete workflow validation

Test Organization with Maven

I use Maven Failsafe plugin to separate different test types:

# Unit tests (fast, no external dependencies)
mvn test

# Integration tests (requires infrastructure)
mvn verify -Pintegration

# All tests including end-to-end
mvn verify -Pfull-test

Unit Testing Deep Dive

Unit tests form the foundation of my testing strategy. Here’s how I structure them for the auth-service:

Core Unit Test Structure

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@TestPropertySource(properties = {
    "spring.cloud.config.enabled=false",
    "eureka.client.enabled=false"
})
class AuthServiceTest {

    @MockBean
    private UserRepository userRepository;
    
    @MockBean
    private ConfigServicePropertySourceLocator configLocator;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Test
    void shouldGenerateValidJwtToken() {
        // Given
        UserDetails userDetails = createTestUser();
        
        // When
        String token = jwtUtil.generateToken(userDetails);
        
        // Then
        assertThat(token).isNotNull();
        assertThat(jwtUtil.extractUsername(token)).isEqualTo("testuser");
        assertThat(jwtUtil.isTokenValid(token, userDetails)).isTrue();
    }
}

Key Unit Testing Patterns

Mocking External Dependencies:

  • @MockBean for Spring components
  • @Mock for regular dependencies
  • @TestConfiguration for test-specific setup

Testing Focus Areas:

  • JWT token generation and validation
  • Password encoding and verification
  • Custom authentication provider logic
  • MapStruct mapper transformations
  • Exception handling scenarios

Example: Testing JWT Authentication

@Test
void shouldValidateJwtTokenCorrectly() {
    // Given
    UserDetails userDetails = User.builder()
        .username("testuser")
        .password("encoded-password")
        .authorities(List.of(new SimpleGrantedAuthority("ROLE_USER")))
        .build();
    
    String token = jwtUtil.generateToken(userDetails);
    
    // When
    boolean isValid = jwtUtil.isTokenValid(token, userDetails);
    
    // Then
    assertThat(isValid).isTrue();
    assertThat(jwtUtil.extractUsername(token)).isEqualTo("testuser");
    assertThat(jwtUtil.extractExpiration(token)).isAfter(new Date());
}

@Test
void shouldRejectExpiredToken() {
    // Given
    String expiredToken = createExpiredToken();
    UserDetails userDetails = createTestUser();
    
    // When & Then
    assertThat(jwtUtil.isTokenValid(expiredToken, userDetails)).isFalse();
}

Integration Testing with Real Infrastructure

Integration tests validate that services work correctly with real infrastructure. Here’s my approach:

Integration Test Configuration

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("integration")
@TestMethodOrder(OrderAnnotation.class)
class AuthControllerIT {

    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @LocalServerPort
    private int port;
    
    @Test
    @Order(1)
    void shouldRegisterNewUser() {
        // Given
        RegisterRequest request = RegisterRequest.builder()
            .username("integration-user")
            .email("test@adventuretube.net")
            .password("securePassword123")
            .build();
        
        // When
        ResponseEntity<AuthResponse> response = restTemplate.postForEntity(
            "http://localhost:" + port + "/api/auth/register",
            request,
            AuthResponse.class
        );
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getToken()).isNotNull();
        
        // Verify database persistence
        Optional<User> savedUser = userRepository.findByUsername("integration-user");
        assertThat(savedUser).isPresent();
    }
}

Testing Against Pi2 Infrastructure

My integration tests connect to the actual infrastructure running on Raspberry Pi 2:

  • Config Server Integration: Pull real configuration from Pi2
  • Database Testing: Use actual PostgreSQL database
  • Eureka Registration: Verify service discovery works
  • Inter-service Communication: Test service-to-service calls

Integration Test Environment Setup

# application-integration.yml
spring:
  config:
    import: "configserver:http://192.168.1.105:9297"
  cloud:
    config:
      profile: integration
      
eureka:
  client:
    service-url:
      defaultZone: http://192.168.1.105:8761/eureka
      
datasource:
  url: jdbc:postgresql://adventuretube.net:5432/adventuretube_test

API Testing with Swagger & Postman

API testing ensures your endpoints work correctly and return proper responses:

Swagger UI Testing

I use Swagger UI for interactive API testing during development:

  1. Navigate to http://localhost:8010/swagger-ui.html
  2. Test authentication endpoints with real requests
  3. Validate request/response schemas
  4. Test error scenarios and edge cases

Automated API Test Suite

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AuthApiTest {

    @Autowired
    private WebTestClient webTestClient;
    
    @Test
    void shouldAuthenticateUser() {
        LoginRequest loginRequest = LoginRequest.builder()
            .username("testuser")
            .password("password123")
            .build();
            
        webTestClient.post()
            .uri("/api/auth/login")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(loginRequest)
            .exchange()
            .expectStatus().isOk()
            .expectBody(AuthResponse.class)
            .value(response -> {
                assertThat(response.getToken()).isNotNull();
                assertThat(response.getUsername()).isEqualTo("testuser");
            });
    }
    
    @Test
    void shouldReturn401ForInvalidCredentials() {
        LoginRequest invalidRequest = LoginRequest.builder()
            .username("testuser")
            .password("wrongpassword")
            .build();
            
        webTestClient.post()
            .uri("/api/auth/login")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(invalidRequest)
            .exchange()
            .expectStatus().isUnauthorized()
            .expectBody(ErrorResponse.class)
            .value(error -> {
                assertThat(error.getMessage()).contains("Invalid credentials");
            });
    }
}

Postman Collection Testing

I maintain Postman collections for comprehensive API testing:

  • Authentication Flow: Register → Login → Access Protected Endpoints
  • Error Scenarios: Invalid tokens, expired sessions, malformed requests
  • Data Validation: Schema validation and business rule verification
  • Performance Testing: Response time benchmarks

Exception Handling Testing

Robust exception handling is crucial for microservices. Here’s how I test the GlobalExceptionHandler:

Exception Test Scenarios

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ExceptionHandlingTest {

    @Test
    void shouldReturn404WhenUserNotFound() {
        // Given
        String nonExistentUserId = "999";
        
        // When
        assertThatThrownBy(() -> userService.findById(nonExistentUserId))
            .isInstanceOf(UserNotFoundException.class)
            .hasMessage("User not found with id: 999");
    }
    
    @Test
    void shouldReturn409ForDuplicateUser() {
        // Given
        RegisterRequest duplicateRequest = createExistingUserRequest();
        
        // When & Then
        assertThatThrownBy(() -> authService.register(duplicateRequest))
            .isInstanceOf(DuplicateException.class)
            .hasMessage("User already exists with username: testuser");
    }
    
    @Test
    void shouldReturn401ForExpiredToken() {
        // Given
        String expiredToken = createExpiredJwtToken();
        
        // When & Then
        assertThatThrownBy(() -> jwtUtil.extractUsername(expiredToken))
            .isInstanceOf(TokenExpiredException.class);
    }
}

HTTP Status Code Validation

@Test
void shouldMapExceptionsToCorrectHttpStatus() {
    Map<Class<? extends Exception>, HttpStatus> expectedMappings = Map.of(
        UserNotFoundException.class, HttpStatus.NOT_FOUND,
        DuplicateException.class, HttpStatus.CONFLICT,
        TokenExpiredException.class, HttpStatus.UNAUTHORIZED,
        ValidationException.class, HttpStatus.BAD_REQUEST
    );
    
    expectedMappings.forEach((exceptionClass, expectedStatus) -> {
        // Test that GlobalExceptionHandler returns correct status
        // for each exception type
    });
}

Database Integration Testing

Database testing ensures data persistence and query performance:

Repository Testing

@DataJpaTest
@TestPropertySource(properties = {
    "spring.jpa.hibernate.ddl-auto=create-drop",
    "spring.datasource.url=jdbc:h2:mem:testdb"
})
class UserRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldFindUserByUsername() {
        // Given
        User testUser = createTestUser();
        entityManager.persistAndFlush(testUser);
        
        // When
        Optional<User> found = userRepository.findByUsername("testuser");
        
        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getEmail()).isEqualTo("test@adventuretube.net");
    }
    
    @Test
    void shouldEnforceUniqueConstraints() {
        // Given
        User user1 = createTestUser();
        User user2 = createTestUserWithSameUsername();
        
        entityManager.persistAndFlush(user1);
        
        // When & Then
        assertThatThrownBy(() -> {
            entityManager.persistAndFlush(user2);
        }).isInstanceOf(DataIntegrityViolationException.class);
    }
}

Real Database Performance Testing

@Test
void shouldPerformEfficientlyWithLargeDataset() {
    // Given
    List<User> users = createLargeUserDataset(10000);
    userRepository.saveAll(users);
    
    // When
    long startTime = System.currentTimeMillis();
    List<User> activeUsers = userRepository.findByActiveTrue();
    long executionTime = System.currentTimeMillis() - startTime;
    
    // Then
    assertThat(executionTime).isLessThan(500); // Max 500ms
    assertThat(activeUsers).hasSizeGreaterThan(5000);
}

Service Discovery Testing

Testing Eureka integration ensures services can find each other:

Service Registration Verification

@SpringBootTest
@TestPropertySource(properties = {
    "eureka.client.service-url.defaultZone=http://192.168.1.105:8761/eureka"
})
class ServiceDiscoveryTest {

    @Autowired
    private EurekaClient eurekaClient;
    
    @Test
    void shouldRegisterWithEureka() {
        // When
        Application application = eurekaClient.getApplication("AUTH-SERVICE");
        
        // Then
        assertThat(application).isNotNull();
        assertThat(application.getInstances()).isNotEmpty();
        
        InstanceInfo instance = application.getInstances().get(0);
        assertThat(instance.getStatus()).isEqualTo(InstanceInfo.InstanceStatus.UP);
    }
    
    @Test
    void shouldDiscoverOtherServices() {
        // When
        List<ServiceInstance> memberServices = discoveryClient.getInstances("MEMBER-SERVICE");
        
        // Then
        assertThat(memberServices).isNotEmpty();
        ServiceInstance instance = memberServices.get(0);
        assertThat(instance.isSecure()).isFalse();
        assertThat(instance.getPort()).isEqualTo(8070);
    }
}

Performance & Load Testing

I use JMeter for load testing and performance benchmarking:

Load Test Configuration

  • Authentication Endpoints: 100 concurrent users, 5-minute ramp-up
  • Database Operations: Connection pool stress testing
  • Memory Usage: Heap analysis under load
  • Response Times: 95th percentile under 200ms

Performance Monitoring

@Test
void shouldMaintainPerformanceUnderLoad() {
    // Given
    int numberOfThreads = 50;
    int requestsPerThread = 100;
    
    // When
    ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
    List<Future<Long>> futures = new ArrayList<>();
    
    for (int i = 0; i < numberOfThreads; i++) {
        futures.add(executor.submit(() -> {
            long totalTime = 0;
            for (int j = 0; j < requestsPerThread; j++) {
                long startTime = System.currentTimeMillis();
                authService.authenticate("testuser", "password");
                totalTime += System.currentTimeMillis() - startTime;
            }
            return totalTime / requestsPerThread;
        }));
    }
    
    // Then
    futures.forEach(future -> {
        try {
            Long avgResponseTime = future.get();
            assertThat(avgResponseTime).isLessThan(100); // Max 100ms average
        } catch (Exception e) {
            fail("Performance test failed: " + e.getMessage());
        }
    });
}

Testing Best Practices

Here are the key principles I follow for microservice testing:

Test Pyramid Implementation

  • Fast Feedback: Unit tests run in under 30 seconds
  • Real Infrastructure: Integration tests use actual services
  • Comprehensive Coverage: Exception scenarios and edge cases
  • Automated Execution: All tests run in CI/CD pipeline

Test Data Management

  • Test Isolation: Each test creates and cleans up its own data
  • Realistic Data: Test data resembles production scenarios
  • Database Seeding: Consistent test data setup
  • Transaction Rollback: Automatic cleanup after tests

What’s Next?

With comprehensive testing ensuring our microservices are bulletproof, we’re ready for the final piece of the puzzle: automated deployment to production.

🧭 Continue Learning My Approach:

⬅️ Introduce AdventureTube Microservice Hub
⬅️ Part 1: Architecture Overview & Design Patterns
⬅️ Part 2: Development Environment & Debugging
Part 3: Testing Strategies (Current)
▶️ Part 4: CI/CD Deployment with JenkinsComplete automation from Git commit to production deployment

In Part 4, I’ll show you how to automate the entire deployment pipeline using Jenkins, Docker, and my Raspberry Pi infrastructure. You’ll see how every Git commit triggers a complete build, test, and deployment cycle that gets your code to production safely and efficiently.


Tags: #Microservices #Testing #SpringBoot #JUnit #Integration #API

Categories: BACKEND(spring-microservice), Testing

Leave a Comment

Your email address will not be published. Required fields are marked *