UI Unit Testing in Spring-based Projects
In Spring-based projects, views may use dependency injection to get references to service and other software components.
To instantiate such views and handle navigation correctly, Vaadin needs special implementations of internal components, such as SpringInstantiator
.
Testing with UIUnitTest
provides specialized base test classes that integrate with the Spring Testing Framework: SpringUIUnitTest
for JUnit 5, and SpringUIUnit4Test
for JUnit 4.
Subclasses can therefore rely on all the features offered by the Spring Testing Framework.
The test class can be annotated with @ContextConfiguration
to give a reduced ApplicationContext
for a faster startup, or the @SpringBootTest
annotation for more complex scenarios.
@ContextConfiguration(classes = ViewTestConfig.class)
class ViewTest extends SpringUIUnitTest {
}
@Configuration
class ViewTestConfig {
@Bean
GreetingService myService() {
return new TestingGreetingService();
}
}
@SpringBootTest
class ViewTest extends SpringUIUnitTest {
}
Set Up View Access Control
To apply view access control, Vaadin requires a ViewAccessChecker
to be registered as a BeforeEnterListener
for the UI.
For @SpringBootTest
annotated tests, the checker is automatically created and configured.
However, when testing with a restricted ApplicationContext, you may want to perform the setup yourself in a Configuration
class by providing a VaadinServiceInitListener
that executes this step.
@Configuration
class TestViewSecurityConfig {
@Bean
VaadinServiceInitListener setupViewSecurityScenario() {
SpringViewAccessChecker viewAccessChecker = new SpringViewAccessChecker(new AccessAnnotationChecker());
viewAccessChecker.setLoginView(LoginView.class);
return event -> {
event.getSource().addUIInitListener(uiEvent -> {
uiEvent.getUI().addBeforeEnterListener(viewAccessChecker);
});
};
}
}
If you are using the Vaadin Spring Add-On, you can instead import the out-of-the-box ViewAccessCheckerInitializer
, which only requires you to define a ViewAccessChecker
bean.
@Configuration
@Import({ViewAccessCheckerInitializer.class})
class TestViewSecurityConfig {
@Bean
ViewAccessChecker viewAccessChecker() {
return new SpringViewAccessChecker(new AccessAnnotationChecker());
}
}
Testing with Spring Security
Vaadin comes with built-in security helpers that enable the annotation-based view access control mechanism, which integrates well with Spring Security.
When using SpringUIUnitTest
, if Spring Security is present on the classpath, the mock environment is instructed to fetch authentication details from Spring SecurityContextHolder
.
With this support, you can use Spring Security test annotations, such as @WithMockUser
, @WithAnonymousUser
or @WithUserDetails
, to simulate different authentication scenarios with test method granularity.
More information is available on the Spring Security documentation site.
Authentication details are available before creating the UI instance and navigating to the default route, so redirects to the login view aren’t performed when simulating logged-in users.
In the same way, custom redirect logic for authenticated users works as expected.
To use Spring Security test annotations, first make sure the dependency is added to the project.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Then extend SpringUIUnitTest
and annotate test methods to set up an authentication scenario.
For the simplest use cases, use @WithMockUser
or @WithAnonymousUser
, providing the username and roles that should be granted.
@SpringBootTest
public class ViewSecurityTest extends SpringUIUnitTest {
@Test
@WithAnonymousUser
void anonymousUser_protectedView_redirectToLogin() {
navigate("protected", LoginView.class);
}
@Test
@WithAnonymousUser
void anonymousUser_publicView_signInLinkPresent() {
// public view is default page
Assertions.assertInstanceOf(PublicView.class, getCurrentView());
Anchor anchor = $(Anchor.class).withText("Sign in").first();
Assertions.assertTrue(
test(anchor).isUsable(),
"Sign in link should be available for anonymous user");
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void adminUser_adminView_viewShown() {
navigate(AdminRoleView.class);
Assertions.assertTrue(
$(Avatar.class).first().isVisible(),
"Avatar should be visible for logged users");
}
}
When custom User objects or complex grant rules should be used, provide a custom UserDetailsService
and annotate the test method with @WithUserDetails
.
@ContextConfiguration(classes = SecurityTestConfig.class)
class SpringUnitSecurityTest extends SpringUIUnitTest {
@Test
@WithUserDetails("admin")
void superuser_adminView_viewShown() {
navigate(AdminRoleView.class);
Assertions.assertTrue(
$(Avatar.class).first().isVisible(),
"Avatar should be visible for logged users");
}
@Test
@WithUserDetails
void user_adminView_accessDenied() {
RouteNotFoundError errorView = navigate("admin-role",
RouteNotFoundError.class);
Assertions.assertTrue(
errorView.getElement().getChild(0).getOuterHTML()
.contains("Reason: Access denied"),
"Admin view should be accessible only by users with ADMIN role");
}
}
@Configuration
class SecurityTestConfig {
@Bean
UserDetailsService mockUserDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
if ("user".equals(username)) {
return new User(username, UUID.randomUUID().toString(),
List.of(
new SimpleGrantedAuthority("ROLE_DEV"),
new SimpleGrantedAuthority("ROLE_USER")
));
}
if ("admin".equals(username)) {
return new User(username, UUID.randomUUID().toString(),
List.of(
new SimpleGrantedAuthority("ROLE_SUPERUSER"),
new SimpleGrantedAuthority("ROLE_ADMIN")
));
}
throw new UsernameNotFoundException(
"User " + username + " not exists");
}
};
}
}
D68CAC9E-6131-45C9-84E6-6D1CA1E44E81