# Testing with Spring Boot

> Integration tests with Testcontainers, and controlling which subscriptions start.

[Spring Boot](/docs/spring-boot/)

To test the annotations, and the catch-up and checkpoint machinery behind them, you need a real MongoDB replica set. Provide it as a `@ServiceConnection` bean so Boot points itself at the container:

```java
@SpringBootTest
@Testcontainers
class ProjectionAnnotationTest {


    @TestConfiguration
    static class Containers {


        @Bean
        @ServiceConnection
        MongoDBContainer mongoDbContainer() {
            return new MongoDBContainer("mongo:8.0").withReplicaSet();
        }
    }
}
```

One trap to watch out for is that `MongoDBContainer.getReplicaSetUrl()` with no argument always targets the database named `test`. You cannot append a suffix to the returned URL either, because MongoDB rejects dots in database names, so the name silently stays `test` and tests collide. When a test needs its own database, ask for it by name with `getReplicaSetUrl(String)`.

## Wiring it into a Spring Boot test

`occurrent-testing-spring-boot` wires the same extension into the application context, so a test autowires it rather than constructing it:

```xml
<dependency>
    <groupId>org.occurrent</groupId>
    <artifactId>occurrent-testing-spring-boot</artifactId>
    <version>0.34.0</version>
    <scope>test</scope>
</dependency>
```

* Java

  ```java
  @SpringBootTest
  @EnableOccurrentTesting
  class OrderProjectionTest {


      @Autowired
      @RegisterExtension
      OccurrentSubscriptionsExtension subscriptions;
  }
  ```

* Kotlin

  ```kotlin
  @SpringBootTest
  @EnableOccurrentTesting
  class OrderProjectionTest {


      @Autowired
      @RegisterExtension
      lateinit var subscriptions: OccurrentSubscriptionsExtension
  }
  ```

Since 0.33.0, the extension bean clears checkpoints on its own. It applies `clearingCheckpoints(..)` automatically once the context has exactly one `CheckpointStorage` bean, so an application with a single checkpoint store needs no further configuration for that half. Your event store and subscription model are otherwise left exactly as the application wires them, so the test still runs against the real store. That is the point, since a subscription is only worth testing against the change streams, checkpoints and catch-up it actually uses.

`@EnableOccurrentTesting` wires the extension for the module you added as a test dependency, `occurrent-testing-junit-jupiter-blocking`, `occurrent-testing-junit-jupiter-reactor`, or both. An application using both stacks at once gets two extension beans, one per stack, autowired by type. Either way, the extension stops every subscription model in the context rather than one, since some applications have more than one that needs stopping, a durable model and a `SynchronousSubscriptionModel` side by side is the ordinary case on the reactive stack. Outside Spring the same rule applies to `stoppedByDefault`, pass every model it needs to stop: `stoppedByDefault(durableModel, synchronousModel)`.

`clearState = true` on `@EnableOccurrentTesting` does the same for the database flush, applying `clearingStateWith(..)` automatically once a store integration is available to flush with, `occurrent-testing-mongodb` plus a `MongoTemplate` bean today. It is `false` by default, since flushing is destructive and not every test wants it.

To configure the flush by hand instead, more than one `CheckpointStorage` bean in the context (which the automatic checkpoint clearing above leaves unresolved), or a narrower sweep than `everyCollectionIn`, `collectionsIn(..)`, `except(..)`, or `droppingTheDatabaseIn(..)` below, configure the injected extension in an `@Autowired` method. The extension bean is prototype scoped, so do not take it as a method parameter, that would configure a different instance than the one the test registers. Configure the field instead, Spring injects fields before methods:

* Java

  ```java
  @Autowired
  @RegisterExtension
  OccurrentSubscriptionsExtension subscriptions;


  @Autowired
  void addStateClearing(MongoTemplate mongoTemplate, CheckpointStorage checkpointStorage) {
      subscriptions.clearingStateWith(OccurrentMongoFlush.everyCollectionIn(mongoTemplate.getDb()))
              .clearingCheckpoints(checkpointStorage);
  }
  ```

* Kotlin

  ```kotlin
  @Autowired
  @RegisterExtension
  lateinit var subscriptions: OccurrentSubscriptionsExtension


  @Autowired
  fun addStateClearing(mongoTemplate: MongoTemplate, checkpointStorage: CheckpointStorage) {
      subscriptions.clearingStateWith(OccurrentMongoFlush.everyCollectionIn(mongoTemplate.db))
          .clearingCheckpoints(checkpointStorage)
  }
  ```

Your application registers its subscriptions at startup through its annotations, so a test never registers them itself. Outside Spring, register them once in `@BeforeAll`. Doing it in `@BeforeEach` fails on the second test with `Subscription <id> is already defined`, and would not work anyway, because JUnit runs an extension’s `beforeEach` before any `@BeforeEach` method, so a subscription created there is never stopped.

`everyCollectionIn` deletes the documents rather than dropping anything, which matters for two reasons.

Dropping a collection or a database invalidates a live MongoDB change stream, and the subscriptions you resume afterwards then receive nothing. Stopping the model first does not save you, because it resumes from a position that points into a collection which no longer exists.

The quieter reason is that your event store creates its unique indexes when it is constructed, so dropping removes them and only a new store brings them back. Spring caches the test context, so nothing constructs one again, and your optimistic-concurrency and duplicate-detection tests keep passing for the rest of the run with no index behind them. That failure never announces itself, which makes it the worse of the two.

It names no collections, on purpose. Occurrent writes to more of them than you are likely to remember: the events, a stream position collection, a DCB checkpoint collection, the checkpoint collection below, and a competing consumer lock collection. A list you maintain by hand stops covering one the day you switch a feature on. Use `collectionsIn(database, ..)` only when the database holds something a test has to keep, and `except(..)` to spare one collection from the sweep.

For the one case deleting cannot serve, a test asserting that a collection or an index does *not* exist, there is `droppingTheDatabaseIn(database)`. Nothing else should use it.

The checkpoints have to go too, which is what `clearingCheckpoints` is for. Resuming a subscription continues from its stored checkpoint, so a subscription left behind by an earlier test picks up whatever that test wrote while it was stopped, and the second test then sees events it never wrote. Clearing the events alone does not prevent this, because the checkpoint is what decides where the resume starts. It works through `CheckpointStorage` rather than a collection name, so it is right whether your checkpoints live beside the events or in Redis. `clearingCheckpointsFor(checkpointStorage, ids...)` names checkpoints to clear for subscriptions no model reports yet, for the same reason `start("someId")` sometimes needs an id the model hasn’t registered.

The in-memory subscription model does not have this problem. Events written while a subscription is stopped are dropped rather than queued, so there is nothing to catch up on when a later test starts it again.

Then keep at least one test with everything running. Deny-by-default means nothing checks two subscriptions reacting to the same event unless you ask it to.

One cost stays whatever the tests do. Every subscription still starts once while the application context boots, and is stopped again before the first test, so on MongoDB you pay for a change stream opened and closed per subscription per context. A JUnit extension runs after the context is refreshed, so nothing in the test can prevent that.

A subscription id that no longer exists throws `IllegalArgumentException` from `resumeSubscription`, so renaming one breaks the tests that named it instead of quietly leaving them asserting nothing.