Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions articles/flow/testing/browserless/environment-differences.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: Environment Differences
page-title: How the Vaadin browserless test environment differs
description: What the browserless environment creates, in which order, and which application behavior therefore needs a different approach in a test.
meta-description: Learn the browserless test lifecycle in Vaadin, and how it affects session-scoped beans and authentication applied during a test.
order: 80
---


= How the Browserless Environment Differs

A browserless test runs your application's server-side code against a mocked Vaadin environment created inside the JUnit test. Views, components, and services behave as they do in a running application, but the environment is built and torn down around each test method rather than by a servlet container at startup.

That ordering is observable. Two application patterns depend on it, and in both the test fails in a way that looks like an application defect: a bean that can't be resolved, and a view that doesn't match the authenticated user.


[#test-lifecycle]
== Test Lifecycle

For a [classname]`SpringBrowserlessTest`, each test method runs through these steps:

. JUnit creates the test instance, and Spring injects its [annotationname]`@Autowired` fields. No Vaadin environment exists yet -- there's no [classname]`VaadinService`, no [classname]`VaadinSession`, and no [classname]`UI`.
. JUnit extension callbacks run. [classname]`SpringExtension` populates the Spring [classname]`SecurityContextHolder` from [annotationname]`@WithMockUser`, [annotationname]`@WithAnonymousUser`, or [annotationname]`@WithUserDetails`.
. The browserless environment is created: [classname]`VaadinService`, [classname]`VaadinSession`, and [classname]`UI`, followed by navigation to the root route. The authentication from the previous step is already in place, so view access control sees the simulated user.
. The test method body runs.
. The environment is torn down, and the session is closed.

[classname]`BrowserlessTest` and [classname]`QuarkusBrowserlessTest` follow the same sequence without the Spring injection step.

The consequence is that the Vaadin session exists only inside the test method. Anything that needs a session -- including anything Vaadin resolves per session -- has to be reached from there, not from a field of the test class.


[#session-scoped-beans]
== Session-Scoped Beans

A [annotationname]`@VaadinSessionScope` or [annotationname]`@SessionScope` bean can't be injected into a test class field. The field is injected in step 1, when no session exists, so the bean can't be resolved:

.Doesn't Work
[source,java]
----
@SpringBootTest
class CartViewTest extends SpringBrowserlessTest {

@Autowired
private Cart cart; // No session exists when this field is injected.
}
----

Because this is an instance creation failure, it fails every test in the class, not only the ones that use the bean.

Inject an [classname]`ObjectProvider` instead, and ask it for the bean inside the test method, where the session is available:

.Works
[source,java]
----
@SpringBootTest
class CartViewTest extends SpringBrowserlessTest {

@Autowired
private ObjectProvider<Cart> cartProvider;

@Test
void addItem_cartContainsItem() {
CartView view = navigate(CartView.class);
Cart cart = cartProvider.getObject();

test(view.addButton).click();

Assertions.assertEquals(1, cart.getItems().size());
}
}
----

Annotating the [classname]`Cart` field with [annotationname]`@Lazy` works as well, and so does injecting [classname]`ApplicationContext` and calling [methodname]`getBean(Cart.class)` from the test method. All three defer the lookup to the moment the bean is first used.

Views and other components aren't affected: they're instantiated during navigation, inside the test method, so their own session-scoped dependencies resolve normally.


[#authentication-applied-during-a-test]
== Authentication Applied During a Test

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you only have a quick look at this page, you might very well think this is how you should to authentication in a test. Could it maybe start by showing the correct way and only after that explain why you would want another approach and how you deal with things if you pick that approach?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restructured that way: the section now opens with a plain @WithMockUser and a test that just navigates, and only then covers a sign-in performed during the test, why the view does not follow it, and the re-navigation. The "prefer doing it before the test method" paragraph that used to close the section is gone, since that is now the opening.


Apply the authentication before the test method starts, with a plain [annotationname]`@WithMockUser`, [annotationname]`@WithAnonymousUser`, or [annotationname]`@WithUserDetails` on the test class or method. It's then in place in step 2, before the environment is created, so the initial navigation already reflects the authenticated user and nothing further is needed:

[source,java]
----
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void adminOpensAdminView_avatarShown() {
AdminView view = navigate(AdminView.class);

Assertions.assertTrue(find(Avatar.class).single().isVisible());
}
----

See <<spring-security#, Spring Security Testing>> for the full setup.

Some scenarios need the sign-in to happen during the test itself, such as a login form, or a view that has to be asserted both before and after the user signs in. Authentication established then -- from the test method body, or with `setupBefore = TestExecutionEvent.TEST_EXECUTION` -- arrives after the environment has navigated in step 3. Subsequent requests use the new authentication, but the view rendered during setup stays in place, so [methodname]`getCurrentView()` still returns the result of that earlier, anonymous navigation.

Navigate again after signing in:

[source,java]
----
@Test
@WithMockUser(username = "admin", roles = "ADMIN",
setupBefore = TestExecutionEvent.TEST_EXECUTION)
void adminSignsInDuringTest_adminViewShown() {
// Setup navigated to the root route while the user was still anonymous,
// and access control redirected that navigation to the login view.
Assertions.assertInstanceOf(LoginView.class, getCurrentView());

// Navigating again applies access control to the current authentication.
navigate(AdminView.class);

Assertions.assertTrue(find(Avatar.class).single().isVisible());
}
----

.Reloading Isn't Enough
[NOTE]
Reloading with [methodname]`Page.reload()` isn't a substitute for navigating again. It recreates the [classname]`UI` and renders the location that is currently active, the same as pressing reload in a browser; when access control has redirected to the login view, that location is the login view.


== Components That Don't Exist Yet

Component queries walk the server-side component tree, which holds only what has been created. A component rendered per item exists once something renders it, and the contents of an overlay are attached only while the overlay is open. Until then, a query returns an empty result rather than an error. See <<overlay-components#, Testing Overlay Components>> for how to reach them.


[discussion-id]`4B6C9E13-7A85-42D0-9F3B-1C8E5D4A2B76`
5 changes: 5 additions & 0 deletions articles/flow/testing/browserless/getting-started.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ To navigate to another registered view, use the [methodname]`navigate()` methods
[methodname]`navigate(Template.class, Collections.singletonMap("param", PARAMETER))`
+
[methodname]`navigate("template/myParam", Template.class)`
- For a location that [since:com.vaadin:vaadin@V25.3]#carries a query string, a fragment, or both#
+
[methodname]`navigate("myParam/parameter?tab=history&page=2", MyParam.class)`
+
The location is split up the same way the browser address bar does it, so the query parameters reach the view through the navigation event. A location consisting only of a fragment, such as `"#details"`, identifies a place within the current page rather than a route, so it leaves the current view in place.

All navigation methods return the instantiated view, so that its fields can be used directly for testing.

Expand Down
4 changes: 4 additions & 0 deletions articles/flow/testing/browserless/spring-security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class TestViewSecurityConfig {

With this support, you can use Spring Security test annotations -- such as [annotationname]`@WithMockUser`, [annotationname]`@WithAnonymousUser`, or [annotationname]`@WithUserDetails` -- to simulate different authentication scenarios with test method granularity. More information is available on the https://docs.spring.io/spring-security/reference/servlet/test/method.html#test-method-withmockuser[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.

.Signing In During a Test
[CAUTION]
This holds for authentication that is in place before the test method starts. A sign-in performed later -- from the test method body, or with `setupBefore = TestExecutionEvent.TEST_EXECUTION` -- happens after the initial navigation, so [methodname]`getCurrentView()` still returns the view rendered for the previous user. Navigate again to apply access control to the new authentication. See <<environment-differences#authentication-applied-during-a-test, Authentication Applied During a Test>>.

To use Spring Security test annotations, first make sure the dependency is added to the project.

[source,xml]
Expand Down
Loading