Skip to content

Commit 10a691b

Browse files
committed
Support inlined SQL statements in @SQL
Prior to this commit, it was only possible to declare SQL statements via @SQL within external script resources (i.e., classpath or file system resources); however, many developers have inquired about the ability to inline SQL statements with @SQL analogous to the support for inlined properties in @TestPropertySource. This commit introduces support for declaring _inlined SQL statements_ in `@Sql` via a new `statements` attribute. Inlined statements are executed after statements in scripts. Issue: SPR-13159
1 parent 3da5917 commit 10a691b

File tree

6 files changed

+181
-34
lines changed

6 files changed

+181
-34
lines changed

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

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
import static java.lang.annotation.RetentionPolicy.*;
2929

3030
/**
31-
* {@code @Sql} is used to annotate a test class or test method to configure SQL
32-
* scripts to be executed against a given database during integration tests.
31+
* {@code @Sql} is used to annotate a test class or test method to configure
32+
* SQL {@link #scripts} and {@link #statements} to be executed against a given
33+
* database during integration tests.
3334
*
3435
* <p>Method-level declarations override class-level declarations.
3536
*
@@ -77,14 +78,14 @@
7778
static enum ExecutionPhase {
7879

7980
/**
80-
* The configured SQL scripts will be executed <em>before</em> the
81-
* corresponding test method.
81+
* The configured SQL scripts and statements will be executed
82+
* <em>before</em> the corresponding test method.
8283
*/
8384
BEFORE_TEST_METHOD,
8485

