Skip to content

Commit 9cb15ba

Browse files
committed
Merge class-level and method-level @SQL declarations
Prior to these commits method-level @SQL declarations always overrode class-level @SQL declarations, which required developers to redeclare class-level @SQL declarations on test methods (e.g., via copy-and-paste) in order to combine them with method-level @SQL declarations. These commits provide developers the ability to optionally merge class-level and method-level @SQL declarations via a new @SqlMergeMode annotation that can be applied at the class level or method level, with method-level declarations of @SqlMergeMode overriding class-level declarations. For example, @SqlMergeMode(MERGE) switches from the default OVERRIDE behavior to merging behavior, with class-level SQL scripts and statements executed before method-level SQL scripts and statements. Closes gh-1835
2 parents b0939a8 + ccdf04e commit 9cb15ba

File tree

7 files changed

+402
-41
lines changed

7 files changed

+402
-41
lines changed

spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,7 +31,8 @@
3131
* SQL {@link #scripts} and {@link #statements} to be executed against a given
3232
* database during integration tests.
3333
*
34-
* <p>Method-level declarations override class-level declarations.
34+
* <p>Method-level declarations override class-level declarations by default,
35+
* but this behavior can be configured via {@link SqlMergeMode @SqlMergeMode}.
3536
*
3637
* <p>Script execution is performed by the {@link SqlScriptsTestExecutionListener},
3738
* which is enabled by default.
@@ -55,6 +56,7 @@
5556
* @author Sam Brannen
5657
* @since 4.1
5758
* @see SqlConfig
59+
* @see SqlMergeMode
5860
* @see SqlGroup
5961
* @see SqlScriptsTestExecutionListener
6062
* @see org.springframework.transaction.annotation.Transactional
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-2019 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+
* https://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.jdbc;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* {@code @SqlMergeMode} is used to annotate a test class or test method to
28+
* configure whether method-level {@code @Sql} declarations are merged with
29+
* class-level {@code @Sql} declarations.
30+
*
31+
* <p>A method-level {@code @SqlMergeMode} declaration overrides a class-level
32+
* declaration.
33+
*
34+
* <p>If {@code @SqlMergeMode} is not declared on a test class or test method,
35+
* {@link MergeMode#OVERRIDE} will be used by default.
36+
*
37+
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
38+
* <em>composed annotations</em> with attribute overrides.
39+
*
40+
* @author Sam Brannen
41+
* @author Dmitry Semukhin
42+
* @since 5.2
43+
* @see Sql
44+
* @see MergeMode#MERGE
45+
* @see MergeMode#OVERRIDE
46+
*/
47+
@Target({ElementType.TYPE, ElementType.METHOD})
48+
@Retention(RetentionPolicy.RUNTIME)
49+
@Documented
50+
@Inherited
51+
public @interface SqlMergeMode {
52+
53+
/**
54+
* Indicates whether method-level {@code @Sql} annotations should be merged
55+
* with class-level {@code @Sql} annotations or override them.
56+
*/
57+
MergeMode value();
58+
59+
60+
/**
61+
* Enumeration of <em>modes</em> that dictate whether method-level {@code @Sql}
62+
* declarations are merged with class-level {@code @Sql} declarations.
63+
*/
64+
enum MergeMode {
65+
66+
/**
67+
* Indicates that method-level {@code @Sql} declarations should be merged
68+
* with class-level {@code @Sql} declarations, with class-level SQL
69+
* scripts and statements executed before method-level scripts and
70+
* statements.
71+
*/
72+
MERGE,
73+
74+
/**
75+
* Indicates that method-level {@code @Sql} declarations should override
76+
* class-level {@code @Sql} declarations.
77+
*/
78+
OVERRIDE
79+
80+
}
81+
82+
}

spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.test.context.jdbc;
1818

19+
import java.lang.reflect.AnnotatedElement;
1920
import java.lang.reflect.Method;
2021
import java.util.List;
2122
import java.util.Set;
@@ -30,11 +31,13 @@
3031
import org.springframework.core.io.ClassPathResource;
3132
import org.springframework.core.io.Resource;
3233
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
34+
import org.springframework.lang.NonNull;
3335
import org.springframework.lang.Nullable;
3436
import org.springframework.test.context.TestContext;
3537
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
3638
import org.springframework.test.context.jdbc.SqlConfig.ErrorMode;
3739
import org.springframework.test.context.jdbc.SqlConfig.TransactionMode;
40+
import org.springframework.test.context.jdbc.SqlMergeMode.MergeMode;
3841
import org.springframework.test.context.support.AbstractTestExecutionListener;
3942
import org.springframework.test.context.transaction.TestContextTransactionUtils;
4043
import org.springframework.test.context.util.TestContextResourceUtils;
@@ -81,6 +84,7 @@
8184
* locate these beans.
8285
*
8386
* @author Sam Brannen
87+
* @author Dmitry Semukhin
8488
* @since 4.1
8589
* @see Sql
8690
* @see SqlConfig
@@ -108,7 +112,7 @@ public final int getOrder() {
108112
* {@link TestContext} <em>before</em> the current test method.
109113
*/
110114
@Override
111-
public void beforeTestMethod(TestContext testContext) throws Exception {
115+
public void beforeTestMethod(TestContext testContext) {
112116
executeSqlScripts(testContext, ExecutionPhase.BEFORE_TEST_METHOD);
113117
}
114118

@@ -117,30 +121,66 @@ public void beforeTestMethod(TestContext testContext) throws Exception {
117121
* {@link TestContext} <em>after</em> the current test method.
118122
*/
119123
@Override
120-
public void afterTestMethod(TestContext testContext) throws Exception {
124+
public void afterTestMethod(TestContext testContext) {
121125
executeSqlScripts(testContext, ExecutionPhase.AFTER_TEST_METHOD);
122126
}
123127

124128
/**
125129
* Execute SQL scripts configured via {@link Sql @Sql} for the supplied
126130
* {@link TestContext} and {@link ExecutionPhase}.
127131
*/
128-
private void executeSqlScripts(TestContext testContext, ExecutionPhase executionPhase) throws Exception {
129-
boolean classLevel = false;
130-
131-
Set<Sql> sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
132-
testContext.getTestMethod(), Sql.class, SqlGroup.class);
133-
if (sqlAnnotations.isEmpty()) {
134-
sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
135-
testContext.getTestClass(), Sql.class, SqlGroup.class);
136-
if (!sqlAnnotations.isEmpty()) {
137-
classLevel = true;
132+
private void executeSqlScripts(TestContext testContext, ExecutionPhase executionPhase) {
133+
Method testMethod = testContext.getTestMethod();
134+
Class<?> testClass = testContext.getTestClass();
135+
136+
if (mergeSqlAnnotations(testContext)) {
137+
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
138+
executeSqlScripts(getSqlAnnotationsFor(testMethod), testContext, executionPhase, false);
139+
}
140+
else {
141+
Set<Sql> methodLevelSqlAnnotations = getSqlAnnotationsFor(testMethod);
142+
if (!methodLevelSqlAnnotations.isEmpty()) {
143+
executeSqlScripts(methodLevelSqlAnnotations, testContext, executionPhase, false);
144+
}
145+
else {
146+
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
138147
}
139148
}
149+
}
140150

141-
for (Sql sql : sqlAnnotations) {
142-
executeSqlScripts(sql, executionPhase, testContext, classLevel);
151+
/**
152+
* Determine if method-level {@code @Sql} annotations should be merged with
153+
* class-level {@code @Sql} annotations.
154+
*/
155+
private boolean mergeSqlAnnotations(TestContext testContext) {
156+
SqlMergeMode sqlMergeMode = getSqlMergeModeFor(testContext.getTestMethod());
157+
if (sqlMergeMode == null) {
158+
sqlMergeMode = getSqlMergeModeFor(testContext.getTestClass());
143159
}
160+
return (sqlMergeMode != null && sqlMergeMode.value() == MergeMode.MERGE);
161+
}
162+
163+
/**
164+
* Get the {@code @SqlMergeMode} annotation declared on the supplied {@code element}.
165+
*/
166+
private SqlMergeMode getSqlMergeModeFor(AnnotatedElement element) {
167+
return AnnotatedElementUtils.findMergedAnnotation(element, SqlMergeMode.class);
168+
}
169+
170+
/**
171+
* Get the {@code @Sql} annotations declared on the supplied {@code element}.
172+
*/
173+
private Set<Sql> getSqlAnnotationsFor(AnnotatedElement element) {
174+
return AnnotatedElementUtils.getMergedRepeatableAnnotations(element, Sql.class, SqlGroup.class);
175+
}
176+
177+
/**
178+
* Execute SQL scripts for the supplied {@link Sql @Sql} annotations.
179+
*/
180+
private void executeSqlScripts(
181+
Set<Sql> sqlAnnotations, TestContext testContext, ExecutionPhase executionPhase, boolean classLevel) {
182+
183+
sqlAnnotations.forEach(sql -> executeSqlScripts(sql, executionPhase, testContext, classLevel));
144184
}
145185

146186
/**
@@ -153,8 +193,8 @@ private void executeSqlScripts(TestContext testContext, ExecutionPhase execution
153193
* @param testContext the current {@code TestContext}
154194
* @param classLevel {@code true} if {@link Sql @Sql} was declared at the class level
155195
*/
156-
private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestContext testContext, boolean classLevel)
157-
throws Exception {
196+
private void executeSqlScripts(
197+
Sql sql, ExecutionPhase executionPhase, TestContext testContext, boolean classLevel) {
158198

159199
if (executionPhase != sql.executionPhase()) {
160200
return;
@@ -166,15 +206,6 @@ private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestConte
166206
mergedSqlConfig, executionPhase, testContext));
167207
}
168208

169-
final ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
170-
populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding());
171-
populator.setSeparator(mergedSqlConfig.getSeparator());
172-
populator.setCommentPrefix(mergedSqlConfig.getCommentPrefix());
173-
populator.setBlockCommentStartDelimiter(mergedSqlConfig.getBlockCommentStartDelimiter());
174-
populator.setBlockCommentEndDelimiter(mergedSqlConfig.getBlockCommentEndDelimiter());
175-
populator.setContinueOnError(mergedSqlConfig.getErrorMode() == ErrorMode.CONTINUE_ON_ERROR);
176-
populator.setIgnoreFailedDrops(mergedSqlConfig.getErrorMode() == ErrorMode.IGNORE_FAILED_DROPS);
177-
178209
String[] scripts = getScripts(sql, testContext, classLevel);
179210
scripts = TestContextResourceUtils.convertToClasspathResourcePaths(testContext.getTestClass(), scripts);
180211
List<Resource> scriptResources = TestContextResourceUtils.convertToResourceList(
@@ -185,6 +216,8 @@ private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestConte
185216
scriptResources.add(new ByteArrayResource(stmt.getBytes(), "from inlined SQL statement: " + stmt));
186217
}
187218
}
219+
220+
ResourceDatabasePopulator populator = createDatabasePopulator(mergedSqlConfig);
188221
populator.setScripts(scriptResources.toArray(new Resource[0]));
189222
if (logger.isDebugEnabled()) {
190223
logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scriptResources));
@@ -225,13 +258,23 @@ private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestConte
225258
TransactionDefinition.PROPAGATION_REQUIRED);
226259
TransactionAttribute txAttr = TestContextTransactionUtils.createDelegatingTransactionAttribute(
227260
testContext, new DefaultTransactionAttribute(propagation));
228-
new TransactionTemplate(txMgr, txAttr).execute(status -> {
229-
populator.execute(finalDataSource);
230-
return null;
231-
});
261+
new TransactionTemplate(txMgr, txAttr).execute(() -> populator.execute(finalDataSource));
232262
}
233263
}
234264

265+
@NonNull
266+
private ResourceDatabasePopulator createDatabasePopulator(MergedSqlConfig mergedSqlConfig) {
267+
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
268+
populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding());
269+
populator.setSeparator(mergedSqlConfig.getSeparator());
270+
populator.setCommentPrefix(mergedSqlConfig.getCommentPrefix());
271+
populator.setBlockCommentStartDelimiter(mergedSqlConfig.getBlockCommentStartDelimiter());
272+
populator.setBlockCommentEndDelimiter(mergedSqlConfig.getBlockCommentEndDelimiter());
273+
populator.setContinueOnError(mergedSqlConfig.getErrorMode() == ErrorMode.CONTINUE_ON_ERROR);
274+
populator.setIgnoreFailedDrops(mergedSqlConfig.getErrorMode() == ErrorMode.IGNORE_FAILED_DROPS);
275+
return populator;
276+
}
277+
235278
@Nullable
236279
private DataSource getDataSourceFromTransactionManager(PlatformTransactionManager transactionManager) {
237280
try {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2002-2019 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+
* https://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.jdbc.merging;
18+
19+
import java.util.List;
20+
21+
import org.springframework.test.annotation.DirtiesContext;
22+
import org.springframework.test.context.ContextConfiguration;
23+
import org.springframework.test.context.jdbc.EmptyDatabaseConfig;
24+
import org.springframework.test.context.jdbc.SqlMergeMode;
25+
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD;
29+
30+
/**
31+
* Abstract base class for tests involving {@link SqlMergeMode @SqlMergeMode}.
32+
*
33+
* @author Sam Brannen
34+
* @since 5.2
35+
*/
36+
@ContextConfiguration(classes = EmptyDatabaseConfig.class)
37+
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD)
38+
abstract class AbstractSqlMergeModeTests extends AbstractTransactionalJUnit4SpringContextTests {
39+
40+
protected void assertUsers(String... expectedUsers) {
41+
List<String> actualUsers = super.jdbcTemplate.queryForList("select name from user", String.class);
42+
assertThat(actualUsers).containsExactlyInAnyOrder(expectedUsers);
43+
}
44+
45+
}

0 commit comments

Comments
 (0)