Skip to content

Commit

Permalink
Introduce @SqlMergeMode for configuring @SQL annotation merging
Browse files Browse the repository at this point in the history
Closes gh-1835
  • Loading branch information
sbrannen committed Jul 21, 2019
1 parent ab88762 commit 89571ea
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
* SQL {@link #scripts} and {@link #statements} to be executed against a given
* database during integration tests.
*
* <p>Method-level declarations override class-level declarations by default.
* This behavior can be adjusted by setting the {@link #mergeMode}.
* <p>Method-level declarations override class-level declarations by default,
* but this behavior can be configured via {@link SqlMergeMode @SqlMergeMode}.
*
* <p>Script execution is performed by the {@link SqlScriptsTestExecutionListener},
* which is enabled by default.
Expand All @@ -54,9 +54,9 @@
* <em>composed annotations</em> with attribute overrides.
*
* @author Sam Brannen
* @author Dmitry Semukhin
* @since 4.1
* @see SqlConfig
* @see SqlMergeMode
* @see SqlGroup
* @see SqlScriptsTestExecutionListener
* @see org.springframework.transaction.annotation.Transactional
Expand Down Expand Up @@ -139,16 +139,6 @@
*/
ExecutionPhase executionPhase() default ExecutionPhase.BEFORE_TEST_METHOD;

/**
* Indicates whether this {@code @Sql} annotation should be merged with
* class-level {@code @Sql} annotations or override them.
* <p>The merge mode is ignored if declared in a class-level {@code @Sql}
* annotation.
* <p>Defaults to {@link MergeMode#OVERRIDE OVERRIDE} for backwards compatibility.
* @since 5.2
*/
MergeMode mergeMode() default MergeMode.OVERRIDE;

/**
* Local configuration for the SQL scripts and statements declared within
* this {@code @Sql} annotation.
Expand Down Expand Up @@ -177,24 +167,4 @@ enum ExecutionPhase {
AFTER_TEST_METHOD
}

/**
* Enumeration of <em>modes</em> that dictate whether method-level {@code @Sql}
* declarations are merged with class-level {@code @Sql} declarations.
* @since 5.2
*/
enum MergeMode {

/**
* Indicates that method-level {@code @Sql} declarations should override
* class-level {@code @Sql} declarations.
*/
OVERRIDE,

/**
* Indicates that method-level {@code @Sql} declarations should be merged
* with class-level {@code @Sql} declarations.
*/
MERGE
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.test.context.jdbc;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* {@code @SqlMergeMode} is used to annotate a test class or test method to
* configure whether method-level {@code @Sql} declarations are merged with
* class-level {@code @Sql} declarations.
*
* <p>A method-level {@code @SqlMergeMode} declaration overrides a class-level
* declaration.
*
* <p>If {@code @SqlMergeMode} is not declared on a test class or test method,
* {@link MergeMode#OVERRIDE} will be used by default.
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em> with attribute overrides.
*
* @author Sam Brannen
* @author Dmitry Semukhin
* @since 5.2
* @see Sql
* @see MergeMode#MERGE
* @see MergeMode#OVERRIDE
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SqlMergeMode {

/**
* Indicates whether method-level {@code @Sql} annotations should be merged
* with class-level {@code @Sql} annotations or override them.
*/
MergeMode value();


/**
* Enumeration of <em>modes</em> that dictate whether method-level {@code @Sql}
* declarations are merged with class-level {@code @Sql} declarations.
*/
enum MergeMode {

/**
* Indicates that method-level {@code @Sql} declarations should be merged
* with class-level {@code @Sql} declarations, with class-level SQL
* scripts and statements executed before method-level scripts and
* statements.
*/
MERGE,

/**
* Indicates that method-level {@code @Sql} declarations should override
* class-level {@code @Sql} declarations.
*/
OVERRIDE

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.sql.DataSource;

import org.apache.commons.logging.Log;
Expand All @@ -38,6 +37,7 @@
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
import org.springframework.test.context.jdbc.SqlConfig.ErrorMode;
import org.springframework.test.context.jdbc.SqlConfig.TransactionMode;
import org.springframework.test.context.jdbc.SqlMergeMode.MergeMode;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.transaction.TestContextTransactionUtils;
import org.springframework.test.context.util.TestContextResourceUtils;
Expand Down Expand Up @@ -130,36 +130,57 @@ public void afterTestMethod(TestContext testContext) {
* {@link TestContext} and {@link ExecutionPhase}.
*/
private void executeSqlScripts(TestContext testContext, ExecutionPhase executionPhase) {
Set<Sql> methodLevelSqls = getSqlAnnotationsFor(testContext.getTestMethod());
List<Sql> methodLevelOverrides = methodLevelSqls.stream()
.filter(s -> s.executionPhase() == executionPhase)
.filter(s -> s.mergeMode() == Sql.MergeMode.OVERRIDE)
.collect(Collectors.toList());
if (methodLevelOverrides.isEmpty()) {
executeScripts(getSqlAnnotationsFor(testContext.getTestClass()), testContext, executionPhase, true);
executeScripts(methodLevelSqls, testContext, executionPhase, false);
} else {
executeScripts(methodLevelOverrides, testContext, executionPhase, false);
Method testMethod = testContext.getTestMethod();
Class<?> testClass = testContext.getTestClass();

if (mergeSqlAnnotations(testContext)) {
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
executeSqlScripts(getSqlAnnotationsFor(testMethod), testContext, executionPhase, false);
}
else {
Set<Sql> methodLevelSqlAnnotations = getSqlAnnotationsFor(testMethod);
if (!methodLevelSqlAnnotations.isEmpty()) {
executeSqlScripts(methodLevelSqlAnnotations, testContext, executionPhase, false);
}
else {
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
}
}
}

/**
* Determine if method-level {@code @Sql} annotations should be merged with
* class-level {@code @Sql} annotations.
*/
private boolean mergeSqlAnnotations(TestContext testContext) {
SqlMergeMode sqlMergeMode = getSqlMergeModeFor(testContext.getTestMethod());
if (sqlMergeMode == null) {
sqlMergeMode = getSqlMergeModeFor(testContext.getTestClass());
}
return (sqlMergeMode != null && sqlMergeMode.value() == MergeMode.MERGE);
}

/**
* Get the {@code @SqlMergeMode} annotation declared on the supplied {@code element}.
*/
private SqlMergeMode getSqlMergeModeFor(AnnotatedElement element) {
return AnnotatedElementUtils.findMergedAnnotation(element, SqlMergeMode.class);
}

/**
* Get the {@link Sql @Sql} annotations declared on the supplied
* {@link AnnotatedElement}.
* Get the {@code @Sql} annotations declared on the supplied {@code element}.
*/
private Set<Sql> getSqlAnnotationsFor(AnnotatedElement annotatedElement) {
return AnnotatedElementUtils.getMergedRepeatableAnnotations(annotatedElement, Sql.class, SqlGroup.class);
private Set<Sql> getSqlAnnotationsFor(AnnotatedElement element) {
return AnnotatedElementUtils.getMergedRepeatableAnnotations(element, Sql.class, SqlGroup.class);
}

/**
* Execute SQL scripts for the supplied {@link Sql @Sql} annotations.
*/
private void executeScripts(
Iterable<Sql> scripts, TestContext testContext, ExecutionPhase executionPhase, boolean classLevel) {
private void executeSqlScripts(
Set<Sql> sqlAnnotations, TestContext testContext, ExecutionPhase executionPhase, boolean classLevel) {

for (Sql sql : scripts) {
executeSqlScripts(sql, executionPhase, testContext, classLevel);
}
sqlAnnotations.forEach(sql -> executeSqlScripts(sql, executionPhase, testContext, classLevel));
}

/**
Expand Down Expand Up @@ -196,7 +217,7 @@ private void executeSqlScripts(
}
}

ResourceDatabasePopulator populator = configurePopulator(mergedSqlConfig);
ResourceDatabasePopulator populator = createDatabasePopulator(mergedSqlConfig);
populator.setScripts(scriptResources.toArray(new Resource[0]));
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scriptResources));
Expand Down Expand Up @@ -242,7 +263,7 @@ private void executeSqlScripts(
}

@NonNull
private ResourceDatabasePopulator configurePopulator(MergedSqlConfig mergedSqlConfig) {
private ResourceDatabasePopulator createDatabasePopulator(MergedSqlConfig mergedSqlConfig) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding());
populator.setSeparator(mergedSqlConfig.getSeparator());
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,32 @@
* limitations under the License.
*/

package org.springframework.test.context.jdbc;
package org.springframework.test.context.jdbc.merging;

import org.junit.Test;
import java.util.List;

import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.jdbc.EmptyDatabaseConfig;
import org.springframework.test.context.jdbc.SqlMergeMode;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;

import static org.junit.Assert.assertEquals;
import static org.springframework.test.context.jdbc.Sql.MergeMode.MERGE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD;

/**
* Transactional integration tests for {@link Sql @Sql} that verify proper
* merging support for class-level and method-level declarations.
* Abstract base class for tests involving {@link SqlMergeMode @SqlMergeMode}.
*
* @author Dmitry Semukhin
* @author Sam Brannen
* @since 5.2
*/
@ContextConfiguration(classes = EmptyDatabaseConfig.class)
@Sql({ "schema.sql", "data-add-catbert.sql" })
@DirtiesContext
public class SqlMethodMergeTests extends AbstractTransactionalJUnit4SpringContextTests {
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD)
abstract class AbstractSqlMergeModeTests extends AbstractTransactionalJUnit4SpringContextTests {

@Test
@Sql(scripts = "data-add-dogbert.sql", mergeMode = MERGE)
public void testMerge() {
assertNumUsers(2);
}

protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the 'user' table.", expected, countRowsInTable("user"));
protected void assertUsers(String... expectedUsers) {
List<String> actualUsers = super.jdbcTemplate.queryForList("select name from user", String.class);
assertThat(actualUsers).containsExactlyInAnyOrder(expectedUsers);
}

}
Loading

0 comments on commit 89571ea

Please sign in to comment.