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?