8586
/**
86-
* The configured SQL scripts will be executed <em>after</em> the
87-
* corresponding test method.
87+
* The configured SQL scripts and statements will be executed
88+
* <em>after</em> the corresponding test method.
8889
*/
8990
AFTER_TEST_METHOD
9091
}
@@ -94,14 +95,19 @@ static enum ExecutionPhase {
9495
* Alias for {@link #scripts}.
9596
* <p>This attribute may <strong>not</strong> be used in conjunction with
9697
* {@link #scripts}, but it may be used instead of {@link #scripts}.
98+
* @see #scripts
99+
* @see #statements
97100
*/
98101
@AliasFor(attribute = "scripts")
99102
String[] value() default {};
100103

101104
/**
102105
* The paths to the SQL scripts to execute.
103106
* <p>This attribute may <strong>not</strong> be used in conjunction with
104-
* {@link #value}, but it may be used instead of {@link #value}.
107+
* {@link #value}, but it may be used instead of {@link #value}. Similarly,
108+
* this attribute may be used in conjunction with or instead of
109+
* {@link #statements}.
110+
*
105111
* <h3>Path Resource Semantics</h3>
106112
* <p>Each path will be interpreted as a Spring
107113
* {@link org.springframework.core.io.Resource Resource}. A plain path
@@ -114,11 +120,12 @@ static enum ExecutionPhase {
114120
* {@link org.springframework.util.ResourceUtils#CLASSPATH_URL_PREFIX classpath:},
115121
* {@link org.springframework.util.ResourceUtils#FILE_URL_PREFIX file:},
116122
* {@code http:}, etc.) will be loaded using the specified resource protocol.
123+
*
117124
* <h3>Default Script Detection</h3>
118-
* <p>If no SQL scripts are specified, an attempt will be made to detect a
119-
* <em>default</em> script depending on where this annotation is declared.
120-
* If a default cannot be detected, an {@link IllegalStateException} will be
121-
* thrown.
125+
* <p>If no SQL scripts or {@link #statements} are specified, an attempt will
126+
* be made to detect a <em>default</em> script depending on where this
127+
* annotation is declared. If a default cannot be detected, an
128+
* {@link IllegalStateException} will be thrown.
122129
* <ul>
123130
* <li><strong>class-level declaration</strong>: if the annotated test class
124131
* is {@code com.example.MyTest}, the corresponding default script is
@@ -128,19 +135,38 @@ static enum ExecutionPhase {
128135
* {@code com.example.MyTest}, the corresponding default script is
129136
* {@code "classpath:com/example/MyTest.testMethod.sql"}.</li>
130137
* </ul>
138+
*
139+
* @see #value
140+
* @see #statements
131141
*/
132142
@AliasFor(attribute = "value")
133143
String[] scripts() default {};
134144

135145
/**
136-
* When the SQL scripts should be executed.
146+
* <em>Inlined SQL statements</em> to execute.
147+
* <p>This attribute may be used in conjunction with or instead of
148+
* {@link #scripts}.
149+
*
150+
* <h3>Ordering</h3>
151+
* <p>Statements declared via this attribute will be executed after
152+
* statements loaded from resource {@link #scripts}. If you wish to have
153+
* inlined statements executed before scripts, simply declare multiple
154+
* instances of {@code @Sql} on the same class or method.
155+
*
156+
* @since 4.2
157+
* @see #scripts
158+
*/
159+
String[] statements() default {};
160+
161+
/**
162+
* When the SQL scripts and statements should be executed.
137163
* <p>Defaults to {@link ExecutionPhase#BEFORE_TEST_METHOD BEFORE_TEST_METHOD}.
138164
*/
139165
ExecutionPhase executionPhase() default ExecutionPhase.BEFORE_TEST_METHOD;
140166

141167
/**
142-
* Local configuration for the SQL scripts declared within this
143-
* {@code @Sql} annotation.
168+
* Local configuration for the SQL scripts and statements declared within
169+
* this {@code @Sql} annotation.
144170
* <p>See the class-level javadocs for {@link SqlConfig} for explanations of
145171
* local vs. global configuration, inheritance, overrides, etc.
146172
* <p>Defaults to an empty {@link SqlConfig @SqlConfig} instance.

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@
1717
package org.springframework.test.context.jdbc;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.List;
2021
import java.util.Set;
22+
2123
import javax.sql.DataSource;
2224

2325
import org.apache.commons.logging.Log;
2426
import org.apache.commons.logging.LogFactory;
2527

2628
import org.springframework.context.ApplicationContext;
2729
import org.springframework.core.annotation.AnnotationUtils;
30+
import org.springframework.core.io.ByteArrayResource;
2831
import org.springframework.core.io.ClassPathResource;
32+
import org.springframework.core.io.Resource;
2933
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
3034
import org.springframework.test.context.TestContext;
3135
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
@@ -45,23 +49,25 @@
4549
import org.springframework.util.ObjectUtils;
4650
import org.springframework.util.ReflectionUtils;
4751
import org.springframework.util.ResourceUtils;
52+
import org.springframework.util.StringUtils;
4853

4954
/**
50-
* {@code TestExecutionListener} that provides support for executing SQL scripts
55+
* {@code TestExecutionListener} that provides support for executing SQL
56+
* {@link Sql#scripts scripts} and inlined {@link Sql#statements statements}
5157
* configured via the {@link Sql @Sql} annotation.
5258
*
53-
* <p>Scripts will be executed {@linkplain #beforeTestMethod(TestContext) before}
59+
* <p>Scripts and inlined statements will be executed {@linkplain #beforeTestMethod(TestContext) before}
5460
* or {@linkplain #afterTestMethod(TestContext) after} execution of the corresponding
5561
* {@linkplain java.lang.reflect.Method test method}, depending on the configured
5662
* value of the {@link Sql#executionPhase executionPhase} flag.
5763
*
58-
* <p>Scripts will be executed without a transaction, within an existing
59-
* Spring-managed transaction, or within an isolated transaction, depending
60-
* on the configured value of {@link SqlConfig#transactionMode} and the
64+
* <p>Scripts and inlined statements will be executed without a transaction,
65+
* within an existing Spring-managed transaction, or within an isolated transaction,
66+
* depending on the configured value of {@link SqlConfig#transactionMode} and the
6167
* presence of a transaction manager.
6268
*
6369
* <h3>Script Resources</h3>
64-
* <p>For details on default script detection and how explicit script locations
70+
* <p>For details on default script detection and how script resource locations
6571
* are interpreted, see {@link Sql#scripts}.
6672
*
6773
* <h3>Required Spring Beans</h3>
@@ -175,9 +181,19 @@ private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestConte
175181

176182
String[] scripts = getScripts(sql, testContext, classLevel);
177183
scripts = TestContextResourceUtils.convertToClasspathResourcePaths(testContext.getTestClass(), scripts);
178-
populator.setScripts(TestContextResourceUtils.convertToResources(testContext.getApplicationContext(), scripts));
184+
List<Resource> scriptResources = TestContextResourceUtils.convertToResourceList(
185+
testContext.getApplicationContext(), scripts);
186+
187+
for (String statement : sql.statements()) {
188+
if (StringUtils.hasText(statement)) {
189+
statement = statement.trim();
190+
scriptResources.add(new ByteArrayResource(statement.getBytes(), "from inlined SQL statement: " + statement));
191+
}
192+
}
193+
194+
populator.setScripts(scriptResources.toArray(new Resource[scriptResources.size()]));
179195
if (logger.isDebugEnabled()) {
180-
logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scripts));
196+
logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scriptResources));
181197
}
182198

183199
String dsName = mergedSqlConfig.getDataSource();
@@ -255,7 +271,7 @@ private DataSource getDataSourceFromTransactionManager(PlatformTransactionManage
255271

256272
private String[] getScripts(Sql sql, TestContext testContext, boolean classLevel) {
257273
String[] scripts = sql.scripts();
258-
if (ObjectUtils.isEmpty(scripts)) {
274+
if (ObjectUtils.isEmpty(scripts) && ObjectUtils.isEmpty(sql.statements())) {
259275
scripts = new String[] { detectDefaultScript(testContext, classLevel) };
260276
}
261277
return scripts;
@@ -289,7 +305,7 @@ private String detectDefaultScript(TestContext testContext, boolean classLevel)
289305
}
290306
else {
291307
String msg = String.format("Could not detect default SQL script for test %s [%s]: "
292-
+ "%s does not exist. Either declare scripts via @Sql or make the "
308+
+ "%s does not exist. Either declare statements or scripts via @Sql or make the "
293309
+ "default SQL script available.", elementType, elementName, classPathResource);
294310
logger.error(msg);
295311
throw new IllegalStateException(msg);

spring-test/src/main/java/org/springframework/test/context/util/TestContextResourceUtils.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -88,20 +88,37 @@ else if (!ResourcePatternUtils.isUrl(path)) {
8888
}
8989

9090
/**
91-
* Convert the supplied paths to {@link Resource} handles using the given
92-
* {@link ResourceLoader}.
91+
* Convert the supplied paths to an array of {@link Resource} handles using
92+
* the given {@link ResourceLoader}.
9393
*
9494
* @param resourceLoader the {@code ResourceLoader} to use to convert the paths
9595
* @param paths the paths to be converted
9696
* @return a new array of resources
97+
* @see #convertToResourceList(ResourceLoader, String...)
9798
* @see #convertToClasspathResourcePaths
9899
*/
99100
public static Resource[] convertToResources(ResourceLoader resourceLoader, String... paths) {
101+
List<Resource> list = convertToResourceList(resourceLoader, paths);
102+
return list.toArray(new Resource[list.size()]);
103+
}
104+
105+
/**
106+
* Convert the supplied paths to a list of {@link Resource} handles using
107+
* the given {@link ResourceLoader}.
108+
*
109+
* @param resourceLoader the {@code ResourceLoader} to use to convert the paths
110+
* @param paths the paths to be converted
111+
* @return a new list of resources
112+
* @since 4.2
113+
* @see #convertToResources(ResourceLoader, String...)
114+
* @see #convertToClasspathResourcePaths
115+
*/
116+
public static List<Resource> convertToResourceList(ResourceLoader resourceLoader, String... paths) {
100117
List<Resource> list = new ArrayList<Resource>();
101118
for (String path : paths) {
102119
list.add(resourceLoader.getResource(path));
103120
}
104-
return list.toArray(new Resource[list.size()]);
121+
return list;
105122
}
106123

107124
}

spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,17 @@ public class SqlScriptsTestExecutionListenerTests {
5050

5151

5252
@Test
53-
public void missingValueAndScriptsAtClassLevel() throws Exception {
54-
Class<?> clazz = MissingValueAndScriptsAtClassLevel.class;
53+
public void missingValueAndScriptsAndStatementsAtClassLevel() throws Exception {
54+
Class<?> clazz = MissingValueAndScriptsAndStatementsAtClassLevel.class;
5555
BDDMockito.<Class<?>> given(testContext.getTestClass()).willReturn(clazz);
5656
given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("foo"));
5757

5858
assertExceptionContains(clazz.getSimpleName() + ".sql");
5959
}
6060

6161
@Test
62-
public void missingValueAndScriptsAtMethodLevel() throws Exception {
63-
Class<?> clazz = MissingValueAndScriptsAtMethodLevel.class;
62+
public void missingValueAndScriptsAndStatementsAtMethodLevel() throws Exception {
63+
Class<?> clazz = MissingValueAndScriptsAndStatementsAtMethodLevel.class;
6464
BDDMockito.<Class<?>> given(testContext.getTestClass()).willReturn(clazz);
6565
given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("foo"));
6666

@@ -126,13 +126,13 @@ private void assertExceptionContains(String msg) throws Exception {
126126
// -------------------------------------------------------------------------
127127

128128
@Sql
129-
static class MissingValueAndScriptsAtClassLevel {
129+
static class MissingValueAndScriptsAndStatementsAtClassLevel {
130130

131131
public void foo() {
132132
}
133133
}
134134

135-
static class MissingValueAndScriptsAtMethodLevel {
135+
static class MissingValueAndScriptsAndStatementsAtMethodLevel {
136136

137137
@Sql
138138
public void foo() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2002-2015 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.jdbc;
18+
19+
import javax.sql.DataSource;
20+
21+
import org.junit.FixMethodOrder;
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
import org.junit.runners.MethodSorters;
25+
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.jdbc.core.JdbcTemplate;
28+
import org.springframework.test.annotation.DirtiesContext;
29+
import org.springframework.test.context.ContextConfiguration;
30+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
31+
import org.springframework.test.jdbc.JdbcTestUtils;
32+
import org.springframework.transaction.annotation.Transactional;
33+
34+
import static org.junit.Assert.*;
35+
36+
/**
37+
* Transactional integration tests for {@link Sql @Sql} support with
38+
* inlined SQL {@link Sql#statements statements}.
39+
*
40+
* @author Sam Brannen
41+
* @since 4.2
42+
* @see TransactionalSqlScriptsTests
43+
*/
44+
@RunWith(SpringJUnit4ClassRunner.class)
45+
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
46+
@ContextConfiguration(classes = EmptyDatabaseConfig.class)
47+
@Transactional
48+
@Sql(
49+
scripts = "schema.sql",
50+
statements = "INSERT INTO user VALUES('Dilbert')"
51+
)
52+
@DirtiesContext
53+
public class TransactionalInlinedStatementsSqlScriptsTests {
54+
55+
protected JdbcTemplate jdbcTemplate;
56+
57+
58+
@Autowired
59+
public void setDataSource(DataSource dataSource) {
60+
this.jdbcTemplate = new JdbcTemplate(dataSource);
61+
}
62+
63+
@Test
64+
// test##_ prefix is required for @FixMethodOrder.
65+
public void test01_classLevelScripts() {
66+
assertNumUsers(1);
67+
}
68+
69+
@Test
70+
@Sql(statements = "DROP TABLE user IF EXISTS")
71+
@Sql("schema.sql")
72+
@Sql(statements = "INSERT INTO user VALUES ('Dilbert'), ('Dogbert'), ('Catbert')")
73+
// test##_ prefix is required for @FixMethodOrder.
74+
public void test02_methodLevelScripts() {
75+
assertNumUsers(3);
76+
}
77+
78+
protected int countRowsInTable(String tableName) {
79+
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
80+
}
81+
82+
protected void assertNumUsers(int expected) {
83+
assertEquals("Number of rows in the 'user' table.", expected, countRowsInTable("user"));
84+
}
85+
86+
}

src/asciidoc/whats-new.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,8 @@ public @interface MyTestConfig {
573573
_before_ a test -- for example, if some rogue (i.e., yet to be
574574
determined) test within a large test suite has corrupted the original
575575
configuration for the `ApplicationContext`.
576+
* `@Sql` now supports execution of _inlined SQL statements_ via a new
577+
`statements` attribute.
576578
* The JDBC XML namespace supports a new `database-name` attribute in
577579
`<jdbc:embedded-database>`, allowing developers to set unique names
578580
for embedded databases –- for example, via a SpEL expression or a

0 commit comments

Comments
 (0)