einige Ergänzungen
This commit is contained in:
+60
@@ -0,0 +1,60 @@
|
||||
package at.mocode.horses.service.config
|
||||
|
||||
import at.mocode.horses.application.usecase.CreateHorseUseCase
|
||||
import at.mocode.horses.application.usecase.TransactionalCreateHorseUseCase
|
||||
import at.mocode.horses.application.usecase.UpdateHorseUseCase
|
||||
import at.mocode.horses.application.usecase.DeleteHorseUseCase
|
||||
import at.mocode.horses.application.usecase.GetHorseUseCase
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/**
|
||||
* Application configuration for the Horses Service.
|
||||
*
|
||||
* This configuration wires the use cases as Spring beans.
|
||||
*/
|
||||
@Configuration
|
||||
class ApplicationConfiguration {
|
||||
|
||||
/**
|
||||
* Creates the CreateHorseUseCase as a Spring bean.
|
||||
*/
|
||||
@Bean
|
||||
fun createHorseUseCase(horseRepository: HorseRepository): CreateHorseUseCase {
|
||||
return CreateHorseUseCase(horseRepository)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the TransactionalCreateHorseUseCase as a Spring bean.
|
||||
* This version ensures all database operations run within a single transaction.
|
||||
*/
|
||||
@Bean
|
||||
fun transactionalCreateHorseUseCase(horseRepository: HorseRepository): TransactionalCreateHorseUseCase {
|
||||
return TransactionalCreateHorseUseCase(horseRepository)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the UpdateHorseUseCase as a Spring bean.
|
||||
*/
|
||||
@Bean
|
||||
fun updateHorseUseCase(horseRepository: HorseRepository): UpdateHorseUseCase {
|
||||
return UpdateHorseUseCase(horseRepository)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the DeleteHorseUseCase as a Spring bean.
|
||||
*/
|
||||
@Bean
|
||||
fun deleteHorseUseCase(horseRepository: HorseRepository): DeleteHorseUseCase {
|
||||
return DeleteHorseUseCase(horseRepository)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the GetHorseUseCase as a Spring bean.
|
||||
*/
|
||||
@Bean
|
||||
fun getHorseUseCase(horseRepository: HorseRepository): GetHorseUseCase {
|
||||
return GetHorseUseCase(horseRepository)
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package at.mocode.horses.service.config
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseConfig
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.horses.infrastructure.persistence.HorseTable
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Component
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
/**
|
||||
* Database configuration for the Horses Service.
|
||||
*
|
||||
* This configuration ensures that Database.connect() is called properly
|
||||
* before any Exposed operations are performed.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("!test")
|
||||
class HorsesDatabaseConfiguration {
|
||||
|
||||
private val log = LoggerFactory.getLogger(HorsesDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initializing database schema for Horses Service...")
|
||||
|
||||
try {
|
||||
// Database connection is already initialized by the gateway
|
||||
// Only initialize the schema for this service
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(HorseTable)
|
||||
log.info("Horse database schema initialized successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize database schema", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun closeDatabase() {
|
||||
log.info("Closing database connection for Horses Service...")
|
||||
try {
|
||||
DatabaseFactory.close()
|
||||
log.info("Database connection closed successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error closing database connection", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-specific database configuration.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("test")
|
||||
class HorsesTestDatabaseConfiguration {
|
||||
|
||||
private val log = LoggerFactory.getLogger(HorsesTestDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeTestDatabase() {
|
||||
log.info("Initializing test database connection for Horses Service...")
|
||||
|
||||
try {
|
||||
// Use H2 in-memory database for tests
|
||||
val testConfig = DatabaseConfig(
|
||||
jdbcUrl = "jdbc:h2:mem:horses_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
username = "sa",
|
||||
password = "",
|
||||
driverClassName = "org.h2.Driver",
|
||||
maxPoolSize = 5,
|
||||
minPoolSize = 1,
|
||||
autoMigrate = true
|
||||
)
|
||||
|
||||
DatabaseFactory.init(testConfig)
|
||||
log.info("Test database connection initialized successfully")
|
||||
|
||||
// Initialize database schema for tests
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(HorseTable)
|
||||
log.info("Test horse database schema initialized successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize test database connection", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun closeTestDatabase() {
|
||||
log.info("Closing test database connection for Horses Service...")
|
||||
try {
|
||||
DatabaseFactory.close()
|
||||
log.info("Test database connection closed successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error closing test database connection", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package at.mocode.horses.service.integration
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
/**
|
||||
* Integration tests to demonstrate and verify transaction context issues with coroutines.
|
||||
*
|
||||
* This test class reproduces the race condition that can occur when multiple
|
||||
* coroutines perform database operations without proper transaction boundaries.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = [
|
||||
"spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop"
|
||||
])
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TransactionContextTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var horseRepository: HorseRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
runBlocking {
|
||||
// Clean up any existing test data
|
||||
// Note: This is a simplified cleanup - in a real scenario you'd have proper cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should demonstrate race condition without transaction boundaries`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting race condition test")
|
||||
|
||||
val lebensnummer = "TEST-RACE-001"
|
||||
val chipNummer = "CHIP-RACE-001"
|
||||
|
||||
// Create two horses with the same identifiers
|
||||
val horse1 = DomPferd(
|
||||
pferdeName = "Race Horse 1",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2020, 1, 1),
|
||||
lebensnummer = lebensnummer,
|
||||
chipNummer = chipNummer,
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
val horse2 = DomPferd(
|
||||
pferdeName = "Race Horse 2",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2020, 1, 2),
|
||||
lebensnummer = lebensnummer, // Same lebensnummer - should cause conflict
|
||||
chipNummer = chipNummer, // Same chipNummer - should cause conflict
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Created horses with duplicate identifiers")
|
||||
|
||||
// Simulate the use case logic: check uniqueness then save
|
||||
// This mimics what CreateHorseUseCase.execute() does without transactions
|
||||
suspend fun createHorseWithChecks(horse: DomPferd): Boolean {
|
||||
return try {
|
||||
// Check uniqueness constraints (like in checkUniquenessConstraints)
|
||||
val existsByLebensnummer = horse.lebensnummer?.let {
|
||||
horseRepository.existsByLebensnummer(it)
|
||||
} ?: false
|
||||
|
||||
val existsByChipNummer = horse.chipNummer?.let {
|
||||
horseRepository.existsByChipNummer(it)
|
||||
} ?: false
|
||||
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: existsByLebensnummer=$existsByLebensnummer, existsByChipNummer=$existsByChipNummer")
|
||||
|
||||
if (existsByLebensnummer || existsByChipNummer) {
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: Uniqueness check failed")
|
||||
false
|
||||
} else {
|
||||
// Save the horse (like in the use case)
|
||||
horseRepository.save(horse)
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: Saved successfully")
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: Exception during creation: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Launch two concurrent coroutines to create horses
|
||||
val results = listOf(
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting creation 1")
|
||||
createHorseWithChecks(horse1)
|
||||
},
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting creation 2")
|
||||
createHorseWithChecks(horse2)
|
||||
}
|
||||
).awaitAll()
|
||||
|
||||
println("[DEBUG_LOG] Both operations completed")
|
||||
println("[DEBUG_LOG] Result 1 success: ${results[0]}")
|
||||
println("[DEBUG_LOG] Result 2 success: ${results[1]}")
|
||||
|
||||
// In a properly transactional system, exactly one should succeed
|
||||
val successCount = results.count { it }
|
||||
val failureCount = results.count { !it }
|
||||
|
||||
println("[DEBUG_LOG] Success count: $successCount, Failure count: $failureCount")
|
||||
|
||||
// Check what actually got saved in the database
|
||||
val savedByLebensnummer = horseRepository.findByLebensnummer(lebensnummer)
|
||||
val savedByChipNummer = horseRepository.findByChipNummer(chipNummer)
|
||||
|
||||
println("[DEBUG_LOG] Found by lebensnummer: ${savedByLebensnummer?.pferdeName}")
|
||||
println("[DEBUG_LOG] Found by chipNummer: ${savedByChipNummer?.pferdeName}")
|
||||
|
||||
// This test demonstrates the issue - without transactions, both operations might succeed
|
||||
// due to race conditions, or the behavior might be unpredictable
|
||||
// The fix should ensure exactly one succeeds and one fails with a proper error
|
||||
assertTrue(successCount >= 1, "At least one operation should succeed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should demonstrate transaction context propagation issue`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting transaction context propagation test")
|
||||
|
||||
// This test will show that without @Transactional, each repository call
|
||||
// runs in its own transaction context, which can lead to inconsistencies
|
||||
|
||||
val horse = DomPferd(
|
||||
pferdeName = "Transaction Test Horse",
|
||||
geschlecht = PferdeGeschlechtE.HENGST,
|
||||
lebensnummer = "TRANS-TEST-001",
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Creating horse with repository operations")
|
||||
|
||||
// Simulate multiple repository operations that should be atomic
|
||||
val existsCheck = horseRepository.existsByLebensnummer("TRANS-TEST-001")
|
||||
println("[DEBUG_LOG] Exists check result: $existsCheck")
|
||||
|
||||
if (!existsCheck) {
|
||||
val savedHorse = horseRepository.save(horse)
|
||||
println("[DEBUG_LOG] Horse saved successfully: ${savedHorse.pferdeName}")
|
||||
assertNotNull(savedHorse)
|
||||
assertEquals("Transaction Test Horse", savedHorse.pferdeName)
|
||||
}
|
||||
|
||||
// The issue is that without @Transactional, if an exception occurs after
|
||||
// the uniqueness checks but before/during save, the database state
|
||||
// might be inconsistent
|
||||
val finalCheck = horseRepository.findByLebensnummer("TRANS-TEST-001")
|
||||
assertNotNull(finalCheck, "Horse should be saved in database")
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package at.mocode.horses.service.integration
|
||||
|
||||
import at.mocode.horses.application.usecase.TransactionalCreateHorseUseCase
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
/**
|
||||
* Integration tests to verify that transaction context issues with coroutines are resolved.
|
||||
*
|
||||
* This test class verifies that the transactional use cases properly handle
|
||||
* concurrent operations and maintain data consistency.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = [
|
||||
"spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop"
|
||||
])
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TransactionalContextTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var horseRepository: HorseRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var transactionalCreateHorseUseCase: TransactionalCreateHorseUseCase
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
runBlocking {
|
||||
// Clean up any existing test data
|
||||
// Note: This is a simplified cleanup - in a real scenario you'd have proper cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle race condition properly with transaction boundaries`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting transactional race condition test")
|
||||
|
||||
val lebensnummer = "TRANS-RACE-001"
|
||||
val chipNummer = "TRANS-CHIP-001"
|
||||
|
||||
// Create two identical horse creation requests
|
||||
val ownerId = uuid4()
|
||||
val request1 = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "Transactional Race Horse 1",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2020, 1, 1),
|
||||
lebensnummer = lebensnummer,
|
||||
chipNummer = chipNummer,
|
||||
besitzerId = ownerId
|
||||
)
|
||||
|
||||
val request2 = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "Transactional Race Horse 2",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2020, 1, 2),
|
||||
lebensnummer = lebensnummer, // Same lebensnummer - should cause conflict
|
||||
chipNummer = chipNummer, // Same chipNummer - should cause conflict
|
||||
besitzerId = ownerId
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Created requests with duplicate identifiers")
|
||||
|
||||
// Launch two concurrent coroutines to create horses using transactional use case
|
||||
val results = listOf(
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting transactional creation 1")
|
||||
transactionalCreateHorseUseCase.execute(request1)
|
||||
},
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting transactional creation 2")
|
||||
transactionalCreateHorseUseCase.execute(request2)
|
||||
}
|
||||
).awaitAll()
|
||||
|
||||
println("[DEBUG_LOG] Both transactional operations completed")
|
||||
println("[DEBUG_LOG] Result 1 success: ${results[0].success}")
|
||||
println("[DEBUG_LOG] Result 2 success: ${results[1].success}")
|
||||
|
||||
// With proper transaction boundaries, exactly one should succeed
|
||||
val successCount = results.count { it.success }
|
||||
val failureCount = results.count { !it.success }
|
||||
|
||||
println("[DEBUG_LOG] Success count: $successCount, Failure count: $failureCount")
|
||||
|
||||
// Verify that exactly one operation succeeded and one failed
|
||||
assertEquals(1, successCount, "Exactly one operation should succeed with proper transactions")
|
||||
assertEquals(1, failureCount, "Exactly one operation should fail with proper transactions")
|
||||
|
||||
// Check what actually got saved in the database
|
||||
val savedByLebensnummer = horseRepository.findByLebensnummer(lebensnummer)
|
||||
val savedByChipNummer = horseRepository.findByChipNummer(chipNummer)
|
||||
|
||||
println("[DEBUG_LOG] Found by lebensnummer: ${savedByLebensnummer?.pferdeName}")
|
||||
println("[DEBUG_LOG] Found by chipNummer: ${savedByChipNummer?.pferdeName}")
|
||||
|
||||
// Verify that exactly one horse was saved
|
||||
assertNotNull(savedByLebensnummer, "One horse should be saved with the lebensnummer")
|
||||
assertNotNull(savedByChipNummer, "One horse should be saved with the chipNummer")
|
||||
assertEquals(savedByLebensnummer?.pferdId, savedByChipNummer?.pferdId, "Both queries should return the same horse")
|
||||
|
||||
// Verify that the failed operation returned proper error
|
||||
val failedResult = results.find { !it.success }
|
||||
assertNotNull(failedResult, "There should be one failed result")
|
||||
assertEquals("UNIQUENESS_ERROR", failedResult?.error?.code, "Failed operation should return uniqueness error")
|
||||
|
||||
println("[DEBUG_LOG] Transactional test completed successfully - race condition properly handled")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should maintain transaction consistency on validation failure`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting transaction consistency test")
|
||||
|
||||
// Create a request with invalid data that will fail validation
|
||||
val request = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "", // Empty name should fail validation
|
||||
geschlecht = PferdeGeschlechtE.HENGST,
|
||||
lebensnummer = "VALIDATION-TEST-001",
|
||||
stockmass = 300, // Invalid height should fail validation
|
||||
besitzerId = uuid4() // Add owner to pass basic validation
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Executing transactional create with invalid data")
|
||||
val result = transactionalCreateHorseUseCase.execute(request)
|
||||
|
||||
println("[DEBUG_LOG] Creation result: success=${result.success}")
|
||||
|
||||
// Verify that the operation failed due to validation
|
||||
assertTrue(!result.success, "Operation should fail due to validation errors")
|
||||
assertEquals("VALIDATION_ERROR", result.error?.code, "Should return validation error")
|
||||
|
||||
// Verify that no horse was saved in the database
|
||||
val savedHorse = horseRepository.findByLebensnummer("VALIDATION-TEST-001")
|
||||
assertTrue(savedHorse == null, "No horse should be saved when validation fails")
|
||||
|
||||
println("[DEBUG_LOG] Transaction consistency test completed - no data saved on validation failure")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should successfully create horse with valid data in transaction`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting successful transactional creation test")
|
||||
|
||||
val request = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "Successful Transaction Horse",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2021, 6, 15),
|
||||
lebensnummer = "SUCCESS-TEST-001",
|
||||
chipNummer = "SUCCESS-CHIP-001",
|
||||
rasse = "Warmblut",
|
||||
stockmass = 165,
|
||||
besitzerId = uuid4() // Add required owner
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Executing transactional create with valid data")
|
||||
val result = transactionalCreateHorseUseCase.execute(request)
|
||||
|
||||
println("[DEBUG_LOG] Creation result: success=${result.success}")
|
||||
|
||||
// Verify that the operation succeeded
|
||||
assertTrue(result.success, "Operation should succeed with valid data")
|
||||
assertNotNull(result.data, "Result should contain the created horse")
|
||||
assertEquals("Successful Transaction Horse", result.data?.pferdeName, "Horse name should match")
|
||||
|
||||
// Verify that the horse was saved in the database
|
||||
val savedHorse = horseRepository.findByLebensnummer("SUCCESS-TEST-001")
|
||||
assertNotNull(savedHorse, "Horse should be saved in database")
|
||||
assertEquals("Successful Transaction Horse", savedHorse.pferdeName, "Saved horse name should match")
|
||||
assertEquals("SUCCESS-CHIP-001", savedHorse.chipNummer, "Saved horse chip number should match")
|
||||
|
||||
println("[DEBUG_LOG] Successful transactional creation test completed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user