import junit.framework.TestCase.assertEquals
import org.junit.After
import org.junit.Test
import java.time.LocalDateTime
import java.util.*
class StudentService(
private val studentRepository: StudentRepository,
private val studentFactory: StudentFactory,
private val logger: Logger,
) {
fun addStudent(addStudentRequest: AddStudentRequest):Student?=
addStudentRequest
.let { studentFactory.produceStudent(it) }
// or .let(studentFactory::produceStudent)
?.also { studentRepository.addStudent(it) }
// or ?.also(studentRepository::addStudent)
fun getStudent(studentId: String): ExposedStudent? =
studentRepository
.getStudent(studentId)
?.also { logger.log("Student found: $it") }
?.let { studentFactory.produceExposed(it) }
// or ?.let(studentFactory::produceExposed)
fun getStudents(semester: String): List<ExposedStudent> =
produceGetStudentsRequest(semester)
.let { studentRepository.getStudents(it) }
// or .let(studentRepository::getStudents)
.also {logger.log("${it.size} students in $semester")}
.map { studentFactory.produceExposed(it) }
// or .map(studentFactory::produceExposed)
private fun produceGetStudentsRequest(
semester: String,
) = GetStudentsRequest().apply {
minResult = 3.0
expectedSemester = semester
}
}
class StudentFactory(
private val timeProvider: TimeProvider,
private val uuidGenerator: UuidGenerator,
) {
fun produceExposed(student: Student): ExposedStudent = ExposedStudent(
name = student.name,
)
fun produceStudent(student: AddStudentRequest): Student? = Student(
name = student.name,
semester = student.semester,
result = student.result,
creationTime = timeProvider.now(),
id = uuidGenerator.generate(),
)
}
class StudentServiceTest {
private val logger = FakeLogger()
private val userRepository = FakeStudentRepository()
private val timeProvider = FakeTimeProvider()
private val uuidGenerator = FakeUuidGenerator()
private val studentFactory = StudentFactory(timeProvider, uuidGenerator)
private val studentService = StudentService(userRepository, studentFactory, logger)
@After
fun cleanup() {
logger.cleanup()
userRepository.cleanup()
timeProvider.cleanup()
uuidGenerator.cleanup()
}
@Test
fun `should add student`() {
// Given
val time1 = LocalDateTime.of(2023, 5, 6, 7, 8)
timeProvider.now = time1
val studentId = "1234"
uuidGenerator.constantUuid = studentId
val addStudentRequest = AddStudentRequest(
name = "Marc",
semester = "1",
result = 4.0,
)
// When
val result = studentService.addStudent(addStudentRequest)
// Then
val expected = Student(
name = "Marc",
semester = "1",
result = 4.0,
creationTime = time1,
id = studentId,
)
assertEquals(expected, result)
assertEquals(expected, userRepository.getStudent(studentId))
}
@Test
fun `should get student`() {
// Given
val studentId = "1234"
uuidGenerator.constantUuid = studentId
val addStudentRequest = AddStudentRequest(
name = "Marc",
semester = "1",
result = 4.0,
)
studentService.addStudent(addStudentRequest)
// When
val result = studentService.getStudent(studentId)
// Then
val expected = ExposedStudent(
name = "Marc",
)
assertEquals(expected, result)
}
@Test
fun `should log when getting student`() {
// Given
val time1 = LocalDateTime.of(2023, 5, 6, 7, 8)
timeProvider.now = time1
val studentId = "1234"
uuidGenerator.constantUuid = studentId
val addStudentRequest = AddStudentRequest(
name = "Marc",
semester = "1",
result = 4.0,
)
studentService.addStudent(addStudentRequest)
// When
val result = studentService.getStudent(studentId)
// Then
val expected = listOf(
"Student found: Student(id=1234, name=Marc, creationTime=2023-05-06T07:08, semester=1, result=4.0)"
)
assertEquals(expected, logger.messages)
}
@Test
fun `should return null when getting non-existing student`() {
// Given
val studentId = "1234"
uuidGenerator.constantUuid = studentId
val addStudentRequest = AddStudentRequest(
name = "Marc",
semester = "1",
result = 4.0,
)
studentService.addStudent(addStudentRequest)
// When
val result = studentService.getStudent("1")
// Then
assertEquals(null, result)
}
@Test
fun `should get students list and log its size`() {
// Given
listOf(
AddStudentRequest("Alex", "Semester1", 4.0),
AddStudentRequest("Bob", "Semester2", 4.0),
AddStudentRequest("Celine", "Semester1", 2.0),
AddStudentRequest("Diana", "Semester1", 5.0),
AddStudentRequest("Ester", "Semester2", 3.0),
AddStudentRequest("Fiona", "Semester1", 4.0),
AddStudentRequest("Gina", "Semester2", 2.9),
AddStudentRequest("Helen", "Semester1", 3.0),
AddStudentRequest("Irene", "Semester1", 4.0),
AddStudentRequest("Jack", "Semester2", 4.0),
AddStudentRequest("Kate", "Semester1", 5.0),
AddStudentRequest("Linda", "Semester1", 3.0),
AddStudentRequest("Marc", "Semester1", 2.0),
).forEach { studentService.addStudent(it) }
// When
val result1 = studentService.getStudents("Semester1")
// Then should receive students from the same semester and with result >= 3.0
assertEquals(
listOf(
ExposedStudent("Alex"),
ExposedStudent("Diana"),
ExposedStudent("Fiona"),
ExposedStudent("Helen"),
ExposedStudent("Irene"),
ExposedStudent("Kate"),
ExposedStudent("Linda"),
), result1
)
assertEquals(listOf("7 students in Semester1"), logger.messages)
// Given
logger.cleanup()
// When
val result = studentService.getStudents("Semester2")
// Then should receive students from the semester and with result >= 3.0
assertEquals(
listOf(
ExposedStudent("Bob"),
ExposedStudent("Ester"),
ExposedStudent("Jack"),
), result
)
assertEquals(listOf("3 students in Semester2"), logger.messages)
}
}
interface StudentRepository {
fun addStudent(student: Student)
fun getStudent(id: String): Student?
fun getStudents(request: GetStudentsRequest): List<Student>
}
class GetStudentsRequest {
var minResult: Double? = null
var expectedSemester: String? = null
}
data class AddStudentRequest(
val name: String,
val semester: String,
val result: Double,
)
data class Student(
val id: String,
val name: String,
val creationTime: LocalDateTime,
val semester: String,
val result: Double,
)
data class ExposedStudent(
val name: String,
)
interface Logger {
fun log(message: String)
}
interface TimeProvider {
fun now(): LocalDateTime
}
class RealTimeProvider : TimeProvider {
override fun now(): LocalDateTime = LocalDateTime.now()
}
class FakeTimeProvider : TimeProvider {
var now: LocalDateTime = INIT_TIME
fun cleanup() {
now = INIT_TIME
}
override fun now(): LocalDateTime = now
companion object {
val INIT_TIME = LocalDateTime.of(2020, 1, 1, 0, 0)
}
}
interface UuidGenerator {
fun generate(): String
}
class RealUuidGenerator : UuidGenerator {
override fun generate(): String = UUID.randomUUID().toString()
}
class FakeUuidGenerator : UuidGenerator {
var constantUuid: String? = null
private var counter: Int = 0
fun cleanup() {
counter = 0
constantUuid = null
}
override fun generate(): String = constantUuid
?: (counter++).toString()
}
class FakeLogger : Logger {
var messages: List<String> = emptyList()
fun cleanup() {
messages = emptyList()
}
override fun log(message: String) {
this.messages += message
}
}
class FakeStudentRepository() : StudentRepository {
private var students: List<Student> = emptyList()
fun cleanup() {
students = emptyList()
}
override fun addStudent(student: Student) {
students += student
}
override fun getStudent(id: String): Student? = students
.find { it.id == id }
override fun getStudents(request: GetStudentsRequest): List<Student> =
students
.filter { request.expectedSemester == null || it.semester == request.expectedSemester }
.filter { request.minResult == null || it.result >= request.minResult!! }
}