Documentation

Documentation versions (currently viewingVaadin 24)

UI Unit Testing in Spring-based Projects

Describes and provides examples how to perform UI Unit Tests.

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 of 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 {

}

Additional Setup

In addition to the TestBench dependency, make sure you add the Spring Boot test starter dependency to your project. Add the following to your pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

Set Up View Access Control

To apply view access control, Vaadin requires a NavigationAccessControl to be registered as a BeforeEnterListener for the UI. For @SpringBootTest annotated tests, the checker is created and configured automatically. 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() {
        SpringNavigationAccessControl accessControl = new SpringNavigationAccessControl();
        accessControl.setLoginView(LoginView.class);
        return event -> {
            event.getSource().addUIInitListener(uiEvent -> {
                uiEvent.getUI().addBeforeEnterListener(accessControl);
            });
        };
    }
}

If you’re using the Vaadin Spring Add-On, you can instead import the out-of-the-box NavigationAccessControlInitializer. It requires only that you define a NavigationAccessControl bean.

@Configuration
@Import({NavigationAccessControlInitializer.class})
class TestViewSecurityConfig {

    @Bean
    NavigationAccessControl navigationAccessControl() {
        return new SpringNavigationAccessControl());
    }
}

NavigationAccessControl has been introduce in Vaadin 24.3. In a project based on an earlier Vaadin version, view security can be configured in the same way, but using the ViewAccessChecker and ViewAccessCheckerInitializer components:

@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);
            });
        };
    }
}
@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. This way 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