4 require 'models/developer'
7 class TransactionTest
< ActiveRecord
::TestCase
8 self.use_transactional_fixtures
= false
9 fixtures
:topics, :developers
12 @first, @second = Topic
.find(1, 2).sort_by
{ |t
| t
.id
}
17 @first.approved
= true
18 @second.approved
= false
23 assert Topic
.find(1).approved
?, "First should have been approved"
24 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
27 def transaction_with_return
29 @first.approved
= true
30 @second.approved
= false
37 def test_successful_with_return
38 class << Topic
.connection
39 alias :real_commit_db_transaction :commit_db_transaction
40 def commit_db_transaction
42 real_commit_db_transaction
47 transaction_with_return
50 assert Topic
.find(1).approved
?, "First should have been approved"
51 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
53 class << Topic
.connection
54 alias :commit_db_transaction :real_commit_db_transaction rescue nil
58 def test_successful_with_instance_method
60 @first.approved
= true
61 @second.approved
= false
66 assert Topic
.find(1).approved
?, "First should have been approved"
67 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
70 def test_failing_on_exception
73 @first.approved
= true
74 @second.approved
= false
83 assert
@first.approved
?, "First should still be changed in the objects"
84 assert
!@second.approved
?, "Second should still be changed in the objects"
86 assert
!Topic
.find(1).approved
?, "First shouldn't have been approved"
87 assert Topic
.find(2).approved
?, "Second should still be approved"
90 def test_raising_exception_in_callback_rollbacks_in_save
91 add_exception_raising_after_save_callback_to_topic
94 @first.approved
= true
98 assert_equal
"Make the transaction rollback", e
.message
99 assert
!Topic
.find(1).approved
?
101 remove_exception_raising_after_save_callback_to_topic
105 def test_cancellation_from_before_destroy_rollbacks_in_destroy
106 add_cancelling_before_destroy_with_db_side_effect_to_topic
108 nbooks_before_destroy
= Book
.count
109 status
= @first.destroy
111 assert_nothing_raised(ActiveRecord
::RecordNotFound) { @first.reload
}
112 assert_equal nbooks_before_destroy
, Book
.count
114 remove_cancelling_before_destroy_with_db_side_effect_to_topic
118 def test_cancellation_from_before_filters_rollbacks_in_save
119 %w(validation save
).each
do |filter
|
120 send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
122 nbooks_before_save
= Book
.count
123 original_author_name
= @first.author_name
124 @first.author_name
+= '_this_should_not_end_up_in_the_db'
127 assert_equal original_author_name
, @first.reload
.author_name
128 assert_equal nbooks_before_save
, Book
.count
130 send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
135 def test_cancellation_from_before_filters_rollbacks_in_save
!
136 %w(validation save
).each
do |filter
|
137 send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
139 nbooks_before_save
= Book
.count
140 original_author_name
= @first.author_name
141 @first.author_name
+= '_this_should_not_end_up_in_the_db'
145 assert_equal original_author_name
, @first.reload
.author_name
146 assert_equal nbooks_before_save
, Book
.count
148 send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
153 def test_callback_rollback_in_create
154 new_topic
= Topic
.new(
155 :title => "A new topic",
156 :author_name => "Ben",
157 :author_email_address => "ben@example.com",
158 :written_on => "2003-07-16t15:28:11.2233+01:00",
159 :last_read => "2004-04-15",
160 :bonus_time => "2005-01-30t15:28:00.00+01:00",
161 :content => "Have a nice day",
163 new_record_snapshot
= new_topic
.new_record
?
164 id_present
= new_topic
.has_attribute
?(Topic
.primary_key
)
165 id_snapshot
= new_topic
.id
167 # Make sure the second save gets the after_create callback called.
170 add_exception_raising_after_create_callback_to_topic
171 new_topic
.approved
= true
175 assert_equal
"Make the transaction rollback", e
.message
176 assert_equal new_record_snapshot
, new_topic
.new_record
?, "The topic should have its old new_record value"
177 assert_equal id_snapshot
, new_topic
.id
, "The topic should have its old id"
178 assert_equal id_present
, new_topic
.has_attribute
?(Topic
.primary_key
)
180 remove_exception_raising_after_create_callback_to_topic
185 def test_nested_explicit_transactions
188 @first.approved
= true
189 @second.approved
= false
195 assert Topic
.find(1).approved
?, "First should have been approved"
196 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
199 def test_manually_rolling_back_a_transaction
201 @first.approved
= true
202 @second.approved
= false
206 raise ActiveRecord
::Rollback
209 assert
@first.approved
?, "First should still be changed in the objects"
210 assert
!@second.approved
?, "Second should still be changed in the objects"
212 assert
!Topic
.find(1).approved
?, "First shouldn't have been approved"
213 assert Topic
.find(2).approved
?, "Second should still be approved"
216 uses_mocha
'mocking connection.commit_db_transaction' do
217 def test_rollback_when_commit_raises
218 Topic
.connection
.expects(:begin_db_transaction)
219 Topic
.connection
.expects(:transaction_active?).returns(true) if current_adapter
?(:PostgreSQLAdapter)
220 Topic
.connection
.expects(:commit_db_transaction).raises('OH NOES')
221 Topic
.connection
.expects(:rollback_db_transaction)
223 assert_raise RuntimeError
do
231 def test_sqlite_add_column_in_transaction_raises_statement_invalid
232 return true unless current_adapter
?(:SQLite3Adapter, :SQLiteAdapter)
234 # Test first if column creation/deletion works correctly when no
235 # transaction is in place.
237 # We go back to the connection for the column queries because
238 # Topic.columns is cached and won't report changes to the DB
240 assert_nothing_raised
do
241 Topic
.reset_column_information
242 Topic
.connection
.add_column('topics', 'stuff', :string)
243 assert Topic
.column_names
.include?('stuff')
245 Topic
.reset_column_information
246 Topic
.connection
.remove_column('topics', 'stuff')
247 assert
!Topic
.column_names
.include?('stuff')
250 # Test now inside a transaction: add_column should raise a StatementInvalid
252 assert_raises(ActiveRecord
::StatementInvalid) { Topic
.connection
.add_column('topics', 'stuff', :string) }
253 raise ActiveRecord
::Rollback
258 def add_exception_raising_after_save_callback_to_topic
259 Topic
.class_eval
{ def after_save() raise "Make the transaction rollback" end }
262 def remove_exception_raising_after_save_callback_to_topic
263 Topic
.class_eval
{ remove_method
:after_save }
266 def add_exception_raising_after_create_callback_to_topic
267 Topic
.class_eval
{ def after_create() raise "Make the transaction rollback" end }
270 def remove_exception_raising_after_create_callback_to_topic
271 Topic
.class_eval
{ remove_method
:after_create }
274 %w(validation save destroy
).each
do |filter
|
275 define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
276 Topic
.class_eval
"def before_#{filter}() Book.create; false end"
279 define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
280 Topic
.class_eval
"remove_method :before_#{filter}"
285 if current_adapter
?(:PostgreSQLAdapter)
286 class ConcurrentTransactionTest
< TransactionTest
287 use_concurrent_connections
289 # This will cause transactions to overlap and fail unless they are performed on
290 # separate database connections.
291 def test_transaction_per_thread
292 assert_nothing_raised
do
293 threads
= (1..3).map
do
296 topic
= Topic
.find(1)
297 topic
.approved
= !topic
.approved
?
299 topic
.approved
= !topic
.approved
?
305 threads
.each
{ |t
| t
.join
}
309 # Test for dirty reads among simultaneous transactions.
310 def test_transaction_isolation__read_committed
311 # Should be invariant.
312 original_salary
= Developer
.find(1).salary
313 temporary_salary
= 200000
315 assert_nothing_raised
do
316 threads
= (1..3).map
do
318 Developer
.transaction
do
319 # Expect original salary.
320 dev
= Developer
.find(1)
321 assert_equal original_salary
, dev
.salary
323 dev
.salary
= temporary_salary
326 # Expect temporary salary.
327 dev
= Developer
.find(1)
328 assert_equal temporary_salary
, dev
.salary
330 dev
.salary
= original_salary
333 # Expect original salary.
334 dev
= Developer
.find(1)
335 assert_equal original_salary
, dev
.salary
340 # Keep our eyes peeled.
341 threads
<< Thread
.new
do
344 Developer
.transaction
do
345 # Always expect original salary.
346 assert_equal original_salary
, Developer
.find(1).salary
351 threads
.each
{ |t
| t
.join
}
354 assert_equal original_salary
, Developer
.find(1).salary