Codename One ships two compatible test frameworks. com.codename1.testing.AbstractTest, driven by the cn1:test Maven goal, is the only way to run tests on a physical iOS or Android device — JUnit Jupiter isn’t available on ParparVM. The JavaSE simulator also exposes a standard JUnit 5 integration so you can write tests that run through Surefire and your IDE’s native test runner; those tests run only in the simulator JVM but in exchange give you reflection, mocking libraries, and the rest of the standard Java testing ecosystem.
Both styles coexist in the same project under common/src/test/java. You pick per test class. This chapter explains when to reach for which, walks through the JUnit annotations, and shows how the two frameworks compare side by side.
When to use JUnit versus AbstractTest
| Framework | Use it for | Don’t use it for |
|---|---|---|
| Tests that must execute on a real device ( | Tests that need reflection, Mockito, AssertJ, parameterized data sets, |
| Simulator-only tests. Anything that wants a full JVM at test time — reflection, mocking libraries, parameterized tests, IDE green-bar integration, | Anything that must also run on a device — JUnit Jupiter isn’t available on ParparVM. |
The two runners discover disjoint sets of test classes (cn1:test looks for com.codename1.testing.UnitTest implementers; Surefire looks for @Test-annotated methods), so a project can mix both. mvn install runs Surefire during the test phase and cn1:test from the javase module’s test profile in the same phase — you don’t lose either test pass.
Dependencies
The cn1app archetype generates a common/pom.xml and javase/pom.xml that already pull in junit-jupiter and codenameone-javase at test scope. If your project predates that wiring, add these dependency blocks yourself:
<dependencies>
<dependency>
<groupId>com.codenameone</groupId>
<artifactId>codenameone-javase</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
</dependencies><dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>Tests live in common/src/test/java. Surefire only runs from the javase module — the common module keeps Surefire skipped (<skipTests>true</skipTests>) to avoid running each test twice, since javase/pom.xml mounts the same sources via <testSourceDirectory>${project.basedir}/../common/src/test/java</testSourceDirectory>.
A minimal JUnit test
Run with:
mvn -pl javase test # all JUnit + cn1:test
mvn -pl javase test -Dtest=GreetingFormTest # one class
mvn -pl javase test -Dtest=GreetingFormTest#formShowsExpectedTitle # one method
@CodenameOneTest
A class-level meta-annotation equivalent to @ExtendWith(CodenameOneExtension.class). The extension does four things:
If
GraphicsEnvironment.isHeadless()is true, throwTestAbortedExceptionfrom@BeforeAll. JUnit reports the class as skipped rather than failed, and the Display singleton is never left half-initialized — so subsequent test classes in the same JVM aren’t poisoned. This makes JUnit-style Codename One tests safe to run on a headless CI runner; they simply skip.Boot
Displayonce per JVM viaDisplay.init(null). The call is idempotent, so multiple@CodenameOneTestclasses share one Display instance for the rest of the test run.Apply any visual-config annotations (
@Theme,@DarkMode,@LargerText,@Orientation,@RTL,@SimulatorProperty) on the EDT in one batch before each test, followed by a single theme refresh.Dispatch
@RunOnEdttest methods throughCN.callSeriallyand rethrow their exceptions on the JUnit thread so stack traces stay clickable in IDEs.
@RunOnEdt
Place on a method (or on the class for all tests) when the body mutates UI state.
Class-level @RunOnEdt also routes @BeforeEach/@AfterEach methods through the EDT. Method-level @RunOnEdt is scoped to that one @Test.
Tests that only exercise pure model or utility code can omit @RunOnEdt and run on the JUnit worker thread — they’re faster because nothing pumps the EDT.
@SimulatorProperty / @SimulatorProperties
Set a property visible to the simulator before the test runs.
The scope field selects where the value lands:
| Scope | Where | When applied |
|---|---|---|
|
| After Display init. Use for properties your app reads via |
|
| Before Display init (class-level only). Use for things the simulator reads at startup, for example |
The JavaSE port compiles at source 1.7, which predates @Repeatable. To set more than one property on the same target, wrap multiple @SimulatorProperty entries inside @SimulatorProperties({…}) rather than repeating the annotation directly.
@Theme
Loads a base theme resource and installs it through UIManager.setThemeProps, then triggers a refresh. The annotation accepts either of two mutually exclusive inputs.
By native theme. Use the NativeTheme enum for the themes bundled into the simulator jar — the same set the simulator’s Simulate > Native Theme menu offers:
The enum carries the resource path (NativeTheme.IOS_MODERN.resourcePath() returns /iOSModernTheme.res) and the simulator-menu label (displayName()), so test reports can render the same name a user sees in the simulator UI.
By resource path. For app themes shipped under src/main/resources or src/test/resources, point value at the .res file with a leading slash:
If both nativeTheme and value are set, nativeTheme wins. If neither is set, the annotation is a no-op.
@DarkMode
Toggles dark/light mode via Display.setDarkMode(Boolean) and refreshes the active form.
@LargerText
Sets the accessibility text-scale multiplier — the same knob exposed by the simulator’s Simulate → Larger Text submenu. Useful for catching layout regressions at accessibility font sizes.
scale = 1.0f restores the default size; common values mirror the menu’s 1.3f / 1.6f / 2.0f presets.
@Orientation
Forces the simulator into portrait or landscape for the test.
This calls a non-persisting setter on JavaSEPort and sets an explicit-portrait flag honored by Display.isPortrait() — so unit-test JVMs (where the canvas inherits the host window’s full size and would otherwise read landscape on every wide screen) get the expected orientation back.
@RTL
Flips the look-and-feel into right-to-left mode (Arabic, Hebrew) via UIManager.getInstance().getLookAndFeel().setRTL(…). The active form is revalidated so existing layouts reflow before the test body asserts.
Annotation resolution
When the same annotation appears at both the class and the method level, method wins. Annotations the method doesn’t override are inherited from the class. Annotations that appear on neither leave Display state alone — the extension never resets a knob the caller didn’t ask for. Use @AfterEach for cross-test cleanup if a class must leave the simulator pristine for the next one.
EDT semantics
@RunOnEdt dispatches the test body through CN.callSerially(…) and uses a latch with wait/notify to block the JUnit worker thread until the EDT-side runnable finishes or timeoutMillis elapses. Throwables from inside the EDT runnable are captured and rethrown on the JUnit thread so the assertion stack trace lands in the IDE as if the test had been invoked directly.
Without @RunOnEdt, the test body runs on the Surefire worker thread — fine for pure model assertions, broken for UI mutation. Forms, components, and UIManager are EDT-confined; touch them off the EDT and you get sporadic deadlocks and rendering glitches.
Headless behavior
Display.init(null) eventually calls JavaSEPort.init(null), which constructs a javax.swing.JFrame to host the simulator canvas. JFrame construction in a headless JVM throws HeadlessException immediately. Therefore:
Local dev (macOS / Windows / Linux desktop): works out of the box.
CI with an X server or Xvfb: works — wrap your build with
xvfb-run mvn testif the runner is otherwise headless.CI without DISPLAY, or with explicit
-Djava.awt.headless=true: every@CodenameOneTestclass aborts viaTestAbortedExceptionfrom@BeforeAll. JUnit reports the class as skipped, not failed, and crucially the Display singleton is never left half-initialized — so subsequent test classes in the same JVM aren’t poisoned.
This is why the framework’s internal CodenameOneExtensionTest (in the Codename One sources) carries @DisabledIfSystemProperty(named = "java.awt.headless", matches = "true"). The extension catches the headless case AWT detects automatically (Linux without DISPLAY); the annotation catches the explicit -Djava.awt.headless=true case. Together they let the test class skip cleanly on a headless runner.
Pure-logic tests (no UI, no @CodenameOneTest) can run on any headless runner without configuration.
Coexistence with cn1:test
The two runners discover disjoint sets of classes:
cn1:testlooks for classes thatimplements com.codename1.testing.UnitTest(whichAbstractTestextends).Surefire (JUnit Jupiter) looks for
@Test-annotated methods.
They don’t trip over each other. mvn install runs Surefire during the test phase, then the cn1:test execution bound in the javase/pom.xml test profile runs in the same phase. To target just one runner:
mvn -pl javase test # both runners
mvn -pl javase test -DskipTests # skip Surefire, cn1:test still runs
mvn -pl javase test -Dtest=NoMatch # filter Surefire to nothing,
# cn1:test still runs
Side-by-side example
The same "sign in, assert Home is shown" check, written both ways:
The UI driving (TestUtils.*) is identical — TestUtils is independent of the runner. What changes is the lifecycle plumbing: shouldExecuteOnEDT() + runTest() returns boolean becomes @RunOnEdt + @Test void + a standard assertion library.
Cross-reference
For the
AbstractTestframework’s full API surface — the TestUtils helpers, thescreenshotTesttolerance algorithm and baseline management — see the com.codename1.testing Javadoc.For the
cn1:testMaven goal, see the test goal appendix.