How to Synchronize

In our current implementation, the debit and credit transactions are processed synchronously by our simplistic Account class:

support_code/18/lib/nice_bank.rb
 
class​ Account
 
def​ credit(amount)
 
@balance = amount
 
end
 
 
def​ balance
 
@balance
 
end
 
 
def​ debit(amount)
 
@balance -= amount
 
end
 
end

This implementation, where the balance is updated during the method call to credit or debit, means that we can be certain the balance will have been updated by the time Cucumber checks the account balance in the final Then step of our scenario.

support_code/18/features/cash_withdrawal.feature
 
Feature:​ Cash Withdrawal​
 
Scenario:​ Successful withdrawal from an account in credit​
 
Given ​my account has been credited with $100​
 
When ​I withdraw $20​
 
Then ​$20 should be dispensed​
 
And ​the balance of my account should be $80

When we change to our new architecture; however, the transactions will be processed by a separate backend service. Because the tests and backend service are running in separate processes, it’s quite possible that Cucumber will run the Then step before the transaction processor has finished its work. If that happened, the test would fail even though the system is actually working as expected: if only Cucumber had waited a few more moments, it would have seen the right balance. This is what we call a flickering scenario, as described in Chapter 6, When Cucumbers Go Bad. How can we tell when it’s safe to check the account balance?

Adding asynchronous components into a system introduces a degree of randomness, but for our tests to be reliable, we need to ensure that the behavior is completely deterministic. To do that, we need to understand how we can synchronize our tests with the system, so that we make our checks only when the system is ready. In Growing Object-Oriented Software, Guided by Tests [FP09], Steve Freeman and Nat Pryce identify two ways to synchronize your tests with an asynchronous system: sampling and listening.

Synchronizing by Listening

Listening for events is the fastest and most reliable way to synchronize your tests with an asynchronous system. For this technique to work, the system under test has to be designed to fire events when certain things happen. The tests subscribe to these events and can use them to create synchronization points in the scenario.

For example, if the Transaction Processor were emitting BalanceUpdated events into a publish-subscribe message channel, we could wait at the top of our Then step until we heard that event. Once we’d received that event, we’d know it was safe to proceed and check the balance. We would use a timeout to ensure the tests didn’t wait forever if something was wrong with the system.

Using events like this involves a sophisticated coordination of your testing and development efforts, but it results in fast tests because they don’t waste any time waiting for the system: as soon as they’re notified of the right event, they spring back into action and carry on.

Synchronization by Sampling

When it isn’t possible to listen to events from the system, the next best option is to repeatedly poll the system for the state change you’re expecting. If it doesn’t appear within a certain timeout, you give up and fail the test.

Sampling can result in tests that are a little bit slower than listening, because of the polling interval. The more often you poll the system for changes, the quicker the tests can react and carry on when the system is working as expected. If you poll too frequently, however, you could put excessive load on the system.

Sampling is usually the pragmatic choice when you don’t have the option to receive events from the system under test. In the rest of this chapter, we’ll show you how to use sampling to make our tests work reliably with the new architecture.

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

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