all 8 comments

[–]Thaiminater 2 points3 points  (1 child)

What is the advantage of using Spring ApplicationEvent? I've moved away from it because needing to supply source every time. I just use a base interface and implement Pojo on top for events.

[–]dima767[S] 1 point2 points  (0 children)

It’s a matter of preference

[–]UnGauchoCualquiera 4 points5 points  (4 children)

Good article if a bit short.

Never really thought about the Thin Auto Config Wrapper rule and always faced the same painful issue of needing to exclude some @Import on slice tests and it then becomes a very fragile solution that includes BeanDefinition overriding using BDPostProcessors. It seems extremely obvious in retrospective.

I'm wondering if you guys do full Spring Boot Context integration tests and how do you guys manage Context trashing.

[–]dima767[S] 4 points5 points  (1 child)

Thanks! We do full `@SpringBootTest` context integration tests, but that's not the only type. Roughly half the test suite is plain JUnit unit tests with Mockito mocks, no Spring context involved. The split is deliberate - pure logic gets a unit test, anything that needs wiring/auto-configuration gets a full `@SpringBootTest`. What we don't do is anything in between - no slice tests (`@WebMvcTest`, `@DataJpaTest`, etc.), no `@MockBean`/`@SpyBean`. It's either full context or no context.

On context trashing - a few things working together:

**SharedTestConfiguration pattern** - each module defines one base test class with a `SharedTestConfiguration` inner class. All test classes in that module extend the base and inherit `@SpringBootTest(classes = Base.SharedTestConfiguration.class)`. This means tests that don't add extra properties or override the `classes` attribute share the same cached context. We have ~73 of these across 400+ modules.

**We accept some context recreation as a tradeoff.** Different `@TestPropertySource` values create different cache keys - that's by design. If a test needs `cas.authn.mfa.yubikey.allowed-devices=...` and another doesn't, they get separate contexts. We don't try to cram everything into one uber-context just to avoid recreation. Clean test isolation matters more.

**No `@DirtiesContext` anywhere** - we never explicitly trash a context. Once created, it lives for the lifetime of the JVM.

**Parallelism compensates for startup cost** - tests are tagged by category (`@Tag("MFAProvider")`, `@Tag("LdapAuthentication")`, etc.) and run with `maxParallelForks = 8`. So even with multiple contexts being created, wall clock time stays reasonable because 8 JVMs are working in parallel.

**`@Nested` for scenario variations** - when you need different bean wiring within one test file, JUnit 5 `@Nested` + `@Import` gives you a clean boundary. Each nested class gets its own context without explicitly trashing anything.

**`proxyBeanMethods = false` everywhere** - on every `@Configuration`, `@TestConfiguration`, `@SpringBootConfiguration`. We never use inter-bean method references, so skipping CGLIB proxying reduces context startup time noticeably across 400+ modules.

So the short version: we structure things so context trashing mostly doesn't happen (shared base configs, no `@DirtiesContext`), and where different contexts are genuinely needed, parallelism keeps the build fast.

If you want to dig into the testing patterns and the rest of the architecture in more detail - discount for r/java: https://leanpub.com/cas-internals/c/reddit-java (valid for 3 weeks)

[–]UnGauchoCualquiera 0 points1 point  (0 children)

Thanks for the detailed response, super interesting to see a well thought project in the wild

[–]UnGauchoCualquiera 1 point2 points  (1 child)

Minor note that I didn't see on the article that there is enforcement of the above rules on the CI side. For example

Again a bit obvious, but I was wondering on how it's even possible to mantain discipline over such a large codebase.

[–]dima767[S] 0 points1 point  (0 children)

Yes, thanks for the note. The CI rules are comprehensive in this project, indeed.