Chapter 14. The Sniper Wins the Auction

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.

First, a Failing Test

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:

Figure 14.1 A sniper bids, then wins

image

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:

image

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:

image

Now we know where we’re going, we can implement the feature.

Who Knows about Bidders?

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).

image

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.

image

The new test fails:

image

The fix is to compare the Sniper identifier to the bidder from the event message.

image

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.

image

The Sniper Has More to Say

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.

image

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:

image

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:

image

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:

image

The Sniper Acquires Some State

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:

Figure 14.2 A Sniper bids, then loses

image

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.

image

image 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.

image We keep our original test, but now it will apply where there are no price updates.

image 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.

image 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.

image 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.

image 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.

Allowances

image

jMock distinguishes between allowed and expected invocations. An allowing() clause says that the object might make this call, but it doesn’t have to—unlike an expectation which will fail the test if the call isn’t made. We make the distinction to help express what is important in a test (the underlying implementation is actually the same): expectations are what we want to confirm to have happened; allowances are supporting infrastructure that helps get the tested objects into the right state, or they’re side effects we don’t care about. We return to this topic in “Allowances and Expectations” (page 277) and we describe the API in Appendix A.

Representing Object State

image

In cases like this, we want to make assertions about an object’s behavior depending on its state, but we don’t want to break encapsulation by exposing how that state is implemented. Instead, the test can listen to the notification events that the Sniper provides to tell interested collaborators about its state in their terms. jMock provides States objects, so that tests can record and make assertions about the state of an object when something significant happens, i.e. when it calls its neighbors; see Appendix A for the syntax.

This is a “logical” representation of what’s going on inside the object, in this case the Sniper. It allows the test to describe what it finds relevant about the Sniper, regardless of how the Sniper is actually implemented. As you’ll see shortly, this separation will allow us to make radical changes to the implementation of the Sniper without changing the tests.

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.

The Sniper Wins

Finally, we can close the loop and have the Sniper win a bid. The next test introduces the Won event.

image

It has the same structure but represents when the Sniper has won. The test fails because the Sniper called sniperLost().

image

We add a flag to represent the Sniper’s state, and implement the new sniperWon() method in the SniperStateDisplayer.

image

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.

Figure 14.3 The Sniper wins

image

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.

Making Steady Progress

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.79.65