View Javadoc
1   /*
2    * Copyright 2002-2013 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.transaction.interceptor;
18  
19  import java.lang.reflect.Method;
20  
21  import org.junit.Test;
22  
23  import org.springframework.dao.OptimisticLockingFailureException;
24  import org.springframework.tests.sample.beans.ITestBean;
25  import org.springframework.tests.sample.beans.TestBean;
26  import org.springframework.transaction.CannotCreateTransactionException;
27  import org.springframework.transaction.MockCallbackPreferringTransactionManager;
28  import org.springframework.transaction.NoTransactionException;
29  import org.springframework.transaction.PlatformTransactionManager;
30  import org.springframework.transaction.TransactionDefinition;
31  import org.springframework.transaction.TransactionStatus;
32  import org.springframework.transaction.TransactionSystemException;
33  import org.springframework.transaction.UnexpectedRollbackException;
34  import org.springframework.transaction.interceptor.TransactionAspectSupport.TransactionInfo;
35  
36  import static org.junit.Assert.*;
37  import static org.mockito.BDDMockito.*;
38  
39  /**
40   * Mock object based tests for transaction aspects.
41   * True unit test in that it tests how the transaction aspect uses
42   * the PlatformTransactionManager helper, rather than indirectly
43   * testing the helper implementation.
44   *
45   * This is a superclass to allow testing both the AOP Alliance MethodInterceptor
46   * and the AspectJ aspect.
47   *
48   * @author Rod Johnson
49   * @since 16.03.2003
50   */
51  public abstract class AbstractTransactionAspectTests {
52  
53  	protected Method exceptionalMethod;
54  
55  	protected Method getNameMethod;
56  
57  	protected Method setNameMethod;
58  
59  
60  	public AbstractTransactionAspectTests() {
61  		try {
62  			// Cache the methods we'll be testing
63  			exceptionalMethod = ITestBean.class.getMethod("exceptional", new Class[] { Throwable.class });
64  			getNameMethod = ITestBean.class.getMethod("getName", (Class[]) null);
65  			setNameMethod = ITestBean.class.getMethod("setName", new Class[] { String.class} );
66  		}
67  		catch (NoSuchMethodException ex) {
68  			throw new RuntimeException("Shouldn't happen", ex);
69  		}
70  	}
71  
72  
73  	@Test
74  	public void noTransaction() throws Exception {
75  		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
76  
77  		TestBean tb = new TestBean();
78  		TransactionAttributeSource tas = new MapTransactionAttributeSource();
79  
80  		// All the methods in this class use the advised() template method
81  		// to obtain a transaction object, configured with the given PlatformTransactionManager
82  		// and transaction attribute source
83  		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
84  
85  		checkTransactionStatus(false);
86  		itb.getName();
87  		checkTransactionStatus(false);
88  
89  		// expect no calls
90  		verifyZeroInteractions(ptm);
91  	}
92  
93  	/**
94  	 * Check that a transaction is created and committed.
95  	 */
96  	@Test
97  	public void transactionShouldSucceed() throws Exception {
98  		TransactionAttribute txatt = new DefaultTransactionAttribute();
99  
100 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
101 		tas.register(getNameMethod, txatt);
102 
103 		TransactionStatus status = mock(TransactionStatus.class);
104 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
105 		// expect a transaction
106 		given(ptm.getTransaction(txatt)).willReturn(status);
107 
108 		TestBean tb = new TestBean();
109 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
110 
111 		checkTransactionStatus(false);
112 		itb.getName();
113 		checkTransactionStatus(false);
114 
115 		verify(ptm).commit(status);
116 	}
117 
118 	/**
119 	 * Check that a transaction is created and committed using
120 	 * CallbackPreferringPlatformTransactionManager.
121 	 */
122 	@Test
123 	public void transactionShouldSucceedWithCallbackPreference() throws Exception {
124 		TransactionAttribute txatt = new DefaultTransactionAttribute();
125 
126 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
127 		tas.register(getNameMethod, txatt);
128 
129 		MockCallbackPreferringTransactionManager ptm = new MockCallbackPreferringTransactionManager();
130 
131 		TestBean tb = new TestBean();
132 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
133 
134 		checkTransactionStatus(false);
135 		itb.getName();
136 		checkTransactionStatus(false);
137 
138 		assertSame(txatt, ptm.getDefinition());
139 		assertFalse(ptm.getStatus().isRollbackOnly());
140 	}
141 
142 	@Test
143 	public void transactionExceptionPropagatedWithCallbackPreference() throws Throwable {
144 		TransactionAttribute txatt = new DefaultTransactionAttribute();
145 
146 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
147 		tas.register(exceptionalMethod, txatt);
148 
149 		MockCallbackPreferringTransactionManager ptm = new MockCallbackPreferringTransactionManager();
150 
151 		TestBean tb = new TestBean();
152 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
153 
154 		checkTransactionStatus(false);
155 		try {
156 			itb.exceptional(new OptimisticLockingFailureException(""));
157 			fail("Should have thrown OptimisticLockingFailureException");
158 		}
159 		catch (OptimisticLockingFailureException ex) {
160 			// expected
161 		}
162 		checkTransactionStatus(false);
163 
164 		assertSame(txatt, ptm.getDefinition());
165 		assertFalse(ptm.getStatus().isRollbackOnly());
166 	}
167 
168 	/**
169 	 * Check that two transactions are created and committed.
170 	 */
171 	@Test
172 	public void twoTransactionsShouldSucceed() throws Exception {
173 		TransactionAttribute txatt = new DefaultTransactionAttribute();
174 
175 		MapTransactionAttributeSource tas1 = new MapTransactionAttributeSource();
176 		tas1.register(getNameMethod, txatt);
177 		MapTransactionAttributeSource tas2 = new MapTransactionAttributeSource();
178 		tas2.register(setNameMethod, txatt);
179 
180 		TransactionStatus status = mock(TransactionStatus.class);
181 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
182 		// expect a transaction
183 		given(ptm.getTransaction(txatt)).willReturn(status);
184 
185 		TestBean tb = new TestBean();
186 		ITestBean itb = (ITestBean) advised(tb, ptm, new TransactionAttributeSource[] {tas1, tas2});
187 
188 		checkTransactionStatus(false);
189 		itb.getName();
190 		checkTransactionStatus(false);
191 		itb.setName("myName");
192 		checkTransactionStatus(false);
193 
194 		verify(ptm, times(2)).commit(status);
195 	}
196 
197 	/**
198 	 * Check that a transaction is created and committed.
199 	 */
200 	@Test
201 	public void transactionShouldSucceedWithNotNew() throws Exception {
202 		TransactionAttribute txatt = new DefaultTransactionAttribute();
203 
204 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
205 		tas.register(getNameMethod, txatt);
206 
207 		TransactionStatus status = mock(TransactionStatus.class);
208 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
209 		// expect a transaction
210 		given(ptm.getTransaction(txatt)).willReturn(status);
211 
212 		TestBean tb = new TestBean();
213 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
214 
215 		checkTransactionStatus(false);
216 		// verification!?
217 		itb.getName();
218 		checkTransactionStatus(false);
219 
220 		verify(ptm).commit(status);
221 	}
222 
223 	@Test
224 	public void enclosingTransactionWithNonTransactionMethodOnAdvisedInside() throws Throwable {
225 		TransactionAttribute txatt = new DefaultTransactionAttribute();
226 
227 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
228 		tas.register(exceptionalMethod, txatt);
229 
230 		TransactionStatus status = mock(TransactionStatus.class);
231 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
232 		// Expect a transaction
233 		given(ptm.getTransaction(txatt)).willReturn(status);
234 
235 		final String spouseName = "innerName";
236 
237 		TestBean outer = new TestBean() {
238 			@Override
239 			public void exceptional(Throwable t) throws Throwable {
240 				TransactionInfo ti = TransactionAspectSupport.currentTransactionInfo();
241 				assertTrue(ti.hasTransaction());
242 				assertEquals(spouseName, getSpouse().getName());
243 			}
244 		};
245 		TestBean inner = new TestBean() {
246 			@Override
247 			public String getName() {
248 				// Assert that we're in the inner proxy
249 				TransactionInfo ti = TransactionAspectSupport.currentTransactionInfo();
250 				assertFalse(ti.hasTransaction());
251 				return spouseName;
252 			}
253 		};
254 
255 		ITestBean outerProxy = (ITestBean) advised(outer, ptm, tas);
256 		ITestBean innerProxy = (ITestBean) advised(inner, ptm, tas);
257 		outer.setSpouse(innerProxy);
258 
259 		checkTransactionStatus(false);
260 
261 		// Will invoke inner.getName, which is non-transactional
262 		outerProxy.exceptional(null);
263 
264 		checkTransactionStatus(false);
265 
266 		verify(ptm).commit(status);
267 	}
268 
269 	@Test
270 	public void enclosingTransactionWithNestedTransactionOnAdvisedInside() throws Throwable {
271 		final TransactionAttribute outerTxatt = new DefaultTransactionAttribute();
272 		final TransactionAttribute innerTxatt = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_NESTED);
273 
274 		Method outerMethod = exceptionalMethod;
275 		Method innerMethod = getNameMethod;
276 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
277 		tas.register(outerMethod, outerTxatt);
278 		tas.register(innerMethod, innerTxatt);
279 
280 		TransactionStatus outerStatus = mock(TransactionStatus.class);
281 		TransactionStatus innerStatus = mock(TransactionStatus.class);
282 
283 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
284 		// Expect a transaction
285 		given(ptm.getTransaction(outerTxatt)).willReturn(outerStatus);
286 		given(ptm.getTransaction(innerTxatt)).willReturn(innerStatus);
287 
288 		final String spouseName = "innerName";
289 
290 		TestBean outer = new TestBean() {
291 			@Override
292 			public void exceptional(Throwable t) throws Throwable {
293 				TransactionInfo ti = TransactionAspectSupport.currentTransactionInfo();
294 				assertTrue(ti.hasTransaction());
295 				assertEquals(outerTxatt, ti.getTransactionAttribute());
296 				assertEquals(spouseName, getSpouse().getName());
297 			}
298 		};
299 		TestBean inner = new TestBean() {
300 			@Override
301 			public String getName() {
302 				// Assert that we're in the inner proxy
303 				TransactionInfo ti = TransactionAspectSupport.currentTransactionInfo();
304 				// Has nested transaction
305 				assertTrue(ti.hasTransaction());
306 				assertEquals(innerTxatt, ti.getTransactionAttribute());
307 				return spouseName;
308 			}
309 		};
310 
311 		ITestBean outerProxy = (ITestBean) advised(outer, ptm, tas);
312 		ITestBean innerProxy = (ITestBean) advised(inner, ptm, tas);
313 		outer.setSpouse(innerProxy);
314 
315 		checkTransactionStatus(false);
316 
317 		// Will invoke inner.getName, which is non-transactional
318 		outerProxy.exceptional(null);
319 
320 		checkTransactionStatus(false);
321 
322 		verify(ptm).commit(innerStatus);
323 		verify(ptm).commit(outerStatus);
324 	}
325 
326 	@Test
327 	public void rollbackOnCheckedException() throws Throwable {
328 		doTestRollbackOnException(new Exception(), true, false);
329 	}
330 
331 	@Test
332 	public void noRollbackOnCheckedException() throws Throwable {
333 		doTestRollbackOnException(new Exception(), false, false);
334 	}
335 
336 	@Test
337 	public void rollbackOnUncheckedException() throws Throwable {
338 		doTestRollbackOnException(new RuntimeException(), true, false);
339 	}
340 
341 	@Test
342 	public void noRollbackOnUncheckedException() throws Throwable {
343 		doTestRollbackOnException(new RuntimeException(), false, false);
344 	}
345 
346 	@Test
347 	public void rollbackOnCheckedExceptionWithRollbackException() throws Throwable {
348 		doTestRollbackOnException(new Exception(), true, true);
349 	}
350 
351 	@Test
352 	public void noRollbackOnCheckedExceptionWithRollbackException() throws Throwable {
353 		doTestRollbackOnException(new Exception(), false, true);
354 	}
355 
356 	@Test
357 	public void rollbackOnUncheckedExceptionWithRollbackException() throws Throwable {
358 		doTestRollbackOnException(new RuntimeException(), true, true);
359 	}
360 
361 	@Test
362 	public void noRollbackOnUncheckedExceptionWithRollbackException() throws Throwable {
363 		doTestRollbackOnException(new RuntimeException(), false, true);
364 	}
365 
366 	/**
367 	 * Check that the given exception thrown by the target can produce the
368 	 * desired behavior with the appropriate transaction attribute.
369 	 * @param ex exception to be thrown by the target
370 	 * @param shouldRollback whether this should cause a transaction rollback
371 	 */
372 	@SuppressWarnings("serial")
373 	protected void doTestRollbackOnException(
374 			final Exception ex, final boolean shouldRollback, boolean rollbackException) throws Exception {
375 
376 		TransactionAttribute txatt = new DefaultTransactionAttribute() {
377 			@Override
378 			public boolean rollbackOn(Throwable t) {
379 				assertTrue(t == ex);
380 				return shouldRollback;
381 			}
382 		};
383 
384 		Method m = exceptionalMethod;
385 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
386 		tas.register(m, txatt);
387 
388 		TransactionStatus status = mock(TransactionStatus.class);
389 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
390 		// Gets additional call(s) from TransactionControl
391 
392 		given(ptm.getTransaction(txatt)).willReturn(status);
393 
394 		TransactionSystemException tex = new TransactionSystemException("system exception");
395 		if (rollbackException) {
396 			if (shouldRollback) {
397 				willThrow(tex).given(ptm).rollback(status);
398 			}
399 			else {
400 				willThrow(tex).given(ptm).commit(status);
401 			}
402 		}
403 
404 		TestBean tb = new TestBean();
405 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
406 
407 		try {
408 			itb.exceptional(ex);
409 			fail("Should have thrown exception");
410 		}
411 		catch (Throwable t) {
412 			if (rollbackException) {
413 				assertEquals("Caught wrong exception", tex, t );
414 			}
415 			else {
416 				assertEquals("Caught wrong exception", ex, t);
417 			}
418 		}
419 
420 		if (!rollbackException) {
421 			if (shouldRollback) {
422 				verify(ptm).rollback(status);
423 			}
424 			else {
425 				verify(ptm).commit(status);
426 			}
427 		}
428 	}
429 
430 	/**
431 	 * Test that TransactionStatus.setRollbackOnly works.
432 	 */
433 	@Test
434 	public void programmaticRollback() throws Exception {
435 		TransactionAttribute txatt = new DefaultTransactionAttribute();
436 
437 		Method m = getNameMethod;
438 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
439 		tas.register(m, txatt);
440 
441 		TransactionStatus status = mock(TransactionStatus.class);
442 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
443 
444 		given(ptm.getTransaction(txatt)).willReturn(status);
445 
446 		final String name = "jenny";
447 		TestBean tb = new TestBean() {
448 			@Override
449 			public String getName() {
450 				TransactionStatus txStatus = TransactionInterceptor.currentTransactionStatus();
451 				txStatus.setRollbackOnly();
452 				return name;
453 			}
454 		};
455 
456 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
457 
458 		// verification!?
459 		assertTrue(name.equals(itb.getName()));
460 
461 		verify(ptm).commit(status);
462 	}
463 
464 	/**
465 	 * Simulate a transaction infrastructure failure.
466 	 * Shouldn't invoke target method.
467 	 */
468 	@Test
469 	public void cannotCreateTransaction() throws Exception {
470 		TransactionAttribute txatt = new DefaultTransactionAttribute();
471 
472 		Method m = getNameMethod;
473 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
474 		tas.register(m, txatt);
475 
476 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
477 		// Expect a transaction
478 		CannotCreateTransactionException ex = new CannotCreateTransactionException("foobar", null);
479 		given(ptm.getTransaction(txatt)).willThrow(ex);
480 
481 		TestBean tb = new TestBean() {
482 			@Override
483 			public String getName() {
484 				throw new UnsupportedOperationException(
485 						"Shouldn't have invoked target method when couldn't create transaction for transactional method");
486 			}
487 		};
488 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
489 
490 		try {
491 			itb.getName();
492 			fail("Shouldn't have invoked method");
493 		}
494 		catch (CannotCreateTransactionException thrown) {
495 			assertTrue(thrown == ex);
496 		}
497 	}
498 
499 	/**
500 	 * Simulate failure of the underlying transaction infrastructure to commit.
501 	 * Check that the target method was invoked, but that the transaction
502 	 * infrastructure exception was thrown to the client
503 	 */
504 	@Test
505 	public void cannotCommitTransaction() throws Exception {
506 		TransactionAttribute txatt = new DefaultTransactionAttribute();
507 
508 		Method m = setNameMethod;
509 		MapTransactionAttributeSource tas = new MapTransactionAttributeSource();
510 		tas.register(m, txatt);
511 		// Method m2 = getNameMethod;
512 		// No attributes for m2
513 
514 		PlatformTransactionManager ptm = mock(PlatformTransactionManager.class);
515 
516 		TransactionStatus status = mock(TransactionStatus.class);
517 		given(ptm.getTransaction(txatt)).willReturn(status);
518 		UnexpectedRollbackException ex = new UnexpectedRollbackException("foobar", null);
519 		willThrow(ex).given(ptm).commit(status);
520 
521 		TestBean tb = new TestBean();
522 		ITestBean itb = (ITestBean) advised(tb, ptm, tas);
523 
524 		String name = "new name";
525 		try {
526 			itb.setName(name);
527 			fail("Shouldn't have succeeded");
528 		}
529 		catch (UnexpectedRollbackException thrown) {
530 			assertTrue(thrown == ex);
531 		}
532 
533 		// Should have invoked target and changed name
534 		assertTrue(itb.getName() == name);
535 	}
536 
537 	protected void checkTransactionStatus(boolean expected) {
538 		try {
539 			TransactionInterceptor.currentTransactionStatus();
540 			if (!expected) {
541 				fail("Should have thrown NoTransactionException");
542 			}
543 		}
544 		catch (NoTransactionException ex) {
545 			if (expected) {
546 				fail("Should have current TransactionStatus");
547 			}
548 		}
549 	}
550 
551 
552 	protected Object advised(
553 			Object target, PlatformTransactionManager ptm, TransactionAttributeSource[] tas) throws Exception {
554 
555 		return advised(target, ptm, new CompositeTransactionAttributeSource(tas));
556 	}
557 
558 	/**
559 	 * Subclasses must implement this to create an advised object based on the
560 	 * given target. In the case of AspectJ, the  advised object will already
561 	 * have been created, as there's no distinction between target and proxy.
562 	 * In the case of Spring's own AOP framework, a proxy must be created
563 	 * using a suitably configured transaction interceptor
564 	 * @param target target if there's a distinct target. If not (AspectJ),
565 	 * return target.
566 	 * @return transactional advised object
567 	 */
568 	protected abstract Object advised(
569 			Object target, PlatformTransactionManager ptm, TransactionAttributeSource tas) throws Exception;
570 
571 }