Run same JUnit test with multiple roles using Spring Security

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 AnnotationDescription
@ValueSourceSupplies a single array of literal values (like Strings, ints, etc.)
@CsvSourceProvides multiple sets of comma (or custom delimiter) separated values
@CsvFileSourceReads test arguments from an external CSV file
@EnumSourceSupplies constants from a specified Enum class
@MethodSourceUses a factory method to generate arguments programmatically
@ArgumentsSourceCustom 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.

Pavan Jadda
Pavan Jadda
Articles: 2

One comment

  1. 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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.