┌───────────┐
│ E2E (az) │ ← BFF endpoint-lər üçün (Testcontainers)
─┼───────────┼─
│ Integration │ ← Repository + Redis + DB testləri
─┼─────────────┼─
│ Unit Tests │ ← Domain service, use case, mapper
─┼───────────────┼─
│ Architecture │ ← ArchUnit (dependency rules)
─┼─────────────────┼─
│ Contract Tests │ ← "No extra fields" enforcement
└───────────────────┘
- Hədəf: Domain service, use case, mapper, utility
- Framework: JUnit 5 + Mockito
- Adlandırma:
ClassName_methodName_shouldExpectedBehavior_whenCondition
- Nümunə:
RegisterUseCaseImpl_execute_shouldThrowException_whenPhoneDuplicate
- Qaydalar:
- Heç bir Spring context yüklənmir
- Bütün dependency-lər mock olunur
- Hər test < 50ms
- Hədəf: JPA repository, Redis adapter, Flyway migration
- Framework: JUnit 5 + Testcontainers + @DataJpaTest
- Adlandırma:
*IntegrationTest.java
- Qaydalar:
- Real PostgreSQL (Testcontainers)
- Real Redis (Testcontainers)
- Spring context yüklənir (amma yalnız lazım olan slice)
- Hədəf: HTTP request/response, validation, status codes
- Framework: MockMvc + @WebMvcTest
- Adlandırma:
*ControllerTest.java
- Qaydalar:
- Service layer mock olunur
- Validation test olunur (bad request)
- Response body structure test olunur
- Hədəf: Response body-nin yalnız icazə verilmiş field-ləri ehtiva etməsi
- Adlandırma:
*ContractTest.java
- Qaydalar:
- Hər BFF endpoint üçün
- Response JSON field-lərini whitelist ilə yoxlayır
- Yeni field əlavə edildikdə test FAIL olur (intentional — review tələb edir)
- Hədəf: Modul boundary-lər, dependency rules
- Framework: ArchUnit
- Nümunə qaydalar:
auth paketi car paketindən asılı olmamalı
- Entity class-ları Jackson annotation istifadə etməməli
- Controller-lər yalnız
bff/ paketində olmalı
- Repository-lər yalnız
infrastructure/ paketində olmalı
// shared/TestFixtures.java
public class TestFixtures {
public static UserCredential aUserCredential() {
return UserCredential.builder()
.phone("+994501234567")
.passwordHash("$2a$10$...")
.build();
}
public static RegisterCommand aRegisterCommand() {
return new RegisterCommand(
"+994501234567",
"123456", // OTP
"Password1!"
);
}
}
| Test tipi |
Pattern |
Nümunə |
| Unit |
{Class}Test.java |
RegisterUseCaseImplTest.java |
| Integration |
{Class}IntegrationTest.java |
JpaUserCredentialRepositoryIntegrationTest.java |
| Controller |
{Controller}Test.java |
AuthBffControllerTest.java |
| Contract |
{Controller}ContractTest.java |
AuthBffContractTest.java |
| Architecture |
ArchitectureTest.java |
ArchitectureTest.java |
| Layer |
Minimum Coverage |
| Domain service |
90% |
| Use case |
85% |
| BFF Controller |
80% |
| Mapper |
100% (MapStruct generated) |
| Repository |
70% (integration) |
| Infrastructure adapter |
70% |
@Testcontainers
@SpringBootTest
class BaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("timescale/timescaledb:latest-pg16")
.withDatabaseName("carnum_test");
@Container
static GenericContainer<?> redis =
new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port",
() -> redis.getMappedPort(6379));
}
}
Hər task üçün:
- Əvvəl test yaz (Red)
- Minimum kodu yaz ki test keçsin (Green)
- Refactor et (Refactor)
- Contract test əlavə et (əgər BFF endpoint-dirsə)
./gradlew test — hamısı yaşıl olmalı