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