When building enterprise applications with Spring Boot and Spring Security, it’s common to implement role-based access control (RBAC). But how do we test the same endpoint with different roles—without duplicating your test code?
Enter JUnit parameterized tests. In this post, we’ll walk through testing an employee reassignment feature, showing how to execute the same JUnit test against different user roles, cleanly and efficiently.
Parameterized Tests
Parameterized tests make it possible to run a test multiple times with different arguments. They are declared just like regular @Test
methods but use the @ParameterizedTest
annotation instead. In addition, you must declare at least one source that will provide the arguments for each invocation and then consume the arguments in the test method.
🔄 Types of Parameter Sources in JUnit 5
JUnit 5 supports several ways to inject different parameters into your test methods. Here are the commonly used ones:
Source Annotation | Description |
---|---|
@ValueSource | Supplies a single array of literal values (like Strings, ints, etc.) |
@CsvSource | Provides multiple sets of comma (or custom delimiter) separated values |
@CsvFileSource | Reads test arguments from an external CSV file |
@EnumSource | Supplies constants from a specified Enum class |
@MethodSource | Uses a factory method to generate arguments programmatically |
@ArgumentsSource | Custom provider via a class that implements ArgumentsProvider |
In our example, we used @CsvSource
because:
- It’s compact
- Easy to read inline
- Flexible with delimiters like
|
to handle complex values (like comma-separated roles)
🔗 For more details, refer to the official docs: JUnit Parameterized Test Sources
💡 Scenario: Create Employee via an API
Let’s say your system allows authorized users—like HR Managers or Admins—to create employee records. The endpoint /api/v1/employees/create
is secured and only certain roles can access it. The Employee entity has 3 fields
Employee
package com.example;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
@Entity
@Table(name = "employee")
@Data
@Builder
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Employee name is required")
@Column(nullable = false)
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@Column(nullable = false, unique = true)
private String email;
@NotBlank(message = "Department is required")
@Column(nullable = false)
private String department;
}
EmployeeCreateRequest
A DTO class to create the employee
package com.example;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record EmployeeCreateRequest(
@NotBlank(message = "Name is required") String name,
@NotBlank(message = "Email is required") @Email String email,
@NotBlank(message = "Department is required") String department
) {}
EmployeeController
A simple Spring Boot REST controller to handle employee creation
package com.example.hrms.controller;
import com.example.hrms.dto.EmployeeCreateRequest;
import com.example.hrms.model.Employee;
import com.example.hrms.service.EmployeeService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/employee")
@RequiredArgsConstructor
public class EmployeeController {
private final EmployeeService service;
public EmployeeController(EmployeeService service) {
this.service = service;
}
@PostMapping("/create")
public Employee createEmployee(@Valid @RequestBody EmployeeCreateRequest request) {
return employeeService.createEmployee(request);
}
}
We’ll test this endpoint using MockMvc, verifying that both roles can create employees.
🔧 The Setup
Here’s what our integration test class looks like, using Spring’s MockMvc
and @ParameterizedTest
.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@AutoConfigureMockMvc
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public class EmployeeIntegrationTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
// Converts a comma-separated string to Spring Security authorities
static List<SimpleGrantedAuthority> getAuthorities(String roles) {
return Arrays.stream(roles.split(","))
.map(SimpleGrantedAuthority::new)
.toList();
}
// Test create endpoint with different roles
@ParameterizedTest
@CsvSource(delimiter = '|', value = {
"[email protected]|ROLE_EMPLOYEE,ROLE_HR_MANAGER",
"[email protected]|ROLE_EMPLOYEE,ROLE_ADMIN"
})
void createEmployee_shouldSucceed_ForAuthorizedRoles(String username, String roles) throws Exception {
var request = new EmployeeCreateRequest("Jane Doe", "[email protected]", "Operations");
mockMvc.perform(post("/api/v1/employee/create")
.with(csrf())
.with(user(username).authorities(getAuthorities(roles)))
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Jane Doe"))
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.department").value("Operations"));
}
}
🧠 Code Breakdown
1. @ParameterizedTest with @CsvSource
@ParameterizedTest
@CsvSource(delimiter = '|', value = {
"[email protected]|ROLE_EMPLOYEE,ROLE_MANAGER",
"[email protected]|ROLE_EMPLOYEE,ROLE_ADMIN"
})
We define a test that runs with multiple inputs:
- One for a Manager
- One for an Admin
Each test run uses a different username and a set of roles.
2. getAuthorities Method
static List<SimpleGrantedAuthority> getAuthorities(String roles) {
return Arrays.stream(roles.split(","))
.map(SimpleGrantedAuthority::new)
.toList();
}
This utility converts a role string like "ROLE_EMPLOYEE,ROLE_MANAGER"
into a list of Spring Security authorities. This is needed because MockMvc
expects authorities for simulating users. But if you prefer to use roles directly, you can skip this and add roles
3. Using MockMvc
with user()
and csrf()
mockMvc.perform(post("/api/v1/employee/create")
.with(csrf())
.with(user(username).authorities(getAuthorities(roles)))
We simulate:
- a user login using
user(username)
- the user’s roles using
.authorities(...)
- a valid CSRF token via
.with(csrf())
which is needed for non-GET requests in Spring Security
4. Validate
.andExpect(status().isOk())
.andExpect(jsonPath("$.newDepartment").value("Operations"));
We assert that:
- The response returns HTTP 200 OK
- The
newDepartment
field in the response matches what we sent
✅ Benefits of This Approach
- No duplicated test logic — run one test for multiple users
- Easy to add more roles — just add to the
@CsvSource
- Realistic security testing — verifies your Spring Security config with different role combinations
- Clean separation of test data vs test logic
🧪 Final Thoughts
Using @ParameterizedTest
with Spring Security’s test utilities allows you to write clean, maintainable, and powerful tests for role-based access. If you’re working on systems with multiple user types, this approach can save you tons of redundant code.
Good one, and also we can do with custom annotation( by having specific role like @read, @admin) create custom annotations and just pass them at specific test level long with @test, annotate @read to the test method. It would be clean and reusable as well.