Skip to content

Commit f667e43

Browse files
committed
Introduce programmatic tx mgmt in the TCF
Historically, Spring's JUnit 3.8 TestCase class hierarchy supported programmatic transaction management of "test-managed transactions" via the protected endTransaction() and startNewTransaction() methods in AbstractTransactionalSpringContextTests. The Spring TestContext Framework (TCF) was introduced in Spring 2.5 to supersede the legacy JUnit 3.8 support classes; however, prior to this commit the TCF has not provided support for programmatically starting or stopping the test-managed transaction. This commit introduces a TestTransaction class in the TCF that provides static utility methods for programmatically interacting with test-managed transactions. Specifically, the following features are supported by TestTransaction and its collaborators. - End the current test-managed transaction. - Start a new test-managed transaction, using the default rollback semantics configured via @TransactionConfiguration and @Rollback. - Flag the current test-managed transaction to be committed. - Flag the current test-managed transaction to be rolled back. Implementation Details: - TransactionContext is now a top-level, package private class. - The existing test transaction management logic has been extracted from TransactionalTestExecutionListener into TransactionContext. - The current TransactionContext is stored in a NamedInheritableThreadLocal that is managed by TransactionContextHolder. - TestTransaction defines the end-user API, interacting with the TransactionContextHolder behind the scenes. - TransactionalTestExecutionListener now delegates to TransactionContext completely for starting and ending transactions. Issue: SPR-5079
1 parent 526b463 commit f667e43

File tree

8 files changed

+663
-108
lines changed

8 files changed

