Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
56c69c7
Implement Serializable in ComponentTester class
TatuLund Nov 18, 2025
63863f1
Add test for Vaadin session serialization
TatuLund Nov 18, 2025
2709347
Fix formatting in ComponentTester class declaration
TatuLund Nov 18, 2025
42ec312
Refactor ComponentTester class declaration
TatuLund Nov 18, 2025
76d1bd4
Fix formatting in ComponentTesterTest.java
TatuLund Nov 18, 2025
9f223dc
Add import for VaadinSession in ComponentTesterTest
TatuLund Nov 18, 2025
5694799
Refactor mockVaadinIsSerializable test method
TatuLund Nov 18, 2025
df0ef6b
Change uiFactory parameter type to UIFactory
TatuLund Nov 18, 2025
db0e386
Update uiFactory parameter to use JvmSerializableLambda
TatuLund Nov 18, 2025
452e682
Refactor uiFactory type in MockSpringVaadinSession
TatuLund Nov 18, 2025
879dd18
Remove unused imports in MockSpringVaadinSession
TatuLund Nov 18, 2025
02ab983
Refactor import statement for UIFactory
TatuLund Nov 18, 2025
bfa5b09
Replace Function0 with UIFactory in MockSpringServletService
TatuLund Nov 18, 2025
47ce343
Update constructor parameter type for uiFactory
TatuLund Nov 18, 2025
c08b663
Remove unused import for UI component
TatuLund Nov 18, 2025
b19ae96
Refactor MockSpringServlet to use UIFactory
TatuLund Nov 18, 2025
4aff22b
Remove unused Kotlin import in MockSpringServlet
TatuLund Nov 18, 2025
19aa423
Update uiFactory to use JvmSerializableLambda
TatuLund Nov 18, 2025
6cc6394
Update ComponentTesterTest.java
TatuLund Nov 18, 2025
336c9bd
Fix duplicate line in mockVaadinIsSerializable test
TatuLund Nov 18, 2025
ea57460
Fix MockRequest factory lambda
TatuLund Nov 19, 2025
09c6309
Bring back old constructors as deprecated
TatuLund Nov 19, 2025
251713f
Add serialization debug utility and use it in test
TatuLund Nov 19, 2025
856515c
Clean-up imports
TatuLund Nov 19, 2025
5f78812
Run formatter
TatuLund Nov 19, 2025
ef5cad7
Undo un-needed changes, clean-up and move tests to right places.
TatuLund Nov 19, 2025
b74e5ea
Mark deprecations for removal
TatuLund Nov 19, 2025
0ed85d7
Merge branch 'main' into tester-serializable
TatuLund Nov 19, 2025
2a6c3b8
Add try - catch
TatuLund Nov 19, 2025
026b0af
Merge branch 'tester-serializable' of https://github.com/vaadin/testb…
TatuLund Nov 19, 2025
03a73d0
Merge branch 'main' into tester-serializable
TatuLund Nov 20, 2025
1d20f08
Improve JavaDoc
TatuLund Nov 20, 2025
0321b56
Merge branch 'tester-serializable' of https://github.com/vaadin/testb…
TatuLund Nov 20, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* Copyright (C) 2000-2025 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See <https://vaadin.com/commercial-license-and-service-terms> for the full
* license.
*/
package com.vaadin.testbench.unit;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;

