Smalltalk Birthday Kata Part 2
At the end of Part 1 I had a working Employee
class.
On with the main logic…
TestCase subclass: BirthdayServiceTestCase [
<comment: nil>
<category: 'BirthdayKata-Tests'>
testNoEmployeesWithBirthday [
<category: 'tests'>
| repository mailSender kata |
repository := InMemoryRepository new.
mailSender := MockEmailSender new.
kata := BirthdayService new repository: repository; mailSender: mailSender.
kata sendGreetings: (Date newDay: 14 month: #Feb year: 2019).
self assert: (mailSender count) equals: 0.
]
]
Nice new shiny test, I want to provide the repository and SMTP sender, and execute the code:
$ gst-sunit -v -f BirthdayKataTest.st BirthdayServiceTestCase
Loading BirthdayKataTest.st
BirthdayServiceTestCase>>##testNoEmployeesWithBirthday did not understand #new
TestVerboseLog>>logError: (SUnit.star#VFS.ZipFile/SUnit.st:608)
BirthdayServiceTestCase(TestCase)>>logError: (SUnit.star#VFS.ZipFile/SUnit.st:870)
And of course, it goes Boom!
There are at least three classes I need to define.
InMemoryRepository
needs to provide employees on-demand.MockEmailSender
needs to accept emails for delivery, I added acount
method to allow me to find out how many emails it has sent.BirthdayService
is the main driver, it uses the repository and email sender, to send emails to employees when it’s their birthday.
Object subclass: InMemoryRepository [
<comment: 'I am just a test repository, I provide employees on demand.'>
<category: 'BirthdayKata-Tests'>
]
Object subclass: MockEmailSender [
<comment: 'I am just a mock email sender, I record emails I should send.'>
<category: 'BirthdayKata-Tests'>
count [
"I return the number of emails sent."
^0
]
]
These are simplest possible versions that I can write to allow me to get that test passing.
Finally, my empty implementation of the BirthdayService driver.
Object subclass: BirthdayService [
<comment: 'I am the main driver for the BirthdayKata.'>
<category: 'BirthdayKata'>
| employeeRepository mailSender |
employeeRepository: anEmployeeRepository [
"Set the value of the receiver's 'employeeRepository' instance variable to
the argument anEmployeeRepository."
<category: 'accessing'>
employeeRepository := anEmployeeRepository.
]
mailSender: aMailSender [
"Set the value of the receiver's 'mailSender' instance variable to the
argument aMailSender."
<category: 'accessing'>
mailSender := aMailSender.
]
sendGreetings: aDate [
"Send emails to all the employees whose birthday is on aDate."
<category: 'accessing'>
]
]
Executing the tests…at the moment, I’m keeping my BirthdayService class in the test file for ease of writing.
$ gst-sunit -v -f BirthdayKataTest.st BirthdayServiceTestCase
Loading BirthdayKataTest.st
BirthdayServiceTestCase>>##testNoEmployeesWithBirthday .
1 run, 1 passes
Ok, that doesn’t say much…
Implementing basic functionality.
At this point, I got going with a simple implementation of my logic.
I’m not going to reproduce the entire code but some parts are worth illustrating:
This is post-initialization logic.
InMemoryRepository class >> new [
"Answer a new instance of the receiver."
<category: 'basic'>
^self basicNew initialize.
]
initialize [
"Initialize the receiver's state"
<category: 'private-initialization'>
employees := Set new.
]
To store the set of dummy employees for the InMemoryRepository
, I added a
Set
to store them in, to initialize the set this is the code, there’s no
special method for initialization, and there are a variety of ways of doing
this, with initialize
being the common name for this.
Notice that new
is a class method, and it delegates to basicNew
before
calling my initialize
method with the result of basicNew
as the receiver.
UPDATE: I see that the unstable release (from nearly 4 years ago) of GNU Smalltalk
has fixed this issue http://smalltalk.gnu.org/news/gnu-smalltalk-3-2-91
The >>#initialize function will now be called when creating an object
.
This is very similar to Ruby’s initialize
mechanism (or constructors in pretty
much every other OO language).
I added the following tests:
- testNoEmployeesWithBirthday
- testOneEmployeeWithBirthday
- testOneEmployeeWithNonMatchingBirthday
- testMultipleEmployeesWithMatchingBirthday
I don’t need to reproduce them all here but they look fairly similar to this.
testOneEmployeeWithBirthday [
<category: 'tests'>
| repository mailSender kata |
repository := InMemoryRepository new.
repository addEmployee: (Employee new birthDate: (Date newDay: 14 month: #Feb year: 1990)).
mailSender := MockEmailSender new.
kata := BirthdayService new employeeRepository: repository; mailSender: mailSender.
kata sendGreetings: (Date newDay: 14 month: #Feb year: 2019).
self assert: (mailSender count) equals: 1.
]
I added a mechanism for adding employees to that Set
that was added, this
simply delegates to add:
on the employees set.
And in the MockEmailSender
I added a count
variable.
sendEmail: anEmail [
"Send an email to an employee."
<category: 'sending-email'>
count := count + 1
]
initialize [
"Initialize the receiver's state"
<category: 'private-initialization'>
count := 0
]
This is the simplest thing that works, all it does is increment the number of
emails that have been sent, but before we can do much more, I need to revisit
my Employee
class, and add some new properties, and do a tiny bit of
refactoring.
Finally…the core business logic…
sendGreetings: aDate [
"Send emails to all the employees whose birthday is on aDate."
<category: 'greetings'>
(self employeesWithBirthday: aDate)
do: [:each | mailSender sendEmail: 'testing']
]
employeesWithBirthday: aDate [
"Answer the employees whose birthday it is."
<category: 'private'>
^employeeRepository
listEmployees select: [:each | each isBirthday: aDate]
]
employeesWithBirthday:
returns a filtered set of employees, and
sendGreetings:
gets the filtered set, and delegates to the mailSender
.
Small bit of refactoring
testNoEmployeesWithBirthday [
<category: 'tests'>
| repository mailSender kata |
repository := InMemoryRepository new.
I was instantiating a new InMemoryRepository
and MockEmailSender
in each
test, but, this is sUnit, so…
setUp [
<category: 'setup'>
super setUp.
repository := InMemoryRepository new.
mailSender := MockSMTPSender new.
]
By overriding setUp
I can easily reduce the repetition.
So, in the next part, back to the Employee
class, employees are more than just a
birthDate y’know.