Skip to content

Latest commit

 

History

History
478 lines (347 loc) · 8.35 KB

File metadata and controls

478 lines (347 loc) · 8.35 KB

Testing Guide

This document describes the testing strategy and practices for the FlossWare Platform.

Test Categories

Tests are categorized using JUnit 5 @Tag annotations for selective execution.

Available Tags

Tag Description Examples
unit Fast, isolated unit tests Method logic, validation, builders
integration Tests with external dependencies Database, network, file I/O
security Security-focused tests Input validation, auth, encryption
performance Performance and load tests Benchmarks, stress tests
slow Long-running tests (>1 second) Large data processing, retries

Running Tests by Category

Run All Tests (Default)

mvn test

Run Only Unit Tests (Fast)

mvn test -Dtest.groups=unit

Run Only Integration Tests

mvn test -Dtest.groups=integration

Run Security Tests

mvn test -Dtest.groups=security

Exclude Slow Tests

mvn test -Dtest.excludedGroups=slow

Multiple Tags (OR logic)

# Run unit OR security tests
mvn test -Dtest.groups="unit | security"

Multiple Tags (AND logic)

# Run tests that are BOTH unit AND security
mvn test -Dtest.groups="unit & security"

Complex Expressions

# Run integration tests but exclude slow ones
mvn test -Dtest.groups=integration -Dtest.excludedGroups=slow

Writing Tests

Basic Test Structure

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

@Tag("unit")
class MyServiceTest {
  
  @Test
  void shouldCalculateCorrectly() {
    // Arrange
    MyService service = new MyService();
    
    // Act
    int result = service.calculate(5, 3);
    
    // Assert
    assertEquals(8, result);
  }
}

Test with Multiple Tags

@Tag("integration")
@Tag("slow")
class DatabaseIntegrationTest {
  
  @Test
  void shouldConnectToDatabase() {
    // Test implementation
  }
}

Security Test Example

@Tag("unit")
@Tag("security")
class InputValidationTest {
  
  @Test
  void shouldRejectPathTraversal() {
    assertThrows(SecurityException.class, () -> {
      validator.validate("../../../etc/passwd");
    });
  }
}

Test Naming Conventions

Method Names

Use descriptive names following this pattern:

// Pattern: should[ExpectedBehavior]When[StateUnderTest]

@Test
void shouldThrowExceptionWhenInputIsNull() { }

@Test
void shouldReturnEmptyListWhenDatabaseIsEmpty() { }

@Test
void shouldCalculateDiscountWhenUserIsPremium() { }

Class Names

// Pattern: [ClassName]Test

class UserServiceTest { }
class ApplicationManagerTest { }
class SecurityValidatorTest { }

Coverage Requirements

  • Minimum: 60% overall coverage (enforced by JaCoCo)
  • Target: 80%+ for critical modules
  • Security Code: 90%+ coverage required

Check Coverage

# Generate coverage report
mvn clean test jacoco:report

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

Coverage by Module

# Run coverage for specific module
cd platform-core
mvn clean test jacoco:report

Test Data

Temporary Files

Use @TempDir for temporary file/directory tests:

import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;

class FileProcessorTest {
  
  @TempDir
  Path tempDir;
  
  @Test
  void shouldProcessFile() {
    Path testFile = tempDir.resolve("test.txt");
    Files.writeString(testFile, "content");
    // Test with testFile
  }
}

Test Fixtures

@BeforeEach
void setUp() {
  // Initialize test data
  testUser = new User("user123", "Test User");
}

@AfterEach
void tearDown() {
  // Clean up resources
}

Assertions

Common Assertions

// Equality
assertEquals(expected, actual);
assertNotEquals(unexpected, actual);

// Boolean
assertTrue(condition);
assertFalse(condition);

// Null checks
assertNull(value);
assertNotNull(value);

// Exceptions
assertThrows(IllegalArgumentException.class, () -> {
  service.methodThatThrows();
});

