-
Notifications
You must be signed in to change notification settings - Fork 226
docs: Document how the browserless test environment differs from a running application #6076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
128 changes: 128 additions & 0 deletions
128
articles/flow/testing/browserless/environment-differences.adoc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| 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` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
@WithMockUserand 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.