public final class SerializationDebugUtil {

private SerializationDebugUtil() {
}

/**
* Asserts that the given object graph is fully serializable. If not, throws
* an AssertionError with a detailed report of non-serializable fields
* found.
* <p>
* Note: When running tests in an IDE, enable
* "sun.io.serialization.extendedDebugInfo" flag for more detailed stack
* traces on serialization errors.
*
* @param root
* the root object to test for serializability
* @param report
* a StringBuilder instance to which the detailed report is
* appended
*/
public static void assertSerializable(Object root, StringBuilder report) {
try {
serialize(root);
} catch (NotSerializableException e) {
if (report == null) {
report = new StringBuilder();
}
report.append(buildReport(root, e));
throw new AssertionError("Serialization failed: " + e.getMessage()
+ "\n" + report.toString(), e);
} catch (IOException ioe) {
throw new AssertionError(
"Unexpected IO failure during serialization: "
+ ioe.getMessage(),
ioe);
}
}

/**
* Asserts that the given object graph is fully serializable. If not, throws
* an AssertionError with a detailed report of non-serializable fields
* found.
* <p>
* Note: When running tests in an IDE, enable
* "sun.io.serialization.extendedDebugInfo" flag for more detailed stack
* traces on serialization errors.
*
* @param root
* the root object to test for serializability
*/
public static void assertSerializable(Object root) {
assertSerializable(root, null);
}

private static void serialize(Object o) throws IOException {
if (o == null) {
return;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
try {
oos.writeObject(o);
} finally {
oos.close();
}
}

private static String buildReport(Object root,
NotSerializableException original) {
StringBuilder sb = new StringBuilder();
sb.append("---- Serialization Debug Report ----\n");
sb.append("Root type: ").append(root.getClass().getName()).append('\n');
sb.append("Original exception: ").append(original).append('\n');

Set<Object> visited = Collections
.newSetFromMap(new IdentityHashMap<>());
List<String> offenders = new ArrayList<>();
inspectObject(root, "root", visited, offenders);

if (offenders.isEmpty()) {
sb.append("No direct non-Serializable fields found.\n");
} else {
sb.append("Non-serializable field paths:\n");
offenders.forEach(p -> sb.append(" - ").append(p).append('\n'));
}
sb.append("------------------------------------");
return sb.toString();
}

private static void inspectObject(Object obj, String path,
Set<Object> visited, List<String> offenders) {
if (obj == null || visited.contains(obj)) {
return;
}
visited.add(obj);

Class<?> cls = obj.getClass();

// Skip Java core known immutable serializable types quickly
if (isKnownSerializableLeaf(cls)) {
return;
}

// If object itself not Serializable, record and do not dive further (to
// avoid noise)
if (!(obj instanceof Serializable)) {
offenders.add(path + " (" + cls.getName() + ")");
return;
}

// Dive into fields
for (Field f : getAllFields(cls)) {
if (shouldSkip(f)) {
continue;
}
try {
f.setAccessible(true);
} catch (Exception ignored) {
continue;
}
Object value;
try {
value = f.get(obj);
} catch (IllegalAccessException ignored) {
continue;
}
if (value == null) {
continue;
}

String childPath = path + "." + f.getName();
if (!(value instanceof Serializable)) {
offenders.add(
childPath + " (" + value.getClass().getName() + ")");
continue;
}

// Try serializing field alone to catch nested problematic graphs
try {
serialize(value);
} catch (NotSerializableException nse) {
// Dive deeper to isolate
inspectObject(value, childPath, visited, offenders);
} catch (IOException ignored) {
// Ignore other IO issues for this isolated attempt
}
}
}

private static boolean shouldSkip(Field f) {
int mod = f.getModifiers();
return Modifier.isStatic(mod) || Modifier.isTransient(mod);
}

private static List<Field> getAllFields(Class<?> cls) {
List<Field> fields = new ArrayList<>();
while (cls != null && cls != Object.class) {
fields.addAll(Arrays.asList(cls.getDeclaredFields()));
cls = cls.getSuperclass();
}
return fields;
}

private static boolean isKnownSerializableLeaf(Class<?> cls) {
return cls.isPrimitive() || cls == String.class
|| Number.class.isAssignableFrom(cls) || cls == Boolean.class
|| cls == Character.class || cls.isEnum()
|| cls.getName().startsWith("java.time.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.vaadin.flow.server.VaadinServletService;
import com.vaadin.flow.spring.SpringServlet;
import com.vaadin.testbench.unit.internal.Routes;
import com.vaadin.testbench.unit.internal.UIFactory;
import com.vaadin.testbench.unit.internal.UtilsKt;

/**
Expand All @@ -46,13 +47,23 @@ public class MockSpringServlet extends SpringServlet {
@NotNull
public final ApplicationContext ctx;
@NotNull
public final Function0<UI> uiFactory;
public final UIFactory uiFactory;

@Deprecated(forRemoval = true)
public MockSpringServlet(@NotNull Routes routes,
@NotNull ApplicationContext ctx, @NotNull Function0<UI> uiFactory) {
super(ctx, false);
this.ctx = ctx;
this.routes = routes;
this.uiFactory = uiFactory::invoke;
}

public MockSpringServlet(@NotNull Routes routes,

@NotNull ApplicationContext ctx, @NotNull UIFactory uiFactory) {
super(ctx, false);
this.ctx = ctx;
this.routes = routes;
this.uiFactory = uiFactory;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.spring.SpringVaadinServletService;
import com.vaadin.testbench.unit.internal.UIFactory;

/**
* A mocking service that performs three very important tasks:
Expand All @@ -35,12 +36,20 @@
*/
public class MockSpringServletService extends SpringVaadinServletService {
@NotNull
private final Function0<UI> uiFactory;
private final UIFactory uiFactory;

@Deprecated(forRemoval = true)
public MockSpringServletService(@NotNull MockSpringServlet servlet,
@NotNull DeploymentConfiguration deploymentConfiguration,
@NotNull ApplicationContext ctx, @NotNull Function0<UI> uiFactory) {
super(servlet, deploymentConfiguration, ctx);
this.uiFactory = uiFactory::invoke;
}

public MockSpringServletService(@NotNull MockSpringServlet servlet,
@NotNull DeploymentConfiguration deploymentConfiguration,
@NotNull ApplicationContext ctx, @NotNull UIFactory uiFactory) {
super(servlet, deploymentConfiguration, ctx);
this.uiFactory = uiFactory;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.testbench.unit.internal.MockVaadin;
import com.vaadin.testbench.unit.internal.UIFactory;

/**
* A Vaadin Session with one important difference:
Expand All @@ -31,11 +32,18 @@
*/
public class MockSpringVaadinSession extends VaadinSession {
@NotNull
private final Function0<UI> uiFactory;
private final UIFactory uiFactory;

@Deprecated(forRemoval = true)
public MockSpringVaadinSession(@NotNull VaadinService service,
@NotNull Function0<UI> uiFactory) {
super(service);
this.uiFactory = uiFactory::invoke;
}

public MockSpringVaadinSession(@NotNull VaadinService service,
@NotNull UIFactory uiFactory) {
super(service);
this.uiFactory = uiFactory;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ object MockVaadin {
@JvmStatic
@JvmOverloads
fun setup(routes: Routes = Routes(),
uiFactory: () -> UI = UIFactory { MockedUI() },
uiFactory: UIFactory = UIFactory { MockedUI() },
lookupServices: Set<Class<*>> = emptySet()) {
// init servlet
val servlet = MockVaadinServlet(routes)
Expand All @@ -109,7 +109,7 @@ object MockVaadin {
* @param lookupServices service classes to be provided to the lookup initializer
*/
@JvmStatic
fun setup(uiFactory: () -> UI = { MockedUI() }, servlet: VaadinServlet,
fun setup(uiFactory: UIFactory = UIFactory { MockedUI() }, servlet: VaadinServlet,
lookupServices: Set<Class<*>> = emptySet()
) {
if (!servlet.isInitialized) {
Expand Down Expand Up @@ -199,7 +199,7 @@ object MockVaadin {
*/
var mockRequestFactory: (MockHttpSession) -> MockRequest = { MockRequest(it) }

private fun createSession(ctx: ServletContext, uiFactory: () -> UI) {
private fun createSession(ctx: ServletContext, uiFactory: UIFactory) {
val service: VaadinServletService = checkNotNull(VaadinService.getCurrent()) as VaadinServletService
val httpSession: MockHttpSession = MockHttpSession.create(ctx)

Expand Down Expand Up @@ -242,7 +242,7 @@ object MockVaadin {
createUI(uiFactory, session)
}

internal fun createUI(uiFactory: () -> UI, session: VaadinSession) {
internal fun createUI(uiFactory: UIFactory, session: VaadinSession) {
val request: VaadinRequest = checkNotNull(VaadinRequest.getCurrent())
val ui: UI = uiFactory()
require(ui.session == null) {
Expand Down Expand Up @@ -373,7 +373,7 @@ object MockVaadin {
* See [MockVaadinSession] on how to call this properly.
*/
@JvmStatic
public fun afterSessionClose(session: VaadinSession, uiFactory: () -> UI) {
public fun afterSessionClose(session: VaadinSession, uiFactory: UIFactory) {
// We need to simulate the actual browser + servlet container behavior here.
// Imagine that we want a test scenario where the user logs out, and we want to check that a login prompt appears.

Expand Down Expand Up @@ -445,4 +445,4 @@ fun interface MockRequestCustomizer {
fun apply(request: MockRequest)
}

fun interface UIFactory : () -> UI, Serializable
fun interface UIFactory : () -> UI, Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import com.vaadin.flow.server.VaadinServlet
import com.vaadin.flow.server.VaadinServletService
import com.vaadin.flow.server.VaadinSession

import com.vaadin.testbench.unit.internal.UIFactory

/**
* A mocking service that performs three very important tasks:
* * Overrides [isAtmosphereAvailable] to tell Vaadin that we don't have Atmosphere (otherwise Vaadin will crash)
Expand All @@ -29,7 +31,7 @@ import com.vaadin.flow.server.VaadinSession
*/
open class MockService(servlet: VaadinServlet,
deploymentConfiguration: DeploymentConfiguration,
val uiFactory: () -> UI = { MockedUI() }
val uiFactory: UIFactory = UIFactory { MockedUI() }
) : VaadinServletService(servlet, deploymentConfiguration) {
// need to have this override. Setting `VaadinService.atmosphereAvailable` to false via
// reflection after the servlet has been initialized is too late, since Atmo is initialized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.testbench.unit.mocks.MockSpringServletService;
Expand Down Expand Up @@ -40,6 +41,12 @@ public void extendingBaseClass_runTest_vaadinSpringMockingIsSetup() {
VaadinSession.getCurrent() instanceof MockSpringVaadinSession);
}

@Test
public void mockVaadinIsSerializable() {
SerializationDebugUtil.assertSerializable(UI.getCurrent());
SerializationDebugUtil.assertSerializable(VaadinSession.getCurrent());
}

// Empty configuration class used only to be able to bootstrap spring
// ApplicationContext
@Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public void extendingBaseClass_runTest_defaultRouteActive() {
getCurrentView() instanceof WelcomeView);
}

@Test
public void mockVaadinIsSerializable() {
SerializationDebugUtil.assertSerializable(UI.getCurrent());
}

}

public static class DiscoverAllRoutesTest extends UIUnit4Test {
Expand Down