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.
collaborator configureFrom: self.
collaborator when: SomeAnnouncement send: #interestingEvent to: 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
super setUp.
collaborator := Collaborator doubleAgent.
collaborator stub: #configureFrom:;
stub: #when:send:to:.
objectUnderTest := ClassUnderTest new.
configuresCollaborator
<test>
collaborator expect: #configureFrom: with: objectUnderTest.
objectUnderTest collaborator: collaborator
subscribesToCollaborator
<test>
collaborator expect: #when:send:to:
with: SomeAnnouncement
with: #interestingEvent
with: objectUnderTest.
objectUnderTest collaborator: collaborator
respondsToInterestingEvent
<test>
objectUnderTest interestingEvent.
"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
super setUp.
collaborator := Collaborator doubleAgent beAnnouncer.
collaborator stub: #configureFrom:.
objectUnderTest := ClassUnderTest new.
configuresCollaborator
<test>
collaborator expect: #configureFrom: with: objectUnderTest.
objectUnderTest collaborator: collaborator
respondsWhenCollaboratorAnnounces
<test>
objectUnderTest collaborator: collaborator.
collaborator announce: 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.

I added #beAnnouncer in DoubleAgents version 9.