CtrlK
BlogDocsLog inGet started
Tessl Logo

giuseppe-trisciuoglio/developer-kit

Comprehensive developer toolkit providing reusable skills for Java/Spring Boot, TypeScript/NestJS/React/Next.js, Python, PHP, AWS CloudFormation, AI/RAG, DevOps, and more.

82

Quality

82%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

Validation failed for skills in this tile
One or more skills have errors that need to be fixed before they can move to Implementation and Discovery review.
Overview
Quality
Evals
Security
Files

best-practices.mdplugins/developer-kit-java/skills/spring-boot-test-patterns/references/

Spring Boot Testing Best Practices

Choose the Right Test Type

Select the most efficient test annotation for your use case:

// Use @DataJpaTest for repository-only tests (fastest)
@DataJpaTest
public class UserRepositoryTest { }

// Use @WebMvcTest for controller-only tests
@WebMvcTest(UserController.class)
public class UserControllerTest { }

// Use @SpringBootTest only for full integration testing
@SpringBootTest
public class UserServiceFullIntegrationTest { }

Use @ServiceConnection for Container Management (Spring Boot 3.5+)

Prefer @ServiceConnection over manual @DynamicPropertySource for cleaner code:

// Good - Spring Boot 3.5+
@TestConfiguration
public class TestConfig {
    @Bean
    @ServiceConnection
    public PostgreSQLContainer<?> postgres() {
        return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"));
    }
}

// Avoid - Manual property registration
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
    // ... more properties
}

Keep Tests Deterministic

Always initialize test data explicitly and never depend on test execution order:

// Good - Explicit setup
@BeforeEach
void setUp() {
    userRepository.deleteAll();
    User user = new User();
    user.setEmail("test@example.com");
    userRepository.save(user);
}

// Avoid - Depending on other tests
@Test
void testUserExists() {
    // Assumes previous test created a user
    Optional<User> user = userRepository.findByEmail("test@example.com");
    assertThat(user).isPresent();
}

Use Transactional Tests Carefully

Mark test classes with @Transactional for automatic rollback, but understand the implications:

@SpringBootTest
@Transactional  // Automatically rolls back after each test
public class UserControllerIntegrationTest {

    @Test
    void shouldCreateUser() throws Exception {
        // Changes will be rolled back after test
        mockMvc.perform(post("/api/users")....)
            .andExpect(status().isCreated());
    }
}

Note: Be aware that @Transactional test behavior may differ from production due to lazy loading and flush semantics.

Organize Tests by Layer

Group related tests in separate classes to optimize context caching:

// Repository tests (uses @DataJpaTest)
public class UserRepositoryTest { }

// Controller tests (uses @WebMvcTest)
public class UserControllerTest { }

// Service tests (uses mocks, no context)
public class UserServiceTest { }

// Full integration tests (uses @SpringBootTest)
public class UserFullIntegrationTest { }

Use Meaningful Assertions

Leverage AssertJ for readable, fluent assertions:

// Good - Clear, readable assertions
assertThat(user.getEmail())
    .isEqualTo("test@example.com");

assertThat(users)
    .hasSize(3)
    .contains(expectedUser);

assertThatThrownBy(() -> userService.save(invalidUser))
    .isInstanceOf(ValidationException.class)
    .hasMessageContaining("Email is required");

// Avoid - JUnit assertions
assertEquals("test@example.com", user.getEmail());
assertTrue(users.size() == 3);

Mock External Dependencies

Mock external services but use real databases for integration tests:

// Good - Mock external services, use real DB
@SpringBootTest
@TestContainerConfig.class
public class OrderServiceTest {

    @MockBean
    private EmailService emailService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldSendConfirmationEmail() {
        // Use real database, mock email service
        Order order = new Order();
        orderService.createOrder(order);

        verify(emailService, times(1)).sendConfirmation(order);
    }
}

// Avoid - Mocking the database layer
@Test
void shouldCreateOrder() {
    when(orderRepository.save(any())).thenReturn(mockOrder);
    // Tests don't verify actual database behavior
}

Use Test Fixtures for Common Data

Create reusable test data builders:

public class UserTestFixture {
    public static User validUser() {
        User user = new User();
        user.setEmail("test@example.com");
        user.setName("Test User");
        return user;
    }

    public static User userWithEmail(String email) {
        User user = validUser();
        user.setEmail(email);
        return user;
    }
}

// Usage in tests
@Test
void shouldSaveUser() {
    User user = UserTestFixture.validUser();
    userRepository.save(user);
    assertThat(userRepository.count()).isEqualTo(1);
}

Document Complex Test Scenarios

Use @DisplayName and comments for complex test logic:

@Test
@DisplayName("Should validate email format and reject duplicates with proper error message")
void shouldValidateEmailBeforePersisting() {
    // Given: Two users with the same email
    User user1 = new User();
    user1.setEmail("test@example.com");
    userRepository.save(user1);

    User user2 = new User();
    user2.setEmail("test@example.com");  // Duplicate email

    // When: Attempting to save duplicate
    // Then: Should throw exception with clear message
    assertThatThrownBy(() -> {
        userRepository.save(user2);
        userRepository.flush();
    })
    .isInstanceOf(DataIntegrityViolationException.class)
    .hasMessageContaining("unique constraint");
}

Avoid Common Pitfalls

// Avoid: Using @DirtiesContext without reason (forces context rebuild)
@SpringBootTest
@DirtiesContext  // DON'T USE unless absolutely necessary
public class ProblematicTest { }

// Avoid: Mixing multiple profiles in same test suite
@SpringBootTest(properties = "spring.profiles.active=dev,test,prod")
public class MultiProfileTest { }

// Avoid: Starting containers manually
@SpringBootTest
public class ManualContainerTest {
    static {
        PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
        postgres.start();  // Avoid - use @ServiceConnection instead
    }
}

// Good: Consistent configuration, minimal context switching
@SpringBootTest
@TestContainerConfig
public class ProperTest { }

Test Naming Conventions

Convention: Use descriptive method names that start with should or test to make test intent explicit.

Naming Rules:

  • Prefix: Start with should or test to clearly indicate test purpose
  • Structure: Use camelCase for readability (no underscores)
  • Clarity: Name should indicate what is being tested and the expected outcome
  • Example pattern: should[ExpectedBehavior]When[Condition]()

Examples:

shouldReturnUsersJson()
shouldThrowNotFoundWhenIdDoesntExist()
shouldPropagateExceptionOnPersistenceError()
shouldSaveAndRetrieveUserFromDatabase()
shouldValidateEmailFormatBeforePersisting()

Apply these rules consistently across all integration test methods.

plugins

developer-kit-java

skills

README.md

tile.json