Testing Transactions in Rails
Ben Scofield, Former Viget
The standard framework for testing in Ruby on Rails is a thing of beauty; there are folders to drop your unit and functional tests into right off the bat, fixtures and mock objects are expected and encouraged, and when you use scaffolding you get stubs for the common CRUD tests for free. That helps make testing less onerous for developers and, as a result, applications are better tested. Even Rails isn't perfect, however, and there are some situations where the testing set-up created out-of-the-box just doesn't work so well -- and, unfortunately, one of those problem situations involves database transactions. Put simply, transactions are a way to keep your data in a valid state. Say you have a banking application that allows people to transfer money from one account to another. The code might look like this:
andy_account.withdraw(300.00) ben_account.deposit(300.00)If all goes well, Andy's account is $300 lighter, while I'm $300 richer -- but, what if the system crashed in the middle, after the withdrawal but before the deposit? When the system comes back up, we'd see that the $300 withdrawal has been lost to the whims of the application. Transactions represent one possible solution to this problem. They run on the database server and make sure that a set of operations either all succeed or all fail. If our current example used transactions, the failure of either operation would cause both to be rolled back, leaving both accounts as they were before the ill-fated transfer attempt. Clearly, then, transactions can be useful. It turns out, though, that when you use transactions in your code in Rails, the tests don't always show what you'd expect them to. The problem is that, by default, all tests in Rails are already using transactions behind the scenes (to make loading and unloading test data fixtures as fast as possible). There's a setting in the TestHelper class that controls this: use_transactional_fixtures. You could just set that to false to fix your transaction tests, but then you'd slow down the entire test suite significantly. A better alternative is to change the setting for the cases that hold transaction tests:
class TransferTest self.use_transaction_fixtures = false ... endIf you have transactional and non-transactional tests in a single case, it can make sense to control this setting at an even more granular level, by separating those tests in different test cases:
class TransferTest ... end class TransactionalTransferTest self.use_transaction_fixtures = false ... endHappy testing!