Announcements and Double Agents

Smalltalk had one of the first implementations of the
Observer design pattern.
Over the years, Visualworks Smalltalk has had several different implementations of
this pattern, from the original #changed/#update approach, to
#triggerEvent:, to
Announcements.

I’ve migrated almost all of my code to Announcements over the past few
years. When I recently started
using mocks and stubs
in my unit tests and wrote
DoubleAgents
to help with that, I quickly ran into a dilemma: How do I test a
complex object that announces?

For a simple announcer, I’ll just test it directly with no mocking and
stubbing. Or, I might inject a direct instance of Announcer if the
announcements are all I need to test.

For a complex collaborator that isn’t an announcer, I’ll use
DoubleAgents and make a test double for the collaborator. I can stub
and mock what I need to in order to write my tests.

But what if the code I’m testing needs to do both? I recently had a
case where I could easily inject the collaborator, but the code under
test needed to send a configuration message to that collaborator
immediately. The collaborator was also an Announcer, and I needed
to test that my object responded appropriately to those announcements.

First, I’ll sketch out the code I’d like to be able to test.
Normally, I use TDD and write the tests first, but for instructional
purposes, let’s look at the code first.

Code To Be Tested

ClassUnderTest>>collaborator:aCollaborator

collaborator:=aCollaborator.

collaboratorconfigureFrom:self.

collaboratorwhen:SomeAnnouncementsend:#interestingEventto:self

ClassUnderTest>>interestingEvent

"respond to event - this method is considered private"

Per Sandi Metz, let’s look at the
messages involved in testing this code:

#collaborator: is an incoming public method on the class under
test; it should be tested.

#configureFrom: is an outgoing command message from the class
under test; we need to test that it gets sent properly, but we don’t
need to test for its effects here. We do that when we test the
collaborator.

#when:send:to: is an outgoing command message from the class under
test; we need to test that it gets sent properly.

#interestingEvent is an incoming method. I consider it to be a
private method, because ClassUnderTest is the only place the name
of the method will appear. However, it could also be considered to
be a public incoming message since it is ultimately sent by the
announcement framework.

One way to test this would be to test the individual methods
separately:

Testing the Methods Separately

"In a DoubleAgentTestCase subclass"

setUp

supersetUp.

collaborator:=CollaboratordoubleAgent.

collaboratorstub:#configureFrom:;

stub:#when:send:to:.

objectUnderTest:=ClassUnderTestnew.

configuresCollaborator

<test>

collaboratorexpect:#configureFrom:with:objectUnderTest.

objectUnderTestcollaborator:collaborator

subscribesToCollaborator

<test>

collaboratorexpect:#when:send:to:

with:SomeAnnouncement

with:#interestingEvent

with:objectUnderTest.

objectUnderTestcollaborator:collaborator

respondsToInterestingEvent

<test>

objectUnderTestinterestingEvent.

"assert something about the effects of #interestingEvent"

This approach works, and tests all of the messages we care about, but
it seems like some private implementation details are leaking out of
the class and into the tests, so I’m not completely happy with it. If
I decide to change how I subscribe to the announcement, or how I
choose to implement the response, the test will have to change.

To make the test less fragile, I’d prefer to test the net effect of
the announcement having occurred rather than the mechanism by which
the effect occurs.

Given that, what I chose to do instead is to
extend DoubleAgents
to allow a DoubleAgent to #beAnnouncer. Using this new feature, I
can instead write the tests this way:

Testing the Methods Separately

"In a DoubleAgentTestCase subclass"

setUp

supersetUp.

collaborator:=CollaboratordoubleAgentbeAnnouncer.

collaboratorstub:#configureFrom:.

objectUnderTest:=ClassUnderTestnew.

configuresCollaborator

<test>

collaboratorexpect:#configureFrom:with:objectUnderTest.

objectUnderTestcollaborator:collaborator

respondsWhenCollaboratorAnnounces

<test>

objectUnderTestcollaborator:collaborator.

collaboratorannounce:SomeAnnouncement.

"assert something about the response to SomeAnnouncement"

Using this approach, the tests don’t need to know any details about
how ClassUnderTest chooses to respond to SomeAnnouncement.
Instead, it only cares about the externally visible effects of the
response. This seems to me to be more maintainable and future-proof.