Documentation

Documentation versions (currently viewingVaadin 23)

You are viewing documentation for Vaadin 23. View latest documentation

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