diff --git a/.claude/skills/document/SKILL.md b/.claude/skills/document/SKILL.md index a7979b362..c530e62b5 100644 --- a/.claude/skills/document/SKILL.md +++ b/.claude/skills/document/SKILL.md @@ -13,6 +13,7 @@ Read `AGENTS.md` (root + relevant module) before making changes. |------|----------|---------| | `README.md` | End users | Usage, configuration, operations table | | `CHANGELOG.md` | End users | Version history, breaking changes | +| `docs/TESTING.md` | Contributors / AI agents | Testing strategy, base classes, patterns, commands | | `AGENTS.md` (root + modules) | AI agents | Architecture, conventions, guardrails | | `.github/CONTRIBUTING.md` | Contributors | Dev setup, code reviews | diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md index e23a58989..31ce67168 100644 --- a/.claude/skills/test/SKILL.md +++ b/.claude/skills/test/SKILL.md @@ -5,22 +5,9 @@ description: Write, update, or fix tests. Use when asked to test code, create te # Test Skill — S3Mock -Read `AGENTS.md` (root + relevant module) before writing tests — especially the Testing, DO/DON'T sections and base classes. +Read **[docs/TESTING.md](../../../docs/TESTING.md)** and `AGENTS.md` (root + relevant module) before writing tests — they define test types, base classes, naming conventions, and running commands. -## Test Types - -### Unit Tests (`*Test.kt`) — `server/src/test/kotlin/.../` -- `@SpringBootTest` with `@MockitoBean` for mocking (see AGENTS.md for details) -- Extend the appropriate base class: `ServiceTestBase`, `StoreTestBase`, or `BaseControllerTest` -- Name the class under test `iut` -- Use `@Autowired` for the class under test - -### Integration Tests (`*IT.kt`) — `integration-tests/src/test/kotlin/.../its/` -- Extend `S3TestBase` for pre-configured `s3Client` (AWS SDK v2) -- Use `givenBucket(testInfo)` for unique bucket names -- Tests run against Docker container — Docker must be running - -## Key Conventions (see AGENTS.md for full list) +## Key Conventions (from AGENTS.md) - **Naming**: Backtick names: `` fun `should create bucket successfully`() `` - **Pattern**: Arrange-Act-Assert @@ -29,24 +16,9 @@ Read `AGENTS.md` (root + relevant module) before writing tests — especially th - **Error cases**: `assertThatThrownBy { ... }.isInstanceOf(AwsServiceException::class.java)` - **Visibility**: `internal class` -## Running Tests - -```bash -./mvnw test -pl server # Unit tests -./mvnw verify -pl integration-tests # All integration tests -./mvnw verify -pl integration-tests -Dit.test=BucketIT # Specific class -./mvnw test -pl server -DskipDocker # Skip Docker -``` - -## Troubleshooting - -- **Docker not running**: Integration tests require Docker -- **Port conflict**: Check `lsof -i :9090` -- **Flaky test**: Check for shared state or ordering dependencies -- **Compilation error**: Run `./mvnw clean install -DskipDocker -DskipTests` first - ## Checklist +- [ ] Read `docs/TESTING.md` and root + module `AGENTS.md` - [ ] Tests pass locally - [ ] Both success and failure cases covered - [ ] Tests are independent (no shared state, UUID bucket names) diff --git a/AGENTS.md b/AGENTS.md index ffa4cf3ba..006118701 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,15 +107,7 @@ See `server/AGENTS.md` for details. ## Testing -- Unit tests: `@SpringBootTest` with `@MockitoBean`, suffix `Test` -- Integration tests: Real AWS SDK v2 against Docker container, suffix `IT` -- Test independence: Each test self-contained -- Name the class under test **`iut`** (implementation under test): `private lateinit var iut: ObjectService` -- **Base classes** — always extend the appropriate base: - - `ServiceTestBase` for service tests - - `StoreTestBase` for store tests - - `BaseControllerTest` for controller tests - - `S3TestBase` for integration tests +See **[docs/TESTING.md](docs/TESTING.md)** for the full testing strategy, base classes, patterns, and commands. ## Build diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 000000000..0f01c1c49 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,106 @@ +# Testing Strategy — S3Mock + +## Test Types + +| Type | Location | Suffix | Purpose | +|------|----------|--------|---------| +| Unit tests | `server/src/test/kotlin/` | `*Test.kt` | Service, store, and controller logic in isolation | +| Integration tests | `integration-tests/src/test/kotlin/.../its/` | `*IT.kt` | Real AWS SDK v2 against a live Docker container | + +## Unit Tests + +Use `@SpringBootTest` with `@MockitoBean` for mocking. Extend the appropriate base class: + +| Base Class | Use For | +|---|---| +| `ServiceTestBase` | Service-layer tests | +| `StoreTestBase` | Store-layer tests | +| `BaseControllerTest` | Controller slice tests (`@WebMvcTest`) | + +Name the class under test **`iut`** (implementation under test); inject with `@Autowired`: + +```kotlin +@SpringBootTest(classes = [ServiceConfiguration::class], webEnvironment = SpringBootTest.WebEnvironment.NONE) +@MockitoBean(types = [BucketService::class, MultipartService::class, MultipartStore::class]) +internal class ObjectServiceTest : ServiceTestBase() { + @Autowired + private lateinit var iut: ObjectService + + @Test + fun `should get object`() { + whenever(bucketStore.getBucketMetadata("bucket")).thenReturn(bucket) + whenever(objectStore.getObject(bucket, "key")).thenReturn(s3Object) + assertThat(iut.getObject("bucket", "key")).isEqualTo(s3Object) + } +} +``` + +## Integration Tests + +Extend `S3TestBase` for a pre-configured `s3Client` (AWS SDK v2). Accept `TestInfo` as a method parameter and use `givenBucket(testInfo)` for unique bucket names: + +```kotlin +internal class MyFeatureIT : S3TestBase() { + @Test + fun `should perform operation`(testInfo: TestInfo) { + // Arrange + val bucketName = givenBucket(testInfo) + + // Act + s3Client.putObject( + PutObjectRequest.builder().bucket(bucketName).key("key").build(), + RequestBody.fromString("content") + ) + + // Assert + val response = s3Client.getObject( + GetObjectRequest.builder().bucket(bucketName).key("key").build() + ) + assertThat(response.readAllBytes().decodeToString()).isEqualTo("content") + } +} +``` + +Access `serviceEndpoint`, `serviceEndpointHttp`, and `serviceEndpointHttps` from `S3TestBase` when needed. + +## Conventions + +- **Naming**: Backtick names with descriptive sentences — `` fun `should create bucket successfully`() `` +- **Visibility**: Mark test classes `internal` +- **Pattern**: Arrange-Act-Assert +- **Assertions**: AssertJ (`assertThat(...)`) — use specific matchers, not just `isNotNull()` +- **Error cases**: Use AssertJ, not JUnit `assertThrows`: + ```kotlin + assertThatThrownBy { s3Client.deleteBucket { it.bucket(bucketName) } } + .isInstanceOf(AwsServiceException::class.java) + .hasMessageContaining("Status Code: 409") + ``` +- **Independence**: Each test creates its own resources — no shared state, UUID-based bucket names +- **Legacy names**: Refactor `testSomething` camelCase names to backtick style when touching existing tests + +## Running Tests + +```bash +./mvnw test -pl server # Unit tests only +./mvnw verify -pl integration-tests # All integration tests +./mvnw verify -pl integration-tests -Dit.test=BucketIT # Specific class +./mvnw verify -pl integration-tests -Dit.test=BucketIT#shouldCreateBucket # Specific method +./mvnw test -pl server -DskipDocker # Skip Docker +``` + +> Integration tests require Docker to be running. + +## Troubleshooting + +- **Docker not running**: Integration tests will fail — start Docker first +- **Port conflict**: Check `lsof -i :9090` +- **Flaky test**: Look for shared state or ordering dependencies +- **Compilation error**: Run `./mvnw clean install -DskipDocker -DskipTests` first + +## Checklist + +- [ ] Tests pass locally +- [ ] Both success and failure cases covered +- [ ] Tests are independent (no shared state, UUID bucket names) +- [ ] Assertions are specific +- [ ] Run `./mvnw ktlint:format` diff --git a/integration-tests/AGENTS.md b/integration-tests/AGENTS.md index 704a1c4b9..c425860bb 100644 --- a/integration-tests/AGENTS.md +++ b/integration-tests/AGENTS.md @@ -2,7 +2,7 @@ > Inherits all conventions from the [root AGENTS.md](../AGENTS.md). Below are module-specific additions only. -Integration tests verifying S3Mock with real AWS SDK v2 clients. +Integration tests verifying S3Mock with real AWS SDK v2 clients. See **[docs/TESTING.md](../docs/TESTING.md)** for the full testing strategy, conventions, patterns, and running commands. ## Structure @@ -23,45 +23,9 @@ Extend `S3TestBase` for access to: - Extend **`S3TestBase`** for all integration tests - Accept **`testInfo: TestInfo`** parameter in test methods for unique resource naming - Use **`givenBucket(testInfo)`** for bucket creation — don't create your own helper -- Use **Arrange-Act-Assert** pattern consistently - Verify HTTP codes, headers (ETag, Content-Type), XML/JSON bodies, and error responses - DON'T mock AWS SDK clients — use actual SDK clients against S3Mock -## Test Pattern - -```kotlin -internal class MyFeatureIT : S3TestBase() { - @Test - fun `should perform operation`(testInfo: TestInfo) { - // Arrange - val bucketName = givenBucket(testInfo) - - // Act - s3Client.putObject( - PutObjectRequest.builder().bucket(bucketName).key("key").build(), - RequestBody.fromString("content") - ) - - // Assert - val response = s3Client.getObject( - GetObjectRequest.builder().bucket(bucketName).key("key").build() - ) - assertThat(response.readAllBytes().decodeToString()).isEqualTo("content") - } -} -``` - -## Common Patterns - -**Multipart**: Initiate → Upload parts → Complete → Verify - -**Errors** (use AssertJ, not JUnit `assertThrows`): -```kotlin -assertThatThrownBy { s3Client.deleteBucket { it.bucket(bucketName) } } - .isInstanceOf(AwsServiceException::class.java) - .hasMessageContaining("Status Code: 409") -``` - ## Running ```bash diff --git a/server/AGENTS.md b/server/AGENTS.md index 4fb1599de..a67cf2311 100644 --- a/server/AGENTS.md +++ b/server/AGENTS.md @@ -47,23 +47,7 @@ server/src/main/kotlin/com/adobe/testing/s3mock/ ## Testing -Service and store unit tests use `@SpringBootTest` with `@MockitoBean`, while controller tests are slice tests using `@WebMvcTest` with `@MockitoBean` and `BaseControllerTest`. Extend the appropriate base class (`ServiceTestBase`, `StoreTestBase`, `BaseControllerTest`): - -```kotlin -@SpringBootTest(classes = [ServiceConfiguration::class], webEnvironment = SpringBootTest.WebEnvironment.NONE) -@MockitoBean(types = [BucketService::class, MultipartService::class, MultipartStore::class]) -internal class ObjectServiceTest : ServiceTestBase() { - @Autowired - private lateinit var iut: ObjectService - - @Test - fun `should get object`() { - whenever(bucketStore.getBucketMetadata("bucket")).thenReturn(bucket) - whenever(objectStore.getObject(bucket, "key")).thenReturn(s3Object) - assertThat(iut.getObject("bucket", "key")).isEqualTo(s3Object) - } -} -``` +See **[docs/TESTING.md](../docs/TESTING.md)** for the full strategy. Service and store tests use `@SpringBootTest` with `@MockitoBean`; controller tests use `@WebMvcTest` with `@MockitoBean` and `BaseControllerTest`. Always extend the appropriate base class (`ServiceTestBase`, `StoreTestBase`, `BaseControllerTest`). ## Configuration diff --git a/testsupport/AGENTS.md b/testsupport/AGENTS.md index 76d31acae..fab526eed 100644 --- a/testsupport/AGENTS.md +++ b/testsupport/AGENTS.md @@ -2,7 +2,7 @@ > Inherits all conventions from the [root AGENTS.md](../AGENTS.md). Below are module-specific additions only. -Test framework integrations for using S3Mock: JUnit 5, Testcontainers, TestNG. +Test framework integrations for using S3Mock: JUnit 5, Testcontainers, TestNG. See **[docs/TESTING.md](../docs/TESTING.md)** for the overall testing strategy. > **Deprecation Notice (6.x):** The JUnit 5, TestNG, and all direct-integration modules will be > removed in S3Mock 6.x. Testcontainers will become the only officially supported testing approach.