Cleaning the Database with Transactions

This is a clever approach that uses the database’s transaction support in a somewhat unusual way. Before the scenario starts, we start a new database transaction in a Before hook. Then, our step definitions and application insert and modify data in the database. However, since this is happening in an uncommitted transaction, nothing actually gets changed in the database until the transaction is committed. Then, when the scenario is over (in an After hook), we do the opposite. We roll the transaction back! All the data that was modified during the scenario gets lost, and the database is back in its original state. You can see how this works in the figure.

images/database-transactions.png

This is actually what we want. It means that the next scenario that comes along starts with a blank slate, and we don’t need to worry about leftovers from the previous scenario.

Unfortunately, the transaction approach doesn’t work for our application. Understanding why this doesn’t work is essential, so we are going to try it anyway. Seeing it fail and understanding why will save you hours of problem solving in the future. It will also help you understand when to use transactions and when not to use them.

Our previous run would have left a row in the accounts table, so let’s remove that before we try to run with transactions:

 $ ​​sqlite3​​ ​​db/bank.db
 sqlite>​​ ​​delete​​ ​​from​​ ​​accounts;
 sqlite>​​ ​​.quit

Now that we have a clean database, let’s configure Cucumber to begin and roll back a transaction. DatabaseCleaner[50] is a Ruby gem that provides an easy way to ensure the database is in a clean state before each scenario starts. Let’s add it to our Gemfile:

 source ​"https://rubygems.org"
 
 gem ​'sinatra'​, ​'2.0.0.beta.2'
 gem ​'service_manager'​, ​'0.6.4'
 gem ​'activerecord'​, ​'5.0.0.1'
 gem ​'sqlite3'​, ​'1.3.11'
 
 group ​:development​ ​do
  gem ​'rspec'​, ​'3.5.0'
  gem ​'cucumber'​, ​'3.0.0.pre.1'
  gem ​'capybara'​, ​'2.9.1'
  gem ​'launchy'​, ​'2.4.3'
» gem ​'database_cleaner'​, ​'1.5.3'
 end

Now we have to define a Before hook that starts a transaction and an After hook that rolls it back. Add the following to features/support/database.rb:

 require ​'database_cleaner'
 
 DatabaseCleaner.strategy = ​:transaction
 
 Before ​do
  DatabaseCleaner.start
 end
 
 After ​do
  DatabaseCleaner.clean
 end

That’s all you need. In transaction mode, DatabaseCleaner.start opens a transaction, and DatabaseCleaner.clean rolls it back. Let’s see how that works:

 == 1 CreateAccounts: migrating ================================================
 -- create_table(:accounts)
  -> 0.0012s
 == 1 CreateAccounts: migrated (0.0013s) =======================================
 
 Starting transaction_processor in ~/message_queues/01 with
  'ruby lib/transaction_processor.rb'
 transaction processor ready
 Server transaction_processor (94557) is up.
 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
 ~/gems/activerecord-5.0.0.1/lib/active_record/core.rb:212:in `find_by!':
  Couldn't find Account (ActiveRecord::RecordNotFound)
  from ~/gems/activerecord-5.0.0.1/lib/
  active_record/dynamic_matchers.rb:65:in `find_by_number!'
  from ~/gems/activerecord-5.0.0.1/lib/
  active_record/dynamic_matchers.rb:19:in `method_missing'
  from lib/transaction_processor.rb:12:in `block (2 levels) in <main>'
  from ~/databases/05/lib/transaction_queue.rb:21:in `read'
  from lib/transaction_processor.rb:8:in `block in <main>'
  from lib/transaction_processor.rb:7:in `loop'
  from lib/transaction_processor.rb:7:in `<main>'
  And the balance of my account should be $80
 
  expected: 80
  got: 0
 
  (compared using ==)
  (RSpec::Expectations::ExpectationNotMetError)
  ./features/step_definitions/account_steps.rb:7
  ./features/support/async_support.rb:8
  ./features/support/async_support.rb:6
  ./features/support/async_support.rb:6
  ./features/step_definitions/account_steps.rb:7
  features/cash_withdrawal.feature:6
 
 Failing Scenarios:
 cucumber features/cash_withdrawal.feature:2
 
 1 scenario (1 failed)
 4 steps (1 failed, 3 passed)
 0m2.093s
 Shutting down transaction_processor (94557)
 Server transaction_processor (94557) is shut down

We told you it wasn’t going to work. It’s time to find out why. The TransactionProcessor couldn’t find the account we created in our first step. It throws an exception, and our scenario fails. Why can’t the TransactionProcessor find the account?

One of the properties of database transactions is that they are isolated. This means that whatever database activity happens inside a transaction cannot be seen by any other database connections. Don’t forget that we have two database connections.

Aslak says:
Aslak says:
Browser Testing and Databases

The problem with transactional cleaning and multiple database connections often occurs when we test web applications. This happens when the web application has a different database connection than Cucumber. The problem typically manifests itself in two situations:

  • Cucumber inserts some data, but it isn’t displayed in the browser.

  • A browser action causes some data to be inserted, but Cucumber can’t see it.

If this happens, you need to make sure you are not starting a transaction anywhere. You have to use the truncation cleaning strategy instead. If you’re logging your database calls, reading the logs can be very useful when diagnosing a problem like this.

In Chapter 15, Using Capybara to Test Ajax Web Applications, you will learn how to drive a browser from Cucumber. Capybara actually runs the web server in a separate thread within the same process, so you might think Cucumber and the web server share the same database connection. In fact, they don’t—each thread also gets its own database connection, which means transactional cleaning won’t work. It obscures data from the other connection.

Cucumber-Rails allows you to override this behavior in features/support/env.rb so that the Cucumber thread shares the connection with the web server. This means you can use the faster transaction cleaning strategy. Be warned that this works only on some databases. It may cause intermittent unexpected results on others!

The first database connection is made by the process that runs Cucumber. Cucumber begins a database transaction and creates an account.

The second database connection is made by the TransactionProcessor, which is started in a separate process by ServiceManager. The TransactionProcessor pops credit and debit messages off a queue, looks up accounts, and updates the balance. However, since the database transaction that Cucumber started hasn’t been committed, the TransactionProcessor can’t see the account we created in or first step.

Of course, we could have committed the transaction after creating the account, but that would defeat our goal of rolling back to get a clean database when the scenario is done. We can’t roll back a committed transaction. The rule is simple: when the application has a different database connection than Cucumber, we cannot use transactions to clean the database. We have to use the other strategy instead: truncation.

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

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