Back to Testing & Quality Assurance

unit-test-service-layer

javaspring bootmockitojunitunit testingservice layerbackend testingtdd
282📄 MIT🕒 2026-06-15Source ↗

Install this skill

npx skills add giuseppe-trisciuoglio/developer-kit

Works 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

  1. Add Mockito and JUnit 5 dependencies to your build configuration
  2. Annotate your test class with @ExtendWith(MockitoExtension.class)
  3. Declare dependencies as @Mock fields
  4. Initialize the target service using @InjectMocks
  5. Define method behaviors using the when(...).thenReturn(...) syntax
  6. 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

Why avoid @SpringBootTest for service unit tests?
@SpringBootTest starts the entire application context, which is slow and unnecessary for testing individual business logic components.
Can I use this for testing methods that return void?
Yes, use doNothing() or doThrow() syntax from Mockito to handle stubbing for methods with a void return type.
How do I handle Optional values returned by my mocks?
Simply configure your mock to return Optional.of(value) or Optional.empty() using the .thenReturn() method.

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.

Source & trust

282 stars📄 MIT🕒 Updated 2026-06-15
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
# Unit Testing Service Layer with Mockito

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)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/unit-test-service-layer/
  3. Save the file as SKILL.md
  4. 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

Read the Master Guide: Mastering Agent Skills

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid testing & quality assurance issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Testing & Quality Assurance and is published by Giuseppe Trisciuoglio, maintained in giuseppe-trisciuoglio/developer-kit.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.