unit-test-service-layer
Install this skill
npx skills add giuseppe-trisciuoglio/developer-kitWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
This skill provides a systematic approach for testing Spring Service layer components in isolation. By employing Mockito's annotation-based dependency injection, it allows developers to validate business logic without the overhead of initializing the full Spring application context or external data sources. The approach centers on using @ExtendWith(MockitoExtension.class) to inject mocks into the target service, ensuring that internal coordination—such as database interactions, email triggers, or external API calls—can be stubbed and verified precisely. By focusing on pure unit testing, this skill enables rapid feedback loops during development, facilitating the validation of complex orchestration logic, exception paths, and state transitions within a service class. It is the standard approach for maintaining high code quality through strict verification of collaborators without triggering infrastructure-level integration tests.
When to Use This Skill
- •Testing conditional logic within service methods
- •Validating retry logic or specific exception handling
- •Ensuring data orchestration across multiple repository calls
- •Mocking third-party clients to simulate network failure scenarios
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- “unit test my spring service class
- “mock dependencies for service layer test
- “how to verify repository calls in mockito
- “test exception handling in service layer
- “create mockito test for java service
Pro Tips
- 💡Always verify interactions with mocked dependencies to ensure your service calls the right methods with the correct arguments.
- 💡Use ArgumentCaptor for complex argument matching in verify calls, especially when capturing values generated within the service.
- 💡Focus your unit tests on the 'what' (behavior) rather than the 'how' (implementation details) to make them more robust to refactoring.
What this skill does
- •Injecting mocked dependencies using @Mock and @InjectMocks
- •Stubbing behavior of repository and service-layer collaborators
- •Verifying invocation counts and exact arguments passed to dependencies
- •Asserting custom exception propagation from underlying layers
- •Capturing complex objects for deep inspection during test assertions
When not to use it
- ✕When testing complex transactional boundaries that require a real database
- ✕When verifying integration with actual external APIs or infrastructure components
- ✕When you need to test the full Spring configuration or bean loading process
Example workflow
- Add Mockito and JUnit 5 dependencies to your build configuration
- Annotate your test class with @ExtendWith(MockitoExtension.class)
- Declare dependencies as @Mock fields
- Initialize the target service using @InjectMocks
- Define method behaviors using the when(...).thenReturn(...) syntax
- Invoke the target method and verify interactions with verify(...) calls
Prerequisites
- –Java development environment
- –Spring Boot project structure
- –JUnit 5 and Mockito libraries in classpath
Pitfalls & limitations
- !Over-mocking behavior, which leads to tests that pass even when the code is logically broken
- !Failing to reset or properly configure mock expectations between tests
- !Using real Spring context features that are not available in a pure unit test environment
FAQ
How it compares
Unlike manual testing or generic prompts, this skill enforces a structured Mockito pattern that keeps tests fast, deterministic, and isolated from side effects.
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
Test @Service annotated classes by mocking all injected dependencies. Focus on business logic validation without starting the Spring container.
## When to Use This Skill
Use this skill when:
- Testing business logic in @Service classes
- Mocking repository and external client dependencies
- Verifying service interactions with mocked collaborators
- Testing complex workflows and orchestration logic
- Want fast, isolated unit tests (no database, no API calls)
- Testing error handling and edge cases in services
## Setup with Mockito and JUnit 5
### Maven
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>### Gradle
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core")
testImplementation("org.mockito:mockito-junit-jupiter")
testImplementation("org.assertj:assertj-core")
}## Basic Pattern: Service with Mocked Dependencies
### Single Dependency
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.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnAllUsers() {
// Arrange
List<User> expectedUsers = List.of(
new User(1L, "Alice"),
new User(2L, "Bob")
);
when(userRepository.findAll()).thenReturn(expectedUsers);
// Act
List<User> result = userService.getAllUsers();
// Assert
assertThat(result).hasSize(2);
assertThat(result).containsExactly(
new User(1L, "Alice"),
new User(2L, "Bob")
);
verify(userRepository, times(1)).findAll();
}
}### Multiple Dependencies
@ExtendWith(MockitoExtension.class)
class UserEnrichmentServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Mock
private AnalyticsClient analyticsClient;
@InjectMocks
private UserEnrichmentService enrichmentService;
@Test
void shouldCreateUserAndSendWelcomeEmail() {
User newUser = new User(1L, "Alice", "[email protected]");
when(userRepository.save(any(User.class))).thenReturn(newUser);
doNothing().when(emailService).sendWelcomeEmail(newUser.getEmail());
User result = enrichmentService.registerNewUser("Alice", "[email protected]");
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("Alice");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("[email protected]");
verify(analyticsClient, never()).trackUserRegistration(any());
}
}## Testing Exception Handling
### Service Throws Expected Exception
@Test
void shouldThrowExceptionWhenUserNotFound() {
when(userRepository.findById(999L))
.thenThrow(new UserNotFoundException("User not found"));
assertThatThrownBy(() -> userService.getUserDetails(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("User not found");
verify(userRepository).findById(999L);
}
@Test
void shouldRethrowRepositoryException() {
when(userRepository.findAll())
.thenThrow(new DataAccessException("Database connection failed"));
assertThatThrownBy(() -> userService.getAllUsers())
.isInstanceOf(DataAccessException.class)
.hasMessageContaining("Database connection failed");
}## Testing Complex Workflows
### Multiple Service Method Calls
@Test
void shouldTransferMoneyBetweenAccounts() {
Account fromAccount = new Account(1L, 1000.0);
Account toAccount = new Account(2L, 500.0);
when(accountRepository.findById(1L)).thenReturn(Optional.of(fromAccount));
when(accountRepository.findById(2L)).thenReturn(Optional.of(toAccount));
when(accountRepository.save(any(Account.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
moneyTransferService.transfer(1L, 2L, 200.0);
// Verify both accounts were updated
verify(accountRepository, times(2)).save(any(Account.class));
assertThat(fromAccount.getBalance()).isEqualTo(800.0);
assertThat(toAccount.getBalance()).isEqualTo(700.0);
}## Argument Capturing and Verification
### Capture Arguments Passed to Mock
import org.mockito.ArgumentCaptor;
@Test
void shouldCaptureUserDataWhenSaving() {
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
userService.createUser("Alice", "[email protected]");
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertThat(capturedUser.getName()).isEqualTo("Alice");
assertThat(capturedUser.getEmail()).isEqualTo("[email protected]");
}
@Test
void shouldCaptureMultipleArgumentsAcrossMultipleCalls() {
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
userService.createUser("Alice", "[email protected]");
userService.createUser("Bob", "[email protected]");
verify(userRepository, times(2)).save(userCaptor.capture());
List<User> capturedUsers = userCaptor.getAllValues();
assertThat(capturedUsers).hasSize(2);
assertThat(capturedUsers.get(0).getName()).isEqualTo("Alice");
assertThat(capturedUsers.get(1).getName()).isEqualTo("Bob");
}## Verification Patterns
### Verify Call Order and Frequency
import org.mockito.InOrder;
@Test
void shouldCallMethodsInCorrectOrder() {
InOrder inOrder = inOrder(userRepository, emailService);
userService.registerNewUser("Alice", "[email protected]");
inOrder.verify(userRepository).save(any(User.class));
inOrder.verify(emailService).sendWelcomeEmail(any());
}
@Test
void shouldCallMethodExactlyOnce() {
userService.getUserDetails(1L);
verify(userRepository, times(1)).findById(1L);
verify(userRepository, never()).findAll();
}## Testing Async/Reactive Services
### Service with CompletableFuture
@Test
void shouldReturnCompletableFutureWhenFetchingAsyncData() {
List<User> users = List.of(new User(1L, "Alice"));
when(userRepository.findAllAsync())
.thenReturn(CompletableFuture.completedFuture(users));
CompletableFuture<List<User>> result = userService.getAllUsersAsync();
assertThat(result).isCompletedWithValue(users);
}## Best Practices
- **Use @ExtendWith(MockitoExtension.class)** for JUnit 5 integration
- **Construct service manually** instead of using reflection when possible
- **Mock only direct dependencies** of the service under test
- **Verify interactions** to ensure correct collaboration
- **Use descriptive variable names**:
expectedUser, actualUser, captor- **Test one behavior per test method** - keep tests focused
- **Avoid testing framework code** - focus on business logic
## Common Patterns
**Partial Mock with Spy**:
@Spy
@InjectMocks
private UserService userService; // Real instance, but can stub some methods
@Test
void shouldUseRealMethodButMockDependency() {
when(userRepository.findById(any())).thenReturn(Optional.of(new User()));
// Calls real userService methods but userRepository is mocked
}**Constructor Injection for Testing**:
// In your service (production code)
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.repository = userRepository;
}
}
// In your test - can inject mocks directly
@Test
void test() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
}## Troubleshooting
**UnfinishedStubbingException**: Ensure all
when() calls are completed with thenReturn(), thenThrow(), or thenAnswer().**UnnecessaryStubbingException**: Remove unused stub definitions. Use
@ExtendWith(MockitoExtension.class) with MockitoExtension.LENIENT if you intentionally have unused stubs.**NullPointerException in test**: Verify
@InjectMocks correctly injects all mocked dependencies into the service constructor.## References
- [Mockito Documentation](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html)
- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/)
- [AssertJ Assertions](https://assertj.github.io/assertj-core-features-highlight.html)
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/unit-test-service-layer/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-service-layer/SKILL.md - Cursor:
~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-service-layer/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-service-layer/SKILL.md
🚀 Install with CLI:npx skills add giuseppe-trisciuoglio/developer-kit