One failure looked like nine endpoint regressions. Every affected test had failed while loading the context, before sending a request. Another came from an agent guessing a package name.

The Spring Boot 3 to 4 Migration Guide covers the broader migration. These are the test errors and fixes from Petclinic’s 3.5.16 to 4.0.9 upgrade, plus the redirect change on 3.4.

Moved imports in Boot 4.0.9

These package changes and file counts are from Petclinic.

Boot 3.5.16 typeBoot 4.0.9 type
org.springframework.boot.test.web.client.TestRestTemplateorg.springframework.boot.resttestclient.TestRestTemplate (5 files)
org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTestorg.springframework.boot.webmvc.test.autoconfigure.WebMvcTest (5 files)
org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestorg.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest
org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabaseorg.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase
org.springframework.boot.web.client.RestTemplateBuilderorg.springframework.boot.restclient.RestTemplateBuilder
org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizerorg.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer

There’s an additional annotation: org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate. It was added to 6 classes.

A @SpringBootTest that injects TestRestTemplate needs that annotation in Boot 4. The moved class also needs the test dependency org.springframework.boot:spring-boot-resttestclient.

cannot find symbol … class JCacheManagerCustomizer … location: package org.springframework.boot.autoconfigure.cache

This compile failure came from an old package reference. The verified destination is org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer.

The table records the packages used on 4.0.9. Check each class before changing its import.

package org.springframework.boot.web.server.test does not exist

An agent moved LocalServerPort to org.springframework.boot.web.server.test. That package doesn’t exist, and the step failed to compile.

LocalServerPort remained in org.springframework.boot.test.web.server. That was checked against the spring-boot-test 4.0.9 jar.

Leave that import alone.

One run got stuck at the 3.5 to 4.0 step on this incorrect import. Undoing the move fixed it.

javax.cache.CacheException: A Cache named [vets] already exists

This one compiled and failed when the tests started their application contexts.

The exception was wrapped in Failed to load ApplicationContext. Subsequent attempts reported ApplicationContext failure threshold (1) exceeded: skipping repeated attempt to load context.

The HTTP test classes had received @AutoConfigureTestRestTemplate. PetClinicIntegrationTests hadn’t, because it uses RestTemplateBuilder rather than TestRestTemplate.

That difference changed the test context configurations. Spring’s test context cache started a second application context in the same JVM. On 3.5, every RANDOM_PORT test class had the same configuration and shared one context.

Petclinic’s CacheConfiguration calls cm.createCache("vets", cacheConfiguration()). Its JCache CacheManager is shared per JVM. When the second context tried to create the same named cache, it failed.

The verified fix was to add @AutoConfigureTestRestTemplate to PetClinicIntegrationTests too. That made the configuration match the other random-port tests. The full suite at that point, 54 tests, passed on 4.0 without the cache error.

The separate save-removal experiment used a 52-test suite with 1 skipped.

The context configuration had to match before the tests could send requests. Changing endpoint assertions would have left the startup failure in place. Read the underlying exception before treating a list of failed test methods as a list of broken endpoints.

Keep the original cache exception with the report when diagnosing this failure. The later threshold message tells you why another context attempt was skipped. The named-cache exception tells you what happened when a context actually tried to start.

Expecting value to be true but was false

This appeared earlier, after the 3.2 to 3.4 step. Two HTTP tests failed on this assertion:

assertThat(post.getStatusCode().is3xxRedirection()).isTrue()

Boot 3.4 auto-detects the JDK HttpClient for TestRestTemplate. That client follows the 302 returned by a form POST. The assertion then sees the 200 response from the next page.

The verified fix in application.properties was:

spring.http.client.factory=simple

That keeps the pre-3.4 HttpURLConnection client, which follows GET redirects only. It preserves what those POST assertions were checking.

In Boot 4.0, the property name changes:

spring.http.clients.imperative.factory=simple

Rename the property at 4.0 when carrying the 3.4 fix forward.

Here the tests reached their status assertion. Locate the failed operation before changing what a test accepts.

Replace @MockBean before the 4.0 jump

@MockBean and @SpyBean are deprecated in 3.4 and gone in 4.0. Their replacements are @MockitoBean and @MockitoSpyBean from org.springframework.test.context.bean.override.mockito.

I made that change while still on Boot 3.4, before moving to 3.5. It affected 4 test classes and took 1m25s in the recorded run, including the build and tests.

Do it on 3.4 or 3.5 so the annotation change has its own passing build before 4.0. It doesn’t need to share a commit with every moved Boot test package.

The name change also doesn’t change what a mocked repository test proves. The missing-save experiment had the same result before and after the upgrade.

Use the classic starters as an intermediate step

The 3.5 to 4.0 move was split into two parts. First, I added spring-boot-starter-classic and spring-boot-starter-test-classic to get compiling on 4.0.

The second part replaced them with the modular starters. spring-boot-starter-web became spring-boot-starter-webmvc. Test-scoped dependencies included spring-boot-starter-webmvc-test and spring-boot-starter-data-jpa-test, plus spring-boot-resttestclient and spring-boot-starter-restclient.

The two parts took 2m50s and 1m55s in one run. Each includes the agent’s work plus the build and tests.

This gives the starter migration an intermediate passing state. It still needs the import changes and context checks described above. The full 2.7 to 4.1 path shows where those commits fit.

Java Modernization Readiness Assessment

15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.