Implementing the New Architecture

So that we can show you what it’s like to have a flickering scenario, we’ll start by changing the architecture underneath the same test code. We’ll demonstrate the flickering scenario and, in a final flourish, fix it by changing the tests to use sampling to synchronize with the system.

Driving Out the Interfaces

The Account class is where the ATM is going to interface with our new backend services. As shown in the figure ​here​, the two touch points are the TransactionQueue and the BalanceStore. Let’s design the interfaces to those objects by changing our Account class to use two imaginary objects that can talk to these services:

 require_relative ​'transaction_queue'
 require_relative ​'balance_store'
 class​ Account
 def​ initialize
  @queue = TransactionQueue.new
  @balance_store = BalanceStore.new
 end
 
 def​ credit(amount)
  @queue.write(​"+​​#{​amount​}​​"​)
 end
 
 def​ balance
  @balance_store.balance
 end
 
 def​ debit(amount)
  @queue.write(​"-​​#{​amount​}​​"​)
 end
 end

We’ve used Ruby 1.9’s require_relative at the top of the file, so we’re assuming that the two new classes will be defined in files that sit in the same directory as this one. Getting the balance is a simple matter of delegating to the BalanceStore. In a more realistic system, we’d need to tell the BalanceStore which account we wanted the balance for, but in our simple example we’re dealing only with a single account, so we don’t need to worry about that.

For debits and credits, we’re serializing the transaction as a string, using a + or - to indicate whether the amount is a credit or a debit, and then writing it to the queue.

Building the TransactionQueue

Let’s build our transaction queue. We want to keep the technology really simple for this example, so we’re going to use the file system as our message store, with each message stored as a file in a messages directory. As a message is read from the queue, we’ll delete the file. Here’s the code:

 require ​'fileutils'
 
 class​ TransactionQueue
 def​ self.clear
  FileUtils.rm_rf(​'messages'​)
  FileUtils.mkdir_p(​'messages'​)
 end
 
 def​ initialize
  @next_id = 1
 end
 
 def​ write(transaction)
  File.open(​"messages/​​#{​@next_id​}​​"​, ​'w'​) { |f| f.puts(transaction) }
  @next_id += 1
 end
 
 def​ read
  next_message_file = Dir[​'messages/*'​].first
 return​ ​unless​ next_message_file
 yield​ File.read(next_message_file)
  FileUtils.rm_rf(next_message_file)
 end
 end

This is fairly simple Ruby code, but it’s the most complicated we’ve had in the book so far, so let’s run through how it works. First we have a class method, TransactionQueue.clear, which we’ll use to ensure the queue is cleaned up between scenarios. When we initialize the TransactionQueue, we create an instance variable @next_id, which will be used to give each new message a unique filename. When we’re asked to write a message, we create a new file in the messages directory, write the contents of the message into the file, and then increment @next_id ready for naming the next message’s file.

When we’re asked to read a message, we try to find a file in the messages directory. If the directory is empty, we just return from the method. If we do find a message, we read it, yield the contents to the caller, and then delete the message from the queue. If you’re new to Ruby, you might not know how yield works, but try not to worry—it will become clear when you see how this code is used. As always, for more information about programming in Ruby, we recommend the classic Programming Ruby 1.9 & 2.0 (4th edition) [FH13].

We’ve put the TransactionQueue in its own file and required it from the main lib/nice_bank.rb program. That’s because we need to use this library class both from the ATM web server and from our TransactionProcessor. We’re going to do the same thing with the BalanceStore, which is what we’ll build next.

Building the BalanceStore

The BalanceStore is a database where the latest account balance is stored. Again, we want to keep the technology simple for this example, so we’ll use a very simple kind of database: a text file on disk. Here’s the code:

 require ​'fileutils'
 
 class​ BalanceStore
 def​ balance
  File.read(​'balance'​).to_i
 end
 
 def​ balance=(new_balance)
  File.open(​'balance'​, ​'w'​) { |f| f.puts(new_balance) }
 end
 end

We have two methods, one that reads the balance and another that sets it. They both work with a file in the root of our project called balance. When it’s asked to set the balance, the BalanceStore opens the balance file and writes the new balance into it. When asked for the balance, the BalanceStore reads the file and converts the contents to a number. Simple!

Now that we’re persisting state to disk in our TransactionQueue and BalanceStore, we need to be careful that we don’t leak any state out of our scenario. Even though we have only a single scenario in our features at the moment, we need to clean up each time it runs so that balances and messages don’t leak from one test run into the next.

Adding Hooks to Reset State

We have two places where state will need to be cleaned up before our scenario runs. We need to set the user’s account balance to zero, and we need to remove any messages that have been left in the transaction queue. Add a file features/support/hooks.rb with the following code in it:

 Before ​do
  BalanceStore.new.balance = 0
  TransactionQueue.clear
 end

We’ve created a new instance of the BalanceStore directly so that we can tell it to set the balance to zero. Then we use the TransactionQueue.clear method we created earlier to empty any messages out of the transaction queue.

Let’s put the last piece in the puzzle by writing our TransactionProcessor.

Building the TransactionProcessor

The TransactionProcessor is a Ruby program that will run in the background, deep in the bowels of our bank’s server room. Here’s the code:

 require_relative ​'transaction_queue'
 require_relative ​'balance_store'
 
 transaction_queue = TransactionQueue.new
 balance_store = BalanceStore.new
 puts ​"transaction processor ready"
 loop​ ​do
  transaction_queue.read ​do​ |message|
  sleep 1
  transaction_amount = message.to_i
  new_balance = balance_store.balance + transaction_amount
  balance_store.balance = new_balance
 end
 end

The program starts by creating an instance of the TransactionQueue and BalanceStore classes. It prints a message to the console to say it’s started up and then enters a loop. The loop tries to read a message off the transaction queue. If it finds one, it pauses for a second, calculates the new balance, and then stores it on the BalanceStore. We’ve introduced the pause to demonstrate the effects of working with an asynchronous component in our system: this delay should mean the test will fail consistently because the backend will take so long to update the balance that Cucumber will have already finished the scenario.

That should complete the implementation of our new architecture. Now it’s time to test it.

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

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