Testing WebJars and static resources in Spring Boot
Packaging browser assets as WebJars can hide a surprisingly broad failure surface. A JavaScript file may be present in the dependency tree, resolve through Spring Boot, and still fail in a browser because its URL contains the wrong version, the response has the wrong content type, or a proxy caches an obsolete response. Unit tests that only call business APIs will not reveal those defects.
The reliable approach starts at the packaged artefact, follows the resource handler, and ends with an HTTP request made under production-like conditions. It should cover versioned and versionless paths where relevant, distinguish missing files from deliberate access controls, and verify that returned bytes are usable by the page. This matters for Australian teams shipping customer portals, banking journeys and public services across different browsers and hosting layers.
How WebJars enter the Spring Boot resource chain
A WebJar follows the standard JAR layout for web resources: assets usually live under META-INF/resources/webjars/<library>/<version>/. Spring Boot’s auto-configured static resource handling can expose those entries without a controller. A request for a CSS, JavaScript, font or source-map file is therefore resolved by the resource chain rather than a @RestController method. Tests should begin by proving that the expected file was packaged, not by assuming a dependency declaration reached the runtime classpath.
Build tools expose the first common fault line. Maven users must distinguish compile scope from test scope, while Gradle projects need the asset on runtimeClasspath as well as the test runtime. Inspecting the assembled application or running jar tf against the dependency can reveal duplicate paths, absent versions and accidental test-only artefacts. The same check is useful when several WebJars bundle a shared library under conflicting version directories. Dependency convergence tools can also expose version conflicts before they reach the packaged server.
Versionless URLs such as /webjars/jquery/jquery.min.js need compatible locator support; they are not an automatic property of every WebJar. Version-aware URLs are more deterministic and make cache invalidation easier. Teams should decide whether HTML references a pinned version, a locator-resolved path, or a generated asset digest, then test that exact contract. A frontend build can otherwise succeed while Spring serves a different version from the one recorded in the lock file.
Resolving paths, versions and cache behaviour
Spring Boot normally maps static locations including classpath:/META-INF/resources/, classpath:/resources/, classpath:/static/ and classpath:/public/. The resource chain confirms that a candidate exists before handling it. A custom ResourceHandlerRegistry, controller advice or spring.web.resources.static-locations can change that behaviour, so tests should exercise the application’s actual configuration rather than a generic web slice that might omit important auto-configuration.
Path resolution also interacts with the servlet context path, URL encoding and trailing rules. If an application lives under /accounts, an absolute asset URL that omits that prefix can escape the application, while encoded characters and proxy rewrites can produce a different route at the edge. Check the status, body, content type and relevant security decision for each URL shape the generated HTML can emit. Relative URLs should also be covered because they resolve differently from root-relative paths.
Cache headers form another part of the contract. A configurable resource cache period may produce Cache-Control, while validators such as ETag and Last-Modified support conditional requests. These behaviours differ from CDN caching and browser heuristics, so an asset can pass locally but remain stale behind a distributed cache. When an asset inventory expands beyond a single library, a technology requirements review helps identify which dependencies, browsers and delivery guarantees actually need verification.
MockMvc tests that expose routing defects
MockMvc provides a fast way to test the Spring resource chain without opening a socket. Prefer a full application context when auto-configuration, security and custom resource handlers are material; a web-slice test may omit them. This example uses a pinned asset path so a deliberate dependency update produces a visible change:
@SpringBootTest
@AutoConfigureMockMvc
class StaticResourceTests {
@Autowired
MockMvc mvc;
@Test
void servesPinnedWebJar() throws Exception {
mvc.perform(get("/webjars/bootstrap/5.3.3/dist/css/bootstrap.min.css"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith("text/css"))
.andExpect(content().string(containsString("--bs-")));
}
}
The strongest assertion checks more than status().isOk(). Verify Content-Type, a stable marker from the file and, when configured, cache headers. A CSS response incorrectly labelled as application/octet-stream may pass a download check but fail under a strict Content-Security-Policy. Sending a conditional request can also confirm that the resource supports the validator expected by production. Keep the fixture small enough that the intended file, rather than incidental HTML, is what matched.
Negative tests are equally valuable. Request an asset that is absent and assert the intended result, commonly 404 Not Found, rather than accepting any non-2xx response. Watch for a single-page-application fallback that converts missing JavaScript into an HTML page with status 200; that response can break module loading while looking healthy to a basic test. If Spring Security protects the route, assert the expected 401, 403 or redirect according to policy instead of mislabelling it as a packaging defect.
Running checks through a real server
For a running-server check, start Spring Boot with webEnvironment = RANDOM_PORT and call the static URL through TestRestTemplate or an HTTP client. This confirms that the servlet container, filters and embedded server cooperate, while a random port avoids collisions on shared CI workers. Prefer a defined port only when an external proxy or fixed-origin test needs it. Compare a small body signature and content type, and avoid automatically following redirects that could conceal a bad path.
Deployment differences remain outside that test. A Sydney SaaS product may publish the application through an AWS Application Load Balancer in ap-southeast-2, while a Melbourne agency may route a gov.au service through Kubernetes ingress. Both layers can alter host names, forwarded prefixes, compression and cache directives. A smoke test against the deployed origin or staging hostname can expose those differences, but it must use a representative asset URL rather than only the home page.
Browser verification closes the remaining gap for Australian audiences. Public-sector releases should be assessed against the Australian Government Digital Service Standard and WCAG 2.2 AA rather than treating 200 as proof of usability. A Playwright or Selenium pass can confirm that CSS applied, JavaScript executed without console errors and the network contained no failed asset. Include Chrome and Edge, plus Safari where iOS or macOS users are supported, because engine differences affect font, module and compression behaviour.
A practical release checklist
The evidence is strongest when each layer answers a different question and the release gate combines the results:
| Layer | Preferred check | Evidence it provides | Typical blind spot |
|---|---|---|---|
| Packaged dependency | Build inspection | Correct versions and resource paths exist | Runtime routing |
| Spring resource chain | Full-context MockMvc | Correct status, content type and body | Proxy and CDN behaviour |
| Embedded server | Random-port HTTP test | Filters and servlet handling cooperate | Browser rendering |
| Production-like host | Staging or browser smoke test | Rewrites, caching, scripts and styles work together | Local developer configuration |
Use these checks as a focused release gate:
- Pin WebJar versions in Maven or Gradle and inspect the packaged paths before publishing the application.
- Add one full-context MockMvc test for a CSS asset and one JavaScript asset, including content type and body markers.
- Run a browser smoke test against the built frontend, checking network failures and console errors.
- Exercise a staging or proxy-hosted URL with the same cache, compression and security headers as production.
The next concrete step is to take one exact WebJar URL from the generated HTML, add it to the full-context test suite with its expected content type and body marker, and make that test a required build check.