In which we add another feature to our Sniper and let it win an auction. We introduce the concept of state to the Sniper which we test by listening to its callbacks. We find that even this early, one of our refactorings has paid off.
We have a Sniper that can respond to price changes by bidding more, but it doesn’t yet know when it’s successful. Our next feature on the to-do list is to win an auction. This involves an extra state transition, as you can see in Figure 14.1:
To represent this, we add an end-to-end test based on sniperMakesAHigherBidButLoses()
with a different conclusion—sniperWinsAnAuctionByBiddingHigher()
. Here’s the test, with the new features highlighted:
In our test infrastructure we add the two methods to check that the user interface shows the two new states to the ApplicationRunner
.
This generates a new failure message:
Now we know where we’re going, we can implement the feature.
The application knows that the Sniper is winning if it’s the bidder for the last price that the auction accepted. We have to decide where to put that logic. Looking again at Figure 13.5 on page 134, one choice would be that the translator could pass the bidder through to the Sniper and let the Sniper decide. That would mean that the Sniper would have to know something about how bidders are identified by the auction, with a risk of pulling in XMPP details that we’ve been careful to keep separate. To decide whether it’s winning, the only thing the Sniper needs to know when a price arrives is, did this price come from me? This is a choice, not an identifier, so we’ll represent it with an enumeration PriceSource
which we include in AuctionEventListener
.1
1. Some developers we know have an allergic reaction to nested types. In Java, we use them as a form of fine-grained scoping. In this case, PriceSource
is always used together with AuctionEventListener
, so it makes sense to bind the two together.
Incidentally, PriceSource
is an example of a value type. We want code that describes the domain of Sniping—not, say, a boolean which we would have to interpret every time we read it; there’s more discussion in “Value Types” (page 59).
We take the view that determining whether this is our price or not is part of the translator’s role. We extend currentPrice()
with a new parameter and change the translator’s unit tests; note that we change the name of the existing test to include the extra feature. We also take the opportunity to pass the Sniper identifier to the translator in SNIPER_ID
. This ties the setup of the translator to the input message in the second test.
The fix is to compare the Sniper identifier to the bidder from the event message.
The work we did in “Tidying Up the Translator” (page 135) to separate the different responsibilities within the translator has paid off here. All we had to do was add a couple of extra methods to AuctionEvent
to get a very readable solution.
Finally, to get all the code through the compiler, we fix joinAuction()
in Main
to pass in the new constructor parameter for the translator. We can get a correctly structured identifier from connection
.
Our immediate end-to-end test failure tells us that we should make the user interface show when the Sniper is winning. Our next implementation step is to follow through by fixing the AuctionSniper
to interpret the isFromSniper
parameter we’ve just added. Once again we start with a unit test.
To get through the compiler, we add the new sniperWinning()
method to SniperListener
which, in turn, means that we add an empty implementation to SniperStateDisplayer
.
The test fails:
This failure is a nice example of trapping a method that we didn’t expect. We set no expectations on the auction
, so calls to any of its methods will fail the test. If you compare this test to bidsHigherAndReportsBiddingWhenNewPriceArrives()
in “The AuctionSniper Bids” (page 126) you’ll also see that we drop the price
and increment
variables and just feed in numbers. That’s because, in this test, there’s no calculation to do, so we don’t need to reference them in an expectation. They’re just details to get us to the interesting behavior.
The fix is straightforward:
Running the end-to-end tests again shows that we’ve fixed the failure that started this chapter (showing Bidding
rather than Winning
). Now we have to make the Sniper win:
We’re about to introduce a step change in the complexity of the Sniper, if only a small one. When the auction closes, we want the Sniper to announce whether it has won or lost, which means that it must know whether it was bidding or winning at the time. This implies that the Sniper will have to maintain some state, which it hasn’t had to so far.
To get to the functionality we want, we’ll start with the simpler cases where the Sniper loses. As Figure 14.2 shows, we’re starting with one- and two-step transitions, before adding the additional step that takes the Sniper to the Won
state:
We start by revisiting an existing unit test and adding a new one. These tests will pass with the current implementation; they’re there to ensure that we don’t break the behavior when we add further transitions.
This introduces some new jMock syntax, states. The idea is to allow us to make assertions about the internal state of the object under test. We’ll come back to this idea in a moment.
We want to keep track of the Sniper’s current state, as signaled by the events it sends out, so we ask context
for a placeholder. The default state is null
.
We keep our original test, but now it will apply where there are no price updates.
The Sniper will call auction
but we really don’t care about that in this test, so we tell the test to ignore this collaborator completely.
When the Sniper sends out a bidding event, it’s telling us that it’s in a bidding
state, which we record here. We use the allowing()
clause to communicate that this is a supporting part of the test, not the part we really care about; see the note below.
This is the phrase that matters, the expectation that we want to assert. If the Sniper isn’t bidding when it makes this call, the test will fail.
This is our first test where we need a sequence of events to get the Sniper into the state we want to test. We just call its methods in order.
The unit test name reportsLostIfAuctionClosesWhenBidding
is very similar to the expectation it enforces:
atLeast(1).of(sniperListener).sniperLost(); when(sniperState.is("bidding"));
That’s not an accident. We put a lot of effort into figuring out which abstractions jMock should support and developing a style that expresses the essential intent of a unit test.
Finally, we can close the loop and have the Sniper win a bid. The next test introduces the Won
event.
It has the same structure but represents when the Sniper has won. The test fails because the Sniper called sniperLost()
.
We add a flag to represent the Sniper’s state, and implement the new sniperWon()
method in the SniperStateDisplayer
.
Having previously made a fuss about PriceSource
, are we being inconsistent here by using a boolean for isWinning
? Our excuse is that we did try an enum for the Sniper state, but it just looked too complicated. The field is private to AuctionSniper
, which is small enough so it’s easy to change later and the code reads well.
The unit and end-to-end tests all pass now, so we can cross off another item from the to-do list in Figure 14.3.
There are more tests we could write—for example, to describe the transitions from bidding to winning and back again, but we’ll leave those as an exercise for you, Dear Reader. Instead, we’ll move on to the next significant change in functionality.
As always, we made steady progress by adding little slices of functionality. First we made the Sniper show when it’s winning, then when it has won. We used empty implementations to get us through the compiler when we weren’t ready to fill in the code, and we stayed focused on the immediate task.
One of the pleasant surprises is that, now the code is growing a little, we’re starting to see some of our earlier effort pay off as new features just fit into the existing structure. The next tasks we have to implement will shake this up.
3.16.79.65