DEV Community

Testing Solon Web Apps with @SolonTest and HttpTester

Testing Solon Web Apps with @SolonTest and HttpTester

Testing a Java web service is most valuable when it checks the application boundary-not just a controller method in isolation. Solon’s test support gives that boundary a compact shape.

With solon-test, a JUnit 5 test can boot a Solon application, receive injected components, and exercise local HTTP endpoints through HttpTester. The result is a test that stays in the same codebase as the service and can grow from a quick debugging aid into a repeatable regression suite.

This article uses Solon v4.0.3 and focuses on the small set of test primitives worth standardizing first: @SolonTest, HttpTester, @Import, and @Rollback.

Start with the standard test dependency

For JUnit 5 projects, add solon-test with test scope. When the application already uses solon-parent v4.0.3, dependency versions are managed by the parent.

<dependency>
  <groupId>org.noear</groupId>
  <artifactId>solon-test</artifactId>
  <scope>test</scope>
</dependency>

Use JUnit 5’s org.junit.jupiter.api.Test. It is easy to accidentally import JUnit 4’s org.junit.Test; keeping the import explicit avoids confusing test-engine failures.

Boot the real application context

@SolonTest names the Solon startup class for a test. That means dependencies can be injected into the test itself, so service-level assertions do not need a separate, hand-wired container.

import org.junit.jupiter.api.Test;
import org.noear.solon.annotation.Inject;
import org.noear.solon.test.SolonTest;

@SolonTest(App.class)
class UserServiceTest {
    @Inject
    UserService userService;

    @Test
    void greets_a_user() {
        assert "Hello Ada".equals(userService.hello("Ada"));
    }
}

The application class is an intentional part of the test contract. A test that starts the same entry point used by the service is more likely to catch missing component discovery, configuration wiring, or startup-time integration mistakes.

@SolonTest can also carry test-specific settings: an environment name via env, startup arguments via args, or application properties via properties. Use these to make a test’s runtime assumptions visible rather than relying on a developer’s local defaults.

Test local HTTP endpoints with HttpTester

For an endpoint, calling a controller directly misses routing, binding, filters, serialization, and the response boundary. Extending HttpTester lets a test issue a request against the local application instead. When the test should use a real listening HTTP server, set enableHttp = true; its default is false.

import org.junit.jupiter.api.Test;
import org.noear.solon.test.HttpTester;
import org.noear.solon.test.SolonTest;

@SolonTest(value = App.class, enableHttp = true)
class GreetingApiTest extends HttpTester {
    @Test
    void returns_a_greeting() {
        String body = path("/hello?name=Ada").get();
        assert "Hello Ada!".equals(body);
    }
}

The same fluent request helper can cover common request shapes. For example, form data and headers remain close to the assertion:

@Test
void accepts_a_form_submission() throws Exception {
    String body = path("/profiles")
        .header("X-Request-Source", "integration-test")
        .data("name", "Ada")
        .post();
    assert body.contains("Ada");
}

The endpoint and response schema in this example are application code, not Solon APIs. The point is the testing boundary: route a request through the local server and assert the observable result.

Keep configuration selection explicit with @Import

Tests often need a dedicated configuration file or a small extra configuration class. Solon’s @Import can import the test resource that the application needs.

import org.junit.jupiter.api.Test;
import org.noear.solon.annotation.Import;
import org.noear.solon.annotation.Inject;
import org.noear.solon.test.SolonTest;

@Import(profiles = "classpath:test/app.yml")
@SolonTest(App.class)
class FeatureFlagTest {
    @Inject("${feature.greeting-enabled:false}")
    boolean greetingEnabled;

    @Test
    void loads_the_test_profile() {
        assert greetingEnabled;
    }
}

Treat test configuration as code: check it into source control, minimize its differences from production, and use fixtures or test-only services for external dependencies. A test becomes difficult to trust when its outcome depends on an undocumented local database or a developer-specific environment variable.

Use transaction rollback for write-path tests

Database tests should prove a write path while leaving the shared test database ready for the next test. @Rollback is provided for transaction rollback at the test method or class level.

import org.junit.jupiter.api.Test;
import org.noear.solon.annotation.Inject;
import org.noear.solon.test.Rollback;
import org.noear.solon.test.SolonTest;

@SolonTest(App.class)
class UserRegistrationTest {
    @Inject
    UserService userService;

    @Test
    @Rollback
    void saves_a_user_without_leaving_fixture_data() {
        userService.register("Ada");
        assert userService.findByName("Ada") != null;
    }
}

Rollback is not a substitute for a clean test-data strategy. It is a useful guard when the application operation participates in the managed transaction. For queues, files, third-party APIs, and asynchronous jobs, choose a dedicated fake, emulator, or cleanup policy instead of assuming a database rollback can undo everything.

Build a small testing ladder

A practical suite does not need every test to boot HTTP. Use the least expensive boundary that still verifies the risk:

  • Pure JUnit test for deterministic business rules with no container dependency.
  • @SolonTest component test for injection, configuration, and service behavior.
  • HttpTester endpoint test for routing, request binding, filters, serialization, and status/body behavior.
  • Focused integration test for a real database or remote dependency only when its contract is the risk being checked.

For a larger service, put endpoint-focused tests in a recognizable package such as apis. This makes the HTTP regression suite easy to run and keeps protocol-level test helpers from being copied into each test class.

A baseline worth keeping

The useful habit is not “test everything through HTTP.” It is making each boundary intentional. @SolonTest gives tests the real Solon application context; HttpTester exercises the local HTTP contract; @Import makes configuration deliberate; and @Rollback helps keep transactional tests isolated.

Start with one critical read endpoint and one important write path. Once those examples establish conventions for startup, configuration, fixtures, and assertions, adding coverage becomes routine rather than a separate testing project.

Official references:

Comments

No comments yet. Start the discussion.