Skip to content

Instantly share code, notes, and snippets.

@jakzal
Created August 24, 2024 11:24
Show Gist options
  • Save jakzal/bc6f58aea0573500a9e0849e168a30ee to your computer and use it in GitHub Desktop.
Save jakzal/bc6f58aea0573500a9e0849e168a30ee to your computer and use it in GitHub Desktop.
Using Testcontainers with Kotlin Exposed
package payment.exposed
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.BeforeEach
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName
import payment.Payment
import payment.PaymentRepositoryContract
@Testcontainers
class ExposedPaymentRepositoryTest :
PaymentRepositoryContract(ExposedPaymentRepository(postgresql.connection)) {
companion object {
@Container
private val postgresql = PostgreSQLContainer(DockerImageName.parse("postgres:16-alpine"))
}
@BeforeEach
fun createSchema(): Unit = transaction(postgresql.connection) {
addLogger(StdOutSqlLogger)
SchemaUtils.create(Payments)
}
override fun givenPayments(vararg payments: Payment): Unit = transaction(postgresql.connection) {
addLogger(StdOutSqlLogger)
payments.forEach { payment ->
Payments.insert {
it[userId] = payment.userId.value
it[amount] = payment.amount.value
it[category] = payment.category.name
it[completedAt] = payment.completedAt
it[description] = payment.description
}
}
}
}
private val <SELF : PostgreSQLContainer<SELF>> PostgreSQLContainer<SELF>.connection
get() = Database.connect(url = jdbcUrl, user = username, password = password)
package payment.inmemory
import payment.Payment
import payment.PaymentRepositoryContract
class InMemoryPaymentRepositoryTest : PaymentRepositoryContract(InMemoryPaymentRepository(persistedPayments)) {
companion object {
private val persistedPayments: MutableList<Payment> = mutableListOf()
}
override fun givenPayments(vararg payments: Payment) {
persistedPayments.addAll(listOf(*payments))
}
}
package payment
import kotlinx.datetime.LocalDateTime
import java.time.Month.APRIL
import java.time.Month.JULY
import java.time.YearMonth
import kotlin.test.Test
abstract class PaymentRepositoryContract(private val repository: PaymentRepository) {
@Test
fun `it returns an empty list if no payments were found for the user`() {
givenPayments(
payment("USR-1", "2023-03-31T23:59"),
payment("USR-1", "2023-04-01T00:00"),
payment("USR-1", "2023-04-30T23:59"),
payment("USR-1", "2023-05-01T11:10"),
)
val payments = repository.findTwoMonthsOfPayments(
UserAndMonthCriteria(UserId("USR-2"), YearMonth.of(2023, APRIL))
)
assert(payments == emptyList<Payment>())
}
@Test
fun `it returns an empty list if no payments were found for the month`() {
givenPayments(
payment("USR-3", "2023-03-31T23:59"),
payment("USR-3", "2023-04-01T00:00"),
payment("USR-3", "2023-04-30T23:59"),
payment("USR-3", "2023-05-01T11:10"),
)
val payments = repository.findTwoMonthsOfPayments(
UserAndMonthCriteria(UserId("USR-3"), YearMonth.of(2023, JULY))
)
assert(payments == emptyList<Payment>())
}
@Test
fun `it returns user payments found for the requested month and one month before`() {
givenPayments(
payment("USR-4", "2023-02-28T23:59"),
payment("USR-4", "2023-03-01T00:00"),
payment("USR-4", "2023-03-31T23:59"),
payment("USR-4", "2023-04-01T00:00"),
payment("USR-4", "2023-04-30T23:59"),
payment("USR-4", "2023-05-01T11:10"),
)
val payments = repository.findTwoMonthsOfPayments(
UserAndMonthCriteria(UserId("USR-4"), YearMonth.of(2023, APRIL))
)
assert(
payments == listOf(
payment("USR-4", "2023-03-01T00:00"),
payment("USR-4", "2023-03-31T23:59"),
payment("USR-4", "2023-04-01T00:00"),
payment("USR-4", "2023-04-30T23:59")
)
)
}
private fun payment(userId: String, completedAt: String) = Payment(
UserId(userId),
Amount(2599),
LocalDateTime.parse(completedAt),
Category("Food"),
"Payment for Food"
)
protected abstract fun givenPayments(vararg payments: Payment)
}
package payment
import kotlinx.datetime.LocalDateTime
data class Payment(
val userId: UserId,
val amount: Amount,
val completedAt: LocalDateTime,
val category: Category,
val description: String
)
@JvmInline
value class Amount(val value: Int)
@JvmInline
value class Category(val name: String)
@JvmInline
value class UserId(val value: String)
package payment
import java.time.YearMonth
interface PaymentRepository {
fun findTwoMonthsOfPayments(criteria: UserAndMonthCriteria): List<Payment>
}
data class UserAndMonthCriteria(val userId: UserId, val month: YearMonth)
package payment.exposed
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
object Payments : Table() {
private val id = integer("id").autoIncrement()
override val primaryKey = PrimaryKey(id)
val userId = text("user_id").index()
val amount = integer("amount")
val completedAt = datetime("completed_at").index()
val category = text("category").index()
val description = text("description")
}
package payment.exposed
import kotlinx.datetime.toKotlinLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import payment.*
class ExposedPaymentRepository(private val connection: Database) : PaymentRepository {
override fun findTwoMonthsOfPayments(criteria: UserAndMonthCriteria): List<Payment> = transaction(connection) {
Payments.selectAll()
.where { Payments.userId eq criteria.userId.value }
.andWhere {
Payments.completedAt greaterEq criteria.month.minusMonths(1).atDay(1).atStartOfDay()
.toKotlinLocalDateTime()
}
.andWhere {
Payments.completedAt lessEq criteria.month.atEndOfMonth().atTime(23, 59).toKotlinLocalDateTime()
}
.orderBy(Payments.completedAt, SortOrder.ASC)
.map { it.toPayment() }
}
private fun ResultRow.toPayment() = Payment(
UserId(this[Payments.userId]),
Amount(this[Payments.amount]),
this[Payments.completedAt],
Category(this[Payments.category]),
this[Payments.description]
)
}
package payment.inmemory
import kotlinx.datetime.toKotlinLocalDateTime
import payment.Payment
import payment.PaymentRepository
import payment.UserAndMonthCriteria
class InMemoryPaymentRepository(private val payments: List<Payment>) : PaymentRepository {
override fun findTwoMonthsOfPayments(criteria: UserAndMonthCriteria): List<Payment> = payments
.filter { it.userId == criteria.userId }
.filter {
it.completedAt >= criteria.month.minusMonths(1).atDay(1).atStartOfDay()
.toKotlinLocalDateTime()
}
.filter { it.completedAt <= criteria.month.atEndOfMonth().atTime(23, 59).toKotlinLocalDateTime() }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment