| name | testing |
| description | Load when writing tests, modifying test infrastructure, working with E2E test base classes, or improving test coverage. |
Testing Guide
Load this context when writing or modifying tests.
Test Strategy
| Type | When to Write | Coverage Goal |
|---|
| E2E | Required for ALL API endpoints | 100% endpoint coverage |
| Unit | Complex logic, exception-heavy code | Selective |
Philosophy: Prefer E2E tests over unit tests. Unit tests only when logic is complex enough to
warrant isolated testing.
E2E Test Structure
Base Classes
abstract class E2ETestBase {
@Autowired
protected lateinit var tokenProvider: AuthTokenProvider
@Autowired
protected lateinit var userRepository: UserRepository
@AfterEach
protected open fun tearDown() {
userRepository.deleteAllInBatch()
}
fun createTestUserAndToken(
email: String = "test-${System.currentTimeMillis()}@example.com",
name: String = "Test User",
providerType: ProviderType = ProviderType.TEST,
): Pair<User, String>
}
Reference: src/test/kotlin/com/neki/e2e/E2ETestBase.kt
Domain-Specific Base Classes
For domain-specific setup, extend the domain base class:
class CreateFolderE2ETest : FolderE2ETestBase() {
}
Test Directory Structure
src/test/kotlin/com/neki/
โโโ e2e/ # E2E tests (organized by domain)
โ โโโ E2ETestBase.kt # Base class for all E2E tests
โ โโโ auth/
โ โ โโโ AuthE2ETest.kt
โ โโโ photo/
โ โ โโโ folder/
โ โ โโโ FolderE2ETestBase.kt
โ โ โโโ CreateFolderE2ETest.kt
โ โ โโโ DeleteFolderE2ETest.kt
โ โ โโโ GetAllFolderE2ETest.kt
โ โ โโโ UpdateFolderE2ETest.kt
โ โโโ user/
โ โโโ UserE2ETest.kt
โโโ auth/ # Unit tests (next to domain)
โ โโโ infra/security/filter/
โ โโโ AuthMdcFilterTest.kt
โโโ common/
โ โโโ filter/
โ โโโ RequestMdcFilterTest.kt
โโโ JasyptTest.kt # Utility tests
Test Patterns
Creating Authenticated Requests
@Test
fun `should create folder successfully`() {
val (user, token) = createTestUserAndToken()
RestAssured.given()
.header("Authorization", "Bearer $token")
.contentType(ContentType.JSON)
.body(CreateFolderRequest(name = "My Folder"))
.`when`()
.post("/api/folders")
.then()
.statusCode(200)
.body("success", equalTo(true))
.body("data.folderId", notNullValue())
}
Testing Error Cases
@Test
fun `should return error when folder name duplicated`() {
val (user, token) = createTestUserAndToken()
createFolder(token, "Duplicate Name")
RestAssured.given()
.header("Authorization", "Bearer $token")
.contentType(ContentType.JSON)
.body(CreateFolderRequest(name = "Duplicate Name"))
.`when`()
.post("/api/folders")
.then()
.statusCode(400)
.body("resultCode", equalTo("D-06"))
}
Test Profile
Tests run with @ActiveProfiles("test"):
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class MyE2ETest : E2ETestBase() {
}
Testing Tools
| Tool | Purpose |
|---|
| Kotest | Test framework with DSL |
| MockK | Mocking library for Kotlin |
| RestAssured | HTTP testing |
| Testcontainers | Database containers |
Jasypt Test Utility
Encrypt sensitive values for configuration:
@Test
fun jasyptGeneratTest() {
val text = "value_to_encrypt"
val encrypted = jasyptStringEncryptor.encrypt(text)
println("ENC($encrypted)")
}
Test Helper Methods
Organization Rules
- Shared helpers go in base classes - Don't duplicate helper methods across test files
- Domain-specific helpers in domain base - E.g.,
createFavoritePhotoImage in
PhotoImageE2ETestBase
- Test-specific helpers stay local - Only create local helpers if truly test-specific
Example:
abstract class PhotoImageE2ETestBase : E2ETestBase() {
protected fun createFavoritePhotoImage(
userId: Long,
mediaId: Long,
folderId: Long? = null
): PhotoImage {
val photo = createPhotoImage(userId, mediaId, folderId)
favoritePhotoRepository.save(
FavoritePhoto(userId = userId, imageId = photo.id!!)
)
return photo
}
}
class GetFavoritePhotosE2ETest : PhotoImageE2ETestBase() {
fun createFavoritePhotoImage(...) {
}
}
Cleanup Order in tearDown
Delete dependent entities FIRST to respect foreign key constraints:
@AfterEach
override fun tearDown() {
favoritePhotoRepository.deleteAllInBatch()
photoImageRepository.deleteAllInBatch()
folderRepository.deleteAllInBatch()
mediaRepository.deleteAllInBatch()
super.tearDown()
}
Checklist for New Endpoints