+663
-108
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2002-2014 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.test.context.transaction;
18+
19+
import org.springframework.test.context.TestExecutionListeners;
20+
import org.springframework.transaction.TransactionStatus;
21+
22+
/**
23+
* {@code TestTransaction} provides a collection of static utility methods for
24+
* programmatic interaction with <em>test-managed transactions</em>.
25+
*
26+
* <p>Test-managed transactions are transactions that are managed by the <em>Spring TestContext Framework</em>.
27+
*
28+
* <p>Support for {@code TestTransaction} is automatically available whenever
29+
* the {@link TransactionalTestExecutionListener} is enabled. Note that the
30+
* {@code TransactionalTestExecutionListener} is typically enabled by default,
31+
* but it can also be manually enabled via the
32+
* {@link TestExecutionListeners @TestExecutionListeners} annotation.
33+
*
34+
* @author Sam Brannen
35+
* @since 4.1
36+
* @see TransactionalTestExecutionListener
37+
*/
38+
public class TestTransaction {
39+
40+
/**
41+
* Determine whether a test-managed transaction is currently <em>active</em>.
42+
* @return {@code true} if a test-managed transaction is currently active
43+
* @see #start()
44+
* @see #end()
45+
*/
46+
public static boolean isActive() {
47+
TransactionContext transactionContext = TransactionContextHolder.getCurrentTransactionContext();
48+
if (transactionContext != null) {
49+
TransactionStatus transactionStatus = transactionContext.getTransactionStatus();
50+
return (transactionStatus != null) && (!transactionStatus.isCompleted());
51+
}
52+
53+
// else
54+
return false;
55+
}
56+
57+
/**
58+
* Determine whether the current test-managed transaction has been
59+
* {@linkplain #flagForRollback() flagged for rollback} or
60+
* {@linkplain #flagForCommit() flagged for commit}.
61+
* @return {@code true} if the current test-managed transaction is flagged
62+
* to be rolled back; {@code false} if the current test-managed transaction
63+
* is flagged to be committed
64+
* @throws IllegalStateException if a transaction is not active for the
65+
* current test
66+
* @see #isActive()
67+
* @see #flagForRollback()
68+
* @see #flagForCommit()
69+
*/
70+
public static boolean isFlaggedForRollback() {
71+
return requireCurrentTransactionContext().isFlaggedForRollback();
72+
}
73+
74+
/**
75+
* Flag the current test-managed transaction for <em>rollback</em>.
76+
* <p>Invoking this method will <em>not</em> end the current transaction.
77+
* Rather, the value of this flag will be used to determine whether or not
78+
* the current test-managed transaction should be rolled back or committed
79+
* once it is {@linkplain #end ended}.
80+
* @throws IllegalStateException if a transaction is not active for the
81+
* current test
82+
* @see #isActive()
83+
* @see #isFlaggedForRollback()
84+
* @see #start()
85+
* @see #end()
86+
*/
87+
public static void flagForRollback() {
88+
setFlaggedForRollback(true);
89+
}
90+
91+
/**
92+
* Flag the current test-managed transaction for <em>commit</em>.
93+
* <p>Invoking this method will <em>not</em> end the current transaction.
94+
* Rather, the value of this flag will be used to determine whether or not
95+
* the current test-managed transaction should be rolled back or committed
96+
* once it is {@linkplain #end ended}.
97+
* @throws IllegalStateException if a transaction is not active for the
98+
* current test
99+
* @see #isActive()
100+
* @see #isFlaggedForRollback()
101+
* @see #start()
102+
* @see #end()
103+
*/
104+
public static void flagForCommit() {
105+
setFlaggedForRollback(false);
106+
}
107+
108+
/**
109+
* Start a new test-managed transaction.
110+
* <p>Only call this method if {@link #end} has been called or if no
111+
* transaction has been previously started.
112+
* @throws IllegalStateException if the transaction context could not be
113+
* retrieved or if a transaction is already active for the current test
114+
* @see #isActive()
115+
* @see #end()
116+
*/
117+
public static void start() {
118+
requireCurrentTransactionContext().startTransaction();
119+
}
120+
121+
/**
122+
* Immediately force a <em>commit</em> or <em>rollback</em> of the current
123+
* test-managed transaction, according to the {@linkplain #isFlaggedForRollback
124+
* rollback flag}.
125+
* @throws IllegalStateException if the transaction context could not be
126+
* retrieved or if a transaction is not active for the current test
127+
* @see #isActive()
128+
* @see #start()
129+
*/
130+
public static void end() {
131+
requireCurrentTransactionContext().endTransaction();
132+
}
133+
134+
private static TransactionContext requireCurrentTransactionContext() {
135+
TransactionContext txContext = TransactionContextHolder.getCurrentTransactionContext();
136+
if (txContext == null) {
137+
throw new IllegalStateException("TransactionContext is not active");
138+
}
139+
return txContext;
140+
}
141+
142+
private static void setFlaggedForRollback(boolean flag) {
143+
requireCurrentTransactionContext().setFlaggedForRollback(flag);
144+
}
145+
146+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2002-2014 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.test.context.transaction;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
import org.springframework.test.context.TestContext;
22+
import org.springframework.transaction.PlatformTransactionManager;
23+
import org.springframework.transaction.TransactionDefinition;
24+
import org.springframework.transaction.TransactionException;
25+
import org.springframework.transaction.TransactionStatus;
26+
27+
/**
28+
* Transaction context for a specific {@link TestContext}.
29+
*
30+
* @author Sam Brannen
31+
* @author Juergen Hoeller
32+
* @since 4.1
33+
* @see org.springframework.transaction.annotation.Transactional
34+
* @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
35+
*/
36+
class TransactionContext {
37+
38+
private static final Log logger = LogFactory.getLog(TransactionContext.class);
39+
40+
private final TestContext testContext;
41+
42+
private final TransactionDefinition transactionDefinition;
43+
44+
private final PlatformTransactionManager transactionManager;
45+
46+
private final boolean defaultRollback;
47+
48+
private boolean flaggedForRollback;
49+
50+
private TransactionStatus transactionStatus;
51+
52+
private volatile int transactionsStarted = 0;
53+
54+
55+
TransactionContext(TestContext testContext, PlatformTransactionManager transactionManager,
56+
TransactionDefinition transactionDefinition, boolean defaultRollback) {
57+
this.testContext = testContext;
58+
this.transactionManager = transactionManager;
59+
this.transactionDefinition = transactionDefinition;
60+
this.defaultRollback = defaultRollback;
61+
this.flaggedForRollback = defaultRollback;
62+
}
63+
64+
TransactionStatus getTransactionStatus() {
65+
return this.transactionStatus;
66+
}
67+
68+
/**
69+
* Has the current transaction been flagged for rollback?
70+
* <p>In other words, should we roll back or commit the current transaction
71+
* upon completion of the current test?
72+
*/
73+
boolean isFlaggedForRollback() {
74+
return this.flaggedForRollback;
75+
}
76+
77+
void setFlaggedForRollback(boolean flaggedForRollback) {
78+
if (this.transactionStatus == null) {
79+
throw new IllegalStateException(String.format(
80+
"Failed to set rollback flag for test context %s: transaction does not exist.", this.testContext));
81+
}
82+
this.flaggedForRollback = flaggedForRollback;
83+
}
84+
85+
/**
86+
* Start a new transaction for the configured {@linkplain #getTestContext test context}.
87+
* <p>Only call this method if {@link #endTransaction} has been called or if no
88+
* transaction has been previously started.
89+
* @throws TransactionException if starting the transaction fails
90+
*/
91+
void startTransaction() {
92+
if (this.transactionStatus != null) {
93+
throw new IllegalStateException(
94+
"Cannot start a new transaction without ending the existing transaction first.");
95+
}
96+
this.flaggedForRollback = this.defaultRollback;
97+
this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
98+
++this.transactionsStarted;
99+
if (logger.isInfoEnabled()) {
100+
logger.info(String.format(
101+
"Began transaction (%s) for test context %s; transaction manager [%s]; rollback [%s]",
102+
this.transactionsStarted, this.testContext, this.transactionManager, flaggedForRollback));
103+
}
104+
}
105+
106+
/**
107+
* Immediately force a <em>commit</em> or <em>rollback</em> of the transaction
108+
* for the configured {@linkplain #getTestContext test context}, according to
109+
* the {@linkplain #isFlaggedForRollback rollback flag}.
110+
*/
111+
void endTransaction() {
112+
if (logger.isTraceEnabled()) {
113+
logger.trace(String.format(
114+
"Ending transaction for test context %s; transaction status [%s]; rollback [%s]", this.testContext,
115+
this.transactionStatus, flaggedForRollback));
116+
}
117+
if (this.transactionStatus == null) {
118+
throw new IllegalStateException(String.format(
119+
"Failed to end transaction for test context %s: transaction does not exist.", this.testContext));
120+
}
121+
122+
try {
123+
if (flaggedForRollback) {
124+
this.transactionManager.rollback(this.transactionStatus);
125+
}
126+
else {
127+
this.transactionManager.commit(this.transactionStatus);
128+
}
129+
}
130+
finally {
131+
this.transactionStatus = null;
132+
}
133+
134+
if (logger.isInfoEnabled()) {
135+
logger.info(String.format("%s transaction after test execution for test context %s.",
136+
(flaggedForRollback ? "Rolled back" : "Committed"), this.testContext));
137+
}
138+
}
139+
140+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2002-2014 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.test.context.transaction;
18+
19+
import org.springframework.core.NamedInheritableThreadLocal;
20+
21+
/**
22+
* {@link InheritableThreadLocal}-based holder for the current {@link TransactionContext}.
23+
*
24+
* @author Sam Brannen
25+
* @since 4.1
26+
*/
27+
class TransactionContextHolder {
28+
29+
private static final ThreadLocal<TransactionContext> currentTransactionContext = new NamedInheritableThreadLocal<TransactionContext>(
30+
"Test Transaction Context");
31+
32+
33+
static TransactionContext getCurrentTransactionContext() {
34+
return currentTransactionContext.get();
35+
}
36+
37+
static void setCurrentTransactionContext(TransactionContext transactionContext) {
38+
currentTransactionContext.set(transactionContext);
39+
}
40+
41+
static TransactionContext removeCurrentTransactionContext() {
42+
synchronized (currentTransactionContext) {
43+
TransactionContext transactionContext = currentTransactionContext.get();
44+
currentTransactionContext.remove();
45+
return transactionContext;
46+
}
47+
}
48+
49+
}

0 commit comments

Comments
 (0)