// Timeout
assertTimeout(Duration.ofSeconds(1), () -> {
  // Fast operation
});

// Collections
assertIterableEquals(expectedList, actualList);

Custom Messages

assertEquals(expected, actual, 
  "User ID should match the created user");

Parameterized Tests

Test multiple inputs efficiently:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;

@Tag("unit")
class ParameterizedExampleTest {
  
  @ParameterizedTest
  @ValueSource(strings = {"user1", "user2", "user3"})
  void shouldValidateUsername(String username) {
    assertTrue(validator.isValid(username));
  }
  
  @ParameterizedTest
  @CsvSource({
    "1, 2, 3",
    "5, 5, 10",
    "10, -5, 5"
  })
  void shouldAddNumbers(int a, int b, int expected) {
    assertEquals(expected, calculator.add(a, b));
  }
}

Mock Objects

Use Mockito for mocking dependencies:

import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class ServiceWithDependenciesTest {
  
  @Mock
  private DatabaseRepository repository;
  
  @Test
  void shouldCallRepository() {
    when(repository.findById("123"))
      .thenReturn(Optional.of(testUser));
    
    User result = service.getUser("123");
    
    verify(repository).findById("123");
    assertEquals(testUser, result);
  }
}

CI/CD Integration

Default CI Pipeline

All tests run by default:

- name: Run tests
  run: mvn clean verify

Fast Feedback (Unit Tests Only)

- name: Quick test
  run: mvn test -Dtest.groups=unit

Full Test Suite

- name: Unit tests
  run: mvn test -Dtest.groups=unit

- name: Integration tests
  run: mvn test -Dtest.groups=integration

- name: Security tests
  run: mvn test -Dtest.groups=security

Nightly Build (All Tests)

- name: Full test suite
  run: mvn clean verify

Best Practices

1. Independent Tests

Each test should be independent and isolated:

// Good - self-contained
@Test
void shouldCalculate() {
  Calculator calc = new Calculator();
  assertEquals(4, calc.add(2, 2));
}

// Bad - depends on execution order
static int counter = 0;

@Test
void firstTest() {
  counter = 5;
}

@Test
void secondTest() {
  assertEquals(5, counter); // Fails if run alone
}

2. One Assertion Concept Per Test

// Good - tests one thing
@Test
void shouldValidateEmail() {
  assertTrue(validator.isValidEmail("user@example.com"));
}

@Test
void shouldRejectInvalidEmail() {
  assertFalse(validator.isValidEmail("invalid"));
}

// Avoid - tests multiple things
@Test
void emailValidation() {
  assertTrue(validator.isValidEmail("user@example.com"));
  assertFalse(validator.isValidEmail("invalid"));
  assertThrows(Exception.class, () -> validator.isValidEmail(null));
}

3. Clear Test Names

// Good - describes behavior
@Test
void shouldReturnNullWhenUserNotFound() { }

// Bad - unclear
@Test
void test1() { }

4. Arrange-Act-Assert Pattern

@Test
void shouldCalculateDiscount() {
  // Arrange
  Order order = new Order(100.00);
  DiscountCalculator calculator = new DiscountCalculator();
  
  // Act
  double discount = calculator.calculate(order);
  
  // Assert
  assertEquals(10.00, discount);
}

5. Test Edge Cases

@Test
void shouldHandleEmptyInput() { }

@Test
void shouldHandleNullInput() { }

@Test
void shouldHandleMaximumValue() { }

@Test
void shouldHandleNegativeValue() { }

Troubleshooting

Tests Pass Locally But Fail in CI

  • Check for timezone dependencies
  • Check for file path separators (Windows vs Linux)
  • Verify test isolation (no shared state)

Flaky Tests

  • Use @Timeout to catch slow tests
  • Avoid Thread.sleep() - use proper synchronization
  • Mock time-dependent operations

Coverage Not Meeting Threshold

# Identify uncovered code
mvn clean test jacoco:report
open target/site/jacoco/index.html

# Add tests for red/yellow highlighted code

Resources