Birthday Greetings Kata part 3
See the previous parts of this series for more information.
Putting the parts together.
In the first part, I made the EmployeeRepository
interface…
interface EmployeeRepository {
fun getAll(): Sequence<Employee>
}
The original problem statement says…
Problem: write a program that
Loads a set of employee records from a flat file Sends a greetings email to all employees whose birthday is today
In the previous parts, I implemented an interface for reading a set of employee records from a flat file, and an interface for sending emails.
I need to join these two together…
First off, starting with stub implementations, these could easily use Mockito or Mockk, but I have a personal preference for stubbing over mocking.
class StubEmployeeRepository(val employees: Sequence<Employee>) : EmployeeRepository {
override fun getAll(): Sequence<Employee> = employees
}
class StubEmailSender() : EmailSender {
val sent = mutableListOf<Map<String, String>>()
override fun send(from: String, to: String, subject: String, body: String) {
sent.add(mapOf(
"to" to to,
"from" to from,
"subject" to subject,
"body" to body
))
}
fun reset() {
sent.clear()
}
}
The only real design decisions here are how to record the sent emails, and how to reset the stub.
The actual tests are obvious enough.
class BirthdayGreetingsServiceTest {
val stubEmailSender = StubEmailSender()
@Before
fun resetEmails() {
stubEmailSender.reset()
}
@Test
fun `when nobody has a matching birthday no emails are sent`() {
val service = makeService(makeEmployees(LocalDate.of(2000, 10, 8)))
service.sendBirthdayGreetings(LocalDate.of(2019, 5, 11))
assertEquals(0, stubEmailSender.sent.size)
}
@Test
fun `when one of the employees has a birthday on the provided date, an email is sent`(){
val date = LocalDate.of(2019, 10, 8)
val service = makeService(makeEmployees(date))
service.sendBirthdayGreetings(date)
assertEquals(1, stubEmailSender.sent.size)
val email = stubEmailSender.sent.first()
assertEquals(email["to"], "test0@example.com")
assertEquals(email["from"], "from@example.com")
assertEquals(email["subject"], "Happy Birthday!")
assertEquals(email["body"], "Happy Birthday, dear test0")
}
@Test
fun `when more than one employee has a birthday, multiple emails are sent`(){
val date = LocalDate.of(2019, 10, 8)
val service = makeService(makeEmployees(date, date))
service.sendBirthdayGreetings(date)
assertEquals(2, stubEmailSender.sent.size)
}
fun makeService(employees: Sequence<Employee>): BirthdayGreetingsService {
val stubRepository = StubEmployeeRepository(employees.asSequence())
return BirthdayGreetingsService("from@example.com", stubRepository, stubEmailSender)
}
fun makeEmployees(vararg dates: LocalDate): Sequence<Employee> {
return dates.asSequence().mapIndexed { i, date ->
Employee("test", "test$i", date, "test$i@example.com")
}
}
}
The tests don’t test failure modes, ideally there would be tests for when sending an email fails, this would involve a design decision, should the code stop, or log out that it failed to send an email and continue?
The implementation is short and sweet.
class BirthdayGreetingsService(val from: String, val employeeRepository: EmployeeRepository, val emailSender: EmailSender) {
fun sendBirthdayGreetings(date: LocalDate) {
employeeRepository
.getAll()
.filter { e -> e.isBirthday(date) }
.forEach(this::sendEmail)
}
fun sendEmail(employee: Employee) {
val body = "Happy Birthday, dear %NAME%".replace("%NAME%", employee.firstName)
emailSender.send(from, employee.email, "Happy Birthday!", body)
}
}
The use of a Sequence
makes sense here, the code reads all the entries from the file and filters them before sending the matching employees to the sendEmail
function, where there’s a trivial templating method used, and then the resulting email body is passed to the emailSender.
In our main function it’s easy enough to join up the parts:
fun main(args: Array<String>) {
val emailSender = SmtpEmailSender(
System.getenv("BIRTHDAY_SMTP_HOST"),
System.getenv("BIRTHDAY_SMTP_PORT").toInt()
)
val employeeRepository = FileEmployeeRepository(args[0])
val greetingService = BirthdayGreetingsService(
System.getenv("BIRTHDAY_FROM_ADDRESS"),
employeeRepository,
emailSender)
greetingService.sendBirthdayGreetings(LocalDate.now())
}
Overall this is a simple Kata to implement, but it opens some interesting ideas up.
- Could the current employee repository be replaced by a SQL database query?
- Could the current employee repository be replaced by an HTTP API call to an employee data service?
- Could the email sending be changed to use Google’s Email API https://developers.google.com/gmail/api/guides/sending ?
- Could the templating be replaced with something like Thymeleaf?
- When there’s a failure sending, how could we recover without sending the emails again?
Does this implementation implement the SOLID principles?