Testing a walking skeleton

22 Nov 2019

Notes based on Growing Object-oriented software guided by tests

The difficulty in writing and passing a first acceptance test is that it is hard to build the test environment and the feature it is testing at the same time. We can deal with this situation by first working out how to build, deploy and test a 'walking skeleton', and then use the infrastructure to write the acceptance tests for the first meaningful feature.

A walking skeleton is an implementation of the thinnest possible slice of real functionality that we can automatically build, deploy, and test end-to-end... For example, in a database-backed web application, a skeleton would show a flat web page with fields from the database.

The walking skeleton and its supporting infrastructure are there to help us work out how to start test-driven development. The first test is disposable. The first test for our first feature is the proper 'test that we want to read'.

We should be able to draw the design for the walking skeleton in a few minutes on a whiteboard.

Once you have a walking skeleton in place (have kickstarted the TDD process), we then start a new feature by writing a failing acceptance test. This failing acceptance tests demonstrates that the system does not yet have the feature we're about to write, and lets us track our progress towards the completion of the feature.

Start by testing the simplest success case (don't start with failure cases because that makes it harder to test domain assumptions earlier).

We start developing a feature by considering the events coming into the system that will trigger the new behaviour. The end-to-end tests will simulate these events arriving.

Write tests that are focused on behaviour, not method names. What is the outcome the object is trying to acheive in different scenarios. Test for those.

Object-oriented style

Designing for maintainability:

Acheiving object-oriented design

Starting with a test means we have to describe what we we want to acheive before we consider how. It also helps with information hiding as we have to decide what needs to be visible from outside the object.

We focus our design effort on how objects collaborate to deliver the functionality we need. The communication patterns between objects are more important.

Splitting a large object into a group of collaborating objects

When starting a new area of code, we might just write code without trying to impose too much structure. This lets us gain more experience and understanding regarding the domain. After a short while, the code may start to become a bit too complex, this is where you start pulling out related units of functionality into smaller collaborating objects.

Sometimes, you could consider this practice a spike which you then throw away and implement cleanly.

Commissioning an auction sniper

Build an application that automatically bids in auctions. It automatically bids slightly higher whenever the price changes, until it reaches a stop price or the auction closes.

Step one: need to agree on basic terms

Step two: Focus on getting a basic application working first.

Communicating with an auction

Slicing

A critical technique with incremental development is learning how to slice up the functionality so that it can be built a little at a time. Each slice should be significant and concrete enough that the team can tell when it’s done, and small enough to be focused on one concept and achievable quickly

Their working skeleton will cut a minimum path through Swing, XMPP and their application, just enough to show that they can plug these components together.

Here is a list containing a sequence of features to build:

The buyers prioritized the user interface over the stop price, partly because they want to make sure they feel comfortable with the application, and partly because there won't be an easy way to add multiple items, each with its own price without a user interface.

Once the above is stable, they can start working on more complicated scenarios, such as retrying if a bid failed or using different strategies for building.

Creating the walking skeleton

We start our walking skeleton by writing a test. The thinnest slice that the authors could imagine testing, is the first item on their to do list. That the auction sniper can join an auction and wait for it to close.

An outline of the test the authors want:

  1. When an auction is selling an item.
  2. And an auction sniper has started to bid in that auction.
  3. Then the auction will receive a join request from the auction sniper.
  4. When an auction announces that it is closed,
  5. Then the auction sniper will show that it lost the auction.

The fake auction (stub) will be as simple as possible. It will connect to an XMPP message broker, receive commands from the sniper to be checked by the test, and allow the test to send back events.

We want our skeleton test to exercise our application as close to end-to-end as possible, to show that the main() method initializes the application correctly and that the components really work together... We also want our tests to be clear about what is being checked, written in terms of the relationship between a sniper and its aucton... We'll start by writing the test as if all the code it needs exists and will fill in the implementations afterwards.


public class AuctionSniperEndToEndTest {
  private final FakeAuctionServer auction = new FakeAuctionServer("item-54321");
  private final ApplicationRunner application = new ApplicationRunner();

  @Test public void sniperJoinsAuctionUntilAuctionCloses() throws Exception {
    auction.startSellingItem();
    auction.startBiddingIn(auction);
    auction.hasReceivedJoinRequestFromSniper();
    auction.announceClosed();
    application.showsSniperHasLostAuction();
  }

  @After public void stopAuction() {
    auction.stop();
  }

  @After public void stopApplication() {
    application.stop()
  }


Naming conventions adopted in the above code:

One of the assumptions that are made in the code above is that a FakeAuctionServer is tied to a given item. Where an auction house hosts multiple auctions that each sell a single item.

The test is only concerned with snippers and auctions, no user interfaces or messaging layers/components, which protects us from implementation changes.

Making the end-to-end test pass

We need to find or write four components: an XMPP message broker, a stub auction that can communicate over XMPP, a GUI testing framework, and a test harness that can cope with the multithreaded, asynchronous architecture. Also need to get the project under version control with an automated build/deploy/test process.

End-to-end testing for event-based systems have to cope with asynchrony. The tests run in parallel with the application and do not know precisely when the application is or isn't ready. This is unlike unit testing, where a test drives an object directly in the same thread and so can make direct assertions about its state and behaviour.

An end-to-end test can't peek inside the target application, so it must wait to detect some visible effect, such as a user interface change or an entry in a log. The usual technique is to poll for the effect and fail if it doesn't happen within a given time limit. There's a further complexity in that the target application has to stabilize after the triggering event long enough for the test to catch the result. An asynchronous test waiting for a value that just flashes on the screen will be too unreliable for an automated build, so a common technique is to control the application and step through the scenario.

While unit tests must all pass every time, some teams only report end-to-end tests that fail several times in a row.

Going to stop here for now, the end-to-end test code is going to take a fresh session to process properly.

It has really helped me to read this and not get stuck over the things that I don't know how to do yet. The first few attempts at reading this book has been really slow going. This time I skipped to the chapters that I could try and outline the key stuff I might need to do when attempting to do this myself. Finding it difficult to even do this tells me that the book is targeted at someone with a lot more experience that me. But that's okay, as I get more experience, a lot of this will just click. It's already getting easier to understand than it was before.

Takeaway action: I'm going to attempt to set up my own, if clumsy end-to-end test in Ruby. Will probably look for a tutorial to copy first, then try and do one for my own project.