diff options
author | Emmanuel Bourg <ebourg@apache.org> | 2014-12-03 14:31:16 +0100 |
---|---|---|
committer | Emmanuel Bourg <ebourg@apache.org> | 2014-12-03 14:31:16 +0100 |
commit | c56370beb0a2bfa263e125fce107dceccee89fd3 (patch) | |
tree | 7ee611ceb0acbbdf7f83abcd72adb854b7d77225 /spring-test/src | |
parent | aa5221b73661fa728dc4e62e1230e9104528c4eb (diff) |
Imported Upstream version 3.2.12
Diffstat (limited to 'spring-test/src')
397 files changed, 41303 insertions, 0 deletions
diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java new file mode 100644 index 00000000..8042e6d1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.env; + +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * Simple {@link ConfigurableEnvironment} implementation exposing + * {@link #setProperty(String, String)} and {@link #withProperty(String, String)} + * methods for testing purposes. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.2 + * @see org.springframework.mock.env.MockPropertySource + */ +public class MockEnvironment extends AbstractEnvironment { + + private MockPropertySource propertySource = new MockPropertySource(); + + /** + * Create a new {@code MockEnvironment} with a single {@link MockPropertySource}. + */ + public MockEnvironment() { + getPropertySources().addLast(propertySource); + } + + /** + * Set a property on the underlying {@link MockPropertySource} for this environment. + */ + public void setProperty(String key, String value) { + propertySource.setProperty(key, value); + } + + /** + * Convenient synonym for {@link #setProperty} that returns the current instance. + * Useful for method chaining and fluent-style use. + * @return this {@link MockEnvironment} instance + * @see MockPropertySource#withProperty + */ + public MockEnvironment withProperty(String key, String value) { + this.setProperty(key, value); + return this; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java new file mode 100644 index 00000000..124637e9 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.env; + +import java.util.Properties; + +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; + +/** + * Simple {@link PropertySource} implementation for use in testing. Accepts + * a user-provided {@link Properties} object, or if omitted during construction, + * the implementation will initialize its own. + * + * The {@link #setProperty} and {@link #withProperty} methods are exposed for + * convenience, for example: + * <pre class="code"> + * {@code + * PropertySource<?> source = new MockPropertySource().withProperty("foo", "bar"); + * } + * </pre> + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.mock.env.MockEnvironment + */ +public class MockPropertySource extends PropertiesPropertySource { + + /** + * {@value} is the default name for {@link MockPropertySource} instances not + * otherwise given an explicit name. + * @see #MockPropertySource() + * @see #MockPropertySource(String) + */ + public static final String MOCK_PROPERTIES_PROPERTY_SOURCE_NAME = "mockProperties"; + + /** + * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME} + * that will maintain its own internal {@link Properties} instance. + */ + public MockPropertySource() { + this(new Properties()); + } + + /** + * Create a new {@code MockPropertySource} with the given name that will + * maintain its own internal {@link Properties} instance. + * @param name the {@linkplain #getName() name} of the property source + */ + public MockPropertySource(String name) { + this(name, new Properties()); + } + + /** + * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME} + * and backed by the given {@link Properties} object. + * @param properties the properties to use + */ + public MockPropertySource(Properties properties) { + this(MOCK_PROPERTIES_PROPERTY_SOURCE_NAME, properties); + } + + /** + * Create a new {@code MockPropertySource} with the given name and backed by the given + * {@link Properties} object. + * @param name the {@linkplain #getName() name} of the property source + * @param properties the properties to use + */ + public MockPropertySource(String name, Properties properties) { + super(name, properties); + } + + /** + * Set the given property on the underlying {@link Properties} object. + */ + public void setProperty(String name, Object value) { + this.source.put(name, value); + } + + /** + * Convenient synonym for {@link #setProperty} that returns the current instance. + * Useful for method chaining and fluent-style use. + * @return this {@link MockPropertySource} instance + */ + public MockPropertySource withProperty(String name, Object value) { + this.setProperty(name, value); + return this; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/env/package-info.java b/spring-test/src/main/java/org/springframework/mock/env/package-info.java new file mode 100644 index 00000000..1772b5c9 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/env/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains mock implementations of the + * {@link org.springframework.core.env.Environment Environment} and + * {@link org.springframework.core.env.PropertySource PropertySource} + * abstractions introduced in Spring 3.1. + * + * <p>These <em>mocks</em> are useful for developing <em>out-of-container</em> + * unit tests for code that depends on environment-specific properties. + */ +package org.springframework.mock.env; diff --git a/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java b/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java new file mode 100644 index 00000000..5de14e99 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.http; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.util.Assert; + +/** + * Mock implementation of {@link HttpInputMessage}. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class MockHttpInputMessage implements HttpInputMessage { + + private final HttpHeaders headers = new HttpHeaders(); + + private final InputStream body; + + + public MockHttpInputMessage(byte[] contents) { + this.body = (contents != null) ? new ByteArrayInputStream(contents) : null; + } + + public MockHttpInputMessage(InputStream body) { + Assert.notNull(body, "'body' must not be null"); + this.body = body; + } + + public HttpHeaders getHeaders() { + return this.headers; + } + + public InputStream getBody() throws IOException { + return this.body; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java b/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java new file mode 100644 index 00000000..a4ef87e7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.mock.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; + +/** + * Mock implementation of {@link HttpOutputMessage}. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class MockHttpOutputMessage implements HttpOutputMessage { + + private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private final HttpHeaders headers = new HttpHeaders(); + + private final ByteArrayOutputStream body = new ByteArrayOutputStream(1024); + + + /** + * Return the headers. + */ + public HttpHeaders getHeaders() { + return this.headers; + } + + /** + * Return the body content. + */ + public OutputStream getBody() throws IOException { + return this.body; + } + + /** + * Return body content as a byte array. + */ + public byte[] getBodyAsBytes() { + return this.body.toByteArray(); + } + + /** + * Return the body content interpreted as a UTF-8 string. + */ + public String getBodyAsString() { + return getBodyAsString(DEFAULT_CHARSET); + } + + /** + * Return the body content as a string. + * @param charset the charset to use to turn the body content to a String + */ + public String getBodyAsString(Charset charset) { + byte[] bytes = getBodyAsBytes(); + try { + return new String(bytes, charset.name()); + } + catch (UnsupportedEncodingException ex) { + // should not occur + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java new file mode 100644 index 00000000..0f46bbbc --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.http.client; + +import java.io.IOException; +import java.net.URI; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.MockHttpOutputMessage; + +/** + * Mock implementation of {@link ClientHttpRequest}. + * + * @author Rossen Stoyanchev + * @author Sam Brannen + * @since 3.2 + */ +public class MockClientHttpRequest extends MockHttpOutputMessage implements ClientHttpRequest { + + private URI uri; + + private HttpMethod httpMethod; + + private boolean executed = false; + + private ClientHttpResponse clientHttpResponse; + + + /** + * Default constructor. + */ + public MockClientHttpRequest() { + } + + /** + * Create an instance with the given HttpMethod and URI. + */ + public MockClientHttpRequest(HttpMethod httpMethod, URI uri) { + this.httpMethod = httpMethod; + this.uri = uri; + } + + public URI getURI() { + return this.uri; + } + + public void setURI(URI uri) { + this.uri = uri; + } + + public HttpMethod getMethod() { + return this.httpMethod; + } + + public void setMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + } + + public void setResponse(ClientHttpResponse clientHttpResponse) { + this.clientHttpResponse = clientHttpResponse; + } + + public boolean isExecuted() { + return this.executed; + } + + /** + * Set the {@link #isExecuted() executed} flag to {@code true} and return the + * configured {@link #setResponse(ClientHttpResponse) response}. + * @see #executeInternal() + */ + public final ClientHttpResponse execute() throws IOException { + this.executed = true; + return executeInternal(); + } + + /** + * The default implementation returns the configured + * {@link #setResponse(ClientHttpResponse) response}. + * + * <p>Override this method to execute the request and provide a response, + * potentially different than the configured response. + */ + protected ClientHttpResponse executeInternal() throws IOException { + return this.clientHttpResponse; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (this.httpMethod != null) { + sb.append(this.httpMethod); + } + if (this.uri != null) { + sb.append(" ").append(this.uri); + } + if (!getHeaders().isEmpty()) { + sb.append(", headers : ").append(getHeaders()); + } + if (sb.length() == 0) { + sb.append("Not yet initialized"); + } + return sb.toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java new file mode 100644 index 00000000..9ed40774 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.http.client; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.MockHttpInputMessage; +import org.springframework.util.Assert; + +/** + * Mock implementation of {@link ClientHttpResponse}. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class MockClientHttpResponse extends MockHttpInputMessage implements ClientHttpResponse { + + private final HttpStatus status; + + + /** + * Constructor with response body as a byte array. + */ + public MockClientHttpResponse(byte[] body, HttpStatus statusCode) { + super(body); + Assert.notNull(statusCode, "statisCode is required"); + this.status = statusCode; + } + + /** + * Constructor with response body as InputStream. + */ + public MockClientHttpResponse(InputStream body, HttpStatus statusCode) { + super(body); + Assert.notNull(statusCode, "statisCode is required"); + this.status = statusCode; + } + + public HttpStatus getStatusCode() throws IOException { + return this.status; + } + + public int getRawStatusCode() throws IOException { + return this.status.value(); + } + + public String getStatusText() throws IOException { + return this.status.getReasonPhrase(); + } + + public void close() { + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java new file mode 100644 index 00000000..927dd34e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2012 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 + * + * http://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. + */ + +/** + * Mock implementations of client-side HTTP abstractions. + * This package contains the {@code MockClientHttpRequest} and + * {@code MockClientHttpResponse}. + */ +package org.springframework.mock.http.client; + diff --git a/spring-test/src/main/java/org/springframework/mock/http/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/package-info.java new file mode 100644 index 00000000..13cf4967 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/http/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2012 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 + * + * http://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. + */ + +/** + * Mock implementations of client/server-side HTTP abstractions. + * This package contains {@code MockHttpInputMessage} and + * {@code MockHttpOutputMessage}. + */ +package org.springframework.mock.http; + diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java b/spring-test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java new file mode 100644 index 00000000..b19aaf5c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/jndi/ExpectedLookupTemplate.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.jndi; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.naming.NamingException; + +import org.springframework.jndi.JndiTemplate; + +/** + * Simple extension of the JndiTemplate class that always returns a given object. + * + * <p>Very useful for testing. Effectively a mock object. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class ExpectedLookupTemplate extends JndiTemplate { + + private final Map<String, Object> jndiObjects = new ConcurrentHashMap<String, Object>(16); + + + /** + * Construct a new JndiTemplate that will always return given objects for + * given names. To be populated through {@code addObject} calls. + * @see #addObject(String, Object) + */ + public ExpectedLookupTemplate() { + } + + /** + * Construct a new JndiTemplate that will always return the given object, + * but honour only requests for the given name. + * @param name the name the client is expected to look up + * @param object the object that will be returned + */ + public ExpectedLookupTemplate(String name, Object object) { + addObject(name, object); + } + + /** + * Add the given object to the list of JNDI objects that this template will expose. + * @param name the name the client is expected to look up + * @param object the object that will be returned + */ + public void addObject(String name, Object object) { + this.jndiObjects.put(name, object); + } + + /** + * If the name is the expected name specified in the constructor, return the + * object provided in the constructor. If the name is unexpected, a + * respective NamingException gets thrown. + */ + public Object lookup(String name) throws NamingException { + Object object = this.jndiObjects.get(name); + if (object == null) { + throw new NamingException("Unexpected JNDI name '" + name + "': expecting " + this.jndiObjects.keySet()); + } + return object; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java new file mode 100644 index 00000000..d35fbdc8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContext.java @@ -0,0 +1,345 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.jndi; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; +import javax.naming.Binding; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.NameNotFoundException; +import javax.naming.NameParser; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.OperationNotSupportedException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * Simple implementation of a JNDI naming context. + * Only supports binding plain Objects to String names. + * Mainly for test environments, but also usable for standalone applications. + * + * <p>This class is not intended for direct usage by applications, although it + * can be used for example to override JndiTemplate's {@code createInitialContext} + * method in unit tests. Typically, SimpleNamingContextBuilder will be used to + * set up a JVM-level JNDI environment. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SimpleNamingContextBuilder + * @see org.springframework.jndi.JndiTemplate#createInitialContext + */ +public class SimpleNamingContext implements Context { + + private final Log logger = LogFactory.getLog(getClass()); + + private final String root; + + private final Hashtable<String, Object> boundObjects; + + private final Hashtable<String, Object> environment = new Hashtable<String, Object>(); + + + /** + * Create a new naming context. + */ + public SimpleNamingContext() { + this(""); + } + + /** + * Create a new naming context with the given naming root. + */ + public SimpleNamingContext(String root) { + this.root = root; + this.boundObjects = new Hashtable<String, Object>(); + } + + /** + * Create a new naming context with the given naming root, + * the given name/object map, and the JNDI environment entries. + */ + public SimpleNamingContext(String root, Hashtable<String, Object> boundObjects, Hashtable<String, Object> env) { + this.root = root; + this.boundObjects = boundObjects; + if (env != null) { + this.environment.putAll(env); + } + } + + + // Actual implementations of Context methods follow + + public NamingEnumeration<NameClassPair> list(String root) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Listing name/class pairs under [" + root + "]"); + } + return new NameClassPairEnumeration(this, root); + } + + public NamingEnumeration<Binding> listBindings(String root) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Listing bindings under [" + root + "]"); + } + return new BindingEnumeration(this, root); + } + + /** + * Look up the object with the given name. + * <p>Note: Not intended for direct use by applications. + * Will be used by any standard InitialContext JNDI lookups. + * @throws javax.naming.NameNotFoundException if the object could not be found + */ + public Object lookup(String lookupName) throws NameNotFoundException { + String name = this.root + lookupName; + if (logger.isDebugEnabled()) { + logger.debug("Static JNDI lookup: [" + name + "]"); + } + if ("".equals(name)) { + return new SimpleNamingContext(this.root, this.boundObjects, this.environment); + } + Object found = this.boundObjects.get(name); + if (found == null) { + if (!name.endsWith("/")) { + name = name + "/"; + } + for (String boundName : this.boundObjects.keySet()) { + if (boundName.startsWith(name)) { + return new SimpleNamingContext(name, this.boundObjects, this.environment); + } + } + throw new NameNotFoundException( + "Name [" + this.root + lookupName + "] not bound; " + this.boundObjects.size() + " bindings: [" + + StringUtils.collectionToDelimitedString(this.boundObjects.keySet(), ",") + "]"); + } + return found; + } + + public Object lookupLink(String name) throws NameNotFoundException { + return lookup(name); + } + + /** + * Bind the given object to the given name. + * Note: Not intended for direct use by applications + * if setting up a JVM-level JNDI environment. + * Use SimpleNamingContextBuilder to set up JNDI bindings then. + * @see org.springframework.mock.jndi.SimpleNamingContextBuilder#bind + */ + public void bind(String name, Object obj) { + if (logger.isInfoEnabled()) { + logger.info("Static JNDI binding: [" + this.root + name + "] = [" + obj + "]"); + } + this.boundObjects.put(this.root + name, obj); + } + + public void unbind(String name) { + if (logger.isInfoEnabled()) { + logger.info("Static JNDI remove: [" + this.root + name + "]"); + } + this.boundObjects.remove(this.root + name); + } + + public void rebind(String name, Object obj) { + bind(name, obj); + } + + public void rename(String oldName, String newName) throws NameNotFoundException { + Object obj = lookup(oldName); + unbind(oldName); + bind(newName, obj); + } + + public Context createSubcontext(String name) { + String subcontextName = this.root + name; + if (!subcontextName.endsWith("/")) { + subcontextName += "/"; + } + Context subcontext = new SimpleNamingContext(subcontextName, this.boundObjects, this.environment); + bind(name, subcontext); + return subcontext; + } + + public void destroySubcontext(String name) { + unbind(name); + } + + public String composeName(String name, String prefix) { + return prefix + name; + } + + public Hashtable<String, Object> getEnvironment() { + return this.environment; + } + + public Object addToEnvironment(String propName, Object propVal) { + return this.environment.put(propName, propVal); + } + + public Object removeFromEnvironment(String propName) { + return this.environment.remove(propName); + } + + public void close() { + } + + + // Unsupported methods follow: no support for javax.naming.Name + + public NamingEnumeration<NameClassPair> list(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public NamingEnumeration<Binding> listBindings(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public Object lookup(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public Object lookupLink(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public void bind(Name name, Object obj) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public void unbind(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public void rebind(Name name, Object obj) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public void rename(Name oldName, Name newName) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public Context createSubcontext(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public void destroySubcontext(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public String getNameInNamespace() throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public NameParser getNameParser(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public NameParser getNameParser(String name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + public Name composeName(Name name, Name prefix) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + + private static abstract class AbstractNamingEnumeration<T> implements NamingEnumeration<T> { + + private Iterator<T> iterator; + + private AbstractNamingEnumeration(SimpleNamingContext context, String proot) throws NamingException { + if (!"".equals(proot) && !proot.endsWith("/")) { + proot = proot + "/"; + } + String root = context.root + proot; + Map<String, T> contents = new HashMap<String, T>(); + for (String boundName : context.boundObjects.keySet()) { + if (boundName.startsWith(root)) { + int startIndex = root.length(); + int endIndex = boundName.indexOf('/', startIndex); + String strippedName = + (endIndex != -1 ? boundName.substring(startIndex, endIndex) : boundName.substring(startIndex)); + if (!contents.containsKey(strippedName)) { + try { + contents.put(strippedName, createObject(strippedName, context.lookup(proot + strippedName))); + } + catch (NameNotFoundException ex) { + // cannot happen + } + } + } + } + if (contents.size() == 0) { + throw new NamingException("Invalid root: [" + context.root + proot + "]"); + } + this.iterator = contents.values().iterator(); + } + + protected abstract T createObject(String strippedName, Object obj); + + public boolean hasMore() { + return this.iterator.hasNext(); + } + + public T next() { + return this.iterator.next(); + } + + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + public T nextElement() { + return this.iterator.next(); + } + + public void close() { + } + } + + + private static class NameClassPairEnumeration extends AbstractNamingEnumeration<NameClassPair> { + + private NameClassPairEnumeration(SimpleNamingContext context, String root) throws NamingException { + super(context, root); + } + + protected NameClassPair createObject(String strippedName, Object obj) { + return new NameClassPair(strippedName, obj.getClass().getName()); + } + } + + + private static class BindingEnumeration extends AbstractNamingEnumeration<Binding> { + + private BindingEnumeration(SimpleNamingContext context, String root) throws NamingException { + super(context, root); + } + + protected Binding createObject(String strippedName, Object obj) { + return new Binding(strippedName, obj); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java new file mode 100644 index 00000000..f813203b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.mock.jndi; + +import java.util.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; +import javax.naming.spi.InitialContextFactoryBuilder; +import javax.naming.spi.NamingManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.ClassUtils; + +/** + * Simple implementation of a JNDI naming context builder. + * + * <p>Mainly targeted at test environments, where each test case can + * configure JNDI appropriately, so that {@code new InitialContext()} + * will expose the required objects. Also usable for standalone applications, + * e.g. for binding a JDBC DataSource to a well-known JNDI location, to be + * able to use traditional J2EE data access code outside of a J2EE container. + * + * <p>There are various choices for DataSource implementations: + * <ul> + * <li>{@code SingleConnectionDataSource} (using the same Connection for all getConnection calls) + * <li>{@code DriverManagerDataSource} (creating a new Connection on each getConnection call) + * <li>Apache's Jakarta Commons DBCP offers {@code org.apache.commons.dbcp.BasicDataSource} (a real pool) + * </ul> + * + * <p>Typical usage in bootstrap code: + * + * <pre class="code"> + * SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder(); + * DataSource ds = new DriverManagerDataSource(...); + * builder.bind("java:comp/env/jdbc/myds", ds); + * builder.activate();</pre> + * + * Note that it's impossible to activate multiple builders within the same JVM, + * due to JNDI restrictions. Thus to configure a fresh builder repeatedly, use + * the following code to get a reference to either an already activated builder + * or a newly activated one: + * + * <pre class="code"> + * SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder(); + * DataSource ds = new DriverManagerDataSource(...); + * builder.bind("java:comp/env/jdbc/myds", ds);</pre> + * + * Note that you <i>should not</i> call {@code activate()} on a builder from + * this factory method, as there will already be an activated one in any case. + * + * <p>An instance of this class is only necessary at setup time. + * An application does not need to keep a reference to it after activation. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @see #emptyActivatedContextBuilder() + * @see #bind(String, Object) + * @see #activate() + * @see SimpleNamingContext + * @see org.springframework.jdbc.datasource.SingleConnectionDataSource + * @see org.springframework.jdbc.datasource.DriverManagerDataSource + */ +public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder { + + /** An instance of this class bound to JNDI */ + private static volatile SimpleNamingContextBuilder activated; + + private static boolean initialized = false; + + private static final Object initializationLock = new Object(); + + + /** + * Checks if a SimpleNamingContextBuilder is active. + * @return the current SimpleNamingContextBuilder instance, + * or {@code null} if none + */ + public static SimpleNamingContextBuilder getCurrentContextBuilder() { + return activated; + } + + /** + * If no SimpleNamingContextBuilder is already configuring JNDI, + * create and activate one. Otherwise take the existing activate + * SimpleNamingContextBuilder, clear it and return it. + * <p>This is mainly intended for test suites that want to + * reinitialize JNDI bindings from scratch repeatedly. + * @return an empty SimpleNamingContextBuilder that can be used + * to control JNDI bindings + */ + public static SimpleNamingContextBuilder emptyActivatedContextBuilder() throws NamingException { + if (activated != null) { + // Clear already activated context builder. + activated.clear(); + } + else { + // Create and activate new context builder. + SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder(); + // The activate() call will cause an assignment to the activated variable. + builder.activate(); + } + return activated; + } + + + private final Log logger = LogFactory.getLog(getClass()); + + private final Hashtable<String,Object> boundObjects = new Hashtable<String,Object>(); + + + /** + * Register the context builder by registering it with the JNDI NamingManager. + * Note that once this has been done, {@code new InitialContext()} will always + * return a context from this factory. Use the {@code emptyActivatedContextBuilder()} + * static method to get an empty context (for example, in test methods). + * @throws IllegalStateException if there's already a naming context builder + * registered with the JNDI NamingManager + */ + public void activate() throws IllegalStateException, NamingException { + logger.info("Activating simple JNDI environment"); + synchronized (initializationLock) { + if (!initialized) { + if (NamingManager.hasInitialContextFactoryBuilder()) { + throw new IllegalStateException( + "Cannot activate SimpleNamingContextBuilder: there is already a JNDI provider registered. " + + "Note that JNDI is a JVM-wide service, shared at the JVM system class loader level, " + + "with no reset option. As a consequence, a JNDI provider must only be registered once per JVM."); + } + NamingManager.setInitialContextFactoryBuilder(this); + initialized = true; + } + } + activated = this; + } + + /** + * Temporarily deactivate this context builder. It will remain registered with + * the JNDI NamingManager but will delegate to the standard JNDI InitialContextFactory + * (if configured) instead of exposing its own bound objects. + * <p>Call {@code activate()} again in order to expose this context builder's own + * bound objects again. Such activate/deactivate sequences can be applied any number + * of times (e.g. within a larger integration test suite running in the same VM). + * @see #activate() + */ + public void deactivate() { + logger.info("Deactivating simple JNDI environment"); + activated = null; + } + + /** + * Clear all bindings in this context builder, while keeping it active. + */ + public void clear() { + this.boundObjects.clear(); + } + + /** + * Bind the given object under the given name, for all naming contexts + * that this context builder will generate. + * @param name the JNDI name of the object (e.g. "java:comp/env/jdbc/myds") + * @param obj the object to bind (e.g. a DataSource implementation) + */ + public void bind(String name, Object obj) { + if (logger.isInfoEnabled()) { + logger.info("Static JNDI binding: [" + name + "] = [" + obj + "]"); + } + this.boundObjects.put(name, obj); + } + + + /** + * Simple InitialContextFactoryBuilder implementation, + * creating a new SimpleNamingContext instance. + * @see SimpleNamingContext + */ + public InitialContextFactory createInitialContextFactory(Hashtable<?,?> environment) { + if (activated == null && environment != null) { + Object icf = environment.get(Context.INITIAL_CONTEXT_FACTORY); + if (icf != null) { + Class<?> icfClass; + if (icf instanceof Class) { + icfClass = (Class<?>) icf; + } + else if (icf instanceof String) { + icfClass = ClassUtils.resolveClassName((String) icf, getClass().getClassLoader()); + } + else { + throw new IllegalArgumentException("Invalid value type for environment key [" + + Context.INITIAL_CONTEXT_FACTORY + "]: " + icf.getClass().getName()); + } + if (!InitialContextFactory.class.isAssignableFrom(icfClass)) { + throw new IllegalArgumentException( + "Specified class does not implement [" + InitialContextFactory.class.getName() + "]: " + icf); + } + try { + return (InitialContextFactory) icfClass.newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Cannot instantiate specified InitialContextFactory: " + icf, ex); + } + } + } + + // Default case... + return new InitialContextFactory() { + @SuppressWarnings("unchecked") + public Context getInitialContext(Hashtable<?,?> environment) { + return new SimpleNamingContext("", boundObjects, (Hashtable<String, Object>) environment); + } + }; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/package-info.java b/spring-test/src/main/java/org/springframework/mock/jndi/package-info.java new file mode 100644 index 00000000..992e2369 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/jndi/package-info.java @@ -0,0 +1,13 @@ + +/** + * + * The simplest implementation of the JNDI SPI that could possibly work. + * + * <p>Useful for setting up a simple JNDI environment for test suites + * or stand-alone applications. If, for example, JDBC DataSources get bound to the + * same JNDI names as within a Java EE container, both application code and + * configuration can be reused without changes. + * + */ +package org.springframework.mock.jndi; + diff --git a/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java new file mode 100644 index 00000000..f97e60e2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletInputStream.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import java.io.IOException; +import java.io.InputStream; +import javax.servlet.ServletInputStream; + +import org.springframework.util.Assert; + +/** + * Delegating implementation of {@link javax.servlet.ServletInputStream}. + * + * <p>Used by {@link MockHttpServletRequest}; typically not directly + * used for testing application controllers. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see MockHttpServletRequest + */ +public class DelegatingServletInputStream extends ServletInputStream { + + private final InputStream sourceStream; + + + /** + * Create a DelegatingServletInputStream for the given source stream. + * @param sourceStream the source stream (never {@code null}) + */ + public DelegatingServletInputStream(InputStream sourceStream) { + Assert.notNull(sourceStream, "Source InputStream must not be null"); + this.sourceStream = sourceStream; + } + + /** + * Return the underlying source stream (never {@code null}). + */ + public final InputStream getSourceStream() { + return this.sourceStream; + } + + + public int read() throws IOException { + return this.sourceStream.read(); + } + + public void close() throws IOException { + super.close(); + this.sourceStream.close(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java new file mode 100644 index 00000000..23694170 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/DelegatingServletOutputStream.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import java.io.IOException; +import java.io.OutputStream; +import javax.servlet.ServletOutputStream; + +import org.springframework.util.Assert; + +/** + * Delegating implementation of {@link javax.servlet.ServletOutputStream}. + * + * <p>Used by {@link MockHttpServletResponse}; typically not directly + * used for testing application controllers. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see MockHttpServletResponse + */ +public class DelegatingServletOutputStream extends ServletOutputStream { + + private final OutputStream targetStream; + + + /** + * Create a DelegatingServletOutputStream for the given target stream. + * @param targetStream the target stream (never {@code null}) + */ + public DelegatingServletOutputStream(OutputStream targetStream) { + Assert.notNull(targetStream, "Target OutputStream must not be null"); + this.targetStream = targetStream; + } + + /** + * Return the underlying target stream (never {@code null}). + */ + public final OutputStream getTargetStream() { + return this.targetStream; + } + + + public void write(int b) throws IOException { + this.targetStream.write(b); + } + + public void flush() throws IOException { + super.flush(); + this.targetStream.flush(); + } + + public void close() throws IOException { + super.close(); + this.targetStream.close(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java b/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java new file mode 100644 index 00000000..e8f5e91d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Internal helper class that serves as value holder for request headers. + * + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0.1 + */ +class HeaderValueHolder { + + private final List<Object> values = new LinkedList<Object>(); + + + public void setValue(Object value) { + this.values.clear(); + this.values.add(value); + } + + public void addValue(Object value) { + this.values.add(value); + } + + public void addValues(Collection<?> values) { + this.values.addAll(values); + } + + public void addValueArray(Object values) { + CollectionUtils.mergeArrayIntoCollection(values, this.values); + } + + public List<Object> getValues() { + return Collections.unmodifiableList(this.values); + } + + public List<String> getStringValues() { + List<String> stringList = new ArrayList<String>(this.values.size()); + for (Object value : this.values) { + stringList.add(value.toString()); + } + return Collections.unmodifiableList(stringList); + } + + public Object getValue() { + return (!this.values.isEmpty() ? this.values.get(0) : null); + } + + public String getStringValue() { + return (!this.values.isEmpty() ? this.values.get(0).toString() : null); + } + + + /** + * Find a HeaderValueHolder by name, ignoring casing. + * @param headers the Map of header names to HeaderValueHolders + * @param name the name of the desired header + * @return the corresponding HeaderValueHolder, + * or {@code null} if none found + */ + public static HeaderValueHolder getByName(Map<String, HeaderValueHolder> headers, String name) { + Assert.notNull(name, "Header name must not be null"); + for (String headerName : headers.keySet()) { + if (headerName.equalsIgnoreCase(name)) { + return headers.get(headerName); + } + } + return null; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java b/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java new file mode 100644 index 00000000..3fb6ca77 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.tagext.BodyContent; + +/** + * Mock implementation of the {@link javax.servlet.jsp.tagext.BodyContent} class. + * + * <p>Used for testing the web framework; only necessary for testing + * applications when testing custom JSP tags. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class MockBodyContent extends BodyContent { + + private final String content; + + + /** + * Create a MockBodyContent for the given response. + * @param content the body content to expose + * @param response the servlet response to wrap + */ + public MockBodyContent(String content, HttpServletResponse response) { + this(content, response, null); + } + + /** + * Create a MockBodyContent for the given response. + * @param content the body content to expose + * @param targetWriter the target Writer to wrap + */ + public MockBodyContent(String content, Writer targetWriter) { + this(content, null, targetWriter); + } + + /** + * Create a MockBodyContent for the given response. + * @param content the body content to expose + * @param response the servlet response to wrap + * @param targetWriter the target Writer to wrap + */ + public MockBodyContent(String content, HttpServletResponse response, Writer targetWriter) { + super(adaptJspWriter(targetWriter, response)); + this.content = content; + } + + private static JspWriter adaptJspWriter(Writer targetWriter, HttpServletResponse response) { + if (targetWriter instanceof JspWriter) { + return (JspWriter) targetWriter; + } + else { + return new MockJspWriter(response, targetWriter); + } + } + + + public Reader getReader() { + return new StringReader(this.content); + } + + public String getString() { + return this.content; + } + + public void writeOut(Writer writer) throws IOException { + writer.write(this.content); + } + + + //--------------------------------------------------------------------- + // Delegating implementations of JspWriter's abstract methods + //--------------------------------------------------------------------- + + public void clear() throws IOException { + getEnclosingWriter().clear(); + } + + public void clearBuffer() throws IOException { + getEnclosingWriter().clearBuffer(); + } + + public void close() throws IOException { + getEnclosingWriter().close(); + } + + public int getRemaining() { + return getEnclosingWriter().getRemaining(); + } + + public void newLine() throws IOException { + getEnclosingWriter().println(); + } + + public void write(char value[], int offset, int length) throws IOException { + getEnclosingWriter().write(value, offset, length); + } + + public void print(boolean value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(char value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(char[] value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(double value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(float value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(int value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(long value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(Object value) throws IOException { + getEnclosingWriter().print(value); + } + + public void print(String value) throws IOException { + getEnclosingWriter().print(value); + } + + public void println() throws IOException { + getEnclosingWriter().println(); + } + + public void println(boolean value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(char value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(char[] value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(double value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(float value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(int value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(long value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(Object value) throws IOException { + getEnclosingWriter().println(value); + } + + public void println(String value) throws IOException { + getEnclosingWriter().println(value); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java b/spring-test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java new file mode 100644 index 00000000..6b69fdb7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockExpressionEvaluator.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.el.ELException; +import javax.servlet.jsp.el.Expression; +import javax.servlet.jsp.el.ExpressionEvaluator; +import javax.servlet.jsp.el.FunctionMapper; +import javax.servlet.jsp.el.VariableResolver; + +import org.apache.taglibs.standard.lang.support.ExpressionEvaluatorManager; + +/** + * Mock implementation of the JSP 2.0 + * {@link javax.servlet.jsp.el.ExpressionEvaluator} interface, delegating to the + * Jakarta JSTL ExpressionEvaluatorManager. + * <p> + * Used for testing the web framework; only necessary for testing applications + * when testing custom JSP tags. + * <p> + * Note that the Jakarta JSTL implementation (jstl.jar, standard.jar) has to be + * available on the class path to use this expression evaluator. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see org.apache.taglibs.standard.lang.support.ExpressionEvaluatorManager + */ +@SuppressWarnings("deprecation") +public class MockExpressionEvaluator extends ExpressionEvaluator { + + private final PageContext pageContext; + + + /** + * Create a new MockExpressionEvaluator for the given PageContext. + * + * @param pageContext the JSP PageContext to run in + */ + public MockExpressionEvaluator(PageContext pageContext) { + this.pageContext = pageContext; + } + + @SuppressWarnings("rawtypes") + public Expression parseExpression(final String expression, final Class expectedType, + final FunctionMapper functionMapper) throws ELException { + + return new Expression() { + + public Object evaluate(VariableResolver variableResolver) throws ELException { + return doEvaluate(expression, expectedType, functionMapper); + } + }; + } + + @SuppressWarnings("rawtypes") + public Object evaluate(String expression, Class expectedType, VariableResolver variableResolver, + FunctionMapper functionMapper) throws ELException { + + if (variableResolver != null) { + throw new IllegalArgumentException("Custom VariableResolver not supported"); + } + return doEvaluate(expression, expectedType, functionMapper); + } + + @SuppressWarnings("rawtypes") + protected Object doEvaluate(String expression, Class expectedType, FunctionMapper functionMapper) + throws ELException { + + if (functionMapper != null) { + throw new IllegalArgumentException("Custom FunctionMapper not supported"); + } + try { + return ExpressionEvaluatorManager.evaluate("JSP EL expression", expression, expectedType, this.pageContext); + } + catch (JspException ex) { + throw new ELException("Parsing of JSP EL expression \"" + expression + "\" failed", ex); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java b/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java new file mode 100644 index 00000000..a80a2fb4 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.mock.web.PassThroughFilterChain; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * <p>Mock implementation of the {@link javax.servlet.FilterChain} interface. Used + * for testing the web framework; also useful for testing custom + * {@link javax.servlet.Filter} implementations. + * + * <p>A {@link MockFilterChain} can be configured with one or more filters and a + * Servlet to invoke. The first time the chain is called, it invokes all filters + * and the Servlet, and saves the request and response. Subsequent invocations + * raise an {@link IllegalStateException} unless {@link #reset()} is called. + * + * @author Juergen Hoeller + * @author Rob Winch + * @author Rossen Stoyanchev + * + * @since 2.0.3 + * @see MockFilterConfig + * @see PassThroughFilterChain + */ +public class MockFilterChain implements FilterChain { + + private ServletRequest request; + + private ServletResponse response; + + private final List<Filter> filters; + + private Iterator<Filter> iterator; + + + /** + * Register a single do-nothing {@link Filter} implementation. The first + * invocation saves the request and response. Subsequent invocations raise + * an {@link IllegalStateException} unless {@link #reset()} is called. + */ + public MockFilterChain() { + this.filters = Collections.emptyList(); + } + + /** + * Create a FilterChain with a Servlet. + * + * @param servlet the Servlet to invoke + * @since 3.2 + */ + public MockFilterChain(Servlet servlet) { + this.filters = initFilterList(servlet); + } + + /** + * Create a {@code FilterChain} with Filter's and a Servlet. + * + * @param servlet the {@link Servlet} to invoke in this {@link FilterChain} + * @param filters the {@link Filter}'s to invoke in this {@link FilterChain} + * @since 3.2 + */ + public MockFilterChain(Servlet servlet, Filter... filters) { + Assert.notNull(filters, "filters cannot be null"); + Assert.noNullElements(filters, "filters cannot contain null values"); + this.filters = initFilterList(servlet, filters); + } + + private static List<Filter> initFilterList(Servlet servlet, Filter... filters) { + Filter[] allFilters = ObjectUtils.addObjectToArray(filters, new ServletFilterProxy(servlet)); + return Arrays.asList(allFilters); + } + + /** + * Return the request that {@link #doFilter} has been called with. + */ + public ServletRequest getRequest() { + return this.request; + } + + /** + * Return the response that {@link #doFilter} has been called with. + */ + public ServletResponse getResponse() { + return this.response; + } + + /** + * Invoke registered {@link Filter}s and/or {@link Servlet} also saving the + * request and response. + */ + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + + if (this.request != null) { + throw new IllegalStateException("This FilterChain has already been called!"); + } + + if (this.iterator == null) { + this.iterator = this.filters.iterator(); + } + + if (this.iterator.hasNext()) { + Filter nextFilter = this.iterator.next(); + nextFilter.doFilter(request, response, this); + } + + this.request = request; + this.response = response; + } + + /** + * Reset the {@link MockFilterChain} allowing it to be invoked again. + */ + public void reset() { + this.request = null; + this.response = null; + this.iterator = null; + } + + + /** + * A filter that simply delegates to a Servlet. + */ + private static class ServletFilterProxy implements Filter { + + private final Servlet delegateServlet; + + private ServletFilterProxy(Servlet servlet) { + Assert.notNull(servlet, "servlet cannot be null"); + this.delegateServlet = servlet; + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + this.delegateServlet.service(request, response); + } + + public void init(FilterConfig filterConfig) throws ServletException { + } + + public void destroy() { + } + + @Override + public String toString() { + return this.delegateServlet.toString(); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java b/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java new file mode 100644 index 00000000..34cafd55 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.FilterConfig} interface. + * + * <p>Used for testing the web framework; also useful for testing + * custom {@link javax.servlet.Filter} implementations. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see MockFilterChain + * @see PassThroughFilterChain + */ +public class MockFilterConfig implements FilterConfig { + + private final ServletContext servletContext; + + private final String filterName; + + private final Map<String, String> initParameters = new LinkedHashMap<String, String>(); + + + /** + * Create a new MockFilterConfig with a default {@link MockServletContext}. + */ + public MockFilterConfig() { + this(null, ""); + } + + /** + * Create a new MockFilterConfig with a default {@link MockServletContext}. + * @param filterName the name of the filter + */ + public MockFilterConfig(String filterName) { + this(null, filterName); + } + + /** + * Create a new MockFilterConfig. + * @param servletContext the ServletContext that the servlet runs in + */ + public MockFilterConfig(ServletContext servletContext) { + this(servletContext, ""); + } + + /** + * Create a new MockFilterConfig. + * @param servletContext the ServletContext that the servlet runs in + * @param filterName the name of the filter + */ + public MockFilterConfig(ServletContext servletContext, String filterName) { + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.filterName = filterName; + } + + + public String getFilterName() { + return filterName; + } + + public ServletContext getServletContext() { + return servletContext; + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + public Enumeration<String> getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java new file mode 100644 index 00000000..178dffd6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -0,0 +1,917 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.mock.web; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.StringUtils; + +/** + * Mock implementation of the {@link javax.servlet.http.HttpServletRequest} interface. + * + * <p>Compatible with Servlet 2.5 and partially with Servlet 3.0 (notable exceptions: + * the {@code getPart(s)} and {@code startAsync} families of methods). + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Rick Evans + * @author Mark Fisher + * @author Sam Brannen + * @since 1.0.2 + */ +public class MockHttpServletRequest implements HttpServletRequest { + + /** + * The default protocol: 'http'. + */ + public static final String DEFAULT_PROTOCOL = "http"; + + /** + * The default server address: '127.0.0.1'. + */ + public static final String DEFAULT_SERVER_ADDR = "127.0.0.1"; + + /** + * The default server name: 'localhost'. + */ + public static final String DEFAULT_SERVER_NAME = "localhost"; + + /** + * The default server port: '80'. + */ + public static final int DEFAULT_SERVER_PORT = 80; + + /** + * The default remote address: '127.0.0.1'. + */ + public static final String DEFAULT_REMOTE_ADDR = "127.0.0.1"; + + /** + * The default remote host: 'localhost'. + */ + public static final String DEFAULT_REMOTE_HOST = "localhost"; + + private static final String CONTENT_TYPE_HEADER = "Content-Type"; + + private static final String CHARSET_PREFIX = "charset="; + + + private boolean active = true; + + + // --------------------------------------------------------------------- + // ServletRequest properties + // --------------------------------------------------------------------- + + private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(); + + private String characterEncoding; + + private byte[] content; + + private String contentType; + + private final Map<String, String[]> parameters = new LinkedHashMap<String, String[]>(16); + + private String protocol = DEFAULT_PROTOCOL; + + private String scheme = DEFAULT_PROTOCOL; + + private String serverName = DEFAULT_SERVER_NAME; + + private int serverPort = DEFAULT_SERVER_PORT; + + private String remoteAddr = DEFAULT_REMOTE_ADDR; + + private String remoteHost = DEFAULT_REMOTE_HOST; + + /** List of locales in descending order */ + private final List<Locale> locales = new LinkedList<Locale>(); + + private boolean secure = false; + + private final ServletContext servletContext; + + private int remotePort = DEFAULT_SERVER_PORT; + + private String localName = DEFAULT_SERVER_NAME; + + private String localAddr = DEFAULT_SERVER_ADDR; + + private int localPort = DEFAULT_SERVER_PORT; + + + // --------------------------------------------------------------------- + // HttpServletRequest properties + // --------------------------------------------------------------------- + + private String authType; + + private Cookie[] cookies; + + private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<HeaderValueHolder>(); + + private String method; + + private String pathInfo; + + private String contextPath = ""; + + private String queryString; + + private String remoteUser; + + private final Set<String> userRoles = new HashSet<String>(); + + private Principal userPrincipal; + + private String requestedSessionId; + + private String requestURI; + + private String servletPath = ""; + + private HttpSession session; + + private boolean requestedSessionIdValid = true; + + private boolean requestedSessionIdFromCookie = true; + + private boolean requestedSessionIdFromURL = false; + + + // --------------------------------------------------------------------- + // Constructors + // --------------------------------------------------------------------- + + /** + * Create a new {@code MockHttpServletRequest} with a default + * {@link MockServletContext}. + * @see #MockHttpServletRequest(ServletContext, String, String) + */ + public MockHttpServletRequest() { + this(null, "", ""); + } + + /** + * Create a new {@code MockHttpServletRequest} with a default + * {@link MockServletContext}. + * @param method the request method (may be {@code null}) + * @param requestURI the request URI (may be {@code null}) + * @see #setMethod + * @see #setRequestURI + * @see #MockHttpServletRequest(ServletContext, String, String) + */ + public MockHttpServletRequest(String method, String requestURI) { + this(null, method, requestURI); + } + + /** + * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}. + * @param servletContext the ServletContext that the request runs in + * (may be {@code null} to use a default {@link MockServletContext}) + * @see #MockHttpServletRequest(ServletContext, String, String) + */ + public MockHttpServletRequest(ServletContext servletContext) { + this(servletContext, "", ""); + } + + /** + * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}, + * {@code method}, and {@code requestURI}. + * <p>The preferred locale will be set to {@link Locale#ENGLISH}. + * @param servletContext the ServletContext that the request runs in (may be + * {@code null} to use a default {@link MockServletContext}) + * @param method the request method (may be {@code null}) + * @param requestURI the request URI (may be {@code null}) + * @see #setMethod + * @see #setRequestURI + * @see #setPreferredLocales + * @see MockServletContext + */ + public MockHttpServletRequest(ServletContext servletContext, String method, String requestURI) { + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.method = method; + this.requestURI = requestURI; + this.locales.add(Locale.ENGLISH); + } + + + // --------------------------------------------------------------------- + // Lifecycle methods + // --------------------------------------------------------------------- + + /** + * Return the ServletContext that this request is associated with. (Not + * available in the standard HttpServletRequest interface for some reason.) + */ + public ServletContext getServletContext() { + return this.servletContext; + } + + /** + * Return whether this request is still active (that is, not completed yet). + */ + public boolean isActive() { + return this.active; + } + + /** + * Mark this request as completed, keeping its state. + */ + public void close() { + this.active = false; + } + + /** + * Invalidate this request, clearing its state. + */ + public void invalidate() { + close(); + clearAttributes(); + } + + /** + * Check whether this request is still active (that is, not completed yet), + * throwing an IllegalStateException if not active anymore. + */ + protected void checkActive() throws IllegalStateException { + if (!this.active) { + throw new IllegalStateException("Request is not active anymore"); + } + } + + + // --------------------------------------------------------------------- + // ServletRequest interface + // --------------------------------------------------------------------- + + public Object getAttribute(String name) { + checkActive(); + return this.attributes.get(name); + } + + public Enumeration<String> getAttributeNames() { + checkActive(); + return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet())); + } + + public String getCharacterEncoding() { + return this.characterEncoding; + } + + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + updateContentTypeHeader(); + } + + private void updateContentTypeHeader() { + if (StringUtils.hasLength(this.contentType)) { + StringBuilder sb = new StringBuilder(this.contentType); + if (!this.contentType.toLowerCase().contains(CHARSET_PREFIX) && + StringUtils.hasLength(this.characterEncoding)) { + sb.append(";").append(CHARSET_PREFIX).append(this.characterEncoding); + } + doAddHeaderValue(CONTENT_TYPE_HEADER, sb.toString(), true); + } + } + + public void setContent(byte[] content) { + this.content = content; + } + + public int getContentLength() { + return (this.content != null ? this.content.length : -1); + } + + public void setContentType(String contentType) { + this.contentType = contentType; + if (contentType != null) { + int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX); + if (charsetIndex != -1) { + this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length()); + } + updateContentTypeHeader(); + } + } + + public String getContentType() { + return this.contentType; + } + + public ServletInputStream getInputStream() { + if (this.content != null) { + return new DelegatingServletInputStream(new ByteArrayInputStream(this.content)); + } + else { + return null; + } + } + + /** + * Set a single value for the specified HTTP parameter. + * <p>If there are already one or more values registered for the given + * parameter name, they will be replaced. + */ + public void setParameter(String name, String value) { + setParameter(name, new String[] {value}); + } + + /** + * Set an array of values for the specified HTTP parameter. + * <p>If there are already one or more values registered for the given + * parameter name, they will be replaced. + */ + public void setParameter(String name, String[] values) { + Assert.notNull(name, "Parameter name must not be null"); + this.parameters.put(name, values); + } + + /** + * Sets all provided parameters <strong>replacing</strong> any existing + * values for the provided parameter names. To add without replacing + * existing values, use {@link #addParameters(java.util.Map)}. + */ + @SuppressWarnings("rawtypes") + public void setParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + for (Object key : params.keySet()) { + Assert.isInstanceOf(String.class, key, + "Parameter map key must be of type [" + String.class.getName() + "]"); + Object value = params.get(key); + if (value instanceof String) { + this.setParameter((String) key, (String) value); + } + else if (value instanceof String[]) { + this.setParameter((String) key, (String[]) value); + } + else { + throw new IllegalArgumentException( + "Parameter map value must be single value " + " or array of type [" + String.class.getName() + "]"); + } + } + } + + /** + * Add a single value for the specified HTTP parameter. + * <p>If there are already one or more values registered for the given + * parameter name, the given value will be added to the end of the list. + */ + public void addParameter(String name, String value) { + addParameter(name, new String[] {value}); + } + + /** + * Add an array of values for the specified HTTP parameter. + * <p>If there are already one or more values registered for the given + * parameter name, the given values will be added to the end of the list. + */ + public void addParameter(String name, String[] values) { + Assert.notNull(name, "Parameter name must not be null"); + String[] oldArr = this.parameters.get(name); + if (oldArr != null) { + String[] newArr = new String[oldArr.length + values.length]; + System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); + System.arraycopy(values, 0, newArr, oldArr.length, values.length); + this.parameters.put(name, newArr); + } + else { + this.parameters.put(name, values); + } + } + + /** + * Adds all provided parameters <strong>without</strong> replacing any + * existing values. To replace existing values, use + * {@link #setParameters(java.util.Map)}. + */ + @SuppressWarnings("rawtypes") + public void addParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + for (Object key : params.keySet()) { + Assert.isInstanceOf(String.class, key, + "Parameter map key must be of type [" + String.class.getName() + "]"); + Object value = params.get(key); + if (value instanceof String) { + this.addParameter((String) key, (String) value); + } + else if (value instanceof String[]) { + this.addParameter((String) key, (String[]) value); + } + else { + throw new IllegalArgumentException("Parameter map value must be single value " + + " or array of type [" + String.class.getName() + "]"); + } + } + } + + /** + * Remove already registered values for the specified HTTP parameter, if any. + */ + public void removeParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + this.parameters.remove(name); + } + + /** + * Removes all existing parameters. + */ + public void removeAllParameters() { + this.parameters.clear(); + } + + public String getParameter(String name) { + String[] arr = (name != null ? this.parameters.get(name) : null); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + public Enumeration<String> getParameterNames() { + return Collections.enumeration(this.parameters.keySet()); + } + + public String[] getParameterValues(String name) { + return (name != null ? this.parameters.get(name) : null); + } + + public Map<String, String[]> getParameterMap() { + return Collections.unmodifiableMap(this.parameters); + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getProtocol() { + return this.protocol; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public String getScheme() { + return this.scheme; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public String getServerName() { + return this.serverName; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public int getServerPort() { + return this.serverPort; + } + + public BufferedReader getReader() throws UnsupportedEncodingException { + if (this.content != null) { + InputStream sourceStream = new ByteArrayInputStream(this.content); + Reader sourceReader = (this.characterEncoding != null) ? + new InputStreamReader(sourceStream, this.characterEncoding) : new InputStreamReader(sourceStream); + return new BufferedReader(sourceReader); + } + else { + return null; + } + } + + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + public String getRemoteAddr() { + return this.remoteAddr; + } + + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + public String getRemoteHost() { + return this.remoteHost; + } + + public void setAttribute(String name, Object value) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + public void removeAttribute(String name) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + this.attributes.remove(name); + } + + /** + * Clear all of this request's attributes. + */ + public void clearAttributes() { + this.attributes.clear(); + } + + /** + * Add a new preferred locale, before any existing locales. + * @see #setPreferredLocales + */ + public void addPreferredLocale(Locale locale) { + Assert.notNull(locale, "Locale must not be null"); + this.locales.add(0, locale); + } + + /** + * Set the list of preferred locales, in descending order, effectively replacing + * any existing locales. + * @see #addPreferredLocale + * @since 3.2 + */ + public void setPreferredLocales(List<Locale> locales) { + Assert.notEmpty(locales, "Locale list must not be empty"); + this.locales.clear(); + this.locales.addAll(locales); + } + + public Locale getLocale() { + return this.locales.get(0); + } + + public Enumeration<Locale> getLocales() { + return Collections.enumeration(this.locales); + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public boolean isSecure() { + return this.secure; + } + + public RequestDispatcher getRequestDispatcher(String path) { + return new MockRequestDispatcher(path); + } + + public String getRealPath(String path) { + return this.servletContext.getRealPath(path); + } + + public void setRemotePort(int remotePort) { + this.remotePort = remotePort; + } + + public int getRemotePort() { + return this.remotePort; + } + + public void setLocalName(String localName) { + this.localName = localName; + } + + public String getLocalName() { + return this.localName; + } + + public void setLocalAddr(String localAddr) { + this.localAddr = localAddr; + } + + public String getLocalAddr() { + return this.localAddr; + } + + public void setLocalPort(int localPort) { + this.localPort = localPort; + } + + public int getLocalPort() { + return this.localPort; + } + + + // --------------------------------------------------------------------- + // HttpServletRequest interface + // --------------------------------------------------------------------- + + public void setAuthType(String authType) { + this.authType = authType; + } + + public String getAuthType() { + return this.authType; + } + + public void setCookies(Cookie... cookies) { + this.cookies = cookies; + } + + public Cookie[] getCookies() { + return this.cookies; + } + + /** + * Add a header entry for the given name. + * <p>If there was no entry for that header name before, the value will be used + * as-is. In case of an existing entry, a String array will be created, + * adding the given value (more specifically, its toString representation) + * as further element. + * <p>Multiple values can only be stored as list of Strings, following the + * Servlet spec (see {@code getHeaders} accessor). As alternative to + * repeated {@code addHeader} calls for individual elements, you can + * use a single call with an entire array or Collection of values as + * parameter. + * @see #getHeaderNames + * @see #getHeader + * @see #getHeaders + * @see #getDateHeader + * @see #getIntHeader + */ + public void addHeader(String name, Object value) { + if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name)) { + setContentType((String) value); + return; + } + doAddHeaderValue(name, value, false); + } + + @SuppressWarnings("rawtypes") + private void doAddHeaderValue(String name, Object value, boolean replace) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + Assert.notNull(value, "Header value must not be null"); + if (header == null || replace) { + header = new HeaderValueHolder(); + this.headers.put(name, header); + } + if (value instanceof Collection) { + header.addValues((Collection) value); + } + else if (value.getClass().isArray()) { + header.addValueArray(value); + } + else { + header.addValue(value); + } + } + + public long getDateHeader(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + Object value = (header != null ? header.getValue() : null); + if (value instanceof Date) { + return ((Date) value).getTime(); + } + else if (value instanceof Number) { + return ((Number) value).longValue(); + } + else if (value != null) { + throw new IllegalArgumentException( + "Value for header '" + name + "' is neither a Date nor a Number: " + value); + } + else { + return -1L; + } + } + + public String getHeader(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + return (header != null ? header.getStringValue() : null); + } + + public Enumeration<String> getHeaders(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + return Collections.enumeration(header != null ? header.getStringValues() : new LinkedList<String>()); + } + + public Enumeration<String> getHeaderNames() { + return Collections.enumeration(this.headers.keySet()); + } + + public int getIntHeader(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + Object value = (header != null ? header.getValue() : null); + if (value instanceof Number) { + return ((Number) value).intValue(); + } + else if (value instanceof String) { + return Integer.parseInt((String) value); + } + else if (value != null) { + throw new NumberFormatException("Value for header '" + name + "' is not a Number: " + value); + } + else { + return -1; + } + } + + public void setMethod(String method) { + this.method = method; + } + + public String getMethod() { + return this.method; + } + + public void setPathInfo(String pathInfo) { + this.pathInfo = pathInfo; + } + + public String getPathInfo() { + return this.pathInfo; + } + + public String getPathTranslated() { + return (this.pathInfo != null ? getRealPath(this.pathInfo) : null); + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + public String getContextPath() { + return this.contextPath; + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + public String getQueryString() { + return this.queryString; + } + + public void setRemoteUser(String remoteUser) { + this.remoteUser = remoteUser; + } + + public String getRemoteUser() { + return this.remoteUser; + } + + public void addUserRole(String role) { + this.userRoles.add(role); + } + + public boolean isUserInRole(String role) { + return (this.userRoles.contains(role) || (this.servletContext instanceof MockServletContext && + ((MockServletContext) this.servletContext).getDeclaredRoles().contains(role))); + } + + public void setUserPrincipal(Principal userPrincipal) { + this.userPrincipal = userPrincipal; + } + + public Principal getUserPrincipal() { + return this.userPrincipal; + } + + public void setRequestedSessionId(String requestedSessionId) { + this.requestedSessionId = requestedSessionId; + } + + public String getRequestedSessionId() { + return this.requestedSessionId; + } + + public void setRequestURI(String requestURI) { + this.requestURI = requestURI; + } + + public String getRequestURI() { + return this.requestURI; + } + + public StringBuffer getRequestURL() { + StringBuffer url = new StringBuffer(this.scheme); + url.append("://").append(this.serverName).append(':').append(this.serverPort); + url.append(getRequestURI()); + return url; + } + + public void setServletPath(String servletPath) { + this.servletPath = servletPath; + } + + public String getServletPath() { + return this.servletPath; + } + + public void setSession(HttpSession session) { + this.session = session; + if (session instanceof MockHttpSession) { + MockHttpSession mockSession = ((MockHttpSession) session); + mockSession.access(); + } + } + + public HttpSession getSession(boolean create) { + checkActive(); + // Reset session if invalidated. + if (this.session instanceof MockHttpSession && ((MockHttpSession) this.session).isInvalid()) { + this.session = null; + } + // Create new session if necessary. + if (this.session == null && create) { + this.session = new MockHttpSession(this.servletContext); + } + return this.session; + } + + public HttpSession getSession() { + return getSession(true); + } + + public void setRequestedSessionIdValid(boolean requestedSessionIdValid) { + this.requestedSessionIdValid = requestedSessionIdValid; + } + + public boolean isRequestedSessionIdValid() { + return this.requestedSessionIdValid; + } + + public void setRequestedSessionIdFromCookie(boolean requestedSessionIdFromCookie) { + this.requestedSessionIdFromCookie = requestedSessionIdFromCookie; + } + + public boolean isRequestedSessionIdFromCookie() { + return this.requestedSessionIdFromCookie; + } + + public void setRequestedSessionIdFromURL(boolean requestedSessionIdFromURL) { + this.requestedSessionIdFromURL = requestedSessionIdFromURL; + } + + public boolean isRequestedSessionIdFromURL() { + return this.requestedSessionIdFromURL; + } + + public boolean isRequestedSessionIdFromUrl() { + return isRequestedSessionIdFromURL(); + } + + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + return (this.userPrincipal != null && this.remoteUser != null && this.authType != null); + } + + public void login(String username, String password) throws ServletException { + throw new ServletException("Username-password authentication not supported - override the login method"); + } + + public void logout() throws ServletException { + this.userPrincipal = null; + this.remoteUser = null; + this.authType = null; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java new file mode 100644 index 00000000..b575c402 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -0,0 +1,603 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.mock.web; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.web.util.WebUtils; + +/** + * Mock implementation of the {@link javax.servlet.http.HttpServletResponse} interface. + * + * <p>Compatible with Servlet 2.5 as well as Servlet 3.0. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 1.0.2 + */ +public class MockHttpServletResponse implements HttpServletResponse { + + private static final String CHARSET_PREFIX = "charset="; + + private static final String CONTENT_TYPE_HEADER = "Content-Type"; + + private static final String CONTENT_LENGTH_HEADER = "Content-Length"; + + private static final String LOCATION_HEADER = "Location"; + + + //--------------------------------------------------------------------- + // ServletResponse properties + //--------------------------------------------------------------------- + + private boolean outputStreamAccessAllowed = true; + + private boolean writerAccessAllowed = true; + + private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; + + private boolean charset = false; + + private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024); + + private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content); + + private PrintWriter writer; + + private int contentLength = 0; + + private String contentType; + + private int bufferSize = 4096; + + private boolean committed; + + private Locale locale = Locale.getDefault(); + + + //--------------------------------------------------------------------- + // HttpServletResponse properties + //--------------------------------------------------------------------- + + private final List<Cookie> cookies = new ArrayList<Cookie>(); + + private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<HeaderValueHolder>(); + + private int status = HttpServletResponse.SC_OK; + + private String errorMessage; + + private String forwardedUrl; + + private final List<String> includedUrls = new ArrayList<String>(); + + + //--------------------------------------------------------------------- + // ServletResponse interface + //--------------------------------------------------------------------- + + /** + * Set whether {@link #getOutputStream()} access is allowed. + * <p>Default is {@code true}. + */ + public void setOutputStreamAccessAllowed(boolean outputStreamAccessAllowed) { + this.outputStreamAccessAllowed = outputStreamAccessAllowed; + } + + /** + * Return whether {@link #getOutputStream()} access is allowed. + */ + public boolean isOutputStreamAccessAllowed() { + return this.outputStreamAccessAllowed; + } + + /** + * Set whether {@link #getWriter()} access is allowed. + * <p>Default is {@code true}. + */ + public void setWriterAccessAllowed(boolean writerAccessAllowed) { + this.writerAccessAllowed = writerAccessAllowed; + } + + /** + * Return whether {@link #getOutputStream()} access is allowed. + */ + public boolean isWriterAccessAllowed() { + return this.writerAccessAllowed; + } + + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + this.charset = true; + updateContentTypeHeader(); + } + + private void updateContentTypeHeader() { + if (this.contentType != null) { + StringBuilder sb = new StringBuilder(this.contentType); + if (!this.contentType.toLowerCase().contains(CHARSET_PREFIX) && this.charset) { + sb.append(";").append(CHARSET_PREFIX).append(this.characterEncoding); + } + doAddHeaderValue(CONTENT_TYPE_HEADER, sb.toString(), true); + } + } + + public String getCharacterEncoding() { + return this.characterEncoding; + } + + public ServletOutputStream getOutputStream() { + if (!this.outputStreamAccessAllowed) { + throw new IllegalStateException("OutputStream access not allowed"); + } + return this.outputStream; + } + + public PrintWriter getWriter() throws UnsupportedEncodingException { + if (!this.writerAccessAllowed) { + throw new IllegalStateException("Writer access not allowed"); + } + if (this.writer == null) { + Writer targetWriter = (this.characterEncoding != null ? + new OutputStreamWriter(this.content, this.characterEncoding) : new OutputStreamWriter(this.content)); + this.writer = new ResponsePrintWriter(targetWriter); + } + return this.writer; + } + + public byte[] getContentAsByteArray() { + flushBuffer(); + return this.content.toByteArray(); + } + + public String getContentAsString() throws UnsupportedEncodingException { + flushBuffer(); + return (this.characterEncoding != null ? + this.content.toString(this.characterEncoding) : this.content.toString()); + } + + public void setContentLength(int contentLength) { + this.contentLength = contentLength; + doAddHeaderValue(CONTENT_LENGTH_HEADER, contentLength, true); + } + + public int getContentLength() { + return this.contentLength; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + if (contentType != null) { + int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX); + if (charsetIndex != -1) { + this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length()); + this.charset = true; + } + updateContentTypeHeader(); + } + } + + public String getContentType() { + return this.contentType; + } + + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + public int getBufferSize() { + return this.bufferSize; + } + + public void flushBuffer() { + setCommitted(true); + } + + public void resetBuffer() { + if (isCommitted()) { + throw new IllegalStateException("Cannot reset buffer - response is already committed"); + } + this.content.reset(); + } + + private void setCommittedIfBufferSizeExceeded() { + int bufSize = getBufferSize(); + if (bufSize > 0 && this.content.size() > bufSize) { + setCommitted(true); + } + } + + public void setCommitted(boolean committed) { + this.committed = committed; + } + + public boolean isCommitted() { + return this.committed; + } + + public void reset() { + resetBuffer(); + this.characterEncoding = null; + this.contentLength = 0; + this.contentType = null; + this.locale = null; + this.cookies.clear(); + this.headers.clear(); + this.status = HttpServletResponse.SC_OK; + this.errorMessage = null; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public Locale getLocale() { + return this.locale; + } + + + //--------------------------------------------------------------------- + // HttpServletResponse interface + //--------------------------------------------------------------------- + + public void addCookie(Cookie cookie) { + Assert.notNull(cookie, "Cookie must not be null"); + this.cookies.add(cookie); + } + + public Cookie[] getCookies() { + return this.cookies.toArray(new Cookie[this.cookies.size()]); + } + + public Cookie getCookie(String name) { + Assert.notNull(name, "Cookie name must not be null"); + for (Cookie cookie : this.cookies) { + if (name.equals(cookie.getName())) { + return cookie; + } + } + return null; + } + + public boolean containsHeader(String name) { + return (HeaderValueHolder.getByName(this.headers, name) != null); + } + + /** + * Return the names of all specified headers as a Set of Strings. + * <p>As of Servlet 3.0, this method is also defined HttpServletResponse. + * @return the {@code Set} of header name {@code Strings}, or an empty {@code Set} if none + */ + public Collection<String> getHeaderNames() { + return this.headers.keySet(); + } + + /** + * Return the primary value for the given header as a String, if any. + * Will return the first value in case of multiple values. + * <p>As of Servlet 3.0, this method is also defined in HttpServletResponse. + * As of Spring 3.1, it returns a stringified value for Servlet 3.0 compatibility. + * Consider using {@link #getHeaderValue(String)} for raw Object access. + * @param name the name of the header + * @return the associated header value, or {@code null} if none + */ + public String getHeader(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + return (header != null ? header.getStringValue() : null); + } + + /** + * Return all values for the given header as a List of Strings. + * <p>As of Servlet 3.0, this method is also defined in HttpServletResponse. + * As of Spring 3.1, it returns a List of stringified values for Servlet 3.0 compatibility. + * Consider using {@link #getHeaderValues(String)} for raw Object access. + * @param name the name of the header + * @return the associated header values, or an empty List if none + */ + public List<String> getHeaders(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + if (header != null) { + return header.getStringValues(); + } + else { + return Collections.emptyList(); + } + } + + /** + * Return the primary value for the given header, if any. + * <p>Will return the first value in case of multiple values. + * @param name the name of the header + * @return the associated header value, or {@code null} if none + */ + public Object getHeaderValue(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + return (header != null ? header.getValue() : null); + } + + /** + * Return all values for the given header as a List of value objects. + * @param name the name of the header + * @return the associated header values, or an empty List if none + */ + public List<Object> getHeaderValues(String name) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + if (header != null) { + return header.getValues(); + } + else { + return Collections.emptyList(); + } + } + + /** + * The default implementation returns the given URL String as-is. + * <p>Can be overridden in subclasses, appending a session id or the like. + */ + public String encodeURL(String url) { + return url; + } + + /** + * The default implementation delegates to {@link #encodeURL}, + * returning the given URL String as-is. + * <p>Can be overridden in subclasses, appending a session id or the like + * in a redirect-specific fashion. For general URL encoding rules, + * override the common {@link #encodeURL} method instead, applying + * to redirect URLs as well as to general URLs. + */ + public String encodeRedirectURL(String url) { + return encodeURL(url); + } + + public String encodeUrl(String url) { + return encodeURL(url); + } + + public String encodeRedirectUrl(String url) { + return encodeRedirectURL(url); + } + + public void sendError(int status, String errorMessage) throws IOException { + if (isCommitted()) { + throw new IllegalStateException("Cannot set error status - response is already committed"); + } + this.status = status; + this.errorMessage = errorMessage; + setCommitted(true); + } + + public void sendError(int status) throws IOException { + if (isCommitted()) { + throw new IllegalStateException("Cannot set error status - response is already committed"); + } + this.status = status; + setCommitted(true); + } + + public void sendRedirect(String url) throws IOException { + if (isCommitted()) { + throw new IllegalStateException("Cannot send redirect - response is already committed"); + } + Assert.notNull(url, "Redirect URL must not be null"); + setHeader(LOCATION_HEADER, url); + setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + setCommitted(true); + } + + public String getRedirectedUrl() { + return getHeader(LOCATION_HEADER); + } + + public void setDateHeader(String name, long value) { + setHeaderValue(name, value); + } + + public void addDateHeader(String name, long value) { + addHeaderValue(name, value); + } + + public void setHeader(String name, String value) { + setHeaderValue(name, value); + } + + public void addHeader(String name, String value) { + addHeaderValue(name, value); + } + + public void setIntHeader(String name, int value) { + setHeaderValue(name, value); + } + + public void addIntHeader(String name, int value) { + addHeaderValue(name, value); + } + + private void setHeaderValue(String name, Object value) { + if (setSpecialHeader(name, value)) { + return; + } + doAddHeaderValue(name, value, true); + } + + private void addHeaderValue(String name, Object value) { + if (setSpecialHeader(name, value)) { + return; + } + doAddHeaderValue(name, value, false); + } + + private boolean setSpecialHeader(String name, Object value) { + if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name)) { + setContentType((String) value); + return true; + } + else if (CONTENT_LENGTH_HEADER.equalsIgnoreCase(name)) { + setContentLength(Integer.parseInt((String) value)); + return true; + } + else { + return false; + } + } + + private void doAddHeaderValue(String name, Object value, boolean replace) { + HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); + Assert.notNull(value, "Header value must not be null"); + if (header == null) { + header = new HeaderValueHolder(); + this.headers.put(name, header); + } + if (replace) { + header.setValue(value); + } + else { + header.addValue(value); + } + } + + public void setStatus(int status) { + this.status = status; + } + + public void setStatus(int status, String errorMessage) { + this.status = status; + this.errorMessage = errorMessage; + } + + public int getStatus() { + return this.status; + } + + public String getErrorMessage() { + return this.errorMessage; + } + + + //--------------------------------------------------------------------- + // Methods for MockRequestDispatcher + //--------------------------------------------------------------------- + + public void setForwardedUrl(String forwardedUrl) { + this.forwardedUrl = forwardedUrl; + } + + public String getForwardedUrl() { + return this.forwardedUrl; + } + + public void setIncludedUrl(String includedUrl) { + this.includedUrls.clear(); + if (includedUrl != null) { + this.includedUrls.add(includedUrl); + } + } + + public String getIncludedUrl() { + int count = this.includedUrls.size(); + if (count > 1) { + throw new IllegalStateException( + "More than 1 URL included - check getIncludedUrls instead: " + this.includedUrls); + } + return (count == 1 ? this.includedUrls.get(0) : null); + } + + public void addIncludedUrl(String includedUrl) { + Assert.notNull(includedUrl, "Included URL must not be null"); + this.includedUrls.add(includedUrl); + } + + public List<String> getIncludedUrls() { + return this.includedUrls; + } + + + /** + * Inner class that adapts the ServletOutputStream to mark the + * response as committed once the buffer size is exceeded. + */ + private class ResponseServletOutputStream extends DelegatingServletOutputStream { + + public ResponseServletOutputStream(OutputStream out) { + super(out); + } + + public void write(int b) throws IOException { + super.write(b); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + public void flush() throws IOException { + super.flush(); + setCommitted(true); + } + } + + + /** + * Inner class that adapts the PrintWriter to mark the + * response as committed once the buffer size is exceeded. + */ + private class ResponsePrintWriter extends PrintWriter { + + public ResponsePrintWriter(Writer out) { + super(out, true); + } + + public void write(char buf[], int off, int len) { + super.write(buf, off, len); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + public void write(String s, int off, int len) { + super.write(s, off, len); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + public void write(int c) { + super.write(c); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + public void flush() { + super.flush(); + setCommitted(true); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java new file mode 100644 index 00000000..548c2016 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; +import javax.servlet.http.HttpSessionContext; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.http.HttpSession} interface. + * + * <p>Compatible with Servlet 2.5 as well as Servlet 3.0. + * + * <p>Used for testing the web framework; also useful for testing application + * controllers. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Mark Fisher + * @author Sam Brannen + * @since 1.0.2 + */ +@SuppressWarnings("deprecation") +public class MockHttpSession implements HttpSession { + + public static final String SESSION_COOKIE_NAME = "JSESSION"; + + + private static int nextId = 1; + + private final String id; + + private final long creationTime = System.currentTimeMillis(); + + private int maxInactiveInterval; + + private long lastAccessedTime = System.currentTimeMillis(); + + private final ServletContext servletContext; + + private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(); + + private boolean invalid = false; + + private boolean isNew = true; + + + /** + * Create a new MockHttpSession with a default {@link MockServletContext}. + * + * @see MockServletContext + */ + public MockHttpSession() { + this(null); + } + + /** + * Create a new MockHttpSession. + * + * @param servletContext the ServletContext that the session runs in + */ + public MockHttpSession(ServletContext servletContext) { + this(servletContext, null); + } + + /** + * Create a new MockHttpSession. + * + * @param servletContext the ServletContext that the session runs in + * @param id a unique identifier for this session + */ + public MockHttpSession(ServletContext servletContext, String id) { + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.id = (id != null ? id : Integer.toString(nextId++)); + } + + public long getCreationTime() { + return this.creationTime; + } + + public String getId() { + return this.id; + } + + public void access() { + this.lastAccessedTime = System.currentTimeMillis(); + this.isNew = false; + } + + public long getLastAccessedTime() { + return this.lastAccessedTime; + } + + public ServletContext getServletContext() { + return this.servletContext; + } + + public void setMaxInactiveInterval(int interval) { + this.maxInactiveInterval = interval; + } + + public int getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + public HttpSessionContext getSessionContext() { + throw new UnsupportedOperationException("getSessionContext"); + } + + public Object getAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + return this.attributes.get(name); + } + + public Object getValue(String name) { + return getAttribute(name); + } + + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet())); + } + + public String[] getValueNames() { + return this.attributes.keySet().toArray(new String[this.attributes.size()]); + } + + public void setAttribute(String name, Object value) { + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value)); + } + } + else { + removeAttribute(name); + } + } + + public void putValue(String name, Object value) { + setAttribute(name, value); + } + + public void removeAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + Object value = this.attributes.remove(name); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + + public void removeValue(String name) { + removeAttribute(name); + } + + /** + * Clear all of this session's attributes. + */ + public void clearAttributes() { + for (Iterator<Map.Entry<String, Object>> it = this.attributes.entrySet().iterator(); it.hasNext();) { + Map.Entry<String, Object> entry = it.next(); + String name = entry.getKey(); + Object value = entry.getValue(); + it.remove(); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + } + + /** + * Invalidates this session then unbinds any objects bound to it. + * + * @throws IllegalStateException if this method is called on an already invalidated session + */ + public void invalidate() { + if (this.invalid) { + throw new IllegalStateException("The session has already been invalidated"); + } + + // else + this.invalid = true; + clearAttributes(); + } + + public boolean isInvalid() { + return this.invalid; + } + + public void setNew(boolean value) { + this.isNew = value; + } + + public boolean isNew() { + return this.isNew; + } + + /** + * Serialize the attributes of this session into an object that can be + * turned into a byte array with standard Java serialization. + * + * @return a representation of this session's serialized state + */ + public Serializable serializeState() { + HashMap<String, Serializable> state = new HashMap<String, Serializable>(); + for (Iterator<Map.Entry<String, Object>> it = this.attributes.entrySet().iterator(); it.hasNext();) { + Map.Entry<String, Object> entry = it.next(); + String name = entry.getKey(); + Object value = entry.getValue(); + it.remove(); + if (value instanceof Serializable) { + state.put(name, (Serializable) value); + } + else { + // Not serializable... Servlet containers usually automatically + // unbind the attribute in this case. + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + } + return state; + } + + /** + * Deserialize the attributes of this session from a state object created by + * {@link #serializeState()}. + * + * @param state a representation of this session's serialized state + */ + @SuppressWarnings("unchecked") + public void deserializeState(Serializable state) { + Assert.isTrue(state instanceof Map, "Serialized state needs to be of type [java.util.Map]"); + this.attributes.putAll((Map<String, Object>) state); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java b/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java new file mode 100644 index 00000000..1d5bf928 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.JspWriter; + +/** + * Mock implementation of the {@link javax.servlet.jsp.JspWriter} class. + * + * <p>Used for testing the web framework; only necessary for testing + * applications when testing custom JSP tags. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class MockJspWriter extends JspWriter { + + private final HttpServletResponse response; + + private PrintWriter targetWriter; + + + /** + * Create a MockJspWriter for the given response, + * using the response's default Writer. + * @param response the servlet response to wrap + */ + public MockJspWriter(HttpServletResponse response) { + this(response, null); + } + + /** + * Create a MockJspWriter for the given plain Writer. + * @param targetWriter the target Writer to wrap + */ + public MockJspWriter(Writer targetWriter) { + this(null, targetWriter); + } + + /** + * Create a MockJspWriter for the given response. + * @param response the servlet response to wrap + * @param targetWriter the target Writer to wrap + */ + public MockJspWriter(HttpServletResponse response, Writer targetWriter) { + super(DEFAULT_BUFFER, true); + this.response = (response != null ? response : new MockHttpServletResponse()); + if (targetWriter instanceof PrintWriter) { + this.targetWriter = (PrintWriter) targetWriter; + } + else if (targetWriter != null) { + this.targetWriter = new PrintWriter(targetWriter); + } + } + + /** + * Lazily initialize the target Writer. + */ + protected PrintWriter getTargetWriter() throws IOException { + if (this.targetWriter == null) { + this.targetWriter = this.response.getWriter(); + } + return this.targetWriter; + } + + + public void clear() throws IOException { + if (this.response.isCommitted()) { + throw new IOException("Response already committed"); + } + this.response.resetBuffer(); + } + + public void clearBuffer() throws IOException { + } + + public void flush() throws IOException { + this.response.flushBuffer(); + } + + public void close() throws IOException { + flush(); + } + + public int getRemaining() { + return Integer.MAX_VALUE; + } + + public void newLine() throws IOException { + getTargetWriter().println(); + } + + public void write(char value[], int offset, int length) throws IOException { + getTargetWriter().write(value, offset, length); + } + + public void print(boolean value) throws IOException { + getTargetWriter().print(value); + } + + public void print(char value) throws IOException { + getTargetWriter().print(value); + } + + public void print(char[] value) throws IOException { + getTargetWriter().print(value); + } + + public void print(double value) throws IOException { + getTargetWriter().print(value); + } + + public void print(float value) throws IOException { + getTargetWriter().print(value); + } + + public void print(int value) throws IOException { + getTargetWriter().print(value); + } + + public void print(long value) throws IOException { + getTargetWriter().print(value); + } + + public void print(Object value) throws IOException { + getTargetWriter().print(value); + } + + public void print(String value) throws IOException { + getTargetWriter().print(value); + } + + public void println() throws IOException { + getTargetWriter().println(); + } + + public void println(boolean value) throws IOException { + getTargetWriter().println(value); + } + + public void println(char value) throws IOException { + getTargetWriter().println(value); + } + + public void println(char[] value) throws IOException { + getTargetWriter().println(value); + } + + public void println(double value) throws IOException { + getTargetWriter().println(value); + } + + public void println(float value) throws IOException { + getTargetWriter().println(value); + } + + public void println(int value) throws IOException { + getTargetWriter().println(value); + } + + public void println(long value) throws IOException { + getTargetWriter().println(value); + } + + public void println(Object value) throws IOException { + getTargetWriter().println(value); + } + + public void println(String value) throws IOException { + getTargetWriter().println(value); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java new file mode 100644 index 00000000..fb57d1f1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.mock.web; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * Mock implementation of the {@link org.springframework.web.multipart.MultipartFile} + * interface. + * + * <p>Useful in conjunction with a {@link MockMultipartHttpServletRequest} + * for testing application controllers that access multipart uploads. + * + * @author Juergen Hoeller + * @author Eric Crampton + * @since 2.0 + * @see MockMultipartHttpServletRequest + */ +public class MockMultipartFile implements MultipartFile { + + private final String name; + + private String originalFilename; + + private String contentType; + + private final byte[] content; + + + /** + * Create a new MockMultipartFile with the given content. + * @param name the name of the file + * @param content the content of the file + */ + public MockMultipartFile(String name, byte[] content) { + this(name, "", null, content); + } + + /** + * Create a new MockMultipartFile with the given content. + * @param name the name of the file + * @param contentStream the content of the file as stream + * @throws IOException if reading from the stream failed + */ + public MockMultipartFile(String name, InputStream contentStream) throws IOException { + this(name, "", null, FileCopyUtils.copyToByteArray(contentStream)); + } + + /** + * Create a new MockMultipartFile with the given content. + * @param name the name of the file + * @param originalFilename the original filename (as on the client's machine) + * @param contentType the content type (if known) + * @param content the content of the file + */ + public MockMultipartFile(String name, String originalFilename, String contentType, byte[] content) { + Assert.hasLength(name, "Name must not be null"); + this.name = name; + this.originalFilename = (originalFilename != null ? originalFilename : ""); + this.contentType = contentType; + this.content = (content != null ? content : new byte[0]); + } + + /** + * Create a new MockMultipartFile with the given content. + * @param name the name of the file + * @param originalFilename the original filename (as on the client's machine) + * @param contentType the content type (if known) + * @param contentStream the content of the file as stream + * @throws IOException if reading from the stream failed + */ + public MockMultipartFile(String name, String originalFilename, String contentType, InputStream contentStream) + throws IOException { + + this(name, originalFilename, contentType, FileCopyUtils.copyToByteArray(contentStream)); + } + + public String getName() { + return this.name; + } + + public String getOriginalFilename() { + return this.originalFilename; + } + + public String getContentType() { + return this.contentType; + } + + public boolean isEmpty() { + return (this.content.length == 0); + } + + public long getSize() { + return this.content.length; + } + + public byte[] getBytes() throws IOException { + return this.content; + } + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(this.content); + } + + public void transferTo(File dest) throws IOException, IllegalStateException { + FileCopyUtils.copy(this.content, dest); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java new file mode 100644 index 00000000..b01c4fdc --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.mock.web; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +/** + * Mock implementation of the + * {@link org.springframework.web.multipart.MultipartHttpServletRequest} interface. + * + * <p>Useful for testing application controllers that access multipart uploads. + * The {@link MockMultipartFile} can be used to populate these mock requests + * with files. + * + * @author Juergen Hoeller + * @author Eric Crampton + * @author Arjen Poutsma + * @since 2.0 + * @see MockMultipartFile + */ +public class MockMultipartHttpServletRequest extends MockHttpServletRequest implements MultipartHttpServletRequest { + + private final MultiValueMap<String, MultipartFile> multipartFiles = + new LinkedMultiValueMap<String, MultipartFile>(); + + + public MockMultipartHttpServletRequest() { + setMethod("POST"); + setContentType("multipart/form-data"); + } + + + /** + * Add a file to this request. The parameter name from the multipart + * form is taken from the {@link MultipartFile#getName()}. + * @param file multipart file to be added + */ + public void addFile(MultipartFile file) { + Assert.notNull(file, "MultipartFile must not be null"); + this.multipartFiles.add(file.getName(), file); + } + + public Iterator<String> getFileNames() { + return this.multipartFiles.keySet().iterator(); + } + + public MultipartFile getFile(String name) { + return this.multipartFiles.getFirst(name); + } + + public List<MultipartFile> getFiles(String name) { + List<MultipartFile> multipartFiles = this.multipartFiles.get(name); + if (multipartFiles != null) { + return multipartFiles; + } + else { + return Collections.emptyList(); + } + } + + public Map<String, MultipartFile> getFileMap() { + return this.multipartFiles.toSingleValueMap(); + } + + public MultiValueMap<String, MultipartFile> getMultiFileMap() { + return new LinkedMultiValueMap<String, MultipartFile>(this.multipartFiles); + } + + public String getMultipartContentType(String paramOrFileName) { + MultipartFile file = getFile(paramOrFileName); + if (file != null) { + return file.getContentType(); + } + else { + return null; + } + } + + public HttpMethod getRequestMethod() { + return HttpMethod.valueOf(getMethod()); + } + + public HttpHeaders getRequestHeaders() { + HttpHeaders headers = new HttpHeaders(); + Enumeration<String> headerNames = getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + headers.put(headerName, Collections.list(getHeaders(headerName))); + } + return headers; + } + + public HttpHeaders getMultipartHeaders(String paramOrFileName) { + String contentType = getMultipartContentType(paramOrFileName); + if (contentType != null) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", contentType); + return headers; + } + else { + return null; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java new file mode 100644 index 00000000..2c5d07e8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java @@ -0,0 +1,353 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import javax.el.ELContext; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.el.ExpressionEvaluator; +import javax.servlet.jsp.el.VariableResolver; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.jsp.PageContext} interface. + * + * <p>Used for testing the web framework; only necessary for testing + * applications when testing custom JSP tags. + * + * <p>Note: Expects initialization via the constructor rather than via the + * {@code PageContext.initialize} method. Does not support writing to + * a JspWriter, request dispatching, and {@code handlePageException} calls. + * + * @author Juergen Hoeller + * @since 1.0.2 + */ +@SuppressWarnings("deprecation") +public class MockPageContext extends PageContext { + + private final ServletContext servletContext; + + private final HttpServletRequest request; + + private final HttpServletResponse response; + + private final ServletConfig servletConfig; + + private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(); + + private JspWriter out; + + + /** + * Create new MockPageContext with a default {@link MockServletContext}, + * {@link MockHttpServletRequest}, {@link MockHttpServletResponse}, + * {@link MockServletConfig}. + */ + public MockPageContext() { + this(null, null, null, null); + } + + /** + * Create new MockPageContext with a default {@link MockHttpServletRequest}, + * {@link MockHttpServletResponse}, {@link MockServletConfig}. + * @param servletContext the ServletContext that the JSP page runs in + * (only necessary when actually accessing the ServletContext) + */ + public MockPageContext(ServletContext servletContext) { + this(servletContext, null, null, null); + } + + /** + * Create new MockPageContext with a MockHttpServletResponse, + * MockServletConfig. + * @param servletContext the ServletContext that the JSP page runs in + * @param request the current HttpServletRequest + * (only necessary when actually accessing the request) + */ + public MockPageContext(ServletContext servletContext, HttpServletRequest request) { + this(servletContext, request, null, null); + } + + /** + * Create new MockPageContext with a MockServletConfig. + * @param servletContext the ServletContext that the JSP page runs in + * @param request the current HttpServletRequest + * @param response the current HttpServletResponse + * (only necessary when actually writing to the response) + */ + public MockPageContext(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response) { + this(servletContext, request, response, null); + } + + /** + * Create new MockServletConfig. + * @param servletContext the ServletContext that the JSP page runs in + * @param request the current HttpServletRequest + * @param response the current HttpServletResponse + * @param servletConfig the ServletConfig (hardly ever accessed from within a tag) + */ + public MockPageContext(ServletContext servletContext, HttpServletRequest request, + HttpServletResponse response, ServletConfig servletConfig) { + + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.request = (request != null ? request : new MockHttpServletRequest(servletContext)); + this.response = (response != null ? response : new MockHttpServletResponse()); + this.servletConfig = (servletConfig != null ? servletConfig : new MockServletConfig(servletContext)); + } + + + public void initialize( + Servlet servlet, ServletRequest request, ServletResponse response, + String errorPageURL, boolean needsSession, int bufferSize, boolean autoFlush) { + + throw new UnsupportedOperationException("Use appropriate constructor"); + } + + public void release() { + } + + public void setAttribute(String name, Object value) { + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + public void setAttribute(String name, Object value, int scope) { + Assert.notNull(name, "Attribute name must not be null"); + switch (scope) { + case PAGE_SCOPE: + setAttribute(name, value); + break; + case REQUEST_SCOPE: + this.request.setAttribute(name, value); + break; + case SESSION_SCOPE: + this.request.getSession().setAttribute(name, value); + break; + case APPLICATION_SCOPE: + this.servletContext.setAttribute(name, value); + break; + default: + throw new IllegalArgumentException("Invalid scope: " + scope); + } + } + + public Object getAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + return this.attributes.get(name); + } + + public Object getAttribute(String name, int scope) { + Assert.notNull(name, "Attribute name must not be null"); + switch (scope) { + case PAGE_SCOPE: + return getAttribute(name); + case REQUEST_SCOPE: + return this.request.getAttribute(name); + case SESSION_SCOPE: + HttpSession session = this.request.getSession(false); + return (session != null ? session.getAttribute(name) : null); + case APPLICATION_SCOPE: + return this.servletContext.getAttribute(name); + default: + throw new IllegalArgumentException("Invalid scope: " + scope); + } + } + + public Object findAttribute(String name) { + Object value = getAttribute(name); + if (value == null) { + value = getAttribute(name, REQUEST_SCOPE); + if (value == null) { + value = getAttribute(name, SESSION_SCOPE); + if (value == null) { + value = getAttribute(name, APPLICATION_SCOPE); + } + } + } + return value; + } + + public void removeAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + this.removeAttribute(name, PageContext.PAGE_SCOPE); + this.removeAttribute(name, PageContext.REQUEST_SCOPE); + this.removeAttribute(name, PageContext.SESSION_SCOPE); + this.removeAttribute(name, PageContext.APPLICATION_SCOPE); + } + + public void removeAttribute(String name, int scope) { + Assert.notNull(name, "Attribute name must not be null"); + switch (scope) { + case PAGE_SCOPE: + this.attributes.remove(name); + break; + case REQUEST_SCOPE: + this.request.removeAttribute(name); + break; + case SESSION_SCOPE: + this.request.getSession().removeAttribute(name); + break; + case APPLICATION_SCOPE: + this.servletContext.removeAttribute(name); + break; + default: + throw new IllegalArgumentException("Invalid scope: " + scope); + } + } + + public int getAttributesScope(String name) { + if (getAttribute(name) != null) { + return PAGE_SCOPE; + } + else if (getAttribute(name, REQUEST_SCOPE) != null) { + return REQUEST_SCOPE; + } + else if (getAttribute(name, SESSION_SCOPE) != null) { + return SESSION_SCOPE; + } + else if (getAttribute(name, APPLICATION_SCOPE) != null) { + return APPLICATION_SCOPE; + } + else { + return 0; + } + } + + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet())); + } + + @SuppressWarnings("unchecked") + public Enumeration<String> getAttributeNamesInScope(int scope) { + switch (scope) { + case PAGE_SCOPE: + return getAttributeNames(); + case REQUEST_SCOPE: + return this.request.getAttributeNames(); + case SESSION_SCOPE: + HttpSession session = this.request.getSession(false); + return (session != null ? session.getAttributeNames() : null); + case APPLICATION_SCOPE: + return this.servletContext.getAttributeNames(); + default: + throw new IllegalArgumentException("Invalid scope: " + scope); + } + } + + public JspWriter getOut() { + if (this.out == null) { + this.out = new MockJspWriter(this.response); + } + return this.out; + } + + public ExpressionEvaluator getExpressionEvaluator() { + return new MockExpressionEvaluator(this); + } + + public ELContext getELContext() { + return null; + } + + public VariableResolver getVariableResolver() { + return null; + } + + public HttpSession getSession() { + return this.request.getSession(); + } + + public Object getPage() { + return this; + } + + public ServletRequest getRequest() { + return this.request; + } + + public ServletResponse getResponse() { + return this.response; + } + + public Exception getException() { + return null; + } + + public ServletConfig getServletConfig() { + return this.servletConfig; + } + + public ServletContext getServletContext() { + return this.servletContext; + } + + public void forward(String path) throws ServletException, IOException { + this.request.getRequestDispatcher(path).forward(this.request, this.response); + } + + public void include(String path) throws ServletException, IOException { + this.request.getRequestDispatcher(path).include(this.request, this.response); + } + + public void include(String path, boolean flush) throws ServletException, IOException { + this.request.getRequestDispatcher(path).include(this.request, this.response); + if (flush) { + this.response.flushBuffer(); + } + } + + public byte[] getContentAsByteArray() { + Assert.isTrue(this.response instanceof MockHttpServletResponse); + return ((MockHttpServletResponse) this.response).getContentAsByteArray(); + } + + public String getContentAsString() throws UnsupportedEncodingException { + Assert.isTrue(this.response instanceof MockHttpServletResponse); + return ((MockHttpServletResponse) this.response).getContentAsString(); + } + + public void handlePageException(Exception ex) throws ServletException, IOException { + throw new ServletException("Page exception", ex); + } + + public void handlePageException(Throwable ex) throws ServletException, IOException { + throw new ServletException("Page exception", ex); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java b/spring-test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java new file mode 100644 index 00000000..88660bb3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockRequestDispatcher.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.RequestDispatcher} interface. + * + * <p>Used for testing the web framework; typically not necessary for + * testing application controllers. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.0.2 + */ +public class MockRequestDispatcher implements RequestDispatcher { + + private final Log logger = LogFactory.getLog(getClass()); + + private final String resource; + + + /** + * Create a new MockRequestDispatcher for the given resource. + * @param resource the server resource to dispatch to, located at a + * particular path or given by a particular name + */ + public MockRequestDispatcher(String resource) { + Assert.notNull(resource, "resource must not be null"); + this.resource = resource; + } + + + public void forward(ServletRequest request, ServletResponse response) { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + if (response.isCommitted()) { + throw new IllegalStateException("Cannot perform forward - response is already committed"); + } + getMockHttpServletResponse(response).setForwardedUrl(this.resource); + if (logger.isDebugEnabled()) { + logger.debug("MockRequestDispatcher: forwarding to [" + this.resource + "]"); + } + } + + public void include(ServletRequest request, ServletResponse response) { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + getMockHttpServletResponse(response).addIncludedUrl(this.resource); + if (logger.isDebugEnabled()) { + logger.debug("MockRequestDispatcher: including [" + this.resource + "]"); + } + } + + /** + * Obtain the underlying {@link MockHttpServletResponse}, unwrapping + * {@link HttpServletResponseWrapper} decorators if necessary. + */ + protected MockHttpServletResponse getMockHttpServletResponse(ServletResponse response) { + if (response instanceof MockHttpServletResponse) { + return (MockHttpServletResponse) response; + } + if (response instanceof HttpServletResponseWrapper) { + return getMockHttpServletResponse(((HttpServletResponseWrapper) response).getResponse()); + } + throw new IllegalArgumentException("MockRequestDispatcher requires MockHttpServletResponse"); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java new file mode 100644 index 00000000..c62ec152 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.ServletConfig} interface. + * + * <p>Used for testing the web framework; typically not necessary for + * testing application controllers. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 1.0.2 + */ +public class MockServletConfig implements ServletConfig { + + private final ServletContext servletContext; + + private final String servletName; + + private final Map<String, String> initParameters = new LinkedHashMap<String, String>(); + + + /** + * Create a new MockServletConfig with a default {@link MockServletContext}. + */ + public MockServletConfig() { + this(null, ""); + } + + /** + * Create a new MockServletConfig with a default {@link MockServletContext}. + * @param servletName the name of the servlet + */ + public MockServletConfig(String servletName) { + this(null, servletName); + } + + /** + * Create a new MockServletConfig. + * @param servletContext the ServletContext that the servlet runs in + */ + public MockServletConfig(ServletContext servletContext) { + this(servletContext, ""); + } + + /** + * Create a new MockServletConfig. + * @param servletContext the ServletContext that the servlet runs in + * @param servletName the name of the servlet + */ + public MockServletConfig(ServletContext servletContext, String servletName) { + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.servletName = servletName; + } + + + public String getServletName() { + return this.servletName; + } + + public ServletContext getServletContext() { + return this.servletContext; + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + public Enumeration<String> getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java new file mode 100644 index 00000000..49b49e58 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -0,0 +1,499 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import javax.activation.FileTypeMap; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.web.util.WebUtils; + +/** + * Mock implementation of the {@link javax.servlet.ServletContext} interface. + * + * <p>Compatible with Servlet 2.5 and partially with Servlet 3.0. Can be configured to + * expose a specific version through {@link #setMajorVersion}/{@link #setMinorVersion}; + * default is 2.5. Note that Servlet 3.0 support is limited: servlet, filter and listener + * registration methods are not supported; neither is cookie or JSP configuration. + * We generally do not recommend to unit-test your ServletContainerInitializers and + * WebApplicationInitializers which is where those registration methods would be used. + * + * <p>Used for testing the Spring web framework; only rarely necessary for testing + * application controllers. As long as application components don't explicitly + * access the {@code ServletContext}, {@code ClassPathXmlApplicationContext} or + * {@code FileSystemXmlApplicationContext} can be used to load the context files + * for testing, even for {@code DispatcherServlet} context definitions. + * + * <p>For setting up a full {@code WebApplicationContext} in a test environment, + * you can use {@code AnnotationConfigWebApplicationContext}, + * {@code XmlWebApplicationContext}, or {@code GenericWebApplicationContext}, + * passing in an appropriate {@code MockServletContext} instance. You might want + * to configure your {@code MockServletContext} with a {@code FileSystemResourceLoader} + * in that case to ensure that resource paths are interpreted as relative filesystem + * locations. + * + * <p>A common setup is to point your JVM working directory to the root of your + * web application directory, in combination with filesystem-based resource loading. + * This allows to load the context files as used in the web application, with + * relative paths getting interpreted correctly. Such a setup will work with both + * {@code FileSystemXmlApplicationContext} (which will load straight from the + * filesystem) and {@code XmlWebApplicationContext} with an underlying + * {@code MockServletContext} (as long as the {@code MockServletContext} has been + * configured with a {@code FileSystemResourceLoader}). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.0.2 + * @see #MockServletContext(org.springframework.core.io.ResourceLoader) + * @see org.springframework.web.context.support.AnnotationConfigWebApplicationContext + * @see org.springframework.web.context.support.XmlWebApplicationContext + * @see org.springframework.web.context.support.GenericWebApplicationContext + * @see org.springframework.context.support.ClassPathXmlApplicationContext + * @see org.springframework.context.support.FileSystemXmlApplicationContext + */ +public class MockServletContext implements ServletContext { + + /** Default Servlet name used by Tomcat, Jetty, JBoss, and GlassFish: {@value}. */ + private static final String COMMON_DEFAULT_SERVLET_NAME = "default"; + + private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir"; + + private final Log logger = LogFactory.getLog(getClass()); + + private final Map<String, ServletContext> contexts = new HashMap<String, ServletContext>(); + + private final Map<String, String> initParameters = new LinkedHashMap<String, String>(); + + private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(); + + private final Set<String> declaredRoles = new HashSet<String>(); + + private final Map<String, RequestDispatcher> namedRequestDispatchers = new HashMap<String, RequestDispatcher>(); + + private final ResourceLoader resourceLoader; + + private final String resourceBasePath; + + private String contextPath = ""; + + private int majorVersion = 2; + + private int minorVersion = 5; + + private int effectiveMajorVersion = 2; + + private int effectiveMinorVersion = 5; + + private String servletContextName = "MockServletContext"; + + private String defaultServletName = COMMON_DEFAULT_SERVLET_NAME; + + + /** + * Create a new MockServletContext, using no base path and a + * DefaultResourceLoader (i.e. the classpath root as WAR root). + * @see org.springframework.core.io.DefaultResourceLoader + */ + public MockServletContext() { + this("", null); + } + + /** + * Create a new MockServletContext, using a DefaultResourceLoader. + * @param resourceBasePath the root directory of the WAR (should not end with a slash) + * @see org.springframework.core.io.DefaultResourceLoader + */ + public MockServletContext(String resourceBasePath) { + this(resourceBasePath, null); + } + + /** + * Create a new MockServletContext, using the specified ResourceLoader + * and no base path. + * @param resourceLoader the ResourceLoader to use (or null for the default) + */ + public MockServletContext(ResourceLoader resourceLoader) { + this("", resourceLoader); + } + + /** + * Create a new MockServletContext using the supplied resource base path and + * resource loader. + * <p>Registers a {@link MockRequestDispatcher} for the Servlet named + * {@linkplain #COMMON_DEFAULT_SERVLET_NAME "default"}. + * @param resourceBasePath the root directory of the WAR (should not end with a slash) + * @param resourceLoader the ResourceLoader to use (or null for the default) + * @see #registerNamedDispatcher + */ + @SuppressWarnings("javadoc") + public MockServletContext(String resourceBasePath, ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + this.resourceBasePath = (resourceBasePath != null ? resourceBasePath : ""); + + // Use JVM temp dir as ServletContext temp dir. + String tempDir = System.getProperty(TEMP_DIR_SYSTEM_PROPERTY); + if (tempDir != null) { + this.attributes.put(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File(tempDir)); + } + + registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName)); + } + + /** + * Build a full resource location for the given path, + * prepending the resource base path of this MockServletContext. + * @param path the path as specified + * @return the full resource path + */ + protected String getResourceLocation(String path) { + if (!path.startsWith("/")) { + path = "/" + path; + } + return this.resourceBasePath + path; + } + + public void setContextPath(String contextPath) { + this.contextPath = (contextPath != null ? contextPath : ""); + } + + /* This is a Servlet API 2.5 method. */ + public String getContextPath() { + return this.contextPath; + } + + public void registerContext(String contextPath, ServletContext context) { + this.contexts.put(contextPath, context); + } + + public ServletContext getContext(String contextPath) { + if (this.contextPath.equals(contextPath)) { + return this; + } + return this.contexts.get(contextPath); + } + + public void setMajorVersion(int majorVersion) { + this.majorVersion = majorVersion; + } + + public int getMajorVersion() { + return this.majorVersion; + } + + public void setMinorVersion(int minorVersion) { + this.minorVersion = minorVersion; + } + + public int getMinorVersion() { + return this.minorVersion; + } + + public void setEffectiveMajorVersion(int effectiveMajorVersion) { + this.effectiveMajorVersion = effectiveMajorVersion; + } + + public int getEffectiveMajorVersion() { + return this.effectiveMajorVersion; + } + + public void setEffectiveMinorVersion(int effectiveMinorVersion) { + this.effectiveMinorVersion = effectiveMinorVersion; + } + + public int getEffectiveMinorVersion() { + return this.effectiveMinorVersion; + } + + /** + * This method uses the Java Activation framework, which returns "application/octet-stream" + * when the mime type is unknown (i.e. it never returns {@code null}). In order to maintain + * the {@link ServletContext#getMimeType(String)} contract, this method returns {@code null} + * if the mimeType is "application/octet-stream", as of Spring 3.2.2. + */ + public String getMimeType(String filePath) { + String mimeType = MimeTypeResolver.getMimeType(filePath); + return ("application/octet-stream".equals(mimeType)) ? null : mimeType; + } + + public Set<String> getResourcePaths(String path) { + String actualPath = (path.endsWith("/") ? path : path + "/"); + Resource resource = this.resourceLoader.getResource(getResourceLocation(actualPath)); + try { + File file = resource.getFile(); + String[] fileList = file.list(); + if (ObjectUtils.isEmpty(fileList)) { + return null; + } + Set<String> resourcePaths = new LinkedHashSet<String>(fileList.length); + for (String fileEntry : fileList) { + String resultPath = actualPath + fileEntry; + if (resource.createRelative(fileEntry).getFile().isDirectory()) { + resultPath += "/"; + } + resourcePaths.add(resultPath); + } + return resourcePaths; + } + catch (IOException ex) { + logger.warn("Couldn't get resource paths for " + resource, ex); + return null; + } + } + + public URL getResource(String path) throws MalformedURLException { + Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + if (!resource.exists()) { + return null; + } + try { + return resource.getURL(); + } + catch (MalformedURLException ex) { + throw ex; + } + catch (IOException ex) { + logger.warn("Couldn't get URL for " + resource, ex); + return null; + } + } + + public InputStream getResourceAsStream(String path) { + Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + if (!resource.exists()) { + return null; + } + try { + return resource.getInputStream(); + } + catch (IOException ex) { + logger.warn("Couldn't open InputStream for " + resource, ex); + return null; + } + } + + public RequestDispatcher getRequestDispatcher(String path) { + if (!path.startsWith("/")) { + throw new IllegalArgumentException("RequestDispatcher path at ServletContext level must start with '/'"); + } + return new MockRequestDispatcher(path); + } + + public RequestDispatcher getNamedDispatcher(String path) { + return this.namedRequestDispatchers.get(path); + } + + /** + * Register a {@link RequestDispatcher} (typically a {@link MockRequestDispatcher}) + * that acts as a wrapper for the named Servlet. + * + * @param name the name of the wrapped Servlet + * @param requestDispatcher the dispatcher that wraps the named Servlet + * @see #getNamedDispatcher + * @see #unregisterNamedDispatcher + */ + public void registerNamedDispatcher(String name, RequestDispatcher requestDispatcher) { + Assert.notNull(name, "RequestDispatcher name must not be null"); + Assert.notNull(requestDispatcher, "RequestDispatcher must not be null"); + this.namedRequestDispatchers.put(name, requestDispatcher); + } + + /** + * Unregister the {@link RequestDispatcher} with the given name. + * + * @param name the name of the dispatcher to unregister + * @see #getNamedDispatcher + * @see #registerNamedDispatcher + */ + public void unregisterNamedDispatcher(String name) { + Assert.notNull(name, "RequestDispatcher name must not be null"); + this.namedRequestDispatchers.remove(name); + } + + /** + * Get the name of the <em>default</em> {@code Servlet}. + * <p>Defaults to {@linkplain #COMMON_DEFAULT_SERVLET_NAME "default"}. + * @see #setDefaultServletName + */ + @SuppressWarnings("javadoc") + public String getDefaultServletName() { + return this.defaultServletName; + } + + /** + * Set the name of the <em>default</em> {@code Servlet}. + * <p>Also {@link #unregisterNamedDispatcher unregisters} the current default + * {@link RequestDispatcher} and {@link #registerNamedDispatcher replaces} + * it with a {@link MockRequestDispatcher} for the provided + * {@code defaultServletName}. + * @param defaultServletName the name of the <em>default</em> {@code Servlet}; + * never {@code null} or empty + * @see #getDefaultServletName + */ + public void setDefaultServletName(String defaultServletName) { + Assert.hasText(defaultServletName, "defaultServletName must not be null or empty"); + unregisterNamedDispatcher(this.defaultServletName); + this.defaultServletName = defaultServletName; + registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName)); + } + + public Servlet getServlet(String name) { + return null; + } + + public Enumeration<Servlet> getServlets() { + return Collections.enumeration(new HashSet<Servlet>()); + } + + public Enumeration<String> getServletNames() { + return Collections.enumeration(new HashSet<String>()); + } + + public void log(String message) { + logger.info(message); + } + + public void log(Exception ex, String message) { + logger.info(message, ex); + } + + public void log(String message, Throwable ex) { + logger.info(message, ex); + } + + public String getRealPath(String path) { + Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + try { + return resource.getFile().getAbsolutePath(); + } + catch (IOException ex) { + logger.warn("Couldn't determine real path of resource " + resource, ex); + return null; + } + } + + public String getServerInfo() { + return "MockServletContext"; + } + + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + public Enumeration<String> getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + + public boolean setInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + if (this.initParameters.containsKey(name)) { + return false; + } + this.initParameters.put(name, value); + return true; + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + public Object getAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + return this.attributes.get(name); + } + + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet())); + } + + public void setAttribute(String name, Object value) { + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + public void removeAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + this.attributes.remove(name); + } + + public void setServletContextName(String servletContextName) { + this.servletContextName = servletContextName; + } + + public String getServletContextName() { + return this.servletContextName; + } + + public ClassLoader getClassLoader() { + return ClassUtils.getDefaultClassLoader(); + } + + public void declareRoles(String... roleNames) { + Assert.notNull(roleNames, "Role names array must not be null"); + for (String roleName : roleNames) { + Assert.hasLength(roleName, "Role name must not be empty"); + this.declaredRoles.add(roleName); + } + } + + public Set<String> getDeclaredRoles() { + return Collections.unmodifiableSet(this.declaredRoles); + } + + + /** + * Inner factory class used to introduce a Java Activation Framework + * dependency when actually asked to resolve a MIME type. + */ + private static class MimeTypeResolver { + + public static String getMimeType(String filePath) { + return FileTypeMap.getDefaultFileTypeMap().getContentType(filePath); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java b/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java new file mode 100644 index 00000000..b3484a0a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.util.Assert; + +/** + * Implementation of the {@link javax.servlet.FilterConfig} interface which + * simply passes the call through to a given Filter/FilterChain combination + * (indicating the next Filter in the chain along with the FilterChain that it is + * supposed to work on) or to a given Servlet (indicating the end of the chain). + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see javax.servlet.Filter + * @see javax.servlet.Servlet + * @see MockFilterChain + */ +public class PassThroughFilterChain implements FilterChain { + + private Filter filter; + + private FilterChain nextFilterChain; + + private Servlet servlet; + + + /** + * Create a new PassThroughFilterChain that delegates to the given Filter, + * calling it with the given FilterChain. + * @param filter the Filter to delegate to + * @param nextFilterChain the FilterChain to use for that next Filter + */ + public PassThroughFilterChain(Filter filter, FilterChain nextFilterChain) { + Assert.notNull(filter, "Filter must not be null"); + Assert.notNull(nextFilterChain, "'FilterChain must not be null"); + this.filter = filter; + this.nextFilterChain = nextFilterChain; + } + + /** + * Create a new PassThroughFilterChain that delegates to the given Servlet. + * @param servlet the Servlet to delegate to + */ + public PassThroughFilterChain(Servlet servlet) { + Assert.notNull(servlet, "Servlet must not be null"); + this.servlet = servlet; + } + + + /** + * Pass the call on to the Filter/Servlet. + */ + public void doFilter(ServletRequest request, ServletResponse response) throws ServletException, IOException { + if (this.filter != null) { + this.filter.doFilter(request, response, this.nextFilterChain); + } + else { + this.servlet.service(request, response); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/package-info.java b/spring-test/src/main/java/org/springframework/mock/web/package-info.java new file mode 100644 index 00000000..be8cb860 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/package-info.java @@ -0,0 +1,15 @@ + +/** + * + * A comprehensive set of Servlet API 2.5 mock objects, + * targeted at usage with Spring's web MVC framework. + * Useful for testing web contexts and controllers. + * + * <p>More convenient to use than dynamic mock objects + * (<a href="http://www.easymock.org">EasyMock</a>) or + * existing Servlet API mock objects + * (<a href="http://www.mockobjects.com">MockObjects</a>). + * + */ +package org.springframework.mock.web; + diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java new file mode 100644 index 00000000..a687ccb6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import javax.portlet.ActionRequest; +import javax.portlet.PortalContext; +import javax.portlet.PortletContext; +import javax.portlet.PortletMode; + +/** + * Mock implementation of the {@link javax.portlet.ActionRequest} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockActionRequest extends MockClientDataRequest implements ActionRequest { + + /** + * Create a new MockActionRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @see org.springframework.mock.web.portlet.MockPortalContext + * @see org.springframework.mock.web.portlet.MockPortletContext + */ + public MockActionRequest() { + super(); + } + + /** + * Create a new MockActionRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @param actionName the name of the action to trigger + */ + public MockActionRequest(String actionName) { + super(); + setParameter(ActionRequest.ACTION_NAME, actionName); + } + + /** + * Create a new MockActionRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @param portletMode the mode that the portlet runs in + */ + public MockActionRequest(PortletMode portletMode) { + super(); + setPortletMode(portletMode); + } + + /** + * Create a new MockActionRequest with a default {@link MockPortalContext}. + * @param portletContext the PortletContext that the request runs in + */ + public MockActionRequest(PortletContext portletContext) { + super(portletContext); + } + + /** + * Create a new MockActionRequest. + * @param portalContext the PortalContext that the request runs in + * @param portletContext the PortletContext that the request runs in + */ + public MockActionRequest(PortalContext portalContext, PortletContext portletContext) { + super(portalContext, portletContext); + } + + + @Override + protected String getLifecyclePhase() { + return ACTION_PHASE; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java new file mode 100644 index 00000000..bec66878 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockActionResponse.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.IOException; +import java.util.Map; +import javax.portlet.ActionResponse; +import javax.portlet.PortalContext; +import javax.portlet.PortletMode; +import javax.portlet.PortletModeException; +import javax.portlet.WindowState; +import javax.portlet.WindowStateException; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.portlet.ActionResponse} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockActionResponse extends MockStateAwareResponse implements ActionResponse { + + private boolean redirectAllowed = true; + + private String redirectedUrl; + + + /** + * Create a new MockActionResponse with a default {@link MockPortalContext}. + * @see MockPortalContext + */ + public MockActionResponse() { + super(); + } + + /** + * Create a new MockActionResponse. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + */ + public MockActionResponse(PortalContext portalContext) { + super(portalContext); + } + + + public void setWindowState(WindowState windowState) throws WindowStateException { + if (this.redirectedUrl != null) { + throw new IllegalStateException("Cannot set WindowState after sendRedirect has been called"); + } + super.setWindowState(windowState); + this.redirectAllowed = false; + } + + public void setPortletMode(PortletMode portletMode) throws PortletModeException { + if (this.redirectedUrl != null) { + throw new IllegalStateException("Cannot set PortletMode after sendRedirect has been called"); + } + super.setPortletMode(portletMode); + this.redirectAllowed = false; + } + + public void setRenderParameters(Map<String, String[]> parameters) { + if (this.redirectedUrl != null) { + throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called"); + } + super.setRenderParameters(parameters); + this.redirectAllowed = false; + } + + public void setRenderParameter(String key, String value) { + if (this.redirectedUrl != null) { + throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called"); + } + super.setRenderParameter(key, value); + this.redirectAllowed = false; + } + + public void setRenderParameter(String key, String[] values) { + if (this.redirectedUrl != null) { + throw new IllegalStateException("Cannot set render parameters after sendRedirect has been called"); + } + super.setRenderParameter(key, values); + this.redirectAllowed = false; + } + + public void sendRedirect(String location) throws IOException { + if (!this.redirectAllowed) { + throw new IllegalStateException( + "Cannot call sendRedirect after windowState, portletMode, or renderParameters have been set"); + } + Assert.notNull(location, "Redirect URL must not be null"); + this.redirectedUrl = location; + } + + public void sendRedirect(String location, String renderUrlParamName) throws IOException { + sendRedirect(location); + if (renderUrlParamName != null) { + setRenderParameter(renderUrlParamName, location); + } + } + + public String getRedirectedUrl() { + return this.redirectedUrl; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockBaseURL.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockBaseURL.java new file mode 100644 index 00000000..aad99205 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockBaseURL.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import javax.portlet.BaseURL; +import javax.portlet.PortletSecurityException; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Mock implementation of the {@link javax.portlet.BaseURL} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class MockBaseURL implements BaseURL { + + public static final String URL_TYPE_RENDER = "render"; + + public static final String URL_TYPE_ACTION = "action"; + + private static final String ENCODING = "UTF-8"; + + + protected final Map<String, String[]> parameters = new LinkedHashMap<String, String[]>(); + + private boolean secure = false; + + private final Map<String, String[]> properties = new LinkedHashMap<String, String[]>(); + + + //--------------------------------------------------------------------- + // BaseURL methods + //--------------------------------------------------------------------- + + public void setParameter(String key, String value) { + Assert.notNull(key, "Parameter key must be null"); + Assert.notNull(value, "Parameter value must not be null"); + this.parameters.put(key, new String[] {value}); + } + + public void setParameter(String key, String[] values) { + Assert.notNull(key, "Parameter key must be null"); + Assert.notNull(values, "Parameter values must not be null"); + this.parameters.put(key, values); + } + + public void setParameters(Map<String, String[]> parameters) { + Assert.notNull(parameters, "Parameters Map must not be null"); + this.parameters.clear(); + this.parameters.putAll(parameters); + } + + public Set<String> getParameterNames() { + return this.parameters.keySet(); + } + + public String getParameter(String name) { + String[] arr = this.parameters.get(name); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + public String[] getParameterValues(String name) { + return this.parameters.get(name); + } + + public Map<String, String[]> getParameterMap() { + return Collections.unmodifiableMap(this.parameters); + } + + public void setSecure(boolean secure) throws PortletSecurityException { + this.secure = secure; + } + + public boolean isSecure() { + return this.secure; + } + + public void write(Writer out) throws IOException { + out.write(toString()); + } + + public void write(Writer out, boolean escapeXML) throws IOException { + out.write(toString()); + } + + public void addProperty(String key, String value) { + String[] values = this.properties.get(key); + if (values != null) { + this.properties.put(key, StringUtils.addStringToArray(values, value)); + } + else { + this.properties.put(key, new String[] {value}); + } + } + + public void setProperty(String key, String value) { + this.properties.put(key, new String[] {value}); + } + + public Map<String, String[]> getProperties() { + return Collections.unmodifiableMap(this.properties); + } + + + protected String encodeParameter(String name, String value) { + try { + return URLEncoder.encode(name, ENCODING) + "=" + URLEncoder.encode(value, ENCODING); + } + catch (UnsupportedEncodingException ex) { + return null; + } + } + + protected String encodeParameter(String name, String[] values) { + try { + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = values.length; i < n; i++) { + sb.append(i > 0 ? ";" : "").append(URLEncoder.encode(name, ENCODING)).append("=") + .append(URLEncoder.encode(values[i], ENCODING)); + } + return sb.toString(); + } + catch (UnsupportedEncodingException ex) { + return null; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockCacheControl.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockCacheControl.java new file mode 100644 index 00000000..0ee9fbeb --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockCacheControl.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import javax.portlet.CacheControl; + +/** + * Mock implementation of the {@link javax.portlet.CacheControl} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockCacheControl implements CacheControl { + + private int expirationTime = 0; + + private boolean publicScope = false; + + private String etag; + + private boolean useCachedContent = false; + + + public int getExpirationTime() { + return this.expirationTime; + } + + public void setExpirationTime(int time) { + this.expirationTime = time; + } + + public boolean isPublicScope() { + return this.publicScope; + } + + public void setPublicScope(boolean publicScope) { + this.publicScope = publicScope; + } + + public String getETag() { + return this.etag; + } + + public void setETag(String token) { + this.etag = token; + } + + public boolean useCachedContent() { + return this.useCachedContent; + } + + public void setUseCachedContent(boolean useCachedContent) { + this.useCachedContent = useCachedContent; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockClientDataRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockClientDataRequest.java new file mode 100644 index 00000000..406c263e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockClientDataRequest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import javax.portlet.ClientDataRequest; +import javax.portlet.PortalContext; +import javax.portlet.PortletContext; + +/** + * Mock implementation of the {@link javax.portlet.ClientDataRequest} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockClientDataRequest extends MockPortletRequest implements ClientDataRequest { + + private String characterEncoding; + + private byte[] content; + + private String contentType; + + private String method; + + + /** + * Create a new MockClientDataRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @see org.springframework.mock.web.portlet.MockPortalContext + * @see org.springframework.mock.web.portlet.MockPortletContext + */ + public MockClientDataRequest() { + super(); + } + + /** + * Create a new MockClientDataRequest with a default {@link MockPortalContext}. + * @param portletContext the PortletContext that the request runs in + */ + public MockClientDataRequest(PortletContext portletContext) { + super(portletContext); + } + + /** + * Create a new MockClientDataRequest. + * @param portalContext the PortalContext that the request runs in + * @param portletContext the PortletContext that the request runs in + */ + public MockClientDataRequest(PortalContext portalContext, PortletContext portletContext) { + super(portalContext, portletContext); + } + + + public void setContent(byte[] content) { + this.content = content; + } + + public InputStream getPortletInputStream() throws IOException { + if (this.content != null) { + return new ByteArrayInputStream(this.content); + } + else { + return null; + } + } + + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + } + + public BufferedReader getReader() throws UnsupportedEncodingException { + if (this.content != null) { + InputStream sourceStream = new ByteArrayInputStream(this.content); + Reader sourceReader = (this.characterEncoding != null) ? + new InputStreamReader(sourceStream, this.characterEncoding) : new InputStreamReader(sourceStream); + return new BufferedReader(sourceReader); + } + else { + return null; + } + } + + public String getCharacterEncoding() { + return this.characterEncoding; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getContentType() { + return this.contentType; + } + + public int getContentLength() { + return (this.content != null ? content.length : -1); + } + + public void setMethod(String method) { + this.method = method; + } + + public String getMethod() { + return this.method; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEvent.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEvent.java new file mode 100644 index 00000000..41e72da9 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEvent.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.Serializable; +import javax.portlet.Event; +import javax.xml.namespace.QName; + +/** + * Mock implementation of the {@link javax.portlet.Event} interface. + * + * @author Juergen Hoeller + * @since 3.0 + * @see MockEventRequest + */ +public class MockEvent implements Event { + + private final QName name; + + private final Serializable value; + + + /** + * Create a new MockEvent with the given name. + * @param name the name of the event + */ + public MockEvent(QName name) { + this.name = name; + this.value = null; + } + + /** + * Create a new MockEvent with the given name and value. + * @param name the name of the event + * @param value the associated payload of the event + */ + public MockEvent(QName name, Serializable value) { + this.name = name; + this.value = value; + } + + /** + * Create a new MockEvent with the given name. + * @param name the name of the event + */ + public MockEvent(String name) { + this.name = new QName(name); + this.value = null; + } + + /** + * Create a new MockEvent with the given name and value. + * @param name the name of the event + * @param value the associated payload of the event + */ + public MockEvent(String name, Serializable value) { + this.name = new QName(name); + this.value = value; + } + + + public QName getQName() { + return this.name; + } + + public String getName() { + return this.name.getLocalPart(); + } + + public Serializable getValue() { + return this.value; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventRequest.java new file mode 100644 index 00000000..a2850e3c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventRequest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import javax.portlet.Event; +import javax.portlet.EventRequest; +import javax.portlet.PortalContext; +import javax.portlet.PortletContext; + +/** + * Mock implementation of the {@link javax.portlet.EventRequest} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockEventRequest extends MockPortletRequest implements EventRequest { + + private final Event event; + + private String method; + + + /** + * Create a new MockEventRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @param event the event that this request wraps + * @see MockEvent + */ + public MockEventRequest(Event event) { + super(); + this.event = event; + } + + /** + * Create a new MockEventRequest with a default {@link MockPortalContext}. + * @param event the event that this request wraps + * @param portletContext the PortletContext that the request runs in + * @see MockEvent + */ + public MockEventRequest(Event event, PortletContext portletContext) { + super(portletContext); + this.event = event; + } + + /** + * Create a new MockEventRequest. + * @param event the event that this request wraps + * @param portalContext the PortletContext that the request runs in + * @param portletContext the PortletContext that the request runs in + */ + public MockEventRequest(Event event, PortalContext portalContext, PortletContext portletContext) { + super(portalContext, portletContext); + this.event = event; + } + + + @Override + protected String getLifecyclePhase() { + return EVENT_PHASE; + } + + public Event getEvent() { + return this.event; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getMethod() { + return this.method; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventResponse.java new file mode 100644 index 00000000..1a00e8b2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockEventResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import javax.portlet.EventRequest; +import javax.portlet.EventResponse; + +/** + * Mock implementation of the {@link javax.portlet.EventResponse} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockEventResponse extends MockStateAwareResponse implements EventResponse { + + public void setRenderParameters(EventRequest request) { + setRenderParameters(request.getParameterMap()); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMimeResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMimeResponse.java new file mode 100644 index 00000000..4944ab64 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMimeResponse.java @@ -0,0 +1,240 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.mock.web.portlet; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Locale; +import javax.portlet.CacheControl; +import javax.portlet.MimeResponse; +import javax.portlet.PortalContext; +import javax.portlet.PortletRequest; +import javax.portlet.PortletURL; +import javax.portlet.ResourceURL; + +import org.springframework.util.CollectionUtils; +import org.springframework.web.util.WebUtils; + +/** + * Mock implementation of the {@link javax.portlet.MimeResponse} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockMimeResponse extends MockPortletResponse implements MimeResponse { + + private PortletRequest request; + + private String contentType; + + private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; + + private PrintWriter writer; + + private Locale locale = Locale.getDefault(); + + private int bufferSize = 4096; + + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024); + + private final CacheControl cacheControl = new MockCacheControl(); + + private boolean committed; + + private String includedUrl; + + private String forwardedUrl; + + + /** + * Create a new MockMimeResponse with a default {@link MockPortalContext}. + * @see org.springframework.mock.web.portlet.MockPortalContext + */ + public MockMimeResponse() { + super(); + } + + /** + * Create a new MockMimeResponse. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + */ + public MockMimeResponse(PortalContext portalContext) { + super(portalContext); + } + + /** + * Create a new MockMimeResponse. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + * @param request the corresponding render/resource request that this response + * is being generated for + */ + public MockMimeResponse(PortalContext portalContext, PortletRequest request) { + super(portalContext); + this.request = request; + } + + + //--------------------------------------------------------------------- + // RenderResponse methods + //--------------------------------------------------------------------- + + public void setContentType(String contentType) { + if (this.request != null) { + Enumeration<String> supportedTypes = this.request.getResponseContentTypes(); + if (!CollectionUtils.contains(supportedTypes, contentType)) { + throw new IllegalArgumentException("Content type [" + contentType + "] not in supported list: " + + Collections.list(supportedTypes)); + } + } + this.contentType = contentType; + } + + public String getContentType() { + return this.contentType; + } + + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + } + + public String getCharacterEncoding() { + return this.characterEncoding; + } + + public PrintWriter getWriter() throws UnsupportedEncodingException { + if (this.writer == null) { + Writer targetWriter = (this.characterEncoding != null ? + new OutputStreamWriter(this.outputStream, this.characterEncoding) : + new OutputStreamWriter(this.outputStream)); + this.writer = new PrintWriter(targetWriter); + } + return this.writer; + } + + public byte[] getContentAsByteArray() { + flushBuffer(); + return this.outputStream.toByteArray(); + } + + public String getContentAsString() throws UnsupportedEncodingException { + flushBuffer(); + return (this.characterEncoding != null ? + this.outputStream.toString(this.characterEncoding) : this.outputStream.toString()); + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public Locale getLocale() { + return this.locale; + } + + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + public int getBufferSize() { + return this.bufferSize; + } + + public void flushBuffer() { + if (this.writer != null) { + this.writer.flush(); + } + try { + this.outputStream.flush(); + } + catch (IOException ex) { + throw new IllegalStateException("Could not flush OutputStream: " + ex.getMessage()); + } + this.committed = true; + } + + public void resetBuffer() { + if (this.committed) { + throw new IllegalStateException("Cannot reset buffer - response is already committed"); + } + this.outputStream.reset(); + } + + public void setCommitted(boolean committed) { + this.committed = committed; + } + + public boolean isCommitted() { + return this.committed; + } + + public void reset() { + resetBuffer(); + this.characterEncoding = null; + this.contentType = null; + this.locale = null; + } + + public OutputStream getPortletOutputStream() throws IOException { + return this.outputStream; + } + + public PortletURL createRenderURL() { + return new MockPortletURL(getPortalContext(), MockPortletURL.URL_TYPE_RENDER); + } + + public PortletURL createActionURL() { + return new MockPortletURL(getPortalContext(), MockPortletURL.URL_TYPE_ACTION); + } + + public ResourceURL createResourceURL() { + return new MockResourceURL(); + } + + public CacheControl getCacheControl() { + return this.cacheControl; + } + + + //--------------------------------------------------------------------- + // Methods for MockPortletRequestDispatcher + //--------------------------------------------------------------------- + + public void setIncludedUrl(String includedUrl) { + this.includedUrl = includedUrl; + } + + public String getIncludedUrl() { + return this.includedUrl; + } + + public void setForwardedUrl(String forwardedUrl) { + this.forwardedUrl = forwardedUrl; + } + + public String getForwardedUrl() { + return this.forwardedUrl; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java new file mode 100644 index 00000000..7df8b547 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockMultipartActionRequest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.mock.web.portlet; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.portlet.multipart.MultipartActionRequest; + +/** + * Mock implementation of the + * {@link org.springframework.web.portlet.multipart.MultipartActionRequest} interface. + * + * <p>Useful for testing application controllers that access multipart uploads. + * The {@link org.springframework.mock.web.MockMultipartFile} can be used to + * populate these mock requests with files. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 2.0 + * @see org.springframework.mock.web.MockMultipartFile + */ +public class MockMultipartActionRequest extends MockActionRequest implements MultipartActionRequest { + + private final MultiValueMap<String, MultipartFile> multipartFiles = + new LinkedMultiValueMap<String, MultipartFile>(); + + + /** + * Add a file to this request. The parameter name from the multipart + * form is taken from the {@link org.springframework.web.multipart.MultipartFile#getName()}. + * @param file multipart file to be added + */ + public void addFile(MultipartFile file) { + Assert.notNull(file, "MultipartFile must not be null"); + this.multipartFiles.add(file.getName(), file); + } + + public Iterator<String> getFileNames() { + return this.multipartFiles.keySet().iterator(); + } + + public MultipartFile getFile(String name) { + return this.multipartFiles.getFirst(name); + } + + public List<MultipartFile> getFiles(String name) { + List<MultipartFile> multipartFiles = this.multipartFiles.get(name); + if (multipartFiles != null) { + return multipartFiles; + } + else { + return Collections.emptyList(); + } + } + + public Map<String, MultipartFile> getFileMap() { + return this.multipartFiles.toSingleValueMap(); + } + + public MultiValueMap<String, MultipartFile> getMultiFileMap() { + return new LinkedMultiValueMap<String, MultipartFile>(this.multipartFiles); + } + + public String getMultipartContentType(String paramOrFileName) { + MultipartFile file = getFile(paramOrFileName); + if (file != null) { + return file.getContentType(); + } + else { + return null; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java new file mode 100644 index 00000000..949d077b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortalContext.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.portlet.PortalContext; +import javax.portlet.PortletMode; +import javax.portlet.WindowState; + +/** + * Mock implementation of the {@link javax.portlet.PortalContext} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortalContext implements PortalContext { + + private final Map<String, String> properties = new HashMap<String, String>(); + + private final List<PortletMode> portletModes; + + private final List<WindowState> windowStates; + + + /** + * Create a new MockPortalContext + * with default PortletModes (VIEW, EDIT, HELP) + * and default WindowStates (NORMAL, MAXIMIZED, MINIMIZED). + * @see javax.portlet.PortletMode + * @see javax.portlet.WindowState + */ + public MockPortalContext() { + this.portletModes = new ArrayList<PortletMode>(3); + this.portletModes.add(PortletMode.VIEW); + this.portletModes.add(PortletMode.EDIT); + this.portletModes.add(PortletMode.HELP); + + this.windowStates = new ArrayList<WindowState>(3); + this.windowStates.add(WindowState.NORMAL); + this.windowStates.add(WindowState.MAXIMIZED); + this.windowStates.add(WindowState.MINIMIZED); + } + + /** + * Create a new MockPortalContext with the given PortletModes and WindowStates. + * @param supportedPortletModes the List of supported PortletMode instances + * @param supportedWindowStates the List of supported WindowState instances + * @see javax.portlet.PortletMode + * @see javax.portlet.WindowState + */ + public MockPortalContext(List<PortletMode> supportedPortletModes, List<WindowState> supportedWindowStates) { + this.portletModes = new ArrayList<PortletMode>(supportedPortletModes); + this.windowStates = new ArrayList<WindowState>(supportedWindowStates); + } + + + public String getPortalInfo() { + return "MockPortal/1.0"; + } + + public void setProperty(String name, String value) { + this.properties.put(name, value); + } + + public String getProperty(String name) { + return this.properties.get(name); + } + + public Enumeration<String> getPropertyNames() { + return Collections.enumeration(this.properties.keySet()); + } + + public Enumeration<PortletMode> getSupportedPortletModes() { + return Collections.enumeration(this.portletModes); + } + + public Enumeration<WindowState> getSupportedWindowStates() { + return Collections.enumeration(this.windowStates); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java new file mode 100644 index 00000000..9d3a8780 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletConfig.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web.portlet; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.portlet.PortletConfig} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletConfig implements PortletConfig { + + private final PortletContext portletContext; + + private final String portletName; + + private final Map<Locale, ResourceBundle> resourceBundles = new HashMap<Locale, ResourceBundle>(); + + private final Map<String, String> initParameters = new LinkedHashMap<String, String>(); + + private final Set<String> publicRenderParameterNames = new LinkedHashSet<String>(); + + private String defaultNamespace = XMLConstants.NULL_NS_URI; + + private final Set<QName> publishingEventQNames = new LinkedHashSet<QName>(); + + private final Set<QName> processingEventQNames = new LinkedHashSet<QName>(); + + private final Set<Locale> supportedLocales = new LinkedHashSet<Locale>(); + + private final Map<String, String[]> containerRuntimeOptions = new LinkedHashMap<String, String[]>(); + + + /** + * Create a new MockPortletConfig with a default {@link MockPortletContext}. + */ + public MockPortletConfig() { + this(null, ""); + } + + /** + * Create a new MockPortletConfig with a default {@link MockPortletContext}. + * @param portletName the name of the portlet + */ + public MockPortletConfig(String portletName) { + this(null, portletName); + } + + /** + * Create a new MockPortletConfig. + * @param portletContext the PortletContext that the portlet runs in + */ + public MockPortletConfig(PortletContext portletContext) { + this(portletContext, ""); + } + + /** + * Create a new MockPortletConfig. + * @param portletContext the PortletContext that the portlet runs in + * @param portletName the name of the portlet + */ + public MockPortletConfig(PortletContext portletContext, String portletName) { + this.portletContext = (portletContext != null ? portletContext : new MockPortletContext()); + this.portletName = portletName; + } + + + public String getPortletName() { + return this.portletName; + } + + public PortletContext getPortletContext() { + return this.portletContext; + } + + public void setResourceBundle(Locale locale, ResourceBundle resourceBundle) { + Assert.notNull(locale, "Locale must not be null"); + this.resourceBundles.put(locale, resourceBundle); + } + + public ResourceBundle getResourceBundle(Locale locale) { + Assert.notNull(locale, "Locale must not be null"); + return this.resourceBundles.get(locale); + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + public Enumeration<String> getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + + public void addPublicRenderParameterName(String name) { + this.publicRenderParameterNames.add(name); + } + + public Enumeration<String> getPublicRenderParameterNames() { + return Collections.enumeration(this.publicRenderParameterNames); + } + + public void setDefaultNamespace(String defaultNamespace) { + this.defaultNamespace = defaultNamespace; + } + + public String getDefaultNamespace() { + return this.defaultNamespace; + } + + public void addPublishingEventQName(QName name) { + this.publishingEventQNames.add(name); + } + + public Enumeration<QName> getPublishingEventQNames() { + return Collections.enumeration(this.publishingEventQNames); + } + + public void addProcessingEventQName(QName name) { + this.processingEventQNames.add(name); + } + + public Enumeration<QName> getProcessingEventQNames() { + return Collections.enumeration(this.processingEventQNames); + } + + public void addSupportedLocale(Locale locale) { + this.supportedLocales.add(locale); + } + + public Enumeration<Locale> getSupportedLocales() { + return Collections.enumeration(this.supportedLocales); + } + + public void addContainerRuntimeOption(String key, String value) { + this.containerRuntimeOptions.put(key, new String[] {value}); + } + + public void addContainerRuntimeOption(String key, String[] values) { + this.containerRuntimeOptions.put(key, values); + } + + public Map<String, String[]> getContainerRuntimeOptions() { + return Collections.unmodifiableMap(this.containerRuntimeOptions); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java new file mode 100644 index 00000000..39dc9f5b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletContext.java @@ -0,0 +1,278 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web.portlet; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import javax.activation.FileTypeMap; +import javax.portlet.PortletContext; +import javax.portlet.PortletRequestDispatcher; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.web.util.WebUtils; + +/** + * Mock implementation of the {@link javax.portlet.PortletContext} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletContext implements PortletContext { + + private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir"; + + + private final Log logger = LogFactory.getLog(getClass()); + + private final String resourceBasePath; + + private final ResourceLoader resourceLoader; + + private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(); + + private final Map<String, String> initParameters = new LinkedHashMap<String, String>(); + + private String portletContextName = "MockPortletContext"; + + private Set<String> containerRuntimeOptions = new LinkedHashSet<String>(); + + + /** + * Create a new MockPortletContext with no base path and a + * DefaultResourceLoader (i.e. the classpath root as WAR root). + * @see org.springframework.core.io.DefaultResourceLoader + */ + public MockPortletContext() { + this("", null); + } + + /** + * Create a new MockPortletContext using a DefaultResourceLoader. + * @param resourceBasePath the WAR root directory (should not end with a slash) + * @see org.springframework.core.io.DefaultResourceLoader + */ + public MockPortletContext(String resourceBasePath) { + this(resourceBasePath, null); + } + + /** + * Create a new MockPortletContext, using the specified ResourceLoader + * and no base path. + * @param resourceLoader the ResourceLoader to use (or null for the default) + */ + public MockPortletContext(ResourceLoader resourceLoader) { + this("", resourceLoader); + } + + /** + * Create a new MockPortletContext. + * @param resourceBasePath the WAR root directory (should not end with a slash) + * @param resourceLoader the ResourceLoader to use (or null for the default) + */ + public MockPortletContext(String resourceBasePath, ResourceLoader resourceLoader) { + this.resourceBasePath = (resourceBasePath != null ? resourceBasePath : ""); + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + + // Use JVM temp dir as PortletContext temp dir. + String tempDir = System.getProperty(TEMP_DIR_SYSTEM_PROPERTY); + if (tempDir != null) { + this.attributes.put(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File(tempDir)); + } + } + + /** + * Build a full resource location for the given path, + * prepending the resource base path of this MockPortletContext. + * @param path the path as specified + * @return the full resource path + */ + protected String getResourceLocation(String path) { + if (!path.startsWith("/")) { + path = "/" + path; + } + return this.resourceBasePath + path; + } + + + public String getServerInfo() { + return "MockPortal/1.0"; + } + + public PortletRequestDispatcher getRequestDispatcher(String path) { + if (!path.startsWith("/")) { + throw new IllegalArgumentException( + "PortletRequestDispatcher path at PortletContext level must start with '/'"); + } + return new MockPortletRequestDispatcher(path); + } + + public PortletRequestDispatcher getNamedDispatcher(String path) { + return null; + } + + public InputStream getResourceAsStream(String path) { + Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + try { + return resource.getInputStream(); + } + catch (IOException ex) { + logger.info("Couldn't open InputStream for " + resource, ex); + return null; + } + } + + public int getMajorVersion() { + return 2; + } + + public int getMinorVersion() { + return 0; + } + + public String getMimeType(String filePath) { + return MimeTypeResolver.getMimeType(filePath); + } + + public String getRealPath(String path) { + Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + try { + return resource.getFile().getAbsolutePath(); + } + catch (IOException ex) { + logger.info("Couldn't determine real path of resource " + resource, ex); + return null; + } + } + + public Set<String> getResourcePaths(String path) { + Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + try { + File file = resource.getFile(); + String[] fileList = file.list(); + String prefix = (path.endsWith("/") ? path : path + "/"); + Set<String> resourcePaths = new HashSet<String>(fileList.length); + for (String fileEntry : fileList) { + resourcePaths.add(prefix + fileEntry); + } + return resourcePaths; + } + catch (IOException ex) { + logger.info("Couldn't get resource paths for " + resource, ex); + return null; + } + } + + public URL getResource(String path) throws MalformedURLException { + Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + try { + return resource.getURL(); + } + catch (IOException ex) { + logger.info("Couldn't get URL for " + resource, ex); + return null; + } + } + + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet())); + } + + public void setAttribute(String name, Object value) { + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + public void removeAttribute(String name) { + this.attributes.remove(name); + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + public Enumeration<String> getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + + public void log(String message) { + logger.info(message); + } + + public void log(String message, Throwable t) { + logger.info(message, t); + } + + public void setPortletContextName(String portletContextName) { + this.portletContextName = portletContextName; + } + + public String getPortletContextName() { + return this.portletContextName; + } + + public void addContainerRuntimeOption(String key) { + this.containerRuntimeOptions.add(key); + } + + public Enumeration<String> getContainerRuntimeOptions() { + return Collections.enumeration(this.containerRuntimeOptions); + } + + + /** + * Inner factory class used to just introduce a Java Activation Framework + * dependency when actually asked to resolve a MIME type. + */ + private static class MimeTypeResolver { + + public static String getMimeType(String filePath) { + return FileTypeMap.getDefaultFileTypeMap().getContentType(filePath); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java new file mode 100644 index 00000000..92ee6c0a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletPreferences.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import javax.portlet.PortletPreferences; +import javax.portlet.PreferencesValidator; +import javax.portlet.ReadOnlyException; +import javax.portlet.ValidatorException; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.portlet.PortletPreferences} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletPreferences implements PortletPreferences { + + private PreferencesValidator preferencesValidator; + + private final Map<String, String[]> preferences = new LinkedHashMap<String, String[]>(); + + private final Set<String> readOnly = new HashSet<String>(); + + + public void setReadOnly(String key, boolean readOnly) { + Assert.notNull(key, "Key must not be null"); + if (readOnly) { + this.readOnly.add(key); + } + else { + this.readOnly.remove(key); + } + } + + public boolean isReadOnly(String key) { + Assert.notNull(key, "Key must not be null"); + return this.readOnly.contains(key); + } + + public String getValue(String key, String def) { + Assert.notNull(key, "Key must not be null"); + String[] values = this.preferences.get(key); + return (values != null && values.length > 0 ? values[0] : def); + } + + public String[] getValues(String key, String[] def) { + Assert.notNull(key, "Key must not be null"); + String[] values = this.preferences.get(key); + return (values != null && values.length > 0 ? values : def); + } + + public void setValue(String key, String value) throws ReadOnlyException { + setValues(key, new String[] {value}); + } + + public void setValues(String key, String[] values) throws ReadOnlyException { + Assert.notNull(key, "Key must not be null"); + if (isReadOnly(key)) { + throw new ReadOnlyException("Preference '" + key + "' is read-only"); + } + this.preferences.put(key, values); + } + + public Enumeration<String> getNames() { + return Collections.enumeration(this.preferences.keySet()); + } + + public Map<String, String[]> getMap() { + return Collections.unmodifiableMap(this.preferences); + } + + public void reset(String key) throws ReadOnlyException { + Assert.notNull(key, "Key must not be null"); + if (isReadOnly(key)) { + throw new ReadOnlyException("Preference '" + key + "' is read-only"); + } + this.preferences.remove(key); + } + + public void setPreferencesValidator(PreferencesValidator preferencesValidator) { + this.preferencesValidator = preferencesValidator; + } + + public void store() throws IOException, ValidatorException { + if (this.preferencesValidator != null) { + this.preferencesValidator.validate(this); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java new file mode 100644 index 00000000..38b51735 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequest.java @@ -0,0 +1,531 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web.portlet; + +import java.security.Principal; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.portlet.PortalContext; +import javax.portlet.PortletContext; +import javax.portlet.PortletMode; +import javax.portlet.PortletPreferences; +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; +import javax.portlet.WindowState; +import javax.servlet.http.Cookie; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Mock implementation of the {@link javax.portlet.PortletRequest} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletRequest implements PortletRequest { + + private boolean active = true; + + private final PortalContext portalContext; + + private final PortletContext portletContext; + + private PortletSession session; + + private WindowState windowState = WindowState.NORMAL; + + private PortletMode portletMode = PortletMode.VIEW; + + private PortletPreferences portletPreferences = new MockPortletPreferences(); + + private final Map<String, List<String>> properties = new LinkedHashMap<String, List<String>>(); + + private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(); + + private final Map<String, String[]> parameters = new LinkedHashMap<String, String[]>(); + + private String authType = null; + + private String contextPath = ""; + + private String remoteUser = null; + + private Principal userPrincipal = null; + + private final Set<String> userRoles = new HashSet<String>(); + + private boolean secure = false; + + private boolean requestedSessionIdValid = true; + + private final List<String> responseContentTypes = new LinkedList<String>(); + + private final List<Locale> locales = new LinkedList<Locale>(); + + private String scheme = "http"; + + private String serverName = "localhost"; + + private int serverPort = 80; + + private String windowID; + + private Cookie[] cookies; + + private final Set<String> publicParameterNames = new HashSet<String>(); + + + /** + * Create a new MockPortletRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * + * @see MockPortalContext + * @see MockPortletContext + */ + public MockPortletRequest() { + this(null, null); + } + + /** + * Create a new MockPortletRequest with a default {@link MockPortalContext}. + * + * @param portletContext the PortletContext that the request runs in + * @see MockPortalContext + */ + public MockPortletRequest(PortletContext portletContext) { + this(null, portletContext); + } + + /** + * Create a new MockPortletRequest. + * + * @param portalContext the PortalContext that the request runs in + * @param portletContext the PortletContext that the request runs in + */ + public MockPortletRequest(PortalContext portalContext, PortletContext portletContext) { + this.portalContext = (portalContext != null ? portalContext : new MockPortalContext()); + this.portletContext = (portletContext != null ? portletContext : new MockPortletContext()); + this.responseContentTypes.add("text/html"); + this.locales.add(Locale.ENGLISH); + this.attributes.put(LIFECYCLE_PHASE, getLifecyclePhase()); + } + + // --------------------------------------------------------------------- + // Lifecycle methods + // --------------------------------------------------------------------- + + /** + * Return the Portlet 2.0 lifecycle id for the current phase. + */ + protected String getLifecyclePhase() { + return null; + } + + /** + * Return whether this request is still active (that is, not completed yet). + */ + public boolean isActive() { + return this.active; + } + + /** + * Mark this request as completed. + */ + public void close() { + this.active = false; + } + + /** + * Check whether this request is still active (that is, not completed yet), + * throwing an IllegalStateException if not active anymore. + */ + protected void checkActive() throws IllegalStateException { + if (!this.active) { + throw new IllegalStateException("Request is not active anymore"); + } + } + + // --------------------------------------------------------------------- + // PortletRequest methods + // --------------------------------------------------------------------- + + public boolean isWindowStateAllowed(WindowState windowState) { + return CollectionUtils.contains(this.portalContext.getSupportedWindowStates(), windowState); + } + + public boolean isPortletModeAllowed(PortletMode portletMode) { + return CollectionUtils.contains(this.portalContext.getSupportedPortletModes(), portletMode); + } + + public void setPortletMode(PortletMode portletMode) { + Assert.notNull(portletMode, "PortletMode must not be null"); + this.portletMode = portletMode; + } + + public PortletMode getPortletMode() { + return this.portletMode; + } + + public void setWindowState(WindowState windowState) { + Assert.notNull(windowState, "WindowState must not be null"); + this.windowState = windowState; + } + + public WindowState getWindowState() { + return this.windowState; + } + + public void setPreferences(PortletPreferences preferences) { + Assert.notNull(preferences, "PortletPreferences must not be null"); + this.portletPreferences = preferences; + } + + public PortletPreferences getPreferences() { + return this.portletPreferences; + } + + public void setSession(PortletSession session) { + this.session = session; + if (session instanceof MockPortletSession) { + MockPortletSession mockSession = ((MockPortletSession) session); + mockSession.access(); + } + } + + public PortletSession getPortletSession() { + return getPortletSession(true); + } + + public PortletSession getPortletSession(boolean create) { + checkActive(); + // Reset session if invalidated. + if (this.session instanceof MockPortletSession && ((MockPortletSession) this.session).isInvalid()) { + this.session = null; + } + // Create new session if necessary. + if (this.session == null && create) { + this.session = new MockPortletSession(this.portletContext); + } + return this.session; + } + + /** + * Set a single value for the specified property. + * <p> + * If there are already one or more values registered for the given property + * key, they will be replaced. + */ + public void setProperty(String key, String value) { + Assert.notNull(key, "Property key must not be null"); + List<String> list = new LinkedList<String>(); + list.add(value); + this.properties.put(key, list); + } + + /** + * Add a single value for the specified property. + * <p> + * If there are already one or more values registered for the given property + * key, the given value will be added to the end of the list. + */ + public void addProperty(String key, String value) { + Assert.notNull(key, "Property key must not be null"); + List<String> oldList = this.properties.get(key); + if (oldList != null) { + oldList.add(value); + } + else { + List<String> list = new LinkedList<String>(); + list.add(value); + this.properties.put(key, list); + } + } + + public String getProperty(String key) { + Assert.notNull(key, "Property key must not be null"); + List<String> list = this.properties.get(key); + return (list != null && list.size() > 0 ? list.get(0) : null); + } + + public Enumeration<String> getProperties(String key) { + Assert.notNull(key, "property key must not be null"); + return Collections.enumeration(this.properties.get(key)); + } + + public Enumeration<String> getPropertyNames() { + return Collections.enumeration(this.properties.keySet()); + } + + public PortalContext getPortalContext() { + return this.portalContext; + } + + public void setAuthType(String authType) { + this.authType = authType; + } + + public String getAuthType() { + return this.authType; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + public String getContextPath() { + return this.contextPath; + } + + public void setRemoteUser(String remoteUser) { + this.remoteUser = remoteUser; + } + + public String getRemoteUser() { + return this.remoteUser; + } + + public void setUserPrincipal(Principal userPrincipal) { + this.userPrincipal = userPrincipal; + } + + public Principal getUserPrincipal() { + return this.userPrincipal; + } + + public void addUserRole(String role) { + this.userRoles.add(role); + } + + public boolean isUserInRole(String role) { + return this.userRoles.contains(role); + } + + public Object getAttribute(String name) { + checkActive(); + return this.attributes.get(name); + } + + public Enumeration<String> getAttributeNames() { + checkActive(); + return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet())); + } + + public void setParameters(Map<String, String[]> parameters) { + Assert.notNull(parameters, "Parameters Map must not be null"); + this.parameters.clear(); + this.parameters.putAll(parameters); + } + + public void setParameter(String key, String value) { + Assert.notNull(key, "Parameter key must be null"); + Assert.notNull(value, "Parameter value must not be null"); + this.parameters.put(key, new String[] { value }); + } + + public void setParameter(String key, String[] values) { + Assert.notNull(key, "Parameter key must be null"); + Assert.notNull(values, "Parameter values must not be null"); + this.parameters.put(key, values); + } + + public void addParameter(String name, String value) { + addParameter(name, new String[] { value }); + } + + public void addParameter(String name, String[] values) { + String[] oldArr = this.parameters.get(name); + if (oldArr != null) { + String[] newArr = new String[oldArr.length + values.length]; + System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); + System.arraycopy(values, 0, newArr, oldArr.length, values.length); + this.parameters.put(name, newArr); + } + else { + this.parameters.put(name, values); + } + } + + public String getParameter(String name) { + String[] arr = this.parameters.get(name); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + public Enumeration<String> getParameterNames() { + return Collections.enumeration(this.parameters.keySet()); + } + + public String[] getParameterValues(String name) { + return this.parameters.get(name); + } + + public Map<String, String[]> getParameterMap() { + return Collections.unmodifiableMap(this.parameters); + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public boolean isSecure() { + return this.secure; + } + + public void setAttribute(String name, Object value) { + checkActive(); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + public void removeAttribute(String name) { + checkActive(); + this.attributes.remove(name); + } + + public String getRequestedSessionId() { + PortletSession session = this.getPortletSession(); + return (session != null ? session.getId() : null); + } + + public void setRequestedSessionIdValid(boolean requestedSessionIdValid) { + this.requestedSessionIdValid = requestedSessionIdValid; + } + + public boolean isRequestedSessionIdValid() { + return this.requestedSessionIdValid; + } + + public void addResponseContentType(String responseContentType) { + this.responseContentTypes.add(responseContentType); + } + + public void addPreferredResponseContentType(String responseContentType) { + this.responseContentTypes.add(0, responseContentType); + } + + public String getResponseContentType() { + return this.responseContentTypes.get(0); + } + + public Enumeration<String> getResponseContentTypes() { + return Collections.enumeration(this.responseContentTypes); + } + + public void addLocale(Locale locale) { + this.locales.add(locale); + } + + public void addPreferredLocale(Locale locale) { + this.locales.add(0, locale); + } + + public Locale getLocale() { + return this.locales.get(0); + } + + public Enumeration<Locale> getLocales() { + return Collections.enumeration(this.locales); + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public String getScheme() { + return this.scheme; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public String getServerName() { + return this.serverName; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public int getServerPort() { + return this.serverPort; + } + + public void setWindowID(String windowID) { + this.windowID = windowID; + } + + public String getWindowID() { + return this.windowID; + } + + public void setCookies(Cookie... cookies) { + this.cookies = cookies; + } + + public Cookie[] getCookies() { + return this.cookies; + } + + public Map<String, String[]> getPrivateParameterMap() { + if (!this.publicParameterNames.isEmpty()) { + Map<String, String[]> filtered = new LinkedHashMap<String, String[]>(); + for (String key : this.parameters.keySet()) { + if (!this.publicParameterNames.contains(key)) { + filtered.put(key, this.parameters.get(key)); + } + } + return filtered; + } + else { + return Collections.unmodifiableMap(this.parameters); + } + } + + public Map<String, String[]> getPublicParameterMap() { + if (!this.publicParameterNames.isEmpty()) { + Map<String, String[]> filtered = new LinkedHashMap<String, String[]>(); + for (String key : this.parameters.keySet()) { + if (this.publicParameterNames.contains(key)) { + filtered.put(key, this.parameters.get(key)); + } + } + return filtered; + } + else { + return Collections.emptyMap(); + } + } + + public void registerPublicParameter(String name) { + this.publicParameterNames.add(name); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java new file mode 100644 index 00000000..ca8695f9 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletRequestDispatcher.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.IOException; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletRequestDispatcher; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.portlet.PortletRequestDispatcher} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletRequestDispatcher implements PortletRequestDispatcher { + + private final Log logger = LogFactory.getLog(getClass()); + + private final String url; + + + /** + * Create a new MockPortletRequestDispatcher for the given URL. + * @param url the URL to dispatch to. + */ + public MockPortletRequestDispatcher(String url) { + Assert.notNull(url, "URL must not be null"); + this.url = url; + } + + + public void include(RenderRequest request, RenderResponse response) throws PortletException, IOException { + include((PortletRequest) request, (PortletResponse) response); + } + + public void include(PortletRequest request, PortletResponse response) throws PortletException, IOException { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + if (!(response instanceof MockMimeResponse)) { + throw new IllegalArgumentException("MockPortletRequestDispatcher requires MockMimeResponse"); + } + ((MockMimeResponse) response).setIncludedUrl(this.url); + if (logger.isDebugEnabled()) { + logger.debug("MockPortletRequestDispatcher: including URL [" + this.url + "]"); + } + } + + public void forward(PortletRequest request, PortletResponse response) throws PortletException, IOException { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + if (!(response instanceof MockMimeResponse)) { + throw new IllegalArgumentException("MockPortletRequestDispatcher requires MockMimeResponse"); + } + ((MockMimeResponse) response).setForwardedUrl(this.url); + if (logger.isDebugEnabled()) { + logger.debug("MockPortletRequestDispatcher: forwarding to URL [" + this.url + "]"); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java new file mode 100644 index 00000000..b556209f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletResponse.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import javax.portlet.PortalContext; +import javax.portlet.PortletResponse; +import javax.servlet.http.Cookie; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.portlet.PortletResponse} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletResponse implements PortletResponse { + + private final PortalContext portalContext; + + private final Map<String, String[]> properties = new LinkedHashMap<String, String[]>(); + + private String namespace = ""; + + private final Set<Cookie> cookies = new LinkedHashSet<Cookie>(); + + private final Map<String, Element[]> xmlProperties = new LinkedHashMap<String, Element[]>(); + + private Document xmlDocument; + + + /** + * Create a new MockPortletResponse with a default {@link MockPortalContext}. + * @see MockPortalContext + */ + public MockPortletResponse() { + this(null); + } + + /** + * Create a new MockPortletResponse. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + */ + public MockPortletResponse(PortalContext portalContext) { + this.portalContext = (portalContext != null ? portalContext : new MockPortalContext()); + } + + /** + * Return the PortalContext that this MockPortletResponse runs in, + * defining the supported PortletModes and WindowStates. + */ + public PortalContext getPortalContext() { + return this.portalContext; + } + + + //--------------------------------------------------------------------- + // PortletResponse methods + //--------------------------------------------------------------------- + + public void addProperty(String key, String value) { + Assert.notNull(key, "Property key must not be null"); + String[] oldArr = this.properties.get(key); + if (oldArr != null) { + String[] newArr = new String[oldArr.length + 1]; + System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); + newArr[oldArr.length] = value; + this.properties.put(key, newArr); + } + else { + this.properties.put(key, new String[] {value}); + } + } + + public void setProperty(String key, String value) { + Assert.notNull(key, "Property key must not be null"); + this.properties.put(key, new String[] {value}); + } + + public Set<String> getPropertyNames() { + return Collections.unmodifiableSet(this.properties.keySet()); + } + + public String getProperty(String key) { + Assert.notNull(key, "Property key must not be null"); + String[] arr = this.properties.get(key); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + public String[] getProperties(String key) { + Assert.notNull(key, "Property key must not be null"); + return this.properties.get(key); + } + + public String encodeURL(String path) { + return path; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getNamespace() { + return this.namespace; + } + + public void addProperty(Cookie cookie) { + Assert.notNull(cookie, "Cookie must not be null"); + this.cookies.add(cookie); + } + + public Cookie[] getCookies() { + return this.cookies.toArray(new Cookie[this.cookies.size()]); + } + + public Cookie getCookie(String name) { + Assert.notNull(name, "Cookie name must not be null"); + for (Cookie cookie : this.cookies) { + if (name.equals(cookie.getName())) { + return cookie; + } + } + return null; + } + + public void addProperty(String key, Element value) { + Assert.notNull(key, "Property key must not be null"); + Element[] oldArr = this.xmlProperties.get(key); + if (oldArr != null) { + Element[] newArr = new Element[oldArr.length + 1]; + System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); + newArr[oldArr.length] = value; + this.xmlProperties.put(key, newArr); + } + else { + this.xmlProperties.put(key, new Element[] {value}); + } + } + + + public Set<String> getXmlPropertyNames() { + return Collections.unmodifiableSet(this.xmlProperties.keySet()); + } + + public Element getXmlProperty(String key) { + Assert.notNull(key, "Property key must not be null"); + Element[] arr = this.xmlProperties.get(key); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + public Element[] getXmlProperties(String key) { + Assert.notNull(key, "Property key must not be null"); + return this.xmlProperties.get(key); + } + + public Element createElement(String tagName) throws DOMException { + if (this.xmlDocument == null) { + try { + this.xmlDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + } + catch (ParserConfigurationException ex) { + throw new DOMException(DOMException.INVALID_STATE_ERR, ex.toString()); + } + } + return this.xmlDocument.createElement(tagName); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java new file mode 100644 index 00000000..5eaec799 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletSession.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web.portlet; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import javax.portlet.PortletContext; +import javax.portlet.PortletSession; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; + +import org.springframework.mock.web.MockHttpSession; + +/** + * Mock implementation of the {@link javax.portlet.PortletSession} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletSession implements PortletSession { + + private static int nextId = 1; + + + private final String id = Integer.toString(nextId++); + + private final long creationTime = System.currentTimeMillis(); + + private int maxInactiveInterval; + + private long lastAccessedTime = System.currentTimeMillis(); + + private final PortletContext portletContext; + + private final Map<String, Object> portletAttributes = new LinkedHashMap<String, Object>(); + + private final Map<String, Object> applicationAttributes = new LinkedHashMap<String, Object>(); + + private boolean invalid = false; + + private boolean isNew = true; + + + /** + * Create a new MockPortletSession with a default {@link MockPortletContext}. + * @see MockPortletContext + */ + public MockPortletSession() { + this(null); + } + + /** + * Create a new MockPortletSession. + * @param portletContext the PortletContext that the session runs in + */ + public MockPortletSession(PortletContext portletContext) { + this.portletContext = (portletContext != null ? portletContext : new MockPortletContext()); + } + + + public Object getAttribute(String name) { + return this.portletAttributes.get(name); + } + + public Object getAttribute(String name, int scope) { + if (scope == PortletSession.PORTLET_SCOPE) { + return this.portletAttributes.get(name); + } + else if (scope == PortletSession.APPLICATION_SCOPE) { + return this.applicationAttributes.get(name); + } + return null; + } + + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(new LinkedHashSet<String>(this.portletAttributes.keySet())); + } + + public Enumeration<String> getAttributeNames(int scope) { + if (scope == PortletSession.PORTLET_SCOPE) { + return Collections.enumeration(new LinkedHashSet<String>(this.portletAttributes.keySet())); + } + else if (scope == PortletSession.APPLICATION_SCOPE) { + return Collections.enumeration(new LinkedHashSet<String>(this.applicationAttributes.keySet())); + } + return null; + } + + public long getCreationTime() { + return this.creationTime; + } + + public String getId() { + return this.id; + } + + public void access() { + this.lastAccessedTime = System.currentTimeMillis(); + setNew(false); + } + + public long getLastAccessedTime() { + return this.lastAccessedTime; + } + + public int getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + /** + * Clear all of this session's attributes. + */ + public void clearAttributes() { + doClearAttributes(this.portletAttributes); + doClearAttributes(this.applicationAttributes); + } + + protected void doClearAttributes(Map<String, Object> attributes) { + for (Iterator<Map.Entry<String, Object>> it = attributes.entrySet().iterator(); it.hasNext();) { + Map.Entry<String, Object> entry = it.next(); + String name = entry.getKey(); + Object value = entry.getValue(); + it.remove(); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound( + new HttpSessionBindingEvent(new MockHttpSession(), name, value)); + } + } + } + + public void invalidate() { + this.invalid = true; + clearAttributes(); + } + + public boolean isInvalid() { + return this.invalid; + } + + public void setNew(boolean value) { + this.isNew = value; + } + + public boolean isNew() { + return this.isNew; + } + + public void removeAttribute(String name) { + this.portletAttributes.remove(name); + } + + public void removeAttribute(String name, int scope) { + if (scope == PortletSession.PORTLET_SCOPE) { + this.portletAttributes.remove(name); + } + else if (scope == PortletSession.APPLICATION_SCOPE) { + this.applicationAttributes.remove(name); + } + } + + public void setAttribute(String name, Object value) { + if (value != null) { + this.portletAttributes.put(name, value); + } + else { + this.portletAttributes.remove(name); + } + } + + public void setAttribute(String name, Object value, int scope) { + if (scope == PortletSession.PORTLET_SCOPE) { + if (value != null) { + this.portletAttributes.put(name, value); + } + else { + this.portletAttributes.remove(name); + } + } + else if (scope == PortletSession.APPLICATION_SCOPE) { + if (value != null) { + this.applicationAttributes.put(name, value); + } + else { + this.applicationAttributes.remove(name); + } + } + } + + public void setMaxInactiveInterval(int interval) { + this.maxInactiveInterval = interval; + } + + public PortletContext getPortletContext() { + return this.portletContext; + } + + public Map<String, Object> getAttributeMap() { + return Collections.unmodifiableMap(this.portletAttributes); + } + + public Map<String, Object> getAttributeMap(int scope) { + if (scope == PortletSession.PORTLET_SCOPE) { + return Collections.unmodifiableMap(this.portletAttributes); + } + else if (scope == PortletSession.APPLICATION_SCOPE) { + return Collections.unmodifiableMap(this.applicationAttributes); + } + else { + return Collections.emptyMap(); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java new file mode 100644 index 00000000..12abdf57 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockPortletURL.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.util.Map; +import javax.portlet.PortalContext; +import javax.portlet.PortletMode; +import javax.portlet.PortletModeException; +import javax.portlet.PortletURL; +import javax.portlet.WindowState; +import javax.portlet.WindowStateException; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Mock implementation of the {@link javax.portlet.PortletURL} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockPortletURL extends MockBaseURL implements PortletURL { + + public static final String URL_TYPE_RENDER = "render"; + + public static final String URL_TYPE_ACTION = "action"; + + + private final PortalContext portalContext; + + private final String urlType; + + private WindowState windowState; + + private PortletMode portletMode; + + + /** + * Create a new MockPortletURL for the given URL type. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + * @param urlType the URL type, for example "render" or "action" + * @see #URL_TYPE_RENDER + * @see #URL_TYPE_ACTION + */ + public MockPortletURL(PortalContext portalContext, String urlType) { + Assert.notNull(portalContext, "PortalContext is required"); + this.portalContext = portalContext; + this.urlType = urlType; + } + + + //--------------------------------------------------------------------- + // PortletURL methods + //--------------------------------------------------------------------- + + public void setWindowState(WindowState windowState) throws WindowStateException { + if (!CollectionUtils.contains(this.portalContext.getSupportedWindowStates(), windowState)) { + throw new WindowStateException("WindowState not supported", windowState); + } + this.windowState = windowState; + } + + public WindowState getWindowState() { + return this.windowState; + } + + public void setPortletMode(PortletMode portletMode) throws PortletModeException { + if (!CollectionUtils.contains(this.portalContext.getSupportedPortletModes(), portletMode)) { + throw new PortletModeException("PortletMode not supported", portletMode); + } + this.portletMode = portletMode; + } + + public PortletMode getPortletMode() { + return this.portletMode; + } + + public void removePublicRenderParameter(String name) { + this.parameters.remove(name); + } + + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(encodeParameter("urlType", this.urlType)); + if (this.windowState != null) { + sb.append(";").append(encodeParameter("windowState", this.windowState.toString())); + } + if (this.portletMode != null) { + sb.append(";").append(encodeParameter("portletMode", this.portletMode.toString())); + } + for (Map.Entry<String, String[]> entry : this.parameters.entrySet()) { + sb.append(";").append(encodeParameter("param_" + entry.getKey(), entry.getValue())); + } + return (isSecure() ? "https:" : "http:") + + "//localhost/mockportlet?" + sb.toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java new file mode 100644 index 00000000..d86ba7f2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderRequest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import javax.portlet.PortalContext; +import javax.portlet.PortletContext; +import javax.portlet.PortletMode; +import javax.portlet.RenderRequest; +import javax.portlet.WindowState; + +/** + * Mock implementation of the {@link javax.portlet.RenderRequest} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockRenderRequest extends MockPortletRequest implements RenderRequest { + + /** + * Create a new MockRenderRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @see MockPortalContext + * @see MockPortletContext + */ + public MockRenderRequest() { + super(); + } + + /** + * Create a new MockRenderRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @param portletMode the mode that the portlet runs in + */ + public MockRenderRequest(PortletMode portletMode) { + super(); + setPortletMode(portletMode); + } + + /** + * Create a new MockRenderRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @param portletMode the mode that the portlet runs in + * @param windowState the window state to run the portlet in + */ + public MockRenderRequest(PortletMode portletMode, WindowState windowState) { + super(); + setPortletMode(portletMode); + setWindowState(windowState); + } + + /** + * Create a new MockRenderRequest with a default {@link MockPortalContext}. + * @param portletContext the PortletContext that the request runs in + */ + public MockRenderRequest(PortletContext portletContext) { + super(portletContext); + } + + /** + * Create a new MockRenderRequest. + * @param portalContext the PortletContext that the request runs in + * @param portletContext the PortletContext that the request runs in + */ + public MockRenderRequest(PortalContext portalContext, PortletContext portletContext) { + super(portalContext, portletContext); + } + + + @Override + protected String getLifecyclePhase() { + return RENDER_PHASE; + } + + public String getETag() { + return getProperty(RenderRequest.ETAG); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java new file mode 100644 index 00000000..a2feeb37 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockRenderResponse.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.util.Collection; +import javax.portlet.PortalContext; +import javax.portlet.PortletMode; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +/** + * Mock implementation of the {@link javax.portlet.RenderResponse} interface. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class MockRenderResponse extends MockMimeResponse implements RenderResponse { + + private String title; + + private Collection<PortletMode> nextPossiblePortletModes; + + + /** + * Create a new MockRenderResponse with a default {@link MockPortalContext}. + * @see MockPortalContext + */ + public MockRenderResponse() { + super(); + } + + /** + * Create a new MockRenderResponse. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + */ + public MockRenderResponse(PortalContext portalContext) { + super(portalContext); + } + + /** + * Create a new MockRenderResponse. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + * @param request the corresponding render request that this response + * is generated for + */ + public MockRenderResponse(PortalContext portalContext, RenderRequest request) { + super(portalContext, request); + } + + + //--------------------------------------------------------------------- + // RenderResponse methods + //--------------------------------------------------------------------- + + public void setTitle(String title) { + this.title = title; + } + + public String getTitle() { + return this.title; + } + + public void setNextPossiblePortletModes(Collection<PortletMode> portletModes) { + this.nextPossiblePortletModes = portletModes; + } + + public Collection<PortletMode> getNextPossiblePortletModes() { + return this.nextPossiblePortletModes; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceRequest.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceRequest.java new file mode 100644 index 00000000..7cce4ba3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceRequest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.portlet.PortalContext; +import javax.portlet.PortletContext; +import javax.portlet.RenderRequest; +import javax.portlet.ResourceRequest; + +/** + * Mock implementation of the {@link javax.portlet.ResourceRequest} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockResourceRequest extends MockClientDataRequest implements ResourceRequest { + + private String resourceID; + + private String cacheability; + + private final Map<String, String[]> privateRenderParameterMap = new LinkedHashMap<String, String[]>(); + + + /** + * Create a new MockResourceRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @see org.springframework.mock.web.portlet.MockPortalContext + * @see org.springframework.mock.web.portlet.MockPortletContext + */ + public MockResourceRequest() { + super(); + } + + /** + * Create a new MockResourceRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @param resourceID the resource id for this request + */ + public MockResourceRequest(String resourceID) { + super(); + this.resourceID = resourceID; + } + + /** + * Create a new MockResourceRequest with a default {@link MockPortalContext} + * and a default {@link MockPortletContext}. + * @param url the resource URL for this request + */ + public MockResourceRequest(MockResourceURL url) { + super(); + this.resourceID = url.getResourceID(); + this.cacheability = url.getCacheability(); + } + + /** + * Create a new MockResourceRequest with a default {@link MockPortalContext}. + * @param portletContext the PortletContext that the request runs in + */ + public MockResourceRequest(PortletContext portletContext) { + super(portletContext); + } + + /** + * Create a new MockResourceRequest. + * @param portalContext the PortalContext that the request runs in + * @param portletContext the PortletContext that the request runs in + */ + public MockResourceRequest(PortalContext portalContext, PortletContext portletContext) { + super(portalContext, portletContext); + } + + + @Override + protected String getLifecyclePhase() { + return RESOURCE_PHASE; + } + + public void setResourceID(String resourceID) { + this.resourceID = resourceID; + } + + public String getResourceID() { + return this.resourceID; + } + + public void setCacheability(String cacheLevel) { + this.cacheability = cacheLevel; + } + + public String getCacheability() { + return this.cacheability; + } + + public String getETag() { + return getProperty(RenderRequest.ETAG); + } + + public void addPrivateRenderParameter(String key, String value) { + this.privateRenderParameterMap.put(key, new String[] {value}); + } + + public void addPrivateRenderParameter(String key, String[] values) { + this.privateRenderParameterMap.put(key, values); + } + + public Map<String, String[]> getPrivateRenderParameterMap() { + return Collections.unmodifiableMap(this.privateRenderParameterMap); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceResponse.java new file mode 100644 index 00000000..297a1968 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceResponse.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import javax.portlet.ResourceResponse; + +/** + * Mock implementation of the {@link javax.portlet.ResourceResponse} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockResourceResponse extends MockMimeResponse implements ResourceResponse { + + private int contentLength = 0; + + + public void setContentLength(int len) { + this.contentLength = len; + } + + public int getContentLength() { + return this.contentLength; + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceURL.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceURL.java new file mode 100644 index 00000000..cce36b18 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockResourceURL.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.util.Map; +import javax.portlet.ResourceURL; + +/** + * Mock implementation of the {@link javax.portlet.ResourceURL} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockResourceURL extends MockBaseURL implements ResourceURL { + + private String resourceID; + + private String cacheability; + + + //--------------------------------------------------------------------- + // ResourceURL methods + //--------------------------------------------------------------------- + + public void setResourceID(String resourceID) { + this.resourceID = resourceID; + } + + public String getResourceID() { + return this.resourceID; + } + + public void setCacheability(String cacheLevel) { + this.cacheability = cacheLevel; + } + + public String getCacheability() { + return this.cacheability; + } + + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(encodeParameter("resourceID", this.resourceID)); + if (this.cacheability != null) { + sb.append(";").append(encodeParameter("cacheability", this.cacheability)); + } + for (Map.Entry<String, String[]> entry : this.parameters.entrySet()) { + sb.append(";").append(encodeParameter("param_" + entry.getKey(), entry.getValue())); + } + return (isSecure() ? "https:" : "http:") + + "//localhost/mockportlet?" + sb.toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/MockStateAwareResponse.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockStateAwareResponse.java new file mode 100644 index 00000000..95df6ed0 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/MockStateAwareResponse.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.portlet.PortalContext; +import javax.portlet.PortletMode; +import javax.portlet.PortletModeException; +import javax.portlet.StateAwareResponse; +import javax.portlet.WindowState; +import javax.portlet.WindowStateException; +import javax.xml.namespace.QName; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Mock implementation of the {@link javax.portlet.StateAwareResponse} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class MockStateAwareResponse extends MockPortletResponse implements StateAwareResponse { + + private WindowState windowState; + + private PortletMode portletMode; + + private final Map<String, String[]> renderParameters = new LinkedHashMap<String, String[]>(); + + private final Map<QName, Serializable> events = new HashMap<QName, Serializable>(); + + + /** + * Create a new MockActionResponse with a default {@link MockPortalContext}. + * @see org.springframework.mock.web.portlet.MockPortalContext + */ + public MockStateAwareResponse() { + super(); + } + + /** + * Create a new MockActionResponse. + * @param portalContext the PortalContext defining the supported + * PortletModes and WindowStates + */ + public MockStateAwareResponse(PortalContext portalContext) { + super(portalContext); + } + + + public void setWindowState(WindowState windowState) throws WindowStateException { + if (!CollectionUtils.contains(getPortalContext().getSupportedWindowStates(), windowState)) { + throw new WindowStateException("WindowState not supported", windowState); + } + this.windowState = windowState; + } + + public WindowState getWindowState() { + return this.windowState; + } + + public void setPortletMode(PortletMode portletMode) throws PortletModeException { + if (!CollectionUtils.contains(getPortalContext().getSupportedPortletModes(), portletMode)) { + throw new PortletModeException("PortletMode not supported", portletMode); + } + this.portletMode = portletMode; + } + + public PortletMode getPortletMode() { + return this.portletMode; + } + + public void setRenderParameters(Map<String, String[]> parameters) { + Assert.notNull(parameters, "Parameters Map must not be null"); + this.renderParameters.clear(); + this.renderParameters.putAll(parameters); + } + + public void setRenderParameter(String key, String value) { + Assert.notNull(key, "Parameter key must not be null"); + Assert.notNull(value, "Parameter value must not be null"); + this.renderParameters.put(key, new String[] {value}); + } + + public void setRenderParameter(String key, String[] values) { + Assert.notNull(key, "Parameter key must not be null"); + Assert.notNull(values, "Parameter values must not be null"); + this.renderParameters.put(key, values); + } + + public String getRenderParameter(String key) { + Assert.notNull(key, "Parameter key must not be null"); + String[] arr = this.renderParameters.get(key); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + public String[] getRenderParameterValues(String key) { + Assert.notNull(key, "Parameter key must not be null"); + return this.renderParameters.get(key); + } + + public Iterator<String> getRenderParameterNames() { + return this.renderParameters.keySet().iterator(); + } + + public Map<String, String[]> getRenderParameterMap() { + return Collections.unmodifiableMap(this.renderParameters); + } + + public void removePublicRenderParameter(String name) { + this.renderParameters.remove(name); + } + + public void setEvent(QName name, Serializable value) { + this.events.put(name, value); + } + + public void setEvent(String name, Serializable value) { + this.events.put(new QName(name), value); + } + + public Iterator<QName> getEventNames() { + return this.events.keySet().iterator(); + } + + public Serializable getEvent(QName name) { + return this.events.get(name); + } + + public Serializable getEvent(String name) { + return this.events.get(new QName(name)); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java new file mode 100644 index 00000000..8a88066c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2009 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 + * + * http://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.mock.web.portlet; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import javax.portlet.PortletContext; +import javax.portlet.PortletRequestDispatcher; +import javax.servlet.ServletContext; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.portlet.PortletContext} interface, + * wrapping an underlying {@link javax.servlet.ServletContext}. + * + * @author Juergen Hoeller + * @since 3.0 + * @see MockPortletContext + */ +public class ServletWrappingPortletContext implements PortletContext { + + private final ServletContext servletContext; + + + /** + * Create a new PortletContext wrapping the given ServletContext. + * @param servletContext the ServletContext to wrap + */ + public ServletWrappingPortletContext(ServletContext servletContext) { + Assert.notNull(servletContext, "ServletContext must not be null"); + this.servletContext = servletContext; + } + + /** + * Return the underlying ServletContext that this PortletContext wraps. + */ + public final ServletContext getServletContext() { + return this.servletContext; + } + + + public String getServerInfo() { + return this.servletContext.getServerInfo(); + } + + public PortletRequestDispatcher getRequestDispatcher(String path) { + return null; + } + + public PortletRequestDispatcher getNamedDispatcher(String name) { + return null; + } + + public InputStream getResourceAsStream(String path) { + return this.servletContext.getResourceAsStream(path); + } + + public int getMajorVersion() { + return 2; + } + + public int getMinorVersion() { + return 0; + } + + public String getMimeType(String file) { + return this.servletContext.getMimeType(file); + } + + public String getRealPath(String path) { + return this.servletContext.getRealPath(path); + } + + @SuppressWarnings("unchecked") + public Set<String> getResourcePaths(String path) { + return this.servletContext.getResourcePaths(path); + } + + public URL getResource(String path) throws MalformedURLException { + return this.servletContext.getResource(path); + } + + public Object getAttribute(String name) { + return this.servletContext.getAttribute(name); + } + + @SuppressWarnings("unchecked") + public Enumeration<String> getAttributeNames() { + return this.servletContext.getAttributeNames(); + } + + public String getInitParameter(String name) { + return this.servletContext.getInitParameter(name); + } + + @SuppressWarnings("unchecked") + public Enumeration<String> getInitParameterNames() { + return this.servletContext.getInitParameterNames(); + } + + public void log(String msg) { + this.servletContext.log(msg); + } + + public void log(String message, Throwable throwable) { + this.servletContext.log(message, throwable); + } + + public void removeAttribute(String name) { + this.servletContext.removeAttribute(name); + } + + public void setAttribute(String name, Object object) { + this.servletContext.setAttribute(name, object); + } + + public String getPortletContextName() { + return this.servletContext.getServletContextName(); + } + + public Enumeration<String> getContainerRuntimeOptions() { + return Collections.enumeration(new HashSet<String>()); + } + +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/package-info.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/package-info.java new file mode 100644 index 00000000..3c28fee4 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/package-info.java @@ -0,0 +1,14 @@ + +/** + * + * A comprehensive set of Portlet API 2.0 mock objects, + * targeted at usage with Spring's web MVC framework. + * Useful for testing web contexts and controllers. + * + * <p>More convenient to use than dynamic mock objects + * (<a href="http://www.easymock.org">EasyMock</a>) or + * existing Portlet API mock objects. + * + */ +package org.springframework.mock.web.portlet; + diff --git a/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java new file mode 100644 index 00000000..6229a8d3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java @@ -0,0 +1,294 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; + +/** + * <p> + * Convenient superclass for JUnit 3.8 based tests depending on a Spring + * context. The test instance itself is populated by Dependency Injection. + * </p> + * <p> + * Really for integration testing, not unit testing. You should <i>not</i> + * normally use the Spring container for unit tests: simply populate your POJOs + * in plain JUnit tests! + * </p> + * <p> + * This supports two modes of populating the test: + * </p> + * <ul> + * <li>Via Setter Dependency Injection. Simply express dependencies on objects + * in the test fixture, and they will be satisfied by autowiring by type. + * <li>Via Field Injection. Declare protected variables of the required type + * which match named beans in the context. This is autowire by name, rather than + * type. This approach is based on an approach originated by Ara Abrahmian. + * Setter Dependency Injection is the default: set the + * {@code populateProtectedVariables} property to {@code true} in + * the constructor to switch on Field Injection. + * </ul> + * + * @author Rod Johnson + * @author Rob Harrop + * @author Rick Evans + * @author Sam Brannen + * @since 1.1.1 + * @see #setDirty + * @see #contextKey + * @see #getContext + * @see #getConfigLocations + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests}) + */ +@Deprecated +@SuppressWarnings({ "unchecked", "rawtypes" }) +public abstract class AbstractDependencyInjectionSpringContextTests extends AbstractSingleSpringContextTests { + + /** + * Constant that indicates no autowiring at all. + * + * @see #setAutowireMode + */ + public static final int AUTOWIRE_NO = 0; + + /** + * Constant that indicates autowiring bean properties by name. + * + * @see #setAutowireMode + */ + public static final int AUTOWIRE_BY_NAME = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME; + + /** + * Constant that indicates autowiring bean properties by type. + * + * @see #setAutowireMode + */ + public static final int AUTOWIRE_BY_TYPE = AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE; + + private boolean populateProtectedVariables = false; + + private int autowireMode = AUTOWIRE_BY_TYPE; + + private boolean dependencyCheck = true; + + private String[] managedVariableNames; + + + /** + * Default constructor for AbstractDependencyInjectionSpringContextTests. + */ + public AbstractDependencyInjectionSpringContextTests() { + } + + /** + * Constructor for AbstractDependencyInjectionSpringContextTests with a + * JUnit name. + * @param name the name of this text fixture + */ + public AbstractDependencyInjectionSpringContextTests(String name) { + super(name); + } + + + /** + * Set whether to populate protected variables of this test case. Default is + * {@code false}. + */ + public final void setPopulateProtectedVariables(boolean populateFields) { + this.populateProtectedVariables = populateFields; + } + + /** + * Return whether to populate protected variables of this test case. + */ + public final boolean isPopulateProtectedVariables() { + return this.populateProtectedVariables; + } + + /** + * Set the autowire mode for test properties set by Dependency Injection. + * <p>The default is {@link #AUTOWIRE_BY_TYPE}. Can be set to + * {@link #AUTOWIRE_BY_NAME} or {@link #AUTOWIRE_NO} instead. + * @see #AUTOWIRE_BY_TYPE + * @see #AUTOWIRE_BY_NAME + * @see #AUTOWIRE_NO + */ + public final void setAutowireMode(final int autowireMode) { + this.autowireMode = autowireMode; + } + + /** + * Return the autowire mode for test properties set by Dependency Injection. + */ + public final int getAutowireMode() { + return this.autowireMode; + } + + /** + * Set whether or not dependency checking should be performed for test + * properties set by Dependency Injection. + * <p>The default is {@code true}, meaning that tests cannot be run + * unless all properties are populated. + */ + public final void setDependencyCheck(final boolean dependencyCheck) { + this.dependencyCheck = dependencyCheck; + } + + /** + * Return whether or not dependency checking should be performed for test + * properties set by Dependency Injection. + */ + public final boolean isDependencyCheck() { + return this.dependencyCheck; + } + + /** + * Prepare this test instance, injecting dependencies into its protected + * fields and its bean properties. + * <p>Note: if the {@link ApplicationContext} for this test instance has not + * been configured (e.g., is {@code null}), dependency injection + * will naturally <strong>not</strong> be performed, but an informational + * message will be written to the log. + * @see #injectDependencies() + */ + protected void prepareTestInstance() throws Exception { + if (getApplicationContext() == null) { + if (this.logger.isInfoEnabled()) { + this.logger.info("ApplicationContext has not been configured for test [" + getClass().getName() + + "]: dependency injection will NOT be performed."); + } + } + else { + injectDependencies(); + } + } + + /** + * Inject dependencies into 'this' instance (that is, this test instance). + * <p>The default implementation populates protected variables if the + * {@link #populateProtectedVariables() appropriate flag is set}, else uses + * autowiring if autowiring is switched on (which it is by default). + * <p>Override this method if you need full control over how dependencies are + * injected into the test instance. + * @throws Exception in case of dependency injection failure + * @throws IllegalStateException if the {@link ApplicationContext} for this + * test instance has not been configured + * @see #populateProtectedVariables() + */ + @SuppressWarnings("javadoc") + protected void injectDependencies() throws Exception { + Assert.state(getApplicationContext() != null, + "injectDependencies() called without first configuring an ApplicationContext"); + if (isPopulateProtectedVariables()) { + if (this.managedVariableNames == null) { + initManagedVariableNames(); + } + populateProtectedVariables(); + } + getApplicationContext().getBeanFactory().autowireBeanProperties(this, getAutowireMode(), isDependencyCheck()); + } + + private void initManagedVariableNames() throws IllegalAccessException { + List managedVarNames = new LinkedList(); + Class clazz = getClass(); + do { + Field[] fields = clazz.getDeclaredFields(); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Found " + fields.length + " fields on " + clazz); + } + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + field.setAccessible(true); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Candidate field: " + field); + } + if (isProtectedInstanceField(field)) { + Object oldValue = field.get(this); + if (oldValue == null) { + managedVarNames.add(field.getName()); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Added managed variable '" + field.getName() + "'"); + } + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Rejected managed variable '" + field.getName() + "'"); + } + } + } + } + clazz = clazz.getSuperclass(); + } while (!clazz.equals(AbstractDependencyInjectionSpringContextTests.class)); + + this.managedVariableNames = (String[]) managedVarNames.toArray(new String[managedVarNames.size()]); + } + + private boolean isProtectedInstanceField(Field field) { + int modifiers = field.getModifiers(); + return !Modifier.isStatic(modifiers) && Modifier.isProtected(modifiers); + } + + private void populateProtectedVariables() throws IllegalAccessException { + for (int i = 0; i < this.managedVariableNames.length; i++) { + String varName = this.managedVariableNames[i]; + Object bean = null; + try { + Field field = findField(getClass(), varName); + bean = getApplicationContext().getBean(varName, field.getType()); + field.setAccessible(true); + field.set(this, bean); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Populated field: " + field); + } + } + catch (NoSuchFieldException ex) { + if (this.logger.isWarnEnabled()) { + this.logger.warn("No field with name '" + varName + "'"); + } + } + catch (NoSuchBeanDefinitionException ex) { + if (this.logger.isWarnEnabled()) { + this.logger.warn("No bean with name '" + varName + "'"); + } + } + } + } + + private Field findField(Class clazz, String name) throws NoSuchFieldException { + try { + return clazz.getDeclaredField(name); + } + catch (NoSuchFieldException ex) { + Class superclass = clazz.getSuperclass(); + if (superclass != AbstractSpringContextTests.class) { + return findField(superclass, name); + } + else { + throw ex; + } + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java new file mode 100644 index 00000000..0d5d0784 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/AbstractSingleSpringContextTests.java @@ -0,0 +1,362 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.ClassUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * <p> + * Abstract JUnit 3.8 test class that holds and exposes a single Spring + * {@link org.springframework.context.ApplicationContext ApplicationContext}. + * </p> + * <p> + * This class will cache contexts based on a <i>context key</i>: normally the + * config locations String array describing the Spring resource descriptors + * making up the context. Unless the {@link #setDirty()} method is called by a + * test, the context will not be reloaded, even across different subclasses of + * this test. This is particularly beneficial if your context is slow to + * construct, for example if you are using Hibernate and the time taken to load + * the mappings is an issue. + * </p> + * <p> + * For such standard usage, simply override the {@link #getConfigLocations()} + * method and provide the desired config files. For alternative configuration + * options, see {@link #getConfigPath()} and {@link #getConfigPaths()}. + * </p> + * <p> + * If you don't want to load a standard context from an array of config + * locations, you can override the {@link #contextKey()} method. In conjunction + * with this you typically need to override the {@link #loadContext(Object)} + * method, which by default loads the locations specified in the + * {@link #getConfigLocations()} method. + * </p> + * <p> + * <b>WARNING:</b> When doing integration tests from within Eclipse, only use + * classpath resource URLs. Else, you may see misleading failures when changing + * context locations. + * </p> + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @see #getConfigLocations() + * @see #contextKey() + * @see #loadContext(Object) + * @see #getApplicationContext() + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests}) + */ +@Deprecated +public abstract class AbstractSingleSpringContextTests extends AbstractSpringContextTests { + + /** Application context this test will run against */ + protected ConfigurableApplicationContext applicationContext; + + private int loadCount = 0; + + + /** + * Default constructor for AbstractSingleSpringContextTests. + */ + public AbstractSingleSpringContextTests() { + } + + /** + * Constructor for AbstractSingleSpringContextTests with a JUnit name. + * @param name the name of this text fixture + */ + public AbstractSingleSpringContextTests(String name) { + super(name); + } + + /** + * This implementation is final. Override {@code onSetUp} for custom behavior. + * @see #onSetUp() + */ + protected final void setUp() throws Exception { + // lazy load, in case getApplicationContext() has not yet been called. + if (this.applicationContext == null) { + this.applicationContext = getContext(contextKey()); + } + prepareTestInstance(); + onSetUp(); + } + + /** + * Prepare this test instance, for example populating its fields. + * The context has already been loaded at the time of this callback. + * <p>The default implementation does nothing. + * @throws Exception in case of preparation failure + */ + protected void prepareTestInstance() throws Exception { + } + + /** + * Subclasses can override this method in place of the {@code setUp()} + * method, which is final in this class. + * <p>The default implementation does nothing. + * @throws Exception simply let any exception propagate + */ + protected void onSetUp() throws Exception { + } + + /** + * Called to say that the "applicationContext" instance variable is dirty + * and should be reloaded. We need to do this if a test has modified the + * context (for example, by replacing a bean definition). + */ + protected void setDirty() { + setDirty(contextKey()); + } + + /** + * This implementation is final. Override {@code onTearDown} for + * custom behavior. + * @see #onTearDown() + */ + protected final void tearDown() throws Exception { + onTearDown(); + } + + /** + * Subclasses can override this to add custom behavior on teardown. + * @throws Exception simply let any exception propagate + */ + protected void onTearDown() throws Exception { + } + + /** + * Return a key for this context. Default is the config location array as + * determined by {@link #getConfigLocations()}. + * <p>If you override this method, you will typically have to override + * {@link #loadContext(Object)} as well, being able to handle the key type + * that this method returns. + * @return the context key + * @see #getConfigLocations() + */ + protected Object contextKey() { + return getConfigLocations(); + } + + /** + * This implementation assumes a key of type String array and loads a + * context from the given locations. + * <p>If you override {@link #contextKey()}, you will typically have to + * override this method as well, being able to handle the key type that + * {@code contextKey()} returns. + * @see #getConfigLocations() + */ + protected ConfigurableApplicationContext loadContext(Object key) throws Exception { + return loadContextLocations((String[]) key); + } + + /** + * Load a Spring ApplicationContext from the given config locations. + * <p>The default implementation creates a standard + * {@link #createApplicationContext GenericApplicationContext}, allowing + * for customizing the internal bean factory through + * {@link #customizeBeanFactory}. + * @param locations the config locations (as Spring resource locations, + * e.g. full classpath locations or any kind of URL) + * @return the corresponding ApplicationContext instance (potentially cached) + * @throws Exception if context loading failed + * @see #createApplicationContext(String[]) + */ + protected ConfigurableApplicationContext loadContextLocations(String[] locations) throws Exception { + ++this.loadCount; + if (this.logger.isInfoEnabled()) { + this.logger.info("Loading context for locations: " + StringUtils.arrayToCommaDelimitedString(locations)); + } + return createApplicationContext(locations); + } + + /** + * Create a Spring {@link ConfigurableApplicationContext} for use by this test. + * <p>The default implementation creates a standard {@link GenericApplicationContext} + * instance, calls the {@link #prepareApplicationContext} prepareApplicationContext} + * method and the {@link #customizeBeanFactory customizeBeanFactory} method to allow + * for customizing the context and its DefaultListableBeanFactory, populates the + * context from the specified config {@code locations} through the configured + * {@link #createBeanDefinitionReader(GenericApplicationContext) BeanDefinitionReader}, + * and finally {@link ConfigurableApplicationContext#refresh() refreshes} the context. + * @param locations the config locations (as Spring resource locations, + * e.g. full classpath locations or any kind of URL) + * @return the GenericApplicationContext instance + * @see #loadContextLocations(String[]) + * @see #customizeBeanFactory(DefaultListableBeanFactory) + * @see #createBeanDefinitionReader(GenericApplicationContext) + */ + protected ConfigurableApplicationContext createApplicationContext(String[] locations) { + GenericApplicationContext context = new GenericApplicationContext(); + prepareApplicationContext(context); + customizeBeanFactory(context.getDefaultListableBeanFactory()); + createBeanDefinitionReader(context).loadBeanDefinitions(locations); + context.refresh(); + return context; + } + + /** + * Prepare the GenericApplicationContext used by this test. + * Called before bean definitions are read. + * <p>The default implementation is empty. Can be overridden in subclasses to + * customize GenericApplicationContext's standard settings. + * @param context the context for which the BeanDefinitionReader should be created + * @see #createApplicationContext + * @see org.springframework.context.support.GenericApplicationContext#setResourceLoader + * @see org.springframework.context.support.GenericApplicationContext#setId + */ + protected void prepareApplicationContext(GenericApplicationContext context) { + } + + /** + * Customize the internal bean factory of the ApplicationContext used by + * this test. Called before bean definitions are read. + * <p>The default implementation is empty. Can be overridden in subclasses to + * customize DefaultListableBeanFactory's standard settings. + * @param beanFactory the newly created bean factory for this context + * @see #loadContextLocations + * @see #createApplicationContext + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowEagerClassLoading + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping + */ + protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { + } + + /** + * Factory method for creating new {@link BeanDefinitionReader}s for + * loading bean definitions into the supplied + * {@link GenericApplicationContext context}. + * <p>The default implementation creates a new {@link XmlBeanDefinitionReader}. + * Can be overridden in subclasses to provide a different + * BeanDefinitionReader implementation. + * @param context the context for which the BeanDefinitionReader should be created + * @return a BeanDefinitionReader for the supplied context + * @see #createApplicationContext(String[]) + * @see BeanDefinitionReader + * @see XmlBeanDefinitionReader + */ + protected BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context) { + return new XmlBeanDefinitionReader(context); + } + + /** + * Subclasses can override this method to return the locations of their + * config files, unless they override {@link #contextKey()} and + * {@link #loadContext(Object)} instead. + * <p>A plain path will be treated as class path location, e.g.: + * "org/springframework/whatever/foo.xml". Note however that you may prefix + * path locations with standard Spring resource prefixes. Therefore, a + * config location path prefixed with "classpath:" with behave the same as a + * plain path, but a config location such as + * "file:/some/path/path/location/appContext.xml" will be treated as a + * filesystem location. + * <p>The default implementation builds config locations for the config paths + * specified through {@link #getConfigPaths()}. + * @return an array of config locations + * @see #getConfigPaths() + * @see org.springframework.core.io.ResourceLoader#getResource(String) + */ + protected String[] getConfigLocations() { + String[] paths = getConfigPaths(); + String[] locations = new String[paths.length]; + for (int i = 0; i < paths.length; i++) { + String path = paths[i]; + if (path.startsWith("/")) { + locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + path; + } + else { + locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + + StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(getClass()) + "/" + path); + } + } + return locations; + } + + /** + * Subclasses can override this method to return paths to their config + * files, relative to the concrete test class. + * <p>A plain path, e.g. "context.xml", will be loaded as classpath resource + * from the same package that the concrete test class is defined in. A path + * starting with a slash is treated as fully qualified class path location, + * e.g.: "/org/springframework/whatever/foo.xml". + * <p>The default implementation builds an array for the config path specified + * through {@link #getConfigPath()}. + * @return an array of config locations + * @see #getConfigPath() + * @see java.lang.Class#getResource(String) + */ + protected String[] getConfigPaths() { + String path = getConfigPath(); + return (path != null ? new String[] { path } : new String[0]); + } + + /** + * Subclasses can override this method to return a single path to a config + * file, relative to the concrete test class. + * <p>A plain path, e.g. "context.xml", will be loaded as classpath resource + * from the same package that the concrete test class is defined in. A path + * starting with a slash is treated as fully qualified class path location, + * e.g.: "/org/springframework/whatever/foo.xml". + * <p>The default implementation simply returns {@code null}. + * @return an array of config locations + * @see #getConfigPath() + * @see Class#getResource(String) + */ + protected String getConfigPath() { + return null; + } + + /** + * Return the ApplicationContext that this base class manages; may be + * {@code null}. + */ + public final ConfigurableApplicationContext getApplicationContext() { + // lazy load, in case setUp() has not yet been called. + if (this.applicationContext == null) { + try { + this.applicationContext = getContext(contextKey()); + } + catch (Exception e) { + // log and continue... + if (this.logger.isDebugEnabled()) { + this.logger.debug("Caught exception while retrieving the ApplicationContext for test [" + + getClass().getName() + "." + getName() + "].", e); + } + } + } + + return this.applicationContext; + } + + /** + * Return the current number of context load attempts. + */ + public final int getLoadCount() { + return this.loadCount; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/AbstractSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractSpringContextTests.java new file mode 100644 index 00000000..d01144ed --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/AbstractSpringContextTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * <p> + * Superclass for JUnit 3.8 test cases using Spring + * {@link org.springframework.context.ApplicationContext ApplicationContexts}. + * </p> + * <p> + * Maintains a static cache of contexts by key. This has significant performance + * benefit if initializing the context would take time. While initializing a + * Spring context itself is very quick, some beans in a context, such as a + * LocalSessionFactoryBean for working with Hibernate, may take some time to + * initialize. Hence it often makes sense to do that initializing once. + * </p> + * <p> + * Any ApplicationContext created by this class will be asked to register a JVM + * shutdown hook for itself. Unless the context gets closed early, all context + * instances will be automatically closed on JVM shutdown. This allows for + * freeing external resources held by beans within the context, e.g. temporary + * files. + * </p> + * <p> + * Normally you won't extend this class directly but rather one of its + * subclasses. + * </p> + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.1.1 + * @see AbstractSingleSpringContextTests + * @see AbstractDependencyInjectionSpringContextTests + * @see AbstractTransactionalSpringContextTests + * @see AbstractTransactionalDataSourceSpringContextTests + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests}) + */ +@Deprecated +public abstract class AbstractSpringContextTests extends ConditionalTestCase { + + /** + * Map of context keys returned by subclasses of this class, to Spring + * contexts. This needs to be static, as JUnit tests are destroyed and + * recreated between running individual test methods. + */ + private static Map<String, ConfigurableApplicationContext> contextKeyToContextMap = + new HashMap<String, ConfigurableApplicationContext>(); + + + /** + * Default constructor for AbstractSpringContextTests. + */ + public AbstractSpringContextTests() { + } + + /** + * Constructor for AbstractSpringContextTests with a JUnit name. + */ + public AbstractSpringContextTests(String name) { + super(name); + } + + + /** + * Explicitly add an ApplicationContext instance under a given key. + * <p>This is not meant to be used by subclasses. It is rather exposed for + * special test suite environments. + * @param key the context key + * @param context the ApplicationContext instance + */ + public final void addContext(Object key, ConfigurableApplicationContext context) { + Assert.notNull(context, "ApplicationContext must not be null"); + contextKeyToContextMap.put(contextKeyString(key), context); + } + + /** + * Return whether there is a cached context for the given key. + * @param key the context key + */ + protected final boolean hasCachedContext(Object key) { + return contextKeyToContextMap.containsKey(contextKeyString(key)); + } + + /** + * Determine if the supplied context {@code key} is <em>empty</em>. + * <p>By default, {@code null} values, empty strings, and zero-length + * arrays are considered <em>empty</em>. + * @param key the context key to check + * @return {@code true} if the supplied context key is empty + */ + protected boolean isContextKeyEmpty(Object key) { + return (key == null) || ((key instanceof String) && !StringUtils.hasText((String) key)) || + ((key instanceof Object[]) && ObjectUtils.isEmpty((Object[]) key)); + } + + /** + * Obtain an ApplicationContext for the given key, potentially cached. + * @param key the context key; may be {@code null}. + * @return the corresponding ApplicationContext instance (potentially cached), + * or {@code null} if the provided {@code key} is <em>empty</em> + */ + protected final ConfigurableApplicationContext getContext(Object key) throws Exception { + if (isContextKeyEmpty(key)) { + return null; + } + String keyString = contextKeyString(key); + ConfigurableApplicationContext ctx = contextKeyToContextMap.get(keyString); + if (ctx == null) { + ctx = loadContext(key); + ctx.registerShutdownHook(); + contextKeyToContextMap.put(keyString, ctx); + } + return ctx; + } + + /** + * Mark the context with the given key as dirty. This will cause the cached + * context to be reloaded before the next test case is executed. + * <p>Call this method only if you change the state of a singleton bean, + * potentially affecting future tests. + */ + protected final void setDirty(Object contextKey) { + String keyString = contextKeyString(contextKey); + ConfigurableApplicationContext ctx = contextKeyToContextMap.remove(keyString); + if (ctx != null) { + ctx.close(); + } + } + + /** + * Subclasses can override this to return a String representation of their + * context key for use in caching and logging. + * @param contextKey the context key + */ + protected String contextKeyString(Object contextKey) { + return ObjectUtils.nullSafeToString(contextKey); + } + + /** + * Load a new ApplicationContext for the given key. + * <p>To be implemented by subclasses. + * @param key the context key + * @return the corresponding ApplicationContext instance (new) + */ + protected abstract ConfigurableApplicationContext loadContext(Object key) throws Exception; + +} diff --git a/spring-test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java new file mode 100644 index 00000000..0f697d11 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.core.io.support.EncodedResource; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.jdbc.JdbcTestUtils; + +/** + * Subclass of AbstractTransactionalSpringContextTests that adds some convenience + * functionality for JDBC access. Expects a {@link javax.sql.DataSource} bean + * to be defined in the Spring application context. + * + * <p>This class exposes a {@link org.springframework.jdbc.core.JdbcTemplate} + * and provides an easy way to delete from the database in a new transaction. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Thomas Risberg + * @since 1.1.1 + * @see #setDataSource(javax.sql.DataSource) + * @see #getJdbcTemplate() + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests}) + */ +@Deprecated +@SuppressWarnings({ "unchecked", "rawtypes" }) +public abstract class AbstractTransactionalDataSourceSpringContextTests + extends AbstractTransactionalSpringContextTests { + + protected JdbcTemplate jdbcTemplate; + + private String sqlScriptEncoding; + + /** + * Did this test delete any tables? If so, we forbid transaction completion, + * and only allow rollback. + */ + private boolean zappedTables; + + + /** + * Default constructor for AbstractTransactionalDataSourceSpringContextTests. + */ + public AbstractTransactionalDataSourceSpringContextTests() { + } + + /** + * Constructor for AbstractTransactionalDataSourceSpringContextTests with a JUnit name. + */ + public AbstractTransactionalDataSourceSpringContextTests(String name) { + super(name); + } + + + /** + * Setter: DataSource is provided by Dependency Injection. + */ + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Return the JdbcTemplate that this base class manages. + */ + public final JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Specify the encoding for SQL scripts, if different from the platform encoding. + * @see #executeSqlScript + */ + public void setSqlScriptEncoding(String sqlScriptEncoding) { + this.sqlScriptEncoding = sqlScriptEncoding; + } + + + /** + * Convenient method to delete all rows from these tables. + * Calling this method will make avoidance of rollback by calling + * {@code setComplete()} impossible. + * @see #setComplete + */ + protected void deleteFromTables(String[] names) { + for (int i = 0; i < names.length; i++) { + int rowCount = this.jdbcTemplate.update("DELETE FROM " + names[i]); + if (logger.isInfoEnabled()) { + logger.info("Deleted " + rowCount + " rows from table " + names[i]); + } + } + this.zappedTables = true; + } + + /** + * Overridden to prevent the transaction committing if a number of tables have been + * cleared, as a defensive measure against accidental <i>permanent</i> wiping of a database. + * @see org.springframework.test.AbstractTransactionalSpringContextTests#setComplete() + */ + protected final void setComplete() { + if (this.zappedTables) { + throw new IllegalStateException("Cannot set complete after deleting tables"); + } + super.setComplete(); + } + + /** + * Count the rows in the given table + * @param tableName table name to count rows in + * @return the number of rows in the table + */ + protected int countRowsInTable(String tableName) { + return this.jdbcTemplate.queryForInt("SELECT COUNT(0) FROM " + tableName); + } + + + /** + * Execute the given SQL script. Will be rolled back by default, + * according to the fate of the current transaction. + * @param sqlResourcePath Spring resource path for the SQL script. + * Should normally be loaded by classpath. + * <p>Statements should be delimited with a semicolon. If statements are not delimited with + * a semicolon then there should be one statement per line. Statements are allowed to span + * lines only if they are delimited with a semicolon. + * <p><b>Do not use this method to execute DDL if you expect rollback.</b> + * @param continueOnError whether or not to continue without throwing + * an exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and continueOnError was false + */ + protected void executeSqlScript(String sqlResourcePath, boolean continueOnError) throws DataAccessException { + if (logger.isInfoEnabled()) { + logger.info("Executing SQL script '" + sqlResourcePath + "'"); + } + + EncodedResource resource = + new EncodedResource(getApplicationContext().getResource(sqlResourcePath), this.sqlScriptEncoding); + long startTime = System.currentTimeMillis(); + List statements = new LinkedList(); + try { + LineNumberReader lnr = new LineNumberReader(resource.getReader()); + String script = JdbcTestUtils.readScript(lnr); + char delimiter = ';'; + if (!JdbcTestUtils.containsSqlScriptDelimiters(script, delimiter)) { + delimiter = '\n'; + } + JdbcTestUtils.splitSqlScript(script, delimiter, statements); + for (Iterator itr = statements.iterator(); itr.hasNext(); ) { + String statement = (String) itr.next(); + try { + int rowsAffected = this.jdbcTemplate.update(statement); + if (logger.isDebugEnabled()) { + logger.debug(rowsAffected + " rows affected by SQL: " + statement); + } + } + catch (DataAccessException ex) { + if (continueOnError) { + if (logger.isWarnEnabled()) { + logger.warn("SQL: " + statement + " failed", ex); + } + } + else { + throw ex; + } + } + } + long elapsedTime = System.currentTimeMillis() - startTime; + logger.info("Done executing SQL scriptBuilder '" + sqlResourcePath + "' in " + elapsedTime + " ms"); + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Failed to open SQL script '" + sqlResourcePath + "'", ex); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java new file mode 100644 index 00000000..af3c6866 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/AbstractTransactionalSpringContextTests.java @@ -0,0 +1,359 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +/** + * Convenient base class for JUnit 3.8 based tests that should occur in a + * transaction, but normally will roll the transaction back on the completion of + * each test. + * + * <p>This is useful in a range of circumstances, allowing the following benefits: + * <ul> + * <li>Ability to delete or insert any data in the database, without affecting + * other tests + * <li>Providing a transactional context for any code requiring a transaction + * <li>Ability to write anything to the database without any need to clean up. + * </ul> + * + * <p>This class is typically very fast, compared to traditional setup/teardown + * scripts. + * + * <p>If data should be left in the database, call the {@link #setComplete()} + * method in each test. The {@link #setDefaultRollback "defaultRollback"} + * property, which defaults to "true", determines whether transactions will + * complete by default. + * + * <p>It is even possible to end the transaction early; for example, to verify lazy + * loading behavior of an O/R mapping tool. (This is a valuable away to avoid + * unexpected errors when testing a web UI, for example.) Simply call the + * {@link #endTransaction()} method. Execution will then occur without a + * transactional context. + * + * <p>The {@link #startNewTransaction()} method may be called after a call to + * {@link #endTransaction()} if you wish to create a new transaction, quite + * independent of the old transaction. The new transaction's default fate will + * be to roll back, unless {@link #setComplete()} is called again during the + * scope of the new transaction. Any number of transactions may be created and + * ended in this way. The final transaction will automatically be rolled back + * when the test case is torn down. + * + * <p>Transactional behavior requires a single bean in the context implementing the + * {@link PlatformTransactionManager} interface. This will be set by the + * superclass's Dependency Injection mechanism. If using the superclass's Field + * Injection mechanism, the implementation should be named "transactionManager". + * This mechanism allows the use of the + * {@link AbstractDependencyInjectionSpringContextTests} superclass even when + * there is more than one transaction manager in the context. + * + * <p><b>This base class can also be used without transaction management, if no + * PlatformTransactionManager bean is found in the context provided.</b> Be + * careful about using this mode, as it allows the potential to permanently + * modify data. This mode is available only if dependency checking is turned off + * in the {@link AbstractDependencyInjectionSpringContextTests} superclass. The + * non-transactional capability is provided to enable use of the same subclass + * in different environments. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.1.1 + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests}) + */ +@Deprecated +public abstract class AbstractTransactionalSpringContextTests extends AbstractDependencyInjectionSpringContextTests { + + /** The transaction manager to use */ + protected PlatformTransactionManager transactionManager; + + /** Should we roll back by default? */ + private boolean defaultRollback = true; + + /** Should we commit the current transaction? */ + private boolean complete = false; + + /** Number of transactions started */ + private int transactionsStarted = 0; + + /** + * Transaction definition used by this test class: by default, a plain + * DefaultTransactionDefinition. Subclasses can change this to cause + * different behavior. + */ + protected TransactionDefinition transactionDefinition= new DefaultTransactionDefinition(); + + /** + * TransactionStatus for this test. Typical subclasses won't need to use it. + */ + protected TransactionStatus transactionStatus; + + + /** + * Default constructor for AbstractTransactionalSpringContextTests. + */ + public AbstractTransactionalSpringContextTests() { + } + + /** + * Constructor for AbstractTransactionalSpringContextTests with a JUnit name. + */ + public AbstractTransactionalSpringContextTests(String name) { + super(name); + } + + + /** + * Specify the transaction manager to use. No transaction management will be + * available if this is not set. Populated through dependency injection by + * the superclass. + * <p> + * This mode works only if dependency checking is turned off in the + * {@link AbstractDependencyInjectionSpringContextTests} superclass. + */ + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + /** + * Subclasses can set this value in their constructor to change the default, + * which is always to roll the transaction back. + */ + public void setDefaultRollback(final boolean defaultRollback) { + this.defaultRollback = defaultRollback; + } + /** + * Get the <em>default rollback</em> flag for this test. + * @see #setDefaultRollback(boolean) + * @return The <em>default rollback</em> flag. + */ + protected boolean isDefaultRollback() { + return this.defaultRollback; + } + + /** + * Determines whether or not to rollback transactions for the current test. + * <p>The default implementation delegates to {@link #isDefaultRollback()}. + * Subclasses can override as necessary. + */ + protected boolean isRollback() { + return isDefaultRollback(); + } + + /** + * Call this method in an overridden {@link #runBare()} method to prevent + * transactional execution. + */ + protected void preventTransaction() { + this.transactionDefinition = null; + } + + /** + * Call this method in an overridden {@link #runBare()} method to override + * the transaction attributes that will be used, so that {@link #setUp()} + * and {@link #tearDown()} behavior is modified. + * @param customDefinition the custom transaction definition + */ + protected void setTransactionDefinition(TransactionDefinition customDefinition) { + this.transactionDefinition = customDefinition; + } + + /** + * This implementation creates a transaction before test execution. + * <p>Override {@link #onSetUpBeforeTransaction()} and/or + * {@link #onSetUpInTransaction()} to add custom set-up behavior for + * transactional execution. Alternatively, override this method for general + * set-up behavior, calling {@code super.onSetUp()} as part of your + * method implementation. + * @throws Exception simply let any exception propagate + * @see #onTearDown() + */ + protected void onSetUp() throws Exception { + this.complete = !this.isRollback(); + + if (this.transactionManager == null) { + this.logger.info("No transaction manager set: test will NOT run within a transaction"); + } + else if (this.transactionDefinition == null) { + this.logger.info("No transaction definition set: test will NOT run within a transaction"); + } + else { + onSetUpBeforeTransaction(); + startNewTransaction(); + try { + onSetUpInTransaction(); + } + catch (final Exception ex) { + endTransaction(); + throw ex; + } + } + } + + /** + * Subclasses can override this method to perform any setup operations, such + * as populating a database table, <i>before</i> the transaction created by + * this class. Only invoked if there <i>is</i> a transaction: that is, if + * {@link #preventTransaction()} has not been invoked in an overridden + * {@link #runTest()} method. + * @throws Exception simply let any exception propagate + */ + protected void onSetUpBeforeTransaction() throws Exception { + } + + /** + * Subclasses can override this method to perform any setup operations, such + * as populating a database table, <i>within</i> the transaction created by + * this class. + * <p><b>NB:</b> Not called if there is no transaction management, due to no + * transaction manager being provided in the context. + * <p>If any {@link Throwable} is thrown, the transaction that has been started + * prior to the execution of this method will be + * {@link #endTransaction() ended} (or rather an attempt will be made to + * {@link #endTransaction() end it gracefully}); The offending + * {@link Throwable} will then be rethrown. + * @throws Exception simply let any exception propagate + */ + protected void onSetUpInTransaction() throws Exception { + } + + /** + * This implementation ends the transaction after test execution. + * <p>Override {@link #onTearDownInTransaction()} and/or + * {@link #onTearDownAfterTransaction()} to add custom tear-down behavior + * for transactional execution. Alternatively, override this method for + * general tear-down behavior, calling {@code super.onTearDown()} as + * part of your method implementation. + * <p>Note that {@link #onTearDownInTransaction()} will only be called if a + * transaction is still active at the time of the test shutdown. In + * particular, it will <i>not</i> be called if the transaction has been + * completed with an explicit {@link #endTransaction()} call before. + * @throws Exception simply let any exception propagate + * @see #onSetUp() + */ + protected void onTearDown() throws Exception { + // Call onTearDownInTransaction and end transaction if the transaction + // is still active. + if (this.transactionStatus != null && !this.transactionStatus.isCompleted()) { + try { + onTearDownInTransaction(); + } + finally { + endTransaction(); + } + } + + // Call onTearDownAfterTransaction if there was at least one + // transaction, even if it has been completed early through an + // endTransaction() call. + if (this.transactionsStarted > 0) { + onTearDownAfterTransaction(); + } + } + + /** + * Subclasses can override this method to run invariant tests here. The + * transaction is <i>still active</i> at this point, so any changes made in + * the transaction will still be visible. However, there is no need to clean + * up the database, as a rollback will follow automatically. + * <p><b>NB:</b> Not called if there is no actual transaction, for example due + * to no transaction manager being provided in the application context. + * @throws Exception simply let any exception propagate + */ + protected void onTearDownInTransaction() throws Exception { + } + + /** + * Subclasses can override this method to perform cleanup after a + * transaction here. At this point, the transaction is <i>not active anymore</i>. + * @throws Exception simply let any exception propagate + */ + protected void onTearDownAfterTransaction() throws Exception { + } + + /** + * Cause the transaction to commit for this test method, even if the test + * method is configured to {@link #isRollback() rollback}. + * @throws IllegalStateException if the operation cannot be set to complete + * as no transaction manager was provided + */ + protected void setComplete() { + if (this.transactionManager == null) { + throw new IllegalStateException("No transaction manager set"); + } + this.complete = true; + } + + /** + * Immediately force a commit or rollback of the transaction, according to + * the {@code complete} and {@link #isRollback() rollback} flags. + * <p>Can be used to explicitly let the transaction end early, for example to + * check whether lazy associations of persistent objects work outside of a + * transaction (that is, have been initialized properly). + * @see #setComplete() + */ + protected void endTransaction() { + final boolean commit = this.complete || !isRollback(); + if (this.transactionStatus != null) { + try { + if (commit) { + this.transactionManager.commit(this.transactionStatus); + this.logger.debug("Committed transaction after execution of test [" + getName() + "]."); + } + else { + this.transactionManager.rollback(this.transactionStatus); + this.logger.debug("Rolled back transaction after execution of test [" + getName() + "]."); + } + } + finally { + this.transactionStatus = null; + } + } + } + + /** + * Start a new transaction. Only call this method if + * {@link #endTransaction()} has been called. {@link #setComplete()} can be + * used again in the new transaction. The fate of the new transaction, by + * default, will be the usual rollback. + * @throws TransactionException if starting the transaction failed + */ + protected void startNewTransaction() throws TransactionException { + if (this.transactionStatus != null) { + throw new IllegalStateException("Cannot start new transaction without ending existing transaction: " + + "Invoke endTransaction() before startNewTransaction()"); + } + if (this.transactionManager == null) { + throw new IllegalStateException("No transaction manager set"); + } + + this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition); + ++this.transactionsStarted; + this.complete = !this.isRollback(); + + if (this.logger.isDebugEnabled()) { + this.logger.debug("Began transaction (" + this.transactionsStarted + "): transaction manager [" + + this.transactionManager + "]; rollback [" + this.isRollback() + "]."); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/AssertThrows.java b/spring-test/src/main/java/org/springframework/test/AssertThrows.java new file mode 100644 index 00000000..2958fe02 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/AssertThrows.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +/** + * Simple method object encapsulation of the 'test-for-Exception' scenario (for JUnit). + * + * <p>Used like so: + * + * <pre class="code"> + * // the class under test + * public class Foo { + * public void someBusinessLogic(String name) { + * if (name == null) { + * throw new IllegalArgumentException("The 'name' argument is required"); + * } + * // rest of business logic here... + * } + * }</pre> + * + * The test for the above bad argument path can be expressed using the + * {@link AssertThrows} class like so: + * + * <pre class="code"> + * public class FooTest { + * public void testSomeBusinessLogicBadArgumentPath() { + * new AssertThrows(IllegalArgumentException.class) { + * public void test() { + * new Foo().someBusinessLogic(null); + * } + * }.runTest(); + * } + * }</pre> + * + * This will result in the test passing if the {@code Foo.someBusinessLogic(..)} + * method threw an {@link IllegalArgumentException}; if it did not, the + * test would fail with the following message: + * + * <pre class="code"> + * "Must have thrown a [class java.lang.IllegalArgumentException]"</pre> + * + * If the <b>wrong</b> type of {@link Exception} was thrown, the + * test will also fail, this time with a message similar to the following: + * + * <pre class="code"> + * "junit.framework.AssertionFailedError: Was expecting a [class java.lang.UnsupportedOperationException] to be thrown, but instead a [class java.lang.IllegalArgumentException] was thrown"</pre> + * + * The test for the correct {@link Exception} respects polymorphism, + * so you can test that any old {@link Exception} is thrown like so: + * + * <pre class="code"> + * public class FooTest { + * public void testSomeBusinessLogicBadArgumentPath() { + * // any Exception will do... + * new AssertThrows(Exception.class) { + * public void test() { + * new Foo().someBusinessLogic(null); + * } + * }.runTest(); + * } + * }</pre> + * + * Intended for use with JUnit 4 and TestNG (as of Spring 3.0). + * You might want to compare this class with the + * {@code junit.extensions.ExceptionTestCase} class. + * + * @author Rick Evans + * @author Juergen Hoeller + * @since 2.0 + * @deprecated favor use of JUnit 4's {@code @Test(expected=...)} support + */ +@Deprecated +@SuppressWarnings({ "unchecked", "rawtypes" }) +public abstract class AssertThrows { + + private final Class expectedException; + + private String failureMessage; + + private Exception actualException; + + + /** + * Create a new instance of the {@link AssertThrows} class. + * @param expectedException the {@link Exception} expected to be + * thrown during the execution of the surrounding test + * @throws IllegalArgumentException if the supplied {@code expectedException} is + * {@code null}; or if said argument is not an {@link Exception}-derived class + */ + public AssertThrows(Class expectedException) { + this(expectedException, null); + } + + /** + * Create a new instance of the {@link AssertThrows} class. + * @param expectedException the {@link Exception} expected to be + * thrown during the execution of the surrounding test + * @param failureMessage the extra, contextual failure message that will be + * included in the failure text if the text fails (can be {@code null}) + * @throws IllegalArgumentException if the supplied {@code expectedException} is + * {@code null}; or if said argument is not an {@link Exception}-derived class + */ + public AssertThrows(Class expectedException, String failureMessage) { + if (expectedException == null) { + throw new IllegalArgumentException("The 'expectedException' argument is required"); + } + if (!Exception.class.isAssignableFrom(expectedException)) { + throw new IllegalArgumentException( + "The 'expectedException' argument is not an Exception type (it obviously must be)"); + } + this.expectedException = expectedException; + this.failureMessage = failureMessage; + } + + + /** + * Return the {@link java.lang.Exception} expected to be thrown during + * the execution of the surrounding test. + */ + protected Class getExpectedException() { + return this.expectedException; + } + + /** + * Set the extra, contextual failure message that will be included + * in the failure text if the text fails. + */ + public void setFailureMessage(String failureMessage) { + this.failureMessage = failureMessage; + } + + /** + * Return the extra, contextual failure message that will be included + * in the failure text if the text fails. + */ + protected String getFailureMessage() { + return this.failureMessage; + } + + + /** + * Subclass must override this {@code abstract} method and + * provide the test logic. + * @throws Exception if an error occurs during the execution of the + * aformentioned test logic + */ + public abstract void test() throws Exception; + + + /** + * The main template method that drives the running of the + * {@link #test() test logic} and the + * {@link #checkExceptionExpectations(Exception) checking} of the + * resulting (expected) {@link java.lang.Exception}. + * @see #test() + * @see #doFail() + * @see #checkExceptionExpectations(Exception) + */ + public void runTest() { + try { + test(); + doFail(); + } + catch (Exception actualException) { + this.actualException = actualException; + checkExceptionExpectations(actualException); + } + } + + /** + * Template method called when the test fails; i.e. the expected + * {@link java.lang.Exception} is <b>not</b> thrown. + * <p>The default implementation simply fails the test via a call to + * {@link org.junit.Assert#fail(String)}. + * <p>If you want to customise the failure message, consider overriding + * {@link #createMessageForNoExceptionThrown()}, and / or supplying an + * extra, contextual failure message via the appropriate constructor overload. + * @see #getFailureMessage() + */ + protected void doFail() { + throw new AssertionError(createMessageForNoExceptionThrown()); + } + + /** + * Creates the failure message used if the test fails + * (i.e. the expected exception is not thrown in the body of the test). + * @return the failure message used if the test fails + * @see #getFailureMessage() + */ + protected String createMessageForNoExceptionThrown() { + StringBuilder sb = new StringBuilder(); + sb.append("Should have thrown a [").append(this.getExpectedException()).append("]"); + if (getFailureMessage() != null) { + sb.append(": ").append(getFailureMessage()); + } + return sb.toString(); + } + + /** + * Does the donkey work of checking (verifying) that the + * {@link Exception} that was thrown in the body of a test is + * an instance of the {@link #getExpectedException()} class (or an + * instance of a subclass). + * <p>If you want to customise the failure message, consider overriding + * {@link #createMessageForWrongThrownExceptionType(Exception)}. + * @param actualException the {@link Exception} that has been thrown + * in the body of a test method (will never be {@code null}) + */ + protected void checkExceptionExpectations(Exception actualException) { + if (!getExpectedException().isAssignableFrom(actualException.getClass())) { + AssertionError error = + new AssertionError(createMessageForWrongThrownExceptionType(actualException)); + error.initCause(actualException); + throw error; + } + } + + /** + * Creates the failure message used if the wrong type + * of {@link java.lang.Exception} is thrown in the body of the test. + * @param actualException the actual exception thrown + * @return the message for the given exception + */ + protected String createMessageForWrongThrownExceptionType(Exception actualException) { + StringBuilder sb = new StringBuilder(); + sb.append("Was expecting a [").append(getExpectedException().getName()); + sb.append("] to be thrown, but instead a [").append(actualException.getClass().getName()); + sb.append("] was thrown."); + return sb.toString(); + } + + + /** + * Expose the actual exception thrown from {@link #test}, if any. + * @return the actual exception, or {@code null} if none + */ + public final Exception getActualException() { + return this.actualException; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/ConditionalTestCase.java b/spring-test/src/main/java/org/springframework/test/ConditionalTestCase.java new file mode 100644 index 00000000..a8448f22 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/ConditionalTestCase.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2007 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 + * + * http://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; + +import junit.framework.TestCase; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Superclass for JUnit 3.8 based tests that allows conditional test execution + * at the individual test method level. The + * {@link #isDisabledInThisEnvironment(String) isDisabledInThisEnvironment()} + * method is invoked before the execution of each test method. Subclasses can + * override that method to return whether or not the given test should be + * executed. Note that the tests will still appear to have executed and passed; + * however, log output will show that the test was not executed. + * + * @author Rod Johnson + * @since 2.0 + * @see #isDisabledInThisEnvironment + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests}) + */ +@Deprecated +public abstract class ConditionalTestCase extends TestCase { + + private static int disabledTestCount; + + + /** + * Return the number of tests disabled in this environment. + */ + public static int getDisabledTestCount() { + return disabledTestCount; + } + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + /** + * Default constructor for ConditionalTestCase. + */ + public ConditionalTestCase() { + } + + /** + * Constructor for ConditionalTestCase with a JUnit name. + */ + public ConditionalTestCase(String name) { + super(name); + } + + public void runBare() throws Throwable { + // getName will return the name of the method being run + if (isDisabledInThisEnvironment(getName())) { + recordDisabled(); + this.logger.info("**** " + getClass().getName() + "." + getName() + " is disabled in this environment: " + + "Total disabled tests = " + getDisabledTestCount()); + return; + } + + // Let JUnit handle execution + super.runBare(); + } + + /** + * Should this test run? + * @param testMethodName name of the test method + * @return whether the test should execute in the current environment + */ + protected boolean isDisabledInThisEnvironment(String testMethodName) { + return false; + } + + /** + * Record a disabled test. + * @return the current disabled test count + */ + protected int recordDisabled() { + return ++disabledTestCount; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java b/spring-test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java new file mode 100644 index 00000000..30c08a22 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/AbstractAnnotationAwareTransactionalTests.java @@ -0,0 +1,313 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; + +import javax.sql.DataSource; + +import junit.framework.AssertionFailedError; + +import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.util.Assert; + +/** + * <p> + * Java 5 specific subclass of + * {@link AbstractTransactionalDataSourceSpringContextTests}, exposing a + * {@link SimpleJdbcTemplate} and obeying annotations for transaction control. + * </p> + * <p> + * For example, test methods can be annotated with the regular Spring + * {@link org.springframework.transaction.annotation.Transactional @Transactional} + * annotation (e.g., to force execution in a read-only transaction) or with the + * {@link NotTransactional @NotTransactional} annotation to prevent any + * transaction being created at all. In addition, individual test methods can be + * annotated with {@link Rollback @Rollback} to override the + * {@link #isDefaultRollback() default rollback} settings. + * </p> + * <p> + * The following list constitutes all annotations currently supported by + * AbstractAnnotationAwareTransactionalTests: + * </p> + * <ul> + * <li>{@link DirtiesContext @DirtiesContext}</li> + * <li>{@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}</li> + * <li>{@link IfProfileValue @IfProfileValue}</li> + * <li>{@link ExpectedException @ExpectedException}</li> + * <li>{@link Timed @Timed}</li> + * <li>{@link Repeat @Repeat}</li> + * <li>{@link org.springframework.transaction.annotation.Transactional @Transactional}</li> + * <li>{@link NotTransactional @NotTransactional}</li> + * <li>{@link Rollback @Rollback}</li> + * </ul> + * + * @author Rod Johnson + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests}) + */ +@Deprecated +public abstract class AbstractAnnotationAwareTransactionalTests extends + AbstractTransactionalDataSourceSpringContextTests { + + protected SimpleJdbcTemplate simpleJdbcTemplate; + + private final TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource(); + + /** + * {@link ProfileValueSource} available to subclasses but primarily intended + * for use in {@link #isDisabledInThisEnvironment(Method)}. + * <p>Set to {@link SystemProfileValueSource} by default for backwards + * compatibility; however, the value may be changed in the + * {@link #AbstractAnnotationAwareTransactionalTests(String)} constructor. + */ + protected ProfileValueSource profileValueSource = SystemProfileValueSource.getInstance(); + + + /** + * Default constructor for AbstractAnnotationAwareTransactionalTests, which + * delegates to {@link #AbstractAnnotationAwareTransactionalTests(String)}. + */ + public AbstractAnnotationAwareTransactionalTests() { + this(null); + } + + /** + * Constructs a new AbstractAnnotationAwareTransactionalTests instance with + * the specified JUnit {@code name} and retrieves the configured (or + * default) {@link ProfileValueSource}. + * @param name the name of the current test + * @see ProfileValueUtils#retrieveProfileValueSource(Class) + */ + public AbstractAnnotationAwareTransactionalTests(String name) { + super(name); + this.profileValueSource = ProfileValueUtils.retrieveProfileValueSource(getClass()); + } + + + @Override + public void setDataSource(DataSource dataSource) { + super.setDataSource(dataSource); + // JdbcTemplate will be identically configured + this.simpleJdbcTemplate = new SimpleJdbcTemplate(this.jdbcTemplate); + } + + /** + * Search for a unique {@link ProfileValueSource} in the supplied + * {@link ApplicationContext}. If found, the + * {@code profileValueSource} for this test will be set to the unique + * {@link ProfileValueSource}. + * @param applicationContext the ApplicationContext in which to search for + * the ProfileValueSource; may not be {@code null} + * @deprecated Use {@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration} instead. + */ + @Deprecated + protected void findUniqueProfileValueSourceFromContext(ApplicationContext applicationContext) { + Assert.notNull(applicationContext, "Can not search for a ProfileValueSource in a null ApplicationContext."); + ProfileValueSource uniqueProfileValueSource = null; + Map<?, ?> beans = applicationContext.getBeansOfType(ProfileValueSource.class); + if (beans.size() == 1) { + uniqueProfileValueSource = (ProfileValueSource) beans.values().iterator().next(); + } + if (uniqueProfileValueSource != null) { + this.profileValueSource = uniqueProfileValueSource; + } + } + + /** + * Overridden to populate transaction definition from annotations. + */ + @Override + public void runBare() throws Throwable { + + // getName will return the name of the method being run. + if (isDisabledInThisEnvironment(getName())) { + // Let superclass log that we didn't run the test. + super.runBare(); + return; + } + + final Method testMethod = getTestMethod(); + + if (isDisabledInThisEnvironment(testMethod)) { + recordDisabled(); + this.logger.info("**** " + getClass().getName() + "." + getName() + " is disabled in this environment: " + + "Total disabled tests=" + getDisabledTestCount()); + return; + } + + TransactionDefinition explicitTransactionDefinition = + this.transactionAttributeSource.getTransactionAttribute(testMethod, getClass()); + if (explicitTransactionDefinition != null) { + this.logger.info("Custom transaction definition [" + explicitTransactionDefinition + "] for test method [" + + getName() + "]."); + setTransactionDefinition(explicitTransactionDefinition); + } + else if (testMethod.isAnnotationPresent(NotTransactional.class)) { + // Don't have any transaction... + preventTransaction(); + } + + // Let JUnit handle execution. We're just changing the state of the test class first. + runTestTimed(new TestExecutionCallback() { + public void run() throws Throwable { + try { + AbstractAnnotationAwareTransactionalTests.super.runBare(); + } + finally { + // Mark the context to be blown away if the test was + // annotated to result in setDirty being invoked + // automatically. + if (testMethod.isAnnotationPresent(DirtiesContext.class)) { + AbstractAnnotationAwareTransactionalTests.this.setDirty(); + } + } + } + }, testMethod); + } + + /** + * Determine if the test for the supplied {@code testMethod} should + * run in the current environment. + * <p>The default implementation is based on + * {@link IfProfileValue @IfProfileValue} semantics. + * @param testMethod the test method + * @return {@code true} if the test is <em>disabled</em> in the current environment + * @see ProfileValueUtils#isTestEnabledInThisEnvironment + */ + protected boolean isDisabledInThisEnvironment(Method testMethod) { + return !ProfileValueUtils.isTestEnabledInThisEnvironment(this.profileValueSource, testMethod, getClass()); + } + + /** + * Get the current test method. + */ + protected Method getTestMethod() { + assertNotNull("TestCase.getName() cannot be null", getName()); + Method testMethod = null; + try { + // Use same algorithm as JUnit itself to retrieve the test method + // about to be executed (the method name is returned by getName). It + // has to be public so we can retrieve it. + testMethod = getClass().getMethod(getName(), (Class[]) null); + } + catch (NoSuchMethodException ex) { + fail("Method '" + getName() + "' not found"); + } + if (!Modifier.isPublic(testMethod.getModifiers())) { + fail("Method '" + getName() + "' should be public"); + } + return testMethod; + } + + /** + * Determine whether or not to rollback transactions for the current test + * by taking into consideration the + * {@link #isDefaultRollback() default rollback} flag and a possible + * method-level override via the {@link Rollback @Rollback} annotation. + * @return the <em>rollback</em> flag for the current test + */ + @Override + protected boolean isRollback() { + boolean rollback = isDefaultRollback(); + Rollback rollbackAnnotation = getTestMethod().getAnnotation(Rollback.class); + if (rollbackAnnotation != null) { + boolean rollbackOverride = rollbackAnnotation.value(); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback [" + + rollback + "] for test [" + getName() + "]."); + } + rollback = rollbackOverride; + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("No method-level @Rollback override: using default rollback [" + rollback + + "] for test [" + getName() + "]."); + } + } + return rollback; + } + + private void runTestTimed(TestExecutionCallback tec, Method testMethod) throws Throwable { + Timed timed = testMethod.getAnnotation(Timed.class); + if (timed == null) { + runTest(tec, testMethod); + } + else { + long startTime = System.currentTimeMillis(); + try { + runTest(tec, testMethod); + } + finally { + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed > timed.millis()) { + fail("Took " + elapsed + " ms; limit was " + timed.millis()); + } + } + } + } + + private void runTest(TestExecutionCallback tec, Method testMethod) throws Throwable { + ExpectedException expectedExceptionAnnotation = testMethod.getAnnotation(ExpectedException.class); + boolean exceptionIsExpected = (expectedExceptionAnnotation != null && expectedExceptionAnnotation.value() != null); + Class<? extends Throwable> expectedException = (exceptionIsExpected ? expectedExceptionAnnotation.value() : null); + + Repeat repeat = testMethod.getAnnotation(Repeat.class); + int runs = ((repeat != null) && (repeat.value() > 1)) ? repeat.value() : 1; + + for (int i = 0; i < runs; i++) { + try { + if (runs > 1 && this.logger != null && this.logger.isInfoEnabled()) { + this.logger.info("Repetition " + (i + 1) + " of test " + testMethod.getName()); + } + tec.run(); + if (exceptionIsExpected) { + fail("Expected exception: " + expectedException.getName()); + } + } + catch (Throwable t) { + if (!exceptionIsExpected) { + throw t; + } + if (!expectedException.isAssignableFrom(t.getClass())) { + // Wrap the unexpected throwable with an explicit message. + AssertionFailedError assertionError = new AssertionFailedError("Unexpected exception, expected<" + + expectedException.getName() + "> but was<" + t.getClass().getName() + ">"); + assertionError.initCause(t); + throw assertionError; + } + } + } + } + + + private static interface TestExecutionCallback { + + void run() throws Throwable; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java new file mode 100644 index 00000000..aa6ed211 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.annotation; + +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; + +/** + * Test annotation which indicates that the + * {@link org.springframework.context.ApplicationContext ApplicationContext} + * associated with a test is <em>dirty</em> and should be closed: + * <ul> + * <li>after the current test, when declared at the method level</li> + * <li>after each test method in the current test class, when declared at the + * class level with class mode set to {@link ClassMode#AFTER_EACH_TEST_METHOD + * AFTER_EACH_TEST_METHOD}</li> + * <li>after the current test class, when declared at the class level with class + * mode set to {@link ClassMode#AFTER_CLASS AFTER_CLASS}</li> + * </ul> + * <p> + * Use this annotation if a test has modified the context — for example, by + * replacing a bean definition or changing the state of a singleton bean. + * Subsequent tests will be supplied a new context. + * </p> + * <p> + * {@code @DirtiesContext} may be used as a class-level and method-level + * annotation within the same class. In such scenarios, the + * {@code ApplicationContext} will be marked as <em>dirty</em> after any + * such annotated method as well as after the entire class. If the + * {@link ClassMode} is set to {@link ClassMode#AFTER_EACH_TEST_METHOD + * AFTER_EACH_TEST_METHOD}, the context will be marked dirty after each test + * method in the class. + * </p> + * + * @author Sam Brannen + * @author Rod Johnson + * @since 2.0 + * @see org.springframework.test.context.ContextConfiguration + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface DirtiesContext { + + /** + * Defines <i>modes</i> which determine how {@code @DirtiesContext} is + * interpreted when used to annotate a test class. + * + * @since 3.0 + */ + static enum ClassMode { + + /** + * The associated {@code ApplicationContext} will be marked as + * <em>dirty</em> after the test class. + */ + AFTER_CLASS, + + /** + * The associated {@code ApplicationContext} will be marked as + * <em>dirty</em> after each test method in the class. + */ + AFTER_EACH_TEST_METHOD; + } + + /** + * Defines <i>modes</i> which determine how the context cache is cleared + * when {@code @DirtiesContext} is used in a test whose context is + * configured as part of a hierarchy via + * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}. + * + * @since 3.2.2 + */ + static enum HierarchyMode { + + /** + * The context cache will be cleared using an <em>exhaustive</em> algorithm + * that includes not only the {@linkplain HierarchyMode#CURRENT_LEVEL current level} + * but also all other context hierarchies that share an ancestor context + * common to the current test. + * + * <p>All {@code ApplicationContexts} that reside in a subhierarchy of + * the common ancestor context will be removed from the context cache and + * closed. + */ + EXHAUSTIVE, + + /** + * The {@code ApplicationContext} for the <em>current level</em> in the + * context hierarchy and all contexts in subhierarchies of the current + * level will be removed from the context cache and closed. + * + * <p>The <em>current level</em> refers to the {@code ApplicationContext} + * at the lowest level in the context hierarchy that is visible from the + * current test. + */ + CURRENT_LEVEL; + } + + + /** + * The <i>mode</i> to use when a test class is annotated with + * {@code @DirtiesContext}. + * <p>Defaults to {@link ClassMode#AFTER_CLASS AFTER_CLASS}. + * <p>Note: Setting the class mode on an annotated test method has no meaning, + * since the mere presence of the {@code @DirtiesContext} annotation on a + * test method is sufficient. + * + * @since 3.0 + */ + ClassMode classMode() default ClassMode.AFTER_CLASS; + + /** + * The context cache clearing <em>mode</em> to use when a context is + * configured as part of a hierarchy via + * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}. + * <p>Defaults to {@link HierarchyMode#EXHAUSTIVE EXHAUSTIVE}. + * + * @since 3.2.2 + */ + HierarchyMode hierarchyMode() default HierarchyMode.EXHAUSTIVE; + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ExpectedException.java b/spring-test/src/main/java/org/springframework/test/annotation/ExpectedException.java new file mode 100644 index 00000000..944f3aea --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/ExpectedException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Test annotation to indicate that a test method is required to throw the + * specified exception. + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @deprecated as of Spring 3.1 in favor of using built-in support for declaring + * expected exceptions in the underlying testing framework (e.g., JUnit, TestNG, etc.) + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Deprecated +public @interface ExpectedException { + + Class<? extends Throwable> value(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java b/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java new file mode 100644 index 00000000..c5c71846 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +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; + +/** + * <p> + * Test annotation to indicate that a test is enabled for a specific testing + * profile or environment. If the configured {@link ProfileValueSource} returns + * a matching {@link #value() value} for the provided {@link #name() name}, the + * test will be enabled. + * </p> + * <p> + * Note: {@code @IfProfileValue} can be applied at the class level, + * the method level, or both. {@code @IfProfileValue} at the class + * level overrides method-level usage of {@code @IfProfileValue} for + * any methods within that class. + * </p> + * <p> + * Examples: when using {@link SystemProfileValueSource} as the + * {@link ProfileValueSource} implementation, you can configure a test method to + * run only on Java VMs from Sun Microsystems as follows: + * </p> + * + * <pre class="code"> + * @IfProfileValue(name = "java.vendor", value = "Sun Microsystems Inc.") + * public void testSomething() { + * // ... + * } + * </pre> + * <p> + * You can alternatively configure {@code @IfProfileValue} with + * <em>OR</em> semantics for multiple {@link #values() values} as follows + * (assuming a {@link ProfileValueSource} has been appropriately configured for + * the "test-groups" name): + * </p> + * + * <pre class="code"> + * @IfProfileValue(name = "test-groups", values = { "unit-tests", "integration-tests" }) + * public void testWhichRunsForUnitOrIntegrationTestGroups() { + * // ... + * } + * </pre> + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @see ProfileValueSource + * @see ProfileValueSourceConfiguration + * @see ProfileValueUtils + * @see org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests + * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests + * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target( { ElementType.TYPE, ElementType.METHOD }) +public @interface IfProfileValue { + + /** + * The {@code name} of the <em>profile value</em> against which to + * test. + */ + String name(); + + /** + * A single, permissible {@code value} of the <em>profile value</em> + * for the given {@link #name() name}. + * <p> + * Note: Assigning values to both {@link #value()} and {@link #values()} + * will lead to a configuration conflict. + */ + String value() default ""; + + /** + * A list of all permissible {@code values} of the + * <em>profile value</em> for the given {@link #name() name}. + * <p> + * Note: Assigning values to both {@link #value()} and {@link #values()} + * will lead to a configuration conflict. + */ + String[] values() default {}; + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/NotTransactional.java b/spring-test/src/main/java/org/springframework/test/annotation/NotTransactional.java new file mode 100644 index 00000000..c8539186 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/NotTransactional.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Test annotation to indicate that a method is <i>not transactional</i>. + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @deprecated as of Spring 3.0, in favor of moving the non-transactional test + * method to a separate (non-transactional) test class or to a + * {@link org.springframework.test.context.transaction.BeforeTransaction + * @BeforeTransaction} or + * {@link org.springframework.test.context.transaction.AfterTransaction + * @AfterTransaction} method. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Deprecated +public @interface NotTransactional { +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java new file mode 100644 index 00000000..05ee407f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +/** + * <p> + * Strategy interface for retrieving <em>profile values</em> for a given + * testing environment. + * </p> + * <p> + * Concrete implementations must provide a {@code public} no-args + * constructor. + * </p> + * <p> + * Spring provides the following out-of-the-box implementations: + * </p> + * <ul> + * <li>{@link SystemProfileValueSource}</li> + * </ul> + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @see ProfileValueSourceConfiguration + * @see IfProfileValue + * @see ProfileValueUtils + */ +public interface ProfileValueSource { + + /** + * Get the <em>profile value</em> indicated by the specified key. + * @param key the name of the <em>profile value</em> + * @return the String value of the <em>profile value</em>, or {@code null} + * if there is no <em>profile value</em> with that key + */ + String get(String key); + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java new file mode 100644 index 00000000..4e240aa6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +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; + +/** + * <p> + * ProfileValueSourceConfiguration is a class-level annotation which is used to + * specify what type of {@link ProfileValueSource} to use when retrieving + * <em>profile values</em> configured via the {@link IfProfileValue + * @IfProfileValue} annotation. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see ProfileValueSource + * @see IfProfileValue + * @see ProfileValueUtils + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ProfileValueSourceConfiguration { + + /** + * <p> + * The type of {@link ProfileValueSource} to use when retrieving + * <em>profile values</em>. + * </p> + * + * @see SystemProfileValueSource + */ + Class<? extends ProfileValueSource> value() default SystemProfileValueSource.class; + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java new file mode 100644 index 00000000..922e34c6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * General utility methods for working with <em>profile values</em>. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ProfileValueSource + * @see ProfileValueSourceConfiguration + * @see IfProfileValue + */ +public abstract class ProfileValueUtils { + + private static final Log logger = LogFactory.getLog(ProfileValueUtils.class); + + + /** + * Retrieves the {@link ProfileValueSource} type for the specified + * {@link Class test class} as configured via the + * {@link ProfileValueSourceConfiguration + * @ProfileValueSourceConfiguration} annotation and instantiates a new + * instance of that type. + * <p> + * If {@link ProfileValueSourceConfiguration + * @ProfileValueSourceConfiguration} is not present on the specified + * class or if a custom {@link ProfileValueSource} is not declared, the + * default {@link SystemProfileValueSource} will be returned instead. + * + * @param testClass The test class for which the ProfileValueSource should + * be retrieved + * @return the configured (or default) ProfileValueSource for the specified + * class + * @see SystemProfileValueSource + */ + @SuppressWarnings("unchecked") + public static ProfileValueSource retrieveProfileValueSource(Class<?> testClass) { + Assert.notNull(testClass, "testClass must not be null"); + + Class<ProfileValueSourceConfiguration> annotationType = ProfileValueSourceConfiguration.class; + ProfileValueSourceConfiguration config = testClass.getAnnotation(annotationType); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved @ProfileValueSourceConfiguration [" + config + "] for test class [" + + testClass.getName() + "]"); + } + + Class<? extends ProfileValueSource> profileValueSourceType; + if (config != null) { + profileValueSourceType = config.value(); + } + else { + profileValueSourceType = (Class<? extends ProfileValueSource>) AnnotationUtils.getDefaultValue(annotationType); + } + if (logger.isDebugEnabled()) { + logger.debug("Retrieved ProfileValueSource type [" + profileValueSourceType + "] for class [" + + testClass.getName() + "]"); + } + + ProfileValueSource profileValueSource; + if (SystemProfileValueSource.class.equals(profileValueSourceType)) { + profileValueSource = SystemProfileValueSource.getInstance(); + } + else { + try { + profileValueSource = profileValueSourceType.newInstance(); + } + catch (Exception e) { + if (logger.isWarnEnabled()) { + logger.warn("Could not instantiate a ProfileValueSource of type [" + profileValueSourceType + + "] for class [" + testClass.getName() + "]: using default.", e); + } + profileValueSource = SystemProfileValueSource.getInstance(); + } + } + + return profileValueSource; + } + + /** + * Determine if the supplied {@code testClass} is <em>enabled</em> in + * the current environment, as specified by the {@link IfProfileValue + * @IfProfileValue} annotation at the class level. + * <p> + * Defaults to {@code true} if no {@link IfProfileValue + * @IfProfileValue} annotation is declared. + * + * @param testClass the test class + * @return {@code true} if the test is <em>enabled</em> in the current + * environment + */ + public static boolean isTestEnabledInThisEnvironment(Class<?> testClass) { + IfProfileValue ifProfileValue = testClass.getAnnotation(IfProfileValue.class); + return isTestEnabledInThisEnvironment(retrieveProfileValueSource(testClass), ifProfileValue); + } + + /** + * Determine if the supplied {@code testMethod} is <em>enabled</em> in + * the current environment, as specified by the {@link IfProfileValue + * @IfProfileValue} annotation, which may be declared on the test + * method itself or at the class level. Class-level usage overrides + * method-level usage. + * <p> + * Defaults to {@code true} if no {@link IfProfileValue + * @IfProfileValue} annotation is declared. + * + * @param testMethod the test method + * @param testClass the test class + * @return {@code true} if the test is <em>enabled</em> in the current + * environment + */ + public static boolean isTestEnabledInThisEnvironment(Method testMethod, Class<?> testClass) { + return isTestEnabledInThisEnvironment(retrieveProfileValueSource(testClass), testMethod, testClass); + } + + /** + * Determine if the supplied {@code testMethod} is <em>enabled</em> in + * the current environment, as specified by the {@link IfProfileValue + * @IfProfileValue} annotation, which may be declared on the test + * method itself or at the class level. Class-level usage overrides + * method-level usage. + * <p> + * Defaults to {@code true} if no {@link IfProfileValue + * @IfProfileValue} annotation is declared. + * + * @param profileValueSource the ProfileValueSource to use to determine if + * the test is enabled + * @param testMethod the test method + * @param testClass the test class + * @return {@code true} if the test is <em>enabled</em> in the current + * environment + */ + public static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource, Method testMethod, + Class<?> testClass) { + + IfProfileValue ifProfileValue = testClass.getAnnotation(IfProfileValue.class); + boolean classLevelEnabled = isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue); + + if (classLevelEnabled) { + ifProfileValue = testMethod.getAnnotation(IfProfileValue.class); + return isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue); + } + + return false; + } + + /** + * Determine if the {@code value} (or one of the {@code values}) + * in the supplied {@link IfProfileValue @IfProfileValue} annotation is + * <em>enabled</em> in the current environment. + * + * @param profileValueSource the ProfileValueSource to use to determine if + * the test is enabled + * @param ifProfileValue the annotation to introspect; may be + * {@code null} + * @return {@code true} if the test is <em>enabled</em> in the current + * environment or if the supplied {@code ifProfileValue} is + * {@code null} + */ + private static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource, + IfProfileValue ifProfileValue) { + + if (ifProfileValue == null) { + return true; + } + + String environmentValue = profileValueSource.get(ifProfileValue.name()); + String[] annotatedValues = ifProfileValue.values(); + if (StringUtils.hasLength(ifProfileValue.value())) { + if (annotatedValues.length > 0) { + throw new IllegalArgumentException("Setting both the 'value' and 'values' attributes " + + "of @IfProfileValue is not allowed: choose one or the other."); + } + annotatedValues = new String[] { ifProfileValue.value() }; + } + + for (String value : annotatedValues) { + if (ObjectUtils.nullSafeEquals(value, environmentValue)) { + return true; + } + } + return false; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java new file mode 100644 index 00000000..4c3dcd3a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Test annotation to indicate that a test method should be invoked repeatedly. + * <p /> + * Note that the scope of execution to be repeated includes execution of the + * test method itself as well as any <em>set up</em> or <em>tear down</em> of + * the test fixture. + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Repeat { + + int value() default 1; + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java new file mode 100644 index 00000000..bb0bc88c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Test annotation to indicate whether or not the transaction for the annotated + * test method should be <em>rolled back</em> after the test method has + * completed. If {@code true}, the transaction will be rolled back; + * otherwise, the transaction will be committed. + * + * @author Sam Brannen + * @since 2.5 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Rollback { + + /** + * Whether or not the transaction for the annotated method should be rolled + * back after the method has completed. + */ + boolean value() default true; + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java new file mode 100644 index 00000000..f806acc1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2007 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 + * + * http://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.annotation; + +import org.springframework.util.Assert; + +/** + * Implementation of {@link ProfileValueSource} which uses system properties as + * the underlying source. + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + */ +public class SystemProfileValueSource implements ProfileValueSource { + + private static final SystemProfileValueSource INSTANCE = new SystemProfileValueSource(); + + + /** + * Obtain the canonical instance of this ProfileValueSource. + */ + public static final SystemProfileValueSource getInstance() { + return INSTANCE; + } + + + /** + * Private constructor, enforcing the singleton pattern. + */ + private SystemProfileValueSource() { + } + + /** + * Get the <em>profile value</em> indicated by the specified key from the + * system properties. + * @see System#getProperty(String) + */ + public String get(String key) { + Assert.hasText(key, "'key' must not be empty"); + return System.getProperty(key); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Timed.java b/spring-test/src/main/java/org/springframework/test/annotation/Timed.java new file mode 100644 index 00000000..a78960ce --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/Timed.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <p> + * Test-specific annotation to indicate that a test method has to finish + * execution in a {@link #millis() specified time period}. + * </p> + * <p> + * If the text execution takes longer than the specified time period, then the + * test is to be considered failed. + * </p> + * <p> + * Note that the time period includes execution of the test method itself, any + * {@link Repeat repetitions} of the test, and any <em>set up</em> or + * <em>tear down</em> of the test fixture. + * </p> + * + * @author Rod Johnson + * @author Sam Brannen + * @since 2.0 + * @see Repeat + * @see AbstractAnnotationAwareTransactionalTests + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Timed { + + /** + * The maximum amount of time (in milliseconds) that a test execution can + * take without being marked as failed due to taking too long. + */ + long millis(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/annotation/package-info.java b/spring-test/src/main/java/org/springframework/test/annotation/package-info.java new file mode 100644 index 00000000..1695b36c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/annotation/package-info.java @@ -0,0 +1,6 @@ +/** + * Support classes for annotation-driven tests. + */ + +package org.springframework.test.annotation; + diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java new file mode 100644 index 00000000..39bfb5d3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +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 ActiveProfiles} is a class-level annotation that is used to declare + * which <em>active bean definition profiles</em> should be used when loading + * an {@link org.springframework.context.ApplicationContext ApplicationContext} + * for test classes. + * + * @author Sam Brannen + * @since 3.1 + * @see SmartContextLoader + * @see MergedContextConfiguration + * @see ContextConfiguration + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.annotation.Profile + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ActiveProfiles { + + /** + * Alias for {@link #profiles}. + * + * <p>This attribute may <strong>not</strong> be used in conjunction + * with {@link #profiles}, but it may be used <em>instead</em> of + * {@link #profiles}. + */ + String[] value() default {}; + + /** + * The bean definition profiles to activate. + * + * <p>This attribute may <strong>not</strong> be used in conjunction + * with {@link #value}, but it may be used <em>instead</em> of + * {@link #value}. + */ + String[] profiles() default {}; + + /** + * Whether or not bean definition profiles from superclasses should be + * <em>inherited</em>. + * + * <p>The default value is {@code true}, which means that a test + * class will <em>inherit</em> bean definition profiles defined by a + * test superclass. Specifically, the bean definition profiles for a test + * class will be appended to the list of bean definition profiles + * defined by a test superclass. Thus, subclasses have the option of + * <em>extending</em> the list of bean definition profiles. + * + * <p>If {@code inheritProfiles} is set to {@code false}, the bean + * definition profiles for the test class will <em>shadow</em> and + * effectively replace any bean definition profiles defined by a superclass. + * + * <p>In the following example, the {@code ApplicationContext} for + * {@code BaseTest} will be loaded using only the "base" + * bean definition profile; beans defined in the "extended" profile + * will therefore not be loaded. In contrast, the {@code ApplicationContext} + * for {@code ExtendedTest} will be loaded using the "base" + * <strong>and</strong> "extended" bean definition profiles. + * <pre class="code"> + * @ActiveProfiles("base") + * @ContextConfiguration + * public class BaseTest { + * // ... + * } + * + * @ActiveProfiles("extended") + * @ContextConfiguration + * public class ExtendedTest extends BaseTest { + * // ... + * } + * </pre> + * + * <p>Note: {@code @ActiveProfiles} can be used when loading an + * {@code ApplicationContext} from path-based resource locations or + * annotated classes. + * + * @see ContextConfiguration#locations + * @see ContextConfiguration#classes + * @see ContextConfiguration#inheritLocations + */ + boolean inheritProfiles() default true; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java new file mode 100644 index 00000000..507d2555 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2014 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 + * + * http://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; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; + +/** + * {@code CacheAwareContextLoaderDelegate} loads application contexts from + * {@link MergedContextConfiguration} by delegating to the + * {@link ContextLoader} configured in the {@code MergedContextConfiguration} + * and interacting transparently with the {@link ContextCache} behind the scenes. + * + * <p>Note: {@code CacheAwareContextLoaderDelegate} does not implement the + * {@link ContextLoader} or {@link SmartContextLoader} interface. + * + * @author Sam Brannen + * @since 3.2.2 + */ +public class CacheAwareContextLoaderDelegate { + + private static final Log logger = LogFactory.getLog(CacheAwareContextLoaderDelegate.class); + + private static final Log statsLogger = LogFactory.getLog("org.springframework.test.context.cache"); + + private final ContextCache contextCache; + + + CacheAwareContextLoaderDelegate(ContextCache contextCache) { + Assert.notNull(contextCache, "ContextCache must not be null"); + this.contextCache = contextCache; + } + + + /** + * Load the {@code ApplicationContext} for the supplied merged context + * configuration. Supports both the {@link SmartContextLoader} and + * {@link ContextLoader} SPIs. + * @throws Exception if an error occurs while loading the application context + */ + private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) + throws Exception { + + ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); + Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " + + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); + + ApplicationContext applicationContext; + + if (contextLoader instanceof SmartContextLoader) { + SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; + applicationContext = smartContextLoader.loadContext(mergedContextConfiguration); + } + else { + String[] locations = mergedContextConfiguration.getLocations(); + Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " + + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); + applicationContext = contextLoader.loadContext(locations); + } + + return applicationContext; + } + + /** + * Load the {@link ApplicationContext application context} for the supplied + * merged context configuration. + * <p>If the context is present in the cache it will simply be returned; + * otherwise, it will be loaded, stored in the cache, and returned. + * @return the application context + * @throws IllegalStateException if an error occurs while retrieving or + * loading the application context + */ + public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) { + synchronized (this.contextCache) { + ApplicationContext context = this.contextCache.get(mergedContextConfiguration); + if (context == null) { + try { + context = loadContextInternal(mergedContextConfiguration); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Storing ApplicationContext in cache under key [%s]", + mergedContextConfiguration)); + } + this.contextCache.put(mergedContextConfiguration, context); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to load ApplicationContext", ex); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s]", + mergedContextConfiguration)); + } + } + + if (statsLogger.isDebugEnabled()) { + statsLogger.debug("Spring test ApplicationContext cache statistics: " + this.contextCache); + } + + return context; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java new file mode 100644 index 00000000..576021bd --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2014 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 + * + * http://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; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.util.Assert; + +/** + * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test environment. + * + * <p>Maintains a cache of {@code ApplicationContexts} keyed by + * {@link MergedContextConfiguration} instances. + * + * <p>This has significant performance benefits if initializing the context would take time. + * While initializing a Spring context itself is very quick, some beans in a context, such + * as a {@code LocalSessionFactoryBean} for working with Hibernate, may take some time to + * initialize. Hence it often makes sense to perform that initialization only once per + * test suite. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + */ +class ContextCache { + + /** + * Map of context keys to Spring {@code ApplicationContext} instances. + */ + private final Map<MergedContextConfiguration, ApplicationContext> contextMap = + new ConcurrentHashMap<MergedContextConfiguration, ApplicationContext>(64); + + /** + * Map of parent keys to sets of children keys, representing a top-down <em>tree</em> + * of context hierarchies. This information is used for determining which subtrees + * need to be recursively removed and closed when removing a context that is a parent + * of other contexts. + */ + private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap = + new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64); + + private final AtomicInteger hitCount = new AtomicInteger(); + + private final AtomicInteger missCount = new AtomicInteger(); + + + /** + * Clear all contexts from the cache and clears context hierarchy information as well. + */ + public void clear() { + this.contextMap.clear(); + this.hierarchyMap.clear(); + } + + /** + * Clear hit and miss count statistics for the cache (i.e., resets counters to zero). + */ + public void clearStatistics() { + this.hitCount.set(0); + this.missCount.set(0); + } + + /** + * Return whether there is a cached context for the given key. + * @param key the context key (never {@code null}) + */ + public boolean contains(MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + return this.contextMap.containsKey(key); + } + + /** + * Obtain a cached {@code ApplicationContext} for the given key. + * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will + * be updated accordingly. + * @param key the context key (never {@code null}) + * @return the corresponding {@code ApplicationContext} instance, or {@code null} + * if not found in the cache + * @see #remove + */ + public ApplicationContext get(MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + ApplicationContext context = this.contextMap.get(key); + if (context == null) { + this.missCount.incrementAndGet(); + } + else { + this.hitCount.incrementAndGet(); + } + return context; + } + + /** + * Get the overall hit count for this cache. + * <p>A <em>hit</em> is an access to the cache, which returned a non-null context + * for a queried key. + */ + public int getHitCount() { + return this.hitCount.get(); + } + + /** + * Get the overall miss count for this cache. + * <p>A <em>miss</em> is an access to the cache, which returned a {@code null} context + * for a queried key. + */ + public int getMissCount() { + return this.missCount.get(); + } + + /** + * Explicitly add an {@code ApplicationContext} instance to the cache under the given key. + * @param key the context key (never {@code null}) + * @param context the {@code ApplicationContext} instance (never {@code null}) + */ + public void put(MergedContextConfiguration key, ApplicationContext context) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(context, "ApplicationContext must not be null"); + + this.contextMap.put(key, context); + MergedContextConfiguration child = key; + MergedContextConfiguration parent = child.getParent(); + while (parent != null) { + Set<MergedContextConfiguration> list = this.hierarchyMap.get(parent); + if (list == null) { + list = new HashSet<MergedContextConfiguration>(); + this.hierarchyMap.put(parent, list); + } + list.add(child); + child = parent; + parent = child.getParent(); + } + } + + /** + * Remove the context with the given key from the cache and explicitly + * {@linkplain ConfigurableApplicationContext#close() close} it if it is an + * instance of {@link ConfigurableApplicationContext}. + * <p>Generally speaking, you would only call this method if you change the + * state of a singleton bean, potentially affecting future interaction with + * the context. + * <p>In addition, the semantics of the supplied {@code HierarchyMode} will + * be honored. See the Javadoc for {@link HierarchyMode} for details. + * @param key the context key; never {@code null} + * @param hierarchyMode the hierarchy mode; may be {@code null} if the context + * is not part of a hierarchy + */ + public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { + Assert.notNull(key, "Key must not be null"); + + // startKey is the level at which to begin clearing the cache, depending + // on the configured hierarchy mode. + MergedContextConfiguration startKey = key; + if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { + while (startKey.getParent() != null) { + startKey = startKey.getParent(); + } + } + + List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>(); + remove(removedContexts, startKey); + + // Remove all remaining references to any removed contexts from the + // hierarchy map. + for (MergedContextConfiguration currentKey : removedContexts) { + for (Set<MergedContextConfiguration> children : this.hierarchyMap.values()) { + children.remove(currentKey); + } + } + + // Remove empty entries from the hierarchy map. + for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { + if (this.hierarchyMap.get(currentKey).isEmpty()) { + this.hierarchyMap.remove(currentKey); + } + } + } + + private void remove(List<MergedContextConfiguration> removedContexts, MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + + Set<MergedContextConfiguration> children = this.hierarchyMap.get(key); + if (children != null) { + for (MergedContextConfiguration child : children) { + // Recurse through lower levels + remove(removedContexts, child); + } + // Remove the set of children for the current context from the hierarchy map. + this.hierarchyMap.remove(key); + } + + // Physically remove and close leaf nodes first (i.e., on the way back up the + // stack as opposed to prior to the recursive call). + ApplicationContext context = this.contextMap.remove(key); + if (context instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) context).close(); + } + removedContexts.add(key); + } + + /** + * Determine the number of contexts currently stored in the cache. If the cache + * contains more than <tt>Integer.MAX_VALUE</tt> elements, returns + * <tt>Integer.MAX_VALUE</tt>. + */ + public int size() { + return this.contextMap.size(); + } + + /** + * Determine the number of parent contexts currently tracked within the cache. + */ + public int getParentContextCount() { + return this.hierarchyMap.size(); + } + + /** + * Generates a text string, which contains the {@linkplain #size() size} as well + * as the {@linkplain #getHitCount() hit}, {@linkplain #getMissCount() miss}, + * and {@linkplain #getParentContextCount() parent context} counts. + */ + @Override + public String toString() { + return new ToStringCreator(this) + .append("size", size()) + .append("hitCount", getHitCount()) + .append("missCount", getMissCount()) + .append("parentContextCount", getParentContextCount()) + .toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java new file mode 100644 index 00000000..7033765c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java @@ -0,0 +1,312 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +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; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * {@code @ContextConfiguration} defines class-level metadata that is + * used to determine how to load and configure an + * {@link org.springframework.context.ApplicationContext ApplicationContext} + * for integration tests. + * + * <h3>Supported Resource Types</h3> + * + * <p>Prior to Spring 3.1, only path-based resource locations were supported. + * As of Spring 3.1, {@linkplain #loader context loaders} may choose to support + * either path-based or class-based resources (but not both). Consequently + * {@code @ContextConfiguration} can be used to declare either path-based + * resource locations (via the {@link #locations} or {@link #value} + * attribute) <i>or</i> annotated classes (via the {@link #classes} + * attribute). + * + * <h3>Annotated Classes</h3> + * + * <p>The term <em>annotated class</em> can refer to any of the following. + * + * <ul> + * <li>A class annotated with + * {@link org.springframework.context.annotation.Configuration @Configuration}</li> + * <li>A component (i.e., a class annotated with + * {@link org.springframework.stereotype.Component @Component}, + * {@link org.springframework.stereotype.Service @Service}, + * {@link org.springframework.stereotype.Repository @Repository}, etc.)</li> + * <li>A JSR-330 compliant class that is annotated with {@code javax.inject} annotations</li> + * <li>Any other class that contains + * {@link org.springframework.context.annotation.Bean @Bean}-methods</li> + * </ul> + * + * <p>Consult the Javadoc for + * {@link org.springframework.context.annotation.Configuration @Configuration} and + * {@link org.springframework.context.annotation.Bean @Bean} + * for further information regarding the configuration and semantics of + * <em>annotated classes</em>. + * + * @author Sam Brannen + * @since 2.5 + * @see ContextHierarchy + * @see ActiveProfiles + * @see ContextLoader + * @see SmartContextLoader + * @see ContextConfigurationAttributes + * @see MergedContextConfiguration + * @see org.springframework.context.ApplicationContext + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ContextConfiguration { + + /** + * Alias for {@link #locations}. + * + * <p>This attribute may <strong>not</strong> be used in conjunction + * with {@link #locations} or {@link #classes}, but it may be used + * instead of {@link #locations}. + * @since 3.0 + * @see #inheritLocations + */ + String[] value() default {}; + + /** + * The resource locations to use for loading an + * {@link org.springframework.context.ApplicationContext ApplicationContext}. + * + * <p>Check out the Javadoc for + * {@link org.springframework.test.context.support.AbstractContextLoader#modifyLocations + * AbstractContextLoader.modifyLocations()} for details on how a location + * String will be interpreted at runtime, in particular in case of a relative + * path. Also, check out the documentation on + * {@link org.springframework.test.context.support.AbstractContextLoader#generateDefaultLocations + * AbstractContextLoader.generateDefaultLocations()} for details on the default + * locations that are going to be used if none are specified. + * + * <p>Note that the above-mentioned default rules only apply for a standard + * {@link org.springframework.test.context.support.AbstractContextLoader + * AbstractContextLoader} subclass such as + * {@link org.springframework.test.context.support.GenericXmlContextLoader + * GenericXmlContextLoader} which is the effective default implementation + * used at runtime if {@code locations} are configured. See the + * documentation for {@link #loader} for further details regarding default + * loaders. + * + * <p>This attribute may <strong>not</strong> be used in conjunction with + * {@link #value} or {@link #classes}, but it may be used instead of + * {@link #value}. + * @since 2.5 + * @see #inheritLocations + */ + String[] locations() default {}; + + /** + * The <em>annotated classes</em> to use for loading an + * {@link org.springframework.context.ApplicationContext ApplicationContext}. + * + * <p>Check out the Javadoc for + * {@link org.springframework.test.context.support.AnnotationConfigContextLoader#detectDefaultConfigurationClasses + * AnnotationConfigContextLoader.detectDefaultConfigurationClasses()} for details + * on how default configuration classes will be detected if no + * <em>annotated classes</em> are specified. See the documentation for + * {@link #loader} for further details regarding default loaders. + * + * <p>This attribute may <strong>not</strong> be used in conjunction with + * {@link #locations} or {@link #value}. + * + * @since 3.1 + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.test.context.support.AnnotationConfigContextLoader + * @see #inheritLocations + */ + Class<?>[] classes() default {}; + + /** + * The application context <em>initializer classes</em> to use for initializing + * a {@link ConfigurableApplicationContext}. + * + * <p>The concrete {@code ConfigurableApplicationContext} type supported by each + * declared initializer must be compatible with the type of {@code ApplicationContext} + * created by the {@link SmartContextLoader} in use. + * + * <p>{@code SmartContextLoader} implementations typically detect whether + * Spring's {@link org.springframework.core.Ordered Ordered} interface has been + * implemented or if the @{@link org.springframework.core.annotation.Order Order} + * annotation is present and sort instances accordingly prior to invoking them. + * + * @since 3.2 + * @see org.springframework.context.ApplicationContextInitializer + * @see org.springframework.context.ConfigurableApplicationContext + * @see #inheritInitializers + * @see #loader + */ + Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers() default {}; + + /** + * Whether or not {@link #locations resource locations} or <em>annotated + * classes</em> from test superclasses should be <em>inherited</em>. + * + * <p>The default value is {@code true}. This means that an annotated + * class will <em>inherit</em> the resource locations or annotated classes + * defined by test superclasses. Specifically, the resource locations or + * annotated classes for a given test class will be appended to the list of + * resource locations or annotated classes defined by test superclasses. + * Thus, subclasses have the option of <em>extending</em> the list of resource + * locations or annotated classes. + * + * <p>If {@code inheritLocations} is set to {@code false}, the + * resource locations or annotated classes for the annotated class + * will <em>shadow</em> and effectively replace any resource locations + * or annotated classes defined by superclasses. + * + * <p>In the following example that uses path-based resource locations, the + * {@link org.springframework.context.ApplicationContext ApplicationContext} + * for {@code ExtendedTest} will be loaded from + * "base-context.xml" <strong>and</strong> + * "extended-context.xml", in that order. Beans defined in + * "extended-context.xml" may therefore override those defined in + * "base-context.xml". + * <pre class="code"> + * @ContextConfiguration("base-context.xml") + * public class BaseTest { + * // ... + * } + * + * @ContextConfiguration("extended-context.xml") + * public class ExtendedTest extends BaseTest { + * // ... + * } + * </pre> + * + * <p>Similarly, in the following example that uses annotated + * classes, the + * {@link org.springframework.context.ApplicationContext ApplicationContext} + * for {@code ExtendedTest} will be loaded from the + * {@code BaseConfig} <strong>and</strong> {@code ExtendedConfig} + * configuration classes, in that order. Beans defined in + * {@code ExtendedConfig} may therefore override those defined in + * {@code BaseConfig}. + * <pre class="code"> + * @ContextConfiguration(classes=BaseConfig.class) + * public class BaseTest { + * // ... + * } + * + * @ContextConfiguration(classes=ExtendedConfig.class) + * public class ExtendedTest extends BaseTest { + * // ... + * } + * </pre> + * @since 2.5 + */ + boolean inheritLocations() default true; + + /** + * Whether or not {@linkplain #initializers context initializers} from test + * superclasses should be <em>inherited</em>. + * + * <p>The default value is {@code true}. This means that an annotated + * class will <em>inherit</em> the application context initializers defined + * by test superclasses. Specifically, the initializers for a given test + * class will be added to the set of initializers defined by test + * superclasses. Thus, subclasses have the option of <em>extending</em> the + * set of initializers. + * + * <p>If {@code inheritInitializers} is set to {@code false}, the + * initializers for the annotated class will <em>shadow</em> and effectively + * replace any initializers defined by superclasses. + * + * <p>In the following example, the + * {@link org.springframework.context.ApplicationContext ApplicationContext} + * for {@code ExtendedTest} will be initialized using + * {@code BaseInitializer} <strong>and</strong> {@code ExtendedInitializer}. + * Note, however, that the order in which the initializers are invoked + * depends on whether they implement {@link org.springframework.core.Ordered + * Ordered} or are annotated with {@link org.springframework.core.annotation.Order + * @Order}. + * <pre class="code"> + * @ContextConfiguration(initializers = BaseInitializer.class) + * public class BaseTest { + * // ... + * } + * + * @ContextConfiguration(initializers = ExtendedInitializer.class) + * public class ExtendedTest extends BaseTest { + * // ... + * } + * </pre> + * @since 3.2 + */ + boolean inheritInitializers() default true; + + /** + * The type of {@link SmartContextLoader} (or {@link ContextLoader}) to use + * for loading an {@link org.springframework.context.ApplicationContext + * ApplicationContext}. + * + * <p>If not specified, the loader will be inherited from the first superclass + * that is annotated with {@code @ContextConfiguration} and specifies an + * explicit loader. If no class in the hierarchy specifies an explicit + * loader, a default loader will be used instead. + * + * <p>The default concrete implementation chosen at runtime will be either + * {@link org.springframework.test.context.support.DelegatingSmartContextLoader + * DelegatingSmartContextLoader} or + * {@link org.springframework.test.context.web.WebDelegatingSmartContextLoader + * WebDelegatingSmartContextLoader} depending on the absence or presence of + * {@link org.springframework.test.context.web.WebAppConfiguration + * @WebAppConfiguration}. For further details on the default behavior + * of various concrete {@code SmartContextLoaders}, check out the Javadoc for + * {@link org.springframework.test.context.support.AbstractContextLoader + * AbstractContextLoader}, + * {@link org.springframework.test.context.support.GenericXmlContextLoader + * GenericXmlContextLoader}, + * {@link org.springframework.test.context.support.AnnotationConfigContextLoader + * AnnotationConfigContextLoader}, + * {@link org.springframework.test.context.web.GenericXmlWebContextLoader + * GenericXmlWebContextLoader}, and + * {@link org.springframework.test.context.web.AnnotationConfigWebContextLoader + * AnnotationConfigWebContextLoader}. + * + * @since 2.5 + */ + Class<? extends ContextLoader> loader() default ContextLoader.class; + + /** + * The name of the context hierarchy level represented by this configuration. + * + * <p>If not specified the name will be inferred based on the numerical level + * within all declared contexts within the hierarchy. + * + * <p>This attribute is only applicable when used within a test class hierarchy + * that is configured using {@code @ContextHierarchy}, in which case the name + * can be used for <em>merging</em> or <em>overriding</em> this configuration + * with configuration of the same name in hierarchy levels defined in superclasses. + * See the Javadoc for {@link ContextHierarchy @ContextHierarchy} for details. + * + * @since 3.2.2 + */ + String name() default ""; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java new file mode 100644 index 00000000..16c724a8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java @@ -0,0 +1,420 @@ +/* + * Copyright 2002-2014 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 + * + * http://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; + +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@code ContextConfigurationAttributes} encapsulates the context configuration + * attributes declared via {@link ContextConfiguration @ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + * @see ContextConfiguration + * @see SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes) + * @see MergedContextConfiguration + */ +public class ContextConfigurationAttributes { + + private static final Log logger = LogFactory.getLog(ContextConfigurationAttributes.class); + + private final Class<?> declaringClass; + + private Class<?>[] classes; + + private String[] locations; + + private final boolean inheritLocations; + + private final Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers; + + private final boolean inheritInitializers; + + private final String name; + + private final Class<? extends ContextLoader> contextLoaderClass; + + + /** + * Construct a new {@link ContextConfigurationAttributes} instance for the + * supplied {@link ContextConfiguration @ContextConfiguration} annotation and + * the {@linkplain Class test class} that declared it. + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @param contextConfiguration the annotation from which to retrieve the attributes + */ + public ContextConfigurationAttributes(Class<?> declaringClass, ContextConfiguration contextConfiguration) { + this(declaringClass, resolveLocations(declaringClass, contextConfiguration), contextConfiguration.classes(), + contextConfiguration.inheritLocations(), contextConfiguration.initializers(), + contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader()); + } + + /** + * Construct a new {@link ContextConfigurationAttributes} instance for the + * supplied {@link AnnotationAttributes} (parsed from a + * {@link ContextConfiguration @ContextConfiguration} annotation) and + * the {@linkplain Class test class} that declared them. + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @param annAttrs the annotation attributes from which to retrieve the attributes + */ + @SuppressWarnings("unchecked") + public ContextConfigurationAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) { + this(declaringClass, + resolveLocations(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getStringArray("value")), + annAttrs.getClassArray("classes"), annAttrs.getBoolean("inheritLocations"), + (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"), + annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"), + (Class<? extends ContextLoader>) annAttrs.getClass("loader")); + } + + /** + * Construct a new {@link ContextConfigurationAttributes} instance for the + * {@linkplain Class test class} that declared the + * {@link ContextConfiguration @ContextConfiguration} annotation and its + * corresponding attributes. + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @param locations the resource locations declared via {@code @ContextConfiguration} + * @param classes the annotated classes declared via {@code @ContextConfiguration} + * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration} + * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration} + * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is + * {@code null} + * @deprecated as of Spring 3.2, use + * {@link #ContextConfigurationAttributes(Class, String[], Class[], boolean, Class[], boolean, String, Class)} + * instead + */ + @Deprecated + public ContextConfigurationAttributes(Class<?> declaringClass, String[] locations, Class<?>[] classes, + boolean inheritLocations, Class<? extends ContextLoader> contextLoaderClass) { + + this(declaringClass, locations, classes, inheritLocations, null, true, null, contextLoaderClass); + } + + /** + * Construct a new {@link ContextConfigurationAttributes} instance for the + * {@linkplain Class test class} that declared the + * {@link ContextConfiguration @ContextConfiguration} annotation and its + * corresponding attributes. + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @param locations the resource locations declared via {@code @ContextConfiguration} + * @param classes the annotated classes declared via {@code @ContextConfiguration} + * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration} + * @param initializers the context initializers declared via {@code @ContextConfiguration} + * @param inheritInitializers the {@code inheritInitializers} flag declared via {@code @ContextConfiguration} + * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration} + * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is + * {@code null} + */ + public ContextConfigurationAttributes( + Class<?> declaringClass, String[] locations, Class<?>[] classes, boolean inheritLocations, + Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers, + boolean inheritInitializers, Class<? extends ContextLoader> contextLoaderClass) { + + this(declaringClass, locations, classes, inheritLocations, initializers, inheritInitializers, null, + contextLoaderClass); + } + + /** + * Construct a new {@link ContextConfigurationAttributes} instance for the + * {@linkplain Class test class} that declared the + * {@link ContextConfiguration @ContextConfiguration} annotation and its + * corresponding attributes. + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @param locations the resource locations declared via {@code @ContextConfiguration} + * @param classes the annotated classes declared via {@code @ContextConfiguration} + * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration} + * @param initializers the context initializers declared via {@code @ContextConfiguration} + * @param inheritInitializers the {@code inheritInitializers} flag declared via {@code @ContextConfiguration} + * @param name the name of level in the context hierarchy, or {@code null} if not applicable + * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration} + * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is + * {@code null} + */ + public ContextConfigurationAttributes( + Class<?> declaringClass, String[] locations, Class<?>[] classes, boolean inheritLocations, + Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers, + boolean inheritInitializers, String name, Class<? extends ContextLoader> contextLoaderClass) { + + Assert.notNull(declaringClass, "declaringClass must not be null"); + Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null"); + + if (!ObjectUtils.isEmpty(locations) && !ObjectUtils.isEmpty(classes) && logger.isDebugEnabled()) { + logger.debug(String.format( + "Test class [%s] has been configured with @ContextConfiguration's 'locations' (or 'value') %s " + + "and 'classes' %s attributes. Most SmartContextLoader implementations support " + + "only one declaration of resources per @ContextConfiguration annotation.", + declaringClass.getName(), ObjectUtils.nullSafeToString(locations), + ObjectUtils.nullSafeToString(classes))); + } + + this.declaringClass = declaringClass; + this.locations = locations; + this.classes = classes; + this.inheritLocations = inheritLocations; + this.initializers = initializers; + this.inheritInitializers = inheritInitializers; + this.name = (StringUtils.hasText(name) ? name : null); + this.contextLoaderClass = contextLoaderClass; + } + + + /** + * Resolve resource locations from the {@link ContextConfiguration#locations() locations} + * and {@link ContextConfiguration#value() value} attributes of the supplied + * {@link ContextConfiguration} annotation. + * @throws IllegalStateException if both the locations and value attributes have been declared + */ + private static String[] resolveLocations(Class<?> declaringClass, ContextConfiguration contextConfiguration) { + return resolveLocations(declaringClass, contextConfiguration.locations(), contextConfiguration.value()); + } + + /** + * Resolve resource locations from the supplied {@code locations} and + * {@code value} arrays, which correspond to attributes of the same names in + * the {@link ContextConfiguration} annotation. + * @throws IllegalStateException if both the locations and value attributes have been declared + */ + private static String[] resolveLocations(Class<?> declaringClass, String[] locations, String[] value) { + Assert.notNull(declaringClass, "declaringClass must not be null"); + if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) { + throw new IllegalStateException(String.format("Test class [%s] has been configured with " + + "@ContextConfiguration's 'value' %s and 'locations' %s attributes. Only one declaration " + + "of resource locations is permitted per @ContextConfiguration annotation.", + declaringClass.getName(), ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations))); + } + else if (!ObjectUtils.isEmpty(value)) { + locations = value; + } + return locations; + } + + + /** + * Get the {@linkplain Class class} that declared the + * {@link ContextConfiguration @ContextConfiguration} annotation. + * @return the declaring class (never {@code null}) + */ + public Class<?> getDeclaringClass() { + return this.declaringClass; + } + + /** + * Set the <em>processed</em> annotated classes, effectively overriding the + * original value declared via {@link ContextConfiguration @ContextConfiguration}. + * @see #getClasses() + */ + public void setClasses(Class<?>... classes) { + this.classes = classes; + } + + /** + * Get the annotated classes that were declared via + * {@link ContextConfiguration @ContextConfiguration}. + * <p>Note: this is a mutable property. The returned value may therefore + * represent a <em>processed</em> value that does not match the original value + * declared via {@link ContextConfiguration @ContextConfiguration}. + * @return the annotated classes; potentially {@code null} or <em>empty</em> + * @see ContextConfiguration#classes + * @see #setClasses(Class[]) + */ + public Class<?>[] getClasses() { + return this.classes; + } + + /** + * Determine if this {@code ContextConfigurationAttributes} instance has + * class-based resources. + * @return {@code true} if the {@link #getClasses() classes} array is not empty + * @see #hasResources() + * @see #hasLocations() + */ + public boolean hasClasses() { + return !ObjectUtils.isEmpty(getClasses()); + } + + /** + * Set the <em>processed</em> resource locations, effectively overriding the + * original value declared via {@link ContextConfiguration @ContextConfiguration}. + * @see #getLocations() + */ + public void setLocations(String... locations) { + this.locations = locations; + } + + /** + * Get the resource locations that were declared via + * {@link ContextConfiguration @ContextConfiguration}. + * <p>Note: this is a mutable property. The returned value may therefore + * represent a <em>processed</em> value that does not match the original value + * declared via {@link ContextConfiguration @ContextConfiguration}. + * @return the resource locations; potentially {@code null} or <em>empty</em> + * @see ContextConfiguration#value + * @see ContextConfiguration#locations + * @see #setLocations(String[]) + */ + public String[] getLocations() { + return this.locations; + } + + /** + * Determine if this {@code ContextConfigurationAttributes} instance has + * path-based resource locations. + * @return {@code true} if the {@link #getLocations() locations} array is not empty + * @see #hasResources() + * @see #hasClasses() + */ + public boolean hasLocations() { + return !ObjectUtils.isEmpty(getLocations()); + } + + /** + * Determine if this {@code ContextConfigurationAttributes} instance has + * either path-based resource locations or class-based resources. + * @return {@code true} if either the {@link #getLocations() locations} + * or the {@link #getClasses() classes} array is not empty + * @see #hasLocations() + * @see #hasClasses() + */ + public boolean hasResources() { + return (hasLocations() || hasClasses()); + } + + /** + * Get the {@code inheritLocations} flag that was declared via + * {@link ContextConfiguration @ContextConfiguration}. + * @return the {@code inheritLocations} flag + * @see ContextConfiguration#inheritLocations + */ + public boolean isInheritLocations() { + return this.inheritLocations; + } + + /** + * Get the {@code ApplicationContextInitializer} classes that were declared via + * {@link ContextConfiguration @ContextConfiguration}. + * @return the {@code ApplicationContextInitializer} classes + * @since 3.2 + */ + public Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] getInitializers() { + return this.initializers; + } + + /** + * Get the {@code inheritInitializers} flag that was declared via + * {@link ContextConfiguration @ContextConfiguration}. + * @return the {@code inheritInitializers} flag + * @since 3.2 + */ + public boolean isInheritInitializers() { + return this.inheritInitializers; + } + + /** + * Get the name of the context hierarchy level that was declared via + * {@link ContextConfiguration @ContextConfiguration}. + * @return the name of the context hierarchy level or {@code null} if not applicable + * @see ContextConfiguration#name() + * @since 3.2.2 + */ + public String getName() { + return this.name; + } + + /** + * Get the {@code ContextLoader} class that was declared via + * {@link ContextConfiguration @ContextConfiguration}. + * @return the {@code ContextLoader} class + * @see ContextConfiguration#loader + */ + public Class<? extends ContextLoader> getContextLoaderClass() { + return this.contextLoaderClass; + } + + + /** + * Determine if the supplied object is equal to this + * {@code ContextConfigurationAttributes} instance by comparing both object's + * {@linkplain #getDeclaringClass() declaring class}, + * {@linkplain #getLocations() locations}, + * {@linkplain #getClasses() annotated classes}, + * {@linkplain #isInheritLocations() inheritLocations flag}, + * {@linkplain #getInitializers() context initializer classes}, + * {@linkplain #isInheritInitializers() inheritInitializers flag}, and the + * {@link #getContextLoaderClass() ContextLoader class}. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ContextConfigurationAttributes)) { + return false; + } + ContextConfigurationAttributes otherAttr = (ContextConfigurationAttributes) other; + return (ObjectUtils.nullSafeEquals(this.declaringClass, otherAttr.declaringClass) && + Arrays.equals(this.classes, otherAttr.classes)) && + Arrays.equals(this.locations, otherAttr.locations) && + this.inheritLocations == otherAttr.inheritLocations && + Arrays.equals(this.initializers, otherAttr.initializers) && + this.inheritInitializers == otherAttr.inheritInitializers && + ObjectUtils.nullSafeEquals(this.name, otherAttr.name) && + ObjectUtils.nullSafeEquals(this.contextLoaderClass, otherAttr.contextLoaderClass); + } + + /** + * Generate a unique hash code for all properties of this + * {@code ContextConfigurationAttributes} instance excluding the + * {@linkplain #getName() name}. + */ + @Override + public int hashCode() { + int result = this.declaringClass.hashCode(); + result = 31 * result + Arrays.hashCode(this.classes); + result = 31 * result + Arrays.hashCode(this.locations); + result = 31 * result + Arrays.hashCode(this.initializers); + return result; + } + + /** + * Provide a String representation of the context configuration attributes + * and declaring class. + */ + @Override + public String toString() { + return new ToStringCreator(this) + .append("declaringClass", this.declaringClass.getName()) + .append("classes", ObjectUtils.nullSafeToString(this.classes)) + .append("locations", ObjectUtils.nullSafeToString(this.locations)) + .append("inheritLocations", this.inheritLocations) + .append("initializers", ObjectUtils.nullSafeToString(this.initializers)) + .append("inheritInitializers", this.inheritInitializers) + .append("name", this.name) + .append("contextLoaderClass", this.contextLoaderClass.getName()) + .toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java new file mode 100644 index 00000000..7eac62d6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +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 @ContextHierarchy} is a class-level annotation that is used to define + * a hierarchy of {@link org.springframework.context.ApplicationContext + * ApplicationContexts} for integration tests. + * + * <h3>Examples</h3> + * <p>The following JUnit-based examples demonstrate common configuration + * scenarios for integration tests that require the use of context hierarchies. + * + * <h4>Single Test Class with Context Hierarchy</h4> + * <p>{@code ControllerIntegrationTests} represents a typical integration testing + * scenario for a Spring MVC web application by declaring a context hierarchy + * consisting of two levels, one for the <em>root</em> {@code WebApplicationContext} + * (with {@code TestAppConfig}) and one for the <em>dispatcher servlet</em> + * {@code WebApplicationContext} (with {@code WebConfig}). The {@code + * WebApplicationContext} that is <em>autowired</em> into the test instance is + * the one for the child context (i.e., the lowest context in the hierarchy). + * + * <pre class="code"> + * @RunWith(SpringJUnit4ClassRunner.class) + * @WebAppConfiguration + * @ContextHierarchy({ + * @ContextConfiguration(classes = TestAppConfig.class), + * @ContextConfiguration(classes = WebConfig.class) + * }) + * public class ControllerIntegrationTests { + * + * @Autowired + * private WebApplicationContext wac; + * + * // ... + * }</pre> + * + * <h4>Class Hierarchy with Implicit Parent Context</h4> + * <p>The following test classes define a context hierarchy within a test class + * hierarchy. {@code AbstractWebTests} declares the configuration for a root + * {@code WebApplicationContext} in a Spring-powered web application. Note, + * however, that {@code AbstractWebTests} does not declare {@code @ContextHierarchy}; + * consequently, subclasses of {@code AbstractWebTests} can optionally participate + * in a context hierarchy or follow the standard semantics for {@code @ContextConfiguration}. + * {@code SoapWebServiceTests} and {@code RestWebServiceTests} both extend + * {@code AbstractWebTests} and define a context hierarchy via {@code @ContextHierarchy}. + * The result is that three application contexts will be loaded (one for each + * declaration of {@code @ContextConfiguration}, and the application context + * loaded based on the configuration in {@code AbstractWebTests} will be set as + * the parent context for each of the contexts loaded for the concrete subclasses. + * + * <pre class="code"> + * @RunWith(SpringJUnit4ClassRunner.class) + * @WebAppConfiguration + * @ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml") + * public abstract class AbstractWebTests {} + * + * @ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml") + * public class SoapWebServiceTests extends AbstractWebTests {} + * + * @ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml") + * public class RestWebServiceTests extends AbstractWebTests {}</pre> + * + * <h4>Class Hierarchy with Merged Context Hierarchy Configuration</h4> + * <p>The following classes demonstrate the use of <em>named</em> hierarchy levels + * in order to <em>merge</em> the configuration for specific levels in a context + * hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code parent} + * and {@code child}. {@code ExtendedTests} extends {@code BaseTests} and instructs + * the Spring TestContext Framework to merge the context configuration for the + * {@code child} hierarchy level, simply by ensuring that the names declared via + * {@link ContextConfiguration#name} are both {@code "child"}. The result is that + * three application contexts will be loaded: one for {@code "/app-config.xml"}, + * one for {@code "/user-config.xml"}, and one for <code>{"/user-config.xml", + * "/order-config.xml"}</code>. As with the previous example, the application + * context loaded from {@code "/app-config.xml"} will be set as the parent context + * for the contexts loaded from {@code "/user-config.xml"} and <code>{"/user-config.xml", + * "/order-config.xml"}</code>. + * + * <pre class="code"> + * @RunWith(SpringJUnit4ClassRunner.class) + * @ContextHierarchy({ + * @ContextConfiguration(name = "parent", locations = "/app-config.xml"), + * @ContextConfiguration(name = "child", locations = "/user-config.xml") + * }) + * public class BaseTests {} + * + * @ContextHierarchy( + * @ContextConfiguration(name = "child", locations = "/order-config.xml") + * ) + * public class ExtendedTests extends BaseTests {}</pre> + * + * <h4>Class Hierarchy with Overridden Context Hierarchy Configuration</h4> + * <p>In contrast to the previous example, this example demonstrates how to + * <em>override</em> the configuration for a given named level in a context hierarchy + * by setting the {@link ContextConfiguration#inheritLocations} flag to {@code false}. + * Consequently, the application context for {@code ExtendedTests} will be loaded + * only from {@code "/test-user-config.xml"} and will have its parent set to the + * context loaded from {@code "/app-config.xml"}. + * + * <pre class="code"> + * @RunWith(SpringJUnit4ClassRunner.class) + * @ContextHierarchy({ + * @ContextConfiguration(name = "parent", locations = "/app-config.xml"), + * @ContextConfiguration(name = "child", locations = "/user-config.xml") + * }) + * public class BaseTests {} + * + * @ContextHierarchy( + * @ContextConfiguration(name = "child", locations = "/test-user-config.xml", inheritLocations=false) + * ) + * public class ExtendedTests extends BaseTests {}</pre> + * + * @author Sam Brannen + * @since 3.2.2 + * @see ContextConfiguration + * @see org.springframework.context.ApplicationContext + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ContextHierarchy { + + /** + * A list of {@link ContextConfiguration @ContextConfiguration} instances, + * each of which defines a level in the context hierarchy. + * + * <p>If you need to merge or override the configuration for a given level + * of the context hierarchy within a test class hierarchy, you must explicitly + * name that level by supplying the same value to the {@link ContextConfiguration#name + * name} attribute in {@code @ContextConfiguration} at each level in the + * class hierarchy. See the class-level Javadoc for examples. + */ + ContextConfiguration[] value(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java new file mode 100644 index 00000000..71faf5e5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import org.springframework.context.ApplicationContext; + +/** + * Strategy interface for loading an {@link ApplicationContext application context} + * for an integration test managed by the Spring TestContext Framework. + * + * <p><b>Note</b>: as of Spring 3.1, implement {@link SmartContextLoader} instead + * of this interface in order to provide support for annotated classes, active + * bean definition profiles, and application context initializers. + * + * <p>Clients of a ContextLoader should call + * {@link #processLocations(Class, String...) processLocations()} prior to + * calling {@link #loadContext(String...) loadContext()} in case the + * ContextLoader provides custom support for modifying or generating locations. + * The results of {@link #processLocations(Class, String...) processLocations()} + * should then be supplied to {@link #loadContext(String...) loadContext()}. + * + * <p>Concrete implementations must provide a {@code public} no-args + * constructor. + * + * <p>Spring provides the following out-of-the-box implementations: + * <ul> + * <li>{@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader}</li> + * <li>{@link org.springframework.test.context.support.GenericPropertiesContextLoader GenericPropertiesContextLoader}</li> + * </ul> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see SmartContextLoader + * @see org.springframework.test.context.support.AnnotationConfigContextLoader AnnotationConfigContextLoader + */ +public interface ContextLoader { + + /** + * Processes application context resource locations for a specified class. + * + * <p>Concrete implementations may choose to modify the supplied locations, + * generate new locations, or simply return the supplied locations unchanged. + * + * @param clazz the class with which the locations are associated: used to + * determine how to process the supplied locations + * @param locations the unmodified locations to use for loading the + * application context (can be {@code null} or empty) + * @return an array of application context resource locations + */ + String[] processLocations(Class<?> clazz, String... locations); + + /** + * Loads a new {@link ApplicationContext context} based on the supplied + * {@code locations}, configures the context, and finally returns + * the context in fully <em>refreshed</em> state. + * + * <p>Configuration locations are generally considered to be classpath + * resources by default. + * + * <p>Concrete implementations should register annotation configuration + * processors with bean factories of {@link ApplicationContext application + * contexts} loaded by this ContextLoader. Beans will therefore automatically + * be candidates for annotation-based dependency injection using + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired}, + * {@link javax.annotation.Resource @Resource}, and + * {@link javax.inject.Inject @Inject}. + * + * <p>Any ApplicationContext loaded by a ContextLoader <strong>must</strong> + * register a JVM shutdown hook for itself. Unless the context gets closed + * early, all context instances will be automatically closed on JVM + * shutdown. This allows for freeing external resources held by beans within + * the context, e.g. temporary files. + * + * @param locations the resource locations to use to load the application context + * @return a new application context + * @throws Exception if context loading failed + */ + ApplicationContext loadContext(String... locations) throws Exception; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java new file mode 100644 index 00000000..f71e447b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java @@ -0,0 +1,707 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import static org.springframework.beans.BeanUtils.instantiateClass; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClassForTypes; +import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Utility methods for working with {@link ContextLoader ContextLoaders} and + * {@link SmartContextLoader SmartContextLoaders} and resolving resource locations, + * annotated classes, active bean definition profiles, and application context + * initializers. + * + * @author Sam Brannen + * @since 3.1 + * @see ContextLoader + * @see SmartContextLoader + * @see ContextConfiguration + * @see ContextConfigurationAttributes + * @see ActiveProfiles + * @see ApplicationContextInitializer + * @see ContextHierarchy + * @see MergedContextConfiguration + */ +abstract class ContextLoaderUtils { + + static final String GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX = "ContextHierarchyLevel#"; + + private static final Log logger = LogFactory.getLog(ContextLoaderUtils.class); + + private static final String DEFAULT_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.support.DelegatingSmartContextLoader"; + private static final String DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.web.WebDelegatingSmartContextLoader"; + + private static final String WEB_APP_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebAppConfiguration"; + private static final String WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebMergedContextConfiguration"; + + + private ContextLoaderUtils() { + /* no-op */ + } + + /** + * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied + * list of {@link ContextConfigurationAttributes} and then instantiate and return that + * {@code ContextLoader}. + * + * <p>If the supplied {@code defaultContextLoaderClassName} is {@code null} or + * <em>empty</em>, depending on the absence or presence of + * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} either + * {@code "org.springframework.test.context.support.DelegatingSmartContextLoader"} or + * {@code "org.springframework.test.context.web.WebDelegatingSmartContextLoader"} will + * be used as the default context loader class name. For details on the class + * resolution process, see {@link #resolveContextLoaderClass}. + * + * @param testClass the test class for which the {@code ContextLoader} should be + * resolved; must not be {@code null} + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em> + * (i.e., as if we were traversing up the class hierarchy) + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use; may be {@code null} or <em>empty</em> + * @return the resolved {@code ContextLoader} for the supplied {@code testClass} + * (never {@code null}) + * @see #resolveContextLoaderClass + */ + static ContextLoader resolveContextLoader(Class<?> testClass, + List<ContextConfigurationAttributes> configAttributesList, String defaultContextLoaderClassName) { + Assert.notNull(testClass, "Class must not be null"); + Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + + if (!StringUtils.hasText(defaultContextLoaderClassName)) { + Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass(); + defaultContextLoaderClassName = webAppConfigClass != null + && testClass.isAnnotationPresent(webAppConfigClass) ? DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME + : DEFAULT_CONTEXT_LOADER_CLASS_NAME; + } + + Class<? extends ContextLoader> contextLoaderClass = resolveContextLoaderClass(testClass, configAttributesList, + defaultContextLoaderClassName); + + return instantiateClass(contextLoaderClass, ContextLoader.class); + } + + /** + * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied + * list of {@link ContextConfigurationAttributes}. + * + * <p>Beginning with the first level in the context configuration attributes hierarchy: + * + * <ol> + * <li>If the {@link ContextConfigurationAttributes#getContextLoaderClass() + * contextLoaderClass} property of {@link ContextConfigurationAttributes} is + * configured with an explicit class, that class will be returned.</li> + * <li>If an explicit {@code ContextLoader} class is not specified at the current + * level in the hierarchy, traverse to the next level in the hierarchy and return to + * step #1.</li> + * <li>If no explicit {@code ContextLoader} class is found after traversing the + * hierarchy, an attempt will be made to load and return the class with the supplied + * {@code defaultContextLoaderClassName}.</li> + * </ol> + * + * @param testClass the class for which to resolve the {@code ContextLoader} class; + * must not be {@code null}; only used for logging purposes + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em> + * (i.e., as if we were traversing up the class hierarchy) + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use; must not be {@code null} or empty + * @return the {@code ContextLoader} class to use for the supplied test class + * @throws IllegalArgumentException if {@code @ContextConfiguration} is not + * <em>present</em> on the supplied test class + * @throws IllegalStateException if the default {@code ContextLoader} class could not + * be loaded + */ + @SuppressWarnings("unchecked") + static Class<? extends ContextLoader> resolveContextLoaderClass(Class<?> testClass, + List<ContextConfigurationAttributes> configAttributesList, String defaultContextLoaderClassName) { + Assert.notNull(testClass, "Class must not be null"); + Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + Assert.hasText(defaultContextLoaderClassName, "Default ContextLoader class name must not be null or empty"); + + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Processing ContextLoader for context configuration attributes %s", + configAttributes)); + } + + Class<? extends ContextLoader> contextLoaderClass = configAttributes.getContextLoaderClass(); + if (!ContextLoader.class.equals(contextLoaderClass)) { + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "Found explicit ContextLoader class [%s] for context configuration attributes %s", + contextLoaderClass.getName(), configAttributes)); + } + return contextLoaderClass; + } + } + + try { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Using default ContextLoader class [%s] for test class [%s]", + defaultContextLoaderClassName, testClass.getName())); + } + return (Class<? extends ContextLoader>) ClassUtils.forName(defaultContextLoaderClassName, + ContextLoaderUtils.class.getClassLoader()); + } + catch (Throwable t) { + throw new IllegalStateException("Could not load default ContextLoader class [" + + defaultContextLoaderClassName + "]. Specify @ContextConfiguration's 'loader' " + + "attribute or make the default loader class available.", t); + } + } + + /** + * Convenience method for creating a {@link ContextConfigurationAttributes} instance + * from the supplied {@link ContextConfiguration} and declaring class and then adding + * the attributes to the supplied list. + */ + private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration, + Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].", + contextConfiguration, declaringClass.getName())); + } + + ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, + contextConfiguration); + if (logger.isTraceEnabled()) { + logger.trace("Resolved context configuration attributes: " + attributes); + } + attributesList.add(attributes); + } + + /** + * Resolve the list of lists of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Class test class} and its + * superclasses, taking into account context hierarchies declared via + * {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + * <p>The outer list represents a top-down ordering of context configuration + * attributes, where each element in the list represents the context configuration + * declared on a given test class in the class hierarchy. Each nested list + * contains the context configuration attributes declared either via a single + * instance of {@code @ContextConfiguration} on the particular class or via + * multiple instances of {@code @ContextConfiguration} declared within a + * single {@code @ContextHierarchy} instance on the particular class. + * Furthermore, each nested list maintains the order in which + * {@code @ContextConfiguration} instances are declared. + * + * <p>Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and + * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of + * {@link ContextConfiguration @ContextConfiguration} will <strong>not</strong> + * be taken into consideration. If these flags need to be honored, that must be + * handled manually when traversing the nested lists returned by this method. + * + * @param testClass the class for which to resolve the context hierarchy attributes + * (must not be {@code null}) + * @return the list of lists of configuration attributes for the specified class; + * never {@code null} + * @throws IllegalArgumentException if the supplied class is {@code null}; if + * neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is + * <em>present</em> on the supplied class; or if a given class in the class hierarchy + * declares both {@code @ContextConfiguration} and {@code @ContextHierarchy} as + * top-level annotations. + * + * @since 3.2.2 + * @see #buildContextHierarchyMap(Class) + * @see #resolveContextConfigurationAttributes(Class) + */ + static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttributes(Class<?> testClass) { + Assert.notNull(testClass, "Class must not be null"); + + final Class<ContextConfiguration> contextConfigType = ContextConfiguration.class; + final Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class; + final List<Class<? extends Annotation>> annotationTypes = Arrays.asList(contextConfigType, contextHierarchyType); + + final List<List<ContextConfigurationAttributes>> hierarchyAttributes = new ArrayList<List<ContextConfigurationAttributes>>(); + + Class<?> declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, testClass); + Assert.notNull(declaringClass, String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]", + contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName())); + + while (declaringClass != null) { + + boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass); + boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass); + + if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) { + String msg = String.format("Test class [%s] has been configured with both @ContextConfiguration " + + "and @ContextHierarchy as class-level annotations. Only one of these annotations may " + + "be declared as a top-level annotation per test class.", declaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>(); + + if (contextConfigDeclaredLocally) { + ContextConfiguration contextConfiguration = declaringClass.getAnnotation(contextConfigType); + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, + configAttributesList); + } + else if (contextHierarchyDeclaredLocally) { + ContextHierarchy contextHierarchy = declaringClass.getAnnotation(contextHierarchyType); + for (ContextConfiguration contextConfiguration : contextHierarchy.value()) { + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, + configAttributesList); + } + } + else { + // This should theoretically actually never happen... + String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration " + + "nor @ContextHierarchy as a class-level annotation.", declaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + hierarchyAttributes.add(0, configAttributesList); + + declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, declaringClass.getSuperclass()); + } + + return hierarchyAttributes; + } + + /** + * Build a <em>context hierarchy map</em> for the supplied {@linkplain Class + * test class} and its superclasses, taking into account context hierarchies + * declared via {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + * <p>Each value in the map represents the consolidated list of {@linkplain + * ContextConfigurationAttributes context configuration attributes} for a + * given level in the context hierarchy (potentially across the test class + * hierarchy), keyed by the {@link ContextConfiguration#name() name} of the + * context hierarchy level. + * + * <p>If a given level in the context hierarchy does not have an explicit + * name (i.e., configured via {@link ContextConfiguration#name}), a name will + * be generated for that hierarchy level by appending the numerical level to + * the {@link #GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX}. + * + * @param testClass the class for which to resolve the context hierarchy map + * (must not be {@code null}) + * @return a map of context configuration attributes for the context hierarchy, + * keyed by context hierarchy level name; never {@code null} + * @throws IllegalArgumentException if the lists of context configuration + * attributes for each level in the {@code @ContextHierarchy} do not define + * unique context configuration within the overall hierarchy. + * + * @since 3.2.2 + * @see #resolveContextHierarchyAttributes(Class) + */ + static Map<String, List<ContextConfigurationAttributes>> buildContextHierarchyMap(Class<?> testClass) { + final Map<String, List<ContextConfigurationAttributes>> map = new LinkedHashMap<String, List<ContextConfigurationAttributes>>(); + int hierarchyLevel = 1; + + for (List<ContextConfigurationAttributes> configAttributesList : resolveContextHierarchyAttributes(testClass)) { + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + String name = configAttributes.getName(); + + // Assign a generated name? + if (!StringUtils.hasText(name)) { + name = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + hierarchyLevel; + } + + // Encountered a new context hierarchy level? + if (!map.containsKey(name)) { + hierarchyLevel++; + map.put(name, new ArrayList<ContextConfigurationAttributes>()); + } + + map.get(name).add(configAttributes); + } + } + + // Check for uniqueness + Set<List<ContextConfigurationAttributes>> set = new HashSet<List<ContextConfigurationAttributes>>(map.values()); + if (set.size() != map.size()) { + String msg = String.format("The @ContextConfiguration elements configured via " + + "@ContextHierarchy in test class [%s] and its superclasses must " + + "define unique contexts per hierarchy level.", testClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + return map; + } + + /** + * Resolve the list of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Class test class} and its + * superclasses. + * + * <p>Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and + * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of + * {@link ContextConfiguration @ContextConfiguration} will <strong>not</strong> + * be taken into consideration. If these flags need to be honored, that must be + * handled manually when traversing the list returned by this method. + * + * @param testClass the class for which to resolve the configuration attributes (must + * not be {@code null}) + * @return the list of configuration attributes for the specified class, ordered + * <em>bottom-up</em> (i.e., as if we were traversing up the class hierarchy); + * never {@code null} + * @throws IllegalArgumentException if the supplied class is {@code null} or if + * {@code @ContextConfiguration} is not <em>present</em> on the supplied class + */ + static List<ContextConfigurationAttributes> resolveContextConfigurationAttributes(Class<?> testClass) { + Assert.notNull(testClass, "Class must not be null"); + + final List<ContextConfigurationAttributes> attributesList = new ArrayList<ContextConfigurationAttributes>(); + + Class<ContextConfiguration> annotationType = ContextConfiguration.class; + Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass); + Assert.notNull(declaringClass, String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", + annotationType.getName(), testClass.getName())); + + while (declaringClass != null) { + ContextConfiguration contextConfiguration = declaringClass.getAnnotation(annotationType); + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, attributesList); + declaringClass = findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass()); + } + + return attributesList; + } + + /** + * Resolve the list of merged {@code ApplicationContextInitializer} classes for the + * supplied list of {@code ContextConfigurationAttributes}. + * + * <p>Note that the {@link ContextConfiguration#inheritInitializers inheritInitializers} + * flag of {@link ContextConfiguration @ContextConfiguration} will be taken into + * consideration. Specifically, if the {@code inheritInitializers} flag is set to + * {@code true} for a given level in the class hierarchy represented by the provided + * configuration attributes, context initializer classes defined at the given level + * will be merged with those defined in higher levels of the class hierarchy. + * + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em> + * (i.e., as if we were traversing up the class hierarchy) + * @return the set of merged context initializer classes, including those from + * superclasses if appropriate (never {@code null}) + * @since 3.2 + */ + static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> resolveInitializerClasses( + List<ContextConfigurationAttributes> configAttributesList) { + Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + + final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Processing context initializers for context configuration attributes %s", + configAttributes)); + } + + initializerClasses.addAll(Arrays.asList(configAttributes.getInitializers())); + + if (!configAttributes.isInheritInitializers()) { + break; + } + } + + return initializerClasses; + } + + /** + * Resolve <em>active bean definition profiles</em> for the supplied {@link Class}. + * + * <p>Note that the {@link ActiveProfiles#inheritProfiles inheritProfiles} flag of + * {@link ActiveProfiles @ActiveProfiles} will be taken into consideration. + * Specifically, if the {@code inheritProfiles} flag is set to {@code true}, profiles + * defined in the test class will be merged with those defined in superclasses. + * + * @param testClass the class for which to resolve the active profiles (must not be + * {@code null}) + * @return the set of active profiles for the specified class, including active + * profiles from superclasses if appropriate (never {@code null}) + * @see ActiveProfiles + * @see org.springframework.context.annotation.Profile + */ + static String[] resolveActiveProfiles(Class<?> testClass) { + Assert.notNull(testClass, "Class must not be null"); + + Class<ActiveProfiles> annotationType = ActiveProfiles.class; + Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass); + + if (declaringClass == null && logger.isDebugEnabled()) { + logger.debug(String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", + annotationType.getName(), testClass.getName())); + } + + final Set<String> activeProfiles = new HashSet<String>(); + + while (declaringClass != null) { + ActiveProfiles annotation = declaringClass.getAnnotation(annotationType); + + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation, + declaringClass.getName())); + } + + String[] profiles = annotation.profiles(); + String[] valueProfiles = annotation.value(); + + if (!ObjectUtils.isEmpty(valueProfiles) && !ObjectUtils.isEmpty(profiles)) { + String msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] " + + "and 'profiles' [%s] attributes. Only one declaration of active bean " + + "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(), + ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles)); + logger.error(msg); + throw new IllegalStateException(msg); + } + else if (!ObjectUtils.isEmpty(valueProfiles)) { + profiles = valueProfiles; + } + + for (String profile : profiles) { + if (StringUtils.hasText(profile)) { + activeProfiles.add(profile.trim()); + } + } + + declaringClass = annotation.inheritProfiles() ? findAnnotationDeclaringClass(annotationType, + declaringClass.getSuperclass()) : null; + } + + return StringUtils.toStringArray(activeProfiles); + } + + /** + * Build the {@link MergedContextConfiguration merged context configuration} for + * the supplied {@link Class testClass} and {@code defaultContextLoaderClassName}, + * taking into account context hierarchies declared via + * {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + * @param testClass the test class for which the {@code MergedContextConfiguration} + * should be built (must not be {@code null}) + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use (may be {@code null}) + * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to + * be passed to the {@code MergedContextConfiguration} constructor + * @return the merged context configuration + * @see #buildContextHierarchyMap(Class) + * @see #buildMergedContextConfiguration(Class, List, String, MergedContextConfiguration, CacheAwareContextLoaderDelegate) + */ + @SuppressWarnings("javadoc") + static MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass, + String defaultContextLoaderClassName, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + + if (testClass.isAnnotationPresent(ContextHierarchy.class)) { + Map<String, List<ContextConfigurationAttributes>> hierarchyMap = buildContextHierarchyMap(testClass); + + MergedContextConfiguration parentConfig = null; + MergedContextConfiguration mergedConfig = null; + + for (List<ContextConfigurationAttributes> list : hierarchyMap.values()) { + List<ContextConfigurationAttributes> reversedList = new ArrayList<ContextConfigurationAttributes>(list); + Collections.reverse(reversedList); + + // Don't use the supplied testClass; instead ensure that we are + // building the MCC for the actual test class that declared the + // configuration for the current level in the context hierarchy. + Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty"); + Class<?> declaringClass = reversedList.get(0).getDeclaringClass(); + + mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList, + defaultContextLoaderClassName, parentConfig, cacheAwareContextLoaderDelegate); + parentConfig = mergedConfig; + } + + // Return the last level in the context hierarchy + return mergedConfig; + } + else { + return buildMergedContextConfiguration(testClass, resolveContextConfigurationAttributes(testClass), + defaultContextLoaderClassName, null, cacheAwareContextLoaderDelegate); + } + } + + /** + * Build the {@link MergedContextConfiguration merged context configuration} for the + * supplied {@link Class testClass}, context configuration attributes, + * {@code defaultContextLoaderClassName}, and parent context configuration. + * + * @param testClass the test class for which the {@code MergedContextConfiguration} + * should be built (must not be {@code null}) + * @param configAttributesList the list of context configuration attributes for the + * specified test class, ordered <em>bottom-up</em> (i.e., as if we were + * traversing up the class hierarchy); never {@code null} or empty + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use (may be {@code null}) + * @param parentConfig the merged context configuration for the parent application + * context in a context hierarchy, or {@code null} if there is no parent + * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to + * be passed to the {@code MergedContextConfiguration} constructor + * @return the merged context configuration + * @see #resolveContextLoader + * @see #resolveContextConfigurationAttributes + * @see SmartContextLoader#processContextConfiguration + * @see ContextLoader#processLocations + * @see #resolveActiveProfiles + * @see MergedContextConfiguration + */ + private static MergedContextConfiguration buildMergedContextConfiguration(final Class<?> testClass, + final List<ContextConfigurationAttributes> configAttributesList, + final String defaultContextLoaderClassName, MergedContextConfiguration parentConfig, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + + final ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList, + defaultContextLoaderClassName); + final List<String> locationsList = new ArrayList<String>(); + final List<Class<?>> classesList = new ArrayList<Class<?>>(); + + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Processing locations and classes for context configuration attributes %s", + configAttributes)); + } + + if (contextLoader instanceof SmartContextLoader) { + SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; + smartContextLoader.processContextConfiguration(configAttributes); + locationsList.addAll(0, Arrays.asList(configAttributes.getLocations())); + classesList.addAll(0, Arrays.asList(configAttributes.getClasses())); + } + else { + String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(), + configAttributes.getLocations()); + locationsList.addAll(0, Arrays.asList(processedLocations)); + // Legacy ContextLoaders don't know how to process classes + } + + if (!configAttributes.isInheritLocations()) { + break; + } + } + + String[] locations = StringUtils.toStringArray(locationsList); + Class<?>[] classes = ClassUtils.toClassArray(classesList); + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = resolveInitializerClasses(configAttributesList); + String[] activeProfiles = resolveActiveProfiles(testClass); + + MergedContextConfiguration mergedConfig = buildWebMergedContextConfiguration(testClass, locations, classes, + initializerClasses, activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + + if (mergedConfig == null) { + mergedConfig = new MergedContextConfiguration(testClass, locations, classes, initializerClasses, + activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + } + + return mergedConfig; + } + + /** + * Load the {@link org.springframework.test.context.web.WebAppConfiguration} + * class, using reflection in order to avoid package cycles. + * + * @return the {@code @WebAppConfiguration} class or {@code null} if it cannot be loaded + * @since 3.2 + */ + @SuppressWarnings("unchecked") + private static Class<? extends Annotation> loadWebAppConfigurationClass() { + Class<? extends Annotation> webAppConfigClass = null; + try { + webAppConfigClass = (Class<? extends Annotation>) ClassUtils.forName(WEB_APP_CONFIGURATION_CLASS_NAME, + ContextLoaderUtils.class.getClassLoader()); + } + catch (Throwable t) { + if (logger.isDebugEnabled()) { + logger.debug("Could not load @WebAppConfiguration class [" + WEB_APP_CONFIGURATION_CLASS_NAME + "].", t); + } + } + return webAppConfigClass; + } + + /** + * Attempt to build a {@link org.springframework.test.context.web.WebMergedContextConfiguration} + * from the supplied arguments, using reflection in order to avoid package cycles. + * + * @return the {@code WebMergedContextConfiguration} or {@code null} if it could not be built + * @since 3.2 + */ + @SuppressWarnings("unchecked") + private static MergedContextConfiguration buildWebMergedContextConfiguration( + Class<?> testClass, + String[] locations, + Class<?>[] classes, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses, + String[] activeProfiles, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) { + + Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass(); + + if (webAppConfigClass != null && testClass.isAnnotationPresent(webAppConfigClass)) { + Annotation annotation = testClass.getAnnotation(webAppConfigClass); + String resourceBasePath = (String) AnnotationUtils.getValue(annotation); + + try { + Class<? extends MergedContextConfiguration> webMergedConfigClass = (Class<? extends MergedContextConfiguration>) ClassUtils.forName( + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, ContextLoaderUtils.class.getClassLoader()); + + Constructor<? extends MergedContextConfiguration> constructor = ClassUtils.getConstructorIfAvailable( + webMergedConfigClass, Class.class, String[].class, Class[].class, Set.class, String[].class, + String.class, ContextLoader.class, CacheAwareContextLoaderDelegate.class, + MergedContextConfiguration.class); + + if (constructor != null) { + return instantiateClass(constructor, testClass, locations, classes, initializerClasses, + activeProfiles, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + } + } + catch (Throwable t) { + if (logger.isDebugEnabled()) { + logger.debug("Could not instantiate [" + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME + "].", t); + } + } + } + + return null; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java new file mode 100644 index 00000000..befe85c7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java @@ -0,0 +1,376 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@code MergedContextConfiguration} encapsulates the <em>merged</em> + * context configuration declared on a test class and all of its superclasses + * via {@link ContextConfiguration @ContextConfiguration} and + * {@link ActiveProfiles @ActiveProfiles}. + * + * <p>Merged resource locations, annotated classes, and active profiles + * represent all declared values in the test class hierarchy taking into + * consideration the semantics of the + * {@link ContextConfiguration#inheritLocations inheritLocations} and + * {@link ActiveProfiles#inheritProfiles inheritProfiles} flags in + * {@code @ContextConfiguration} and {@code @ActiveProfiles}, respectively. + * + * <p>A {@link SmartContextLoader} uses {@code MergedContextConfiguration} + * to load an {@link org.springframework.context.ApplicationContext ApplicationContext}. + * + * <p>{@code MergedContextConfiguration} is also used by the {@link TestContext} + * as the context cache key for caching an + * {@link org.springframework.context.ApplicationContext ApplicationContext} + * that was loaded using properties of this {@code MergedContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + * @see ContextConfiguration + * @see ActiveProfiles + * @see ContextConfigurationAttributes + * @see SmartContextLoader#loadContext(MergedContextConfiguration) + */ +public class MergedContextConfiguration implements Serializable { + + private static final long serialVersionUID = -3290560718464957422L; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0]; + private static final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> EMPTY_INITIALIZER_CLASSES = // + Collections.<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> emptySet(); + + private final Class<?> testClass; + private final String[] locations; + private final Class<?>[] classes; + private final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses; + private final String[] activeProfiles; + private final ContextLoader contextLoader; + private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; + private final MergedContextConfiguration parent; + + + private static String[] processLocations(String[] locations) { + return locations == null ? EMPTY_STRING_ARRAY : locations; + } + + private static Class<?>[] processClasses(Class<?>[] classes) { + return classes == null ? EMPTY_CLASS_ARRAY : classes; + } + + private static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> processContextInitializerClasses( + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses) { + return contextInitializerClasses == null ? EMPTY_INITIALIZER_CLASSES + : Collections.unmodifiableSet(contextInitializerClasses); + } + + private static String[] processActiveProfiles(String[] activeProfiles) { + if (activeProfiles == null) { + return EMPTY_STRING_ARRAY; + } + + // Active profiles must be unique and sorted in order to support proper + // cache key generation. Specifically, profile sets {foo,bar} and + // {bar,foo} must both result in the same array (e.g., [bar,foo]). + SortedSet<String> sortedProfilesSet = new TreeSet<String>(Arrays.asList(activeProfiles)); + return StringUtils.toStringArray(sortedProfilesSet); + } + + /** + * Generate a null-safe {@link String} representation of the supplied + * {@link ContextLoader} based solely on the fully qualified name of the + * loader or "null" if the supplied loaded is {@code null}. + */ + protected static String nullSafeToString(ContextLoader contextLoader) { + return contextLoader == null ? "null" : contextLoader.getClass().getName(); + } + + /** + * Create a new {@code MergedContextConfiguration} instance for the + * supplied test class, resource locations, annotated classes, active + * profiles, and {@code ContextLoader}. + * + * <p>If a {@code null} value is supplied for {@code locations}, + * {@code classes}, or {@code activeProfiles} an empty array will + * be stored instead. Furthermore, active profiles will be sorted, and duplicate + * profiles will be removed. + * + * @param testClass the test class for which the configuration was merged + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param activeProfiles the merged active bean definition profiles + * @param contextLoader the resolved {@code ContextLoader} + * @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader) + */ + public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes, + String[] activeProfiles, ContextLoader contextLoader) { + this(testClass, locations, classes, null, activeProfiles, contextLoader); + } + + /** + * Create a new {@code MergedContextConfiguration} instance for the + * supplied test class, resource locations, annotated classes, context + * initializers, active profiles, and {@code ContextLoader}. + * + * <p>If a {@code null} value is supplied for {@code locations}, + * {@code classes}, or {@code activeProfiles} an empty array will + * be stored instead. If a {@code null} value is supplied for the + * {@code contextInitializerClasses} an empty set will be stored instead. + * Furthermore, active profiles will be sorted, and duplicate profiles will + * be removed. + * + * @param testClass the test class for which the configuration was merged + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param contextLoader the resolved {@code ContextLoader} + * @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration) + */ + public MergedContextConfiguration( + Class<?> testClass, + String[] locations, + Class<?>[] classes, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses, + String[] activeProfiles, ContextLoader contextLoader) { + this(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader, null, null); + } + + /** + * Create a new {@code MergedContextConfiguration} instance for the + * supplied test class, resource locations, annotated classes, context + * initializers, active profiles, {@code ContextLoader}, and parent + * configuration. + * + * <p>If a {@code null} value is supplied for {@code locations}, + * {@code classes}, or {@code activeProfiles} an empty array will + * be stored instead. If a {@code null} value is supplied for the + * {@code contextInitializerClasses} an empty set will be stored instead. + * Furthermore, active profiles will be sorted, and duplicate profiles will + * be removed. + * + * @param testClass the test class for which the configuration was merged + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate a cache-aware context loader + * delegate with which to retrieve the parent context + * @param parent the parent configuration or {@code null} if there is no parent + * @since 3.2.2 + */ + public MergedContextConfiguration( + Class<?> testClass, + String[] locations, + Class<?>[] classes, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses, + String[] activeProfiles, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) { + this.testClass = testClass; + this.locations = processLocations(locations); + this.classes = processClasses(classes); + this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses); + this.activeProfiles = processActiveProfiles(activeProfiles); + this.contextLoader = contextLoader; + this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate; + this.parent = parent; + } + + /** + * Get the {@linkplain Class test class} associated with this {@code MergedContextConfiguration}. + */ + public Class<?> getTestClass() { + return testClass; + } + + /** + * Get the merged resource locations for the {@linkplain #getTestClass() test class}. + */ + public String[] getLocations() { + return locations; + } + + /** + * Get the merged annotated classes for the {@linkplain #getTestClass() test class}. + */ + public Class<?>[] getClasses() { + return classes; + } + + /** + * Get the merged {@code ApplicationContextInitializer} classes for the + * {@linkplain #getTestClass() test class}. + */ + public Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> getContextInitializerClasses() { + return contextInitializerClasses; + } + + /** + * Get the merged active bean definition profiles for the {@linkplain #getTestClass() test class}. + */ + public String[] getActiveProfiles() { + return activeProfiles; + } + + /** + * Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}. + */ + public ContextLoader getContextLoader() { + return contextLoader; + } + + /** + * Get the {@link MergedContextConfiguration} for the parent application context in a + * context hierarchy. + * + * @return the parent configuration or {@code null} if there is no parent + * @see #getParentApplicationContext() + * @since 3.2.2 + */ + public MergedContextConfiguration getParent() { + return this.parent; + } + + /** + * Get the parent {@link ApplicationContext} for the context defined by this + * {@code MergedContextConfiguration} from the context cache. + * <p> + * If the parent context has not yet been loaded, it will be loaded, stored in the + * cache, and then returned. + * + * @return the parent {@code ApplicationContext} or {@code null} if there is no parent + * @see #getParent() + * @since 3.2.2 + */ + public ApplicationContext getParentApplicationContext() { + if (parent == null) { + return null; + } + + Assert.state(cacheAwareContextLoaderDelegate != null, + "Cannot retrieve a parent application context without access to the CacheAwareContextLoaderDelegate."); + return cacheAwareContextLoaderDelegate.loadContext(parent); + } + + /** + * Generate a unique hash code for all properties of this + * {@code MergedContextConfiguration} excluding the + * {@linkplain #getTestClass() test class}. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(locations); + result = prime * result + Arrays.hashCode(classes); + result = prime * result + contextInitializerClasses.hashCode(); + result = prime * result + Arrays.hashCode(activeProfiles); + result = prime * result + (parent == null ? 0 : parent.hashCode()); + result = prime * result + nullSafeToString(contextLoader).hashCode(); + return result; + } + + /** + * Determine if the supplied object is equal to this {@code MergedContextConfiguration} + * instance by comparing both object's {@linkplain #getLocations() locations}, + * {@linkplain #getClasses() annotated classes}, + * {@linkplain #getContextInitializerClasses() context initializer classes}, + * {@linkplain #getActiveProfiles() active profiles}, + * {@linkplain #getParent() parents}, and the fully qualified names of their + * {@link #getContextLoader() ContextLoaders}. + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (!(obj instanceof MergedContextConfiguration)) { + return false; + } + + final MergedContextConfiguration that = (MergedContextConfiguration) obj; + + if (!Arrays.equals(this.locations, that.locations)) { + return false; + } + + if (!Arrays.equals(this.classes, that.classes)) { + return false; + } + + if (!this.contextInitializerClasses.equals(that.contextInitializerClasses)) { + return false; + } + + if (!Arrays.equals(this.activeProfiles, that.activeProfiles)) { + return false; + } + + if (this.parent == null) { + if (that.parent != null) { + return false; + } + } + else if (!this.parent.equals(that.parent)) { + return false; + } + + if (!nullSafeToString(this.contextLoader).equals(nullSafeToString(that.contextLoader))) { + return false; + } + + return true; + } + + /** + * Provide a String representation of the {@linkplain #getTestClass() test class}, + * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes}, + * {@linkplain #getContextInitializerClasses() context initializer classes}, + * {@linkplain #getActiveProfiles() active profiles}, the name of the + * {@link #getContextLoader() ContextLoader}, and the + * {@linkplain #getParent() parent configuration}. + */ + @Override + public String toString() { + return new ToStringCreator(this)// + .append("testClass", testClass)// + .append("locations", ObjectUtils.nullSafeToString(locations))// + .append("classes", ObjectUtils.nullSafeToString(classes))// + .append("contextInitializerClasses", ObjectUtils.nullSafeToString(contextInitializerClasses))// + .append("activeProfiles", ObjectUtils.nullSafeToString(activeProfiles))// + .append("contextLoader", nullSafeToString(contextLoader))// + .append("parent", parent)// + .toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java new file mode 100644 index 00000000..d0495ca0 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import org.springframework.context.ApplicationContext; + +/** + * Strategy interface for loading an {@link ApplicationContext application context} + * for an integration test managed by the Spring TestContext Framework. + * + * <p>The {@code SmartContextLoader} SPI supersedes the {@link ContextLoader} SPI + * introduced in Spring 2.5: a {@code SmartContextLoader} can choose to process + * either resource locations or annotated classes. Furthermore, a + * {@code SmartContextLoader} can set active bean definition profiles in the + * context that it loads (see {@link MergedContextConfiguration#getActiveProfiles()} + * and {@link #loadContext(MergedContextConfiguration)}). + * + * <p>See the Javadoc for {@link ContextConfiguration @ContextConfiguration} + * for a definition of <em>annotated class</em>. + * + * <p>Clients of a {@code SmartContextLoader} should call + * {@link #processContextConfiguration(ContextConfigurationAttributes) + * processContextConfiguration()} prior to calling + * {@link #loadContext(MergedContextConfiguration) loadContext()}. This gives a + * {@code SmartContextLoader} the opportunity to provide custom support for + * modifying resource locations or detecting default resource locations or + * default configuration classes. The results of + * {@link #processContextConfiguration(ContextConfigurationAttributes) + * processContextConfiguration()} should be merged for all classes in the + * hierarchy of the root test class and then supplied to + * {@link #loadContext(MergedContextConfiguration) loadContext()}. + * + * <p>Even though {@code SmartContextLoader} extends {@code ContextLoader}, + * clients should favor {@code SmartContextLoader}-specific methods over those + * defined in {@code ContextLoader}, particularly because a + * {@code SmartContextLoader} may choose not to support methods defined in the + * {@code ContextLoader} SPI. + * + * <p>Concrete implementations must provide a {@code public} no-args constructor. + * + * <p>Spring provides the following out-of-the-box implementations: + * <ul> + * <li>{@link org.springframework.test.context.support.DelegatingSmartContextLoader DelegatingSmartContextLoader}</li> + * <li>{@link org.springframework.test.context.support.AnnotationConfigContextLoader AnnotationConfigContextLoader}</li> + * <li>{@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader}</li> + * <li>{@link org.springframework.test.context.support.GenericPropertiesContextLoader GenericPropertiesContextLoader}</li> + * <li>{@link org.springframework.test.context.web.WebDelegatingSmartContextLoader WebDelegatingSmartContextLoader}</li> + * <li>{@link org.springframework.test.context.web.AnnotationConfigWebContextLoader AnnotationConfigWebContextLoader}</li> + * <li>{@link org.springframework.test.context.web.GenericXmlWebContextLoader GenericXmlWebContextLoader}</li> + * </ul> + * + * @author Sam Brannen + * @since 3.1 + * @see ContextConfiguration + * @see ActiveProfiles + * @see ContextConfigurationAttributes + * @see MergedContextConfiguration + */ +public interface SmartContextLoader extends ContextLoader { + + /** + * Processes the {@link ContextConfigurationAttributes} for a given test class. + * <p>Concrete implementations may choose to <em>modify</em> the {@code locations} + * or {@code classes} in the supplied {@link ContextConfigurationAttributes}, + * <em>generate</em> default configuration locations, or <em>detect</em> + * default configuration classes if the supplied values are {@code null} + * or empty. + * <p><b>Note</b>: in contrast to a standard {@code ContextLoader}, a + * {@code SmartContextLoader} <b>must</b> <em>preemptively</em> verify that + * a generated or detected default actually exists before setting the corresponding + * {@code locations} or {@code classes} property in the supplied + * {@link ContextConfigurationAttributes}. Consequently, leaving the + * {@code locations} or {@code classes} property empty signals that + * this {@code SmartContextLoader} was not able to generate or detect defaults. + * @param configAttributes the context configuration attributes to process + */ + void processContextConfiguration(ContextConfigurationAttributes configAttributes); + + /** + * Loads a new {@link ApplicationContext context} based on the supplied + * {@link MergedContextConfiguration merged context configuration}, + * configures the context, and finally returns the context in a fully + * <em>refreshed</em> state. + * <p>Concrete implementations should register annotation configuration + * processors with bean factories of + * {@link ApplicationContext application contexts} loaded by this + * {@code SmartContextLoader}. Beans will therefore automatically be + * candidates for annotation-based dependency injection using + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired}, + * {@link javax.annotation.Resource @Resource}, and + * {@link javax.inject.Inject @Inject}. In addition, concrete implementations + * should set the active bean definition profiles in the context's + * {@link org.springframework.core.env.Environment Environment}. + * <p>Any {@code ApplicationContext} loaded by a + * {@code SmartContextLoader} <strong>must</strong> register a JVM + * shutdown hook for itself. Unless the context gets closed early, all context + * instances will be automatically closed on JVM shutdown. This allows for + * freeing of external resources held by beans within the context (e.g., + * temporary files). + * @param mergedConfig the merged context configuration to use to load the + * application context + * @return a new application context + * @throws Exception if context loading failed + * @see #processContextConfiguration(ContextConfigurationAttributes) + * @see org.springframework.context.annotation.AnnotationConfigUtils + * #registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry) + * @see MergedContextConfiguration#getActiveProfiles() + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment() + */ + ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java new file mode 100644 index 00000000..9324340f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2014 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 + * + * http://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; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.core.AttributeAccessorSupport; +import org.springframework.core.style.ToStringCreator; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.util.Assert; + +/** + * {@code TestContext} encapsulates the context in which a test is executed, + * agnostic of the actual testing framework in use. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + */ +public class TestContext extends AttributeAccessorSupport { + + private static final long serialVersionUID = -5827157174866681233L; + + private static final Log logger = LogFactory.getLog(TestContext.class); + + private final ContextCache contextCache; + + private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; + + private final MergedContextConfiguration mergedContextConfiguration; + + private final Class<?> testClass; + + private Object testInstance; + + private Method testMethod; + + private Throwable testException; + + + /** + * Delegates to {@link #TestContext(Class, ContextCache, String)} with a + * value of {@code null} for the default {@code ContextLoader} class name. + */ + TestContext(Class<?> testClass, ContextCache contextCache) { + this(testClass, contextCache, null); + } + + /** + * Construct a new test context for the supplied {@linkplain Class test class} + * and {@linkplain ContextCache context cache} and parse the corresponding + * {@link ContextConfiguration @ContextConfiguration} or + * {@link ContextHierarchy @ContextHierarchy} annotation, if present. + * <p>If the supplied class name for the default {@code ContextLoader} + * is {@code null} or <em>empty</em> and no concrete {@code ContextLoader} + * class is explicitly supplied via {@code @ContextConfiguration}, a + * {@link org.springframework.test.context.support.DelegatingSmartContextLoader + * DelegatingSmartContextLoader} or + * {@link org.springframework.test.context.web.WebDelegatingSmartContextLoader + * WebDelegatingSmartContextLoader} will be used instead. + * @param testClass the test class for which the test context should be + * constructed (must not be {@code null}) + * @param contextCache the context cache from which the constructed test + * context should retrieve application contexts (must not be {@code null}) + * @param defaultContextLoaderClassName the name of the default + * {@code ContextLoader} class to use (may be {@code null}) + */ + TestContext(Class<?> testClass, ContextCache contextCache, String defaultContextLoaderClassName) { + Assert.notNull(testClass, "Test class must not be null"); + Assert.notNull(contextCache, "ContextCache must not be null"); + + this.testClass = testClass; + this.contextCache = contextCache; + this.cacheAwareContextLoaderDelegate = new CacheAwareContextLoaderDelegate(contextCache); + + MergedContextConfiguration mergedContextConfiguration; + + if (testClass.isAnnotationPresent(ContextConfiguration.class) || + testClass.isAnnotationPresent(ContextHierarchy.class)) { + mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass, + defaultContextLoaderClassName, cacheAwareContextLoaderDelegate); + } + else { + if (logger.isInfoEnabled()) { + logger.info(String.format( + "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]", + testClass.getName())); + } + mergedContextConfiguration = new MergedContextConfiguration(testClass, null, null, null, null); + } + + this.mergedContextConfiguration = mergedContextConfiguration; + } + + + /** + * Get the {@link ApplicationContext application context} for this test + * context, possibly cached. + * @return the application context + * @throws IllegalStateException if an error occurs while retrieving the + * application context + */ + public ApplicationContext getApplicationContext() { + return this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration); + } + + /** + * Get the {@link Class test class} for this test context. + * @return the test class (never {@code null}) + */ + public Class<?> getTestClass() { + return this.testClass; + } + + /** + * Get the current {@link Object test instance} for this test context. + * <p>Note: this is a mutable property. + * @return the current test instance (may be {@code null}) + * @see #updateState(Object, Method, Throwable) + */ + public Object getTestInstance() { + return this.testInstance; + } + + /** + * Get the current {@link Method test method} for this test context. + * <p>Note: this is a mutable property. + * @return the current test method (may be {@code null}) + * @see #updateState(Object, Method, Throwable) + */ + public Method getTestMethod() { + return this.testMethod; + } + + /** + * Get the {@link Throwable exception} that was thrown during execution of + * the {@link #getTestMethod() test method}. + * <p>Note: this is a mutable property. + * @return the exception that was thrown, or {@code null} if no + * exception was thrown + * @see #updateState(Object, Method, Throwable) + */ + public Throwable getTestException() { + return this.testException; + } + + /** + * Call this method to signal that the {@linkplain ApplicationContext application + * context} associated with this test context is <em>dirty</em> and should be + * discarded. Do this if a test has modified the context — for example, + * by replacing a bean definition or modifying the state of a singleton bean. + * @deprecated as of Spring 3.2.2; use + * {@link #markApplicationContextDirty(DirtiesContext.HierarchyMode)} instead. + */ + @Deprecated + public void markApplicationContextDirty() { + markApplicationContextDirty((HierarchyMode) null); + } + + /** + * Call this method to signal that the {@linkplain ApplicationContext application + * context} associated with this test context is <em>dirty</em> and should be + * discarded. Do this if a test has modified the context — for example, + * by replacing a bean definition or modifying the state of a singleton bean. + * @param hierarchyMode the context cache clearing mode to be applied if the + * context is part of a hierarchy (may be {@code null}) + */ + public void markApplicationContextDirty(HierarchyMode hierarchyMode) { + synchronized (this.contextCache) { + this.contextCache.remove(this.mergedContextConfiguration, hierarchyMode); + } + } + + /** + * Update this test context to reflect the state of the currently executing test. + * @param testInstance the current test instance (may be {@code null}) + * @param testMethod the current test method (may be {@code null}) + * @param testException the exception that was thrown in the test method, or + * {@code null} if no exception was thrown + */ + void updateState(Object testInstance, Method testMethod, Throwable testException) { + this.testInstance = testInstance; + this.testMethod = testMethod; + this.testException = testException; + } + + + /** + * Provide a String representation of this test context's state. + */ + @Override + public String toString() { + return new ToStringCreator(this) + .append("testClass", this.testClass) + .append("testInstance", this.testInstance) + .append("testMethod", this.testMethod) + .append("testException", this.testException) + .append("mergedContextConfiguration", this.mergedContextConfiguration) + .toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java new file mode 100644 index 00000000..4cf8b13f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -0,0 +1,463 @@ +/* + * Copyright 2002-2014 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 + * + * http://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; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * {@code TestContextManager} is the main entry point into the <em>Spring + * TestContext Framework</em>, which provides support for loading and accessing + * {@link org.springframework.context.ApplicationContext application contexts}, + * dependency injection of test instances, + * {@link org.springframework.transaction.annotation.Transactional transactional} + * execution of test methods, etc. + * + * <p>Specifically, a {@code TestContextManager} is responsible for managing a + * single {@link TestContext} and signaling events to all registered + * {@link TestExecutionListener TestExecutionListeners} at well defined test + * execution points: + * + * <ul> + * <li>{@link #beforeTestClass() before test class execution}: prior to any + * <em>before class methods</em> of a particular testing framework (e.g., JUnit + * 4's {@link org.junit.BeforeClass @BeforeClass})</li> + * <li>{@link #prepareTestInstance(Object) test instance preparation}: + * immediately following instantiation of the test instance</li> + * <li>{@link #beforeTestMethod(Object, Method) before test method execution}: + * prior to any <em>before methods</em> of a particular testing framework (e.g., + * JUnit 4's {@link org.junit.Before @Before})</li> + * <li>{@link #afterTestMethod(Object, Method, Throwable) after test method + * execution}: after any <em>after methods</em> of a particular testing + * framework (e.g., JUnit 4's {@link org.junit.After @After})</li> + * <li>{@link #afterTestClass() after test class execution}: after any + * <em>after class methods</em> of a particular testing framework (e.g., JUnit + * 4's {@link org.junit.AfterClass @AfterClass})</li> + * </ul> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see TestContext + * @see TestExecutionListeners + * @see ContextConfiguration + * @see org.springframework.test.context.transaction.TransactionConfiguration + */ +public class TestContextManager { + + private static final String[] DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES = new String[] { + "org.springframework.test.context.web.ServletTestExecutionListener", + "org.springframework.test.context.support.DependencyInjectionTestExecutionListener", + "org.springframework.test.context.support.DirtiesContextTestExecutionListener", + "org.springframework.test.context.transaction.TransactionalTestExecutionListener" }; + + private static final Log logger = LogFactory.getLog(TestContextManager.class); + + /** + * Cache of Spring application contexts. + * <p>This needs to be static, since test instances may be destroyed and + * recreated between invocations of individual test methods, as is the case + * with JUnit. + */ + static final ContextCache contextCache = new ContextCache(); + + private final TestContext testContext; + + private final List<TestExecutionListener> testExecutionListeners = new ArrayList<TestExecutionListener>(); + + + /** + * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class} + * and automatically {@link #registerTestExecutionListeners register} the + * {@link TestExecutionListener TestExecutionListeners} configured for the test class + * via the {@link TestExecutionListeners @TestExecutionListeners} annotation. + * <p>Delegates to {@link #TestContextManager(Class, String)} with a value of + * {@code null} for the default {@code ContextLoader} class name. + */ + public TestContextManager(Class<?> testClass) { + this(testClass, null); + } + + /** + * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class} + * and automatically {@link #registerTestExecutionListeners register} the + * {@link TestExecutionListener TestExecutionListeners} configured for the test class + * via the {@link TestExecutionListeners @TestExecutionListeners} annotation. + * @param testClass the test class to be managed + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} class + * to use (may be {@code null}) + * @see #registerTestExecutionListeners + */ + public TestContextManager(Class<?> testClass, String defaultContextLoaderClassName) { + this.testContext = new TestContext(testClass, contextCache, defaultContextLoaderClassName); + registerTestExecutionListeners(retrieveTestExecutionListeners(testClass)); + } + + + /** + * Get the {@link TestContext} managed by this {@code TestContextManager}. + */ + protected final TestContext getTestContext() { + return this.testContext; + } + + /** + * Register the supplied {@link TestExecutionListener TestExecutionListeners} + * by appending them to the set of listeners used by this {@code TestContextManager}. + */ + public void registerTestExecutionListeners(TestExecutionListener... testExecutionListeners) { + for (TestExecutionListener listener : testExecutionListeners) { + if (logger.isTraceEnabled()) { + logger.trace("Registering TestExecutionListener: " + listener); + } + this.testExecutionListeners.add(listener); + } + } + + /** + * Get the current {@link TestExecutionListener TestExecutionListeners} + * registered for this {@code TestContextManager}. + * <p>Allows for modifications, e.g. adding a listener to the beginning of the list. + * However, make sure to keep the list stable while actually executing tests. + */ + public final List<TestExecutionListener> getTestExecutionListeners() { + return this.testExecutionListeners; + } + + /** + * Get a copy of the {@link TestExecutionListener TestExecutionListeners} + * registered for this {@code TestContextManager} in reverse order. + */ + private List<TestExecutionListener> getReversedTestExecutionListeners() { + List<TestExecutionListener> listenersReversed = new ArrayList<TestExecutionListener>(getTestExecutionListeners()); + Collections.reverse(listenersReversed); + return listenersReversed; + } + + /** + * Retrieve an array of newly instantiated {@link TestExecutionListener TestExecutionListeners} + * for the specified {@link Class class}. If {@link TestExecutionListeners @TestExecutionListeners} + * is not <em>present</em> on the supplied class, the default listeners will be returned. + * <p>Note that the {@link TestExecutionListeners#inheritListeners() inheritListeners} flag of + * {@link TestExecutionListeners @TestExecutionListeners} will be taken into consideration. + * Specifically, if the {@code inheritListeners} flag is set to {@code true}, listeners + * defined in the annotated class will be appended to the listeners defined in superclasses. + * @param clazz the test class for which the listeners should be retrieved + * @return an array of TestExecutionListeners for the specified class + */ + private TestExecutionListener[] retrieveTestExecutionListeners(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + Class<TestExecutionListeners> annotationType = TestExecutionListeners.class; + List<Class<? extends TestExecutionListener>> classesList = new ArrayList<Class<? extends TestExecutionListener>>(); + Class<?> declaringClass = AnnotationUtils.findAnnotationDeclaringClass(annotationType, clazz); + + // Use defaults? + if (declaringClass == null) { + if (logger.isDebugEnabled()) { + logger.debug("@TestExecutionListeners is not present for class [" + clazz.getName() + "]: using defaults."); + } + classesList.addAll(getDefaultTestExecutionListenerClasses()); + } + else { + // Traverse the class hierarchy... + while (declaringClass != null) { + TestExecutionListeners testExecutionListeners = declaringClass.getAnnotation(annotationType); + if (logger.isTraceEnabled()) { + logger.trace("Retrieved @TestExecutionListeners [" + testExecutionListeners + + "] for declaring class [" + declaringClass.getName() + "]."); + } + + Class<? extends TestExecutionListener>[] valueListenerClasses = testExecutionListeners.value(); + Class<? extends TestExecutionListener>[] listenerClasses = testExecutionListeners.listeners(); + if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) { + throw new IllegalStateException(String.format("Class [%s] configured with @TestExecutionListeners' " + + "'value' [%s] and 'listeners' [%s] attributes. Use one or the other, but not both.", + declaringClass.getName(), ObjectUtils.nullSafeToString(valueListenerClasses), + ObjectUtils.nullSafeToString(listenerClasses))); + } + else if (!ObjectUtils.isEmpty(valueListenerClasses)) { + listenerClasses = valueListenerClasses; + } + + if (listenerClasses != null) { + classesList.addAll(0, Arrays.<Class<? extends TestExecutionListener>> asList(listenerClasses)); + } + declaringClass = (testExecutionListeners.inheritListeners() ? AnnotationUtils.findAnnotationDeclaringClass( + annotationType, declaringClass.getSuperclass()) : null); + } + } + + List<TestExecutionListener> listeners = new ArrayList<TestExecutionListener>(classesList.size()); + for (Class<? extends TestExecutionListener> listenerClass : classesList) { + NoClassDefFoundError ncdfe = null; + try { + listeners.add(BeanUtils.instantiateClass(listenerClass)); + } + catch (NoClassDefFoundError err) { + ncdfe = err; + } + catch (BeanInstantiationException ex) { + if (ex.getCause() instanceof NoClassDefFoundError) { + ncdfe = (NoClassDefFoundError) ex.getCause(); + } + } + if (ncdfe != null) { + if (logger.isInfoEnabled()) { + logger.info(String.format("Could not instantiate TestExecutionListener [%s]. " + + "Specify custom listener classes or make the default listener classes " + + "(and their required dependencies) available. Offending class: [%s]", + listenerClass.getName(), ncdfe.getMessage())); + } + } + } + return listeners.toArray(new TestExecutionListener[listeners.size()]); + } + + /** + * Determine the default {@link TestExecutionListener} classes. + */ + @SuppressWarnings("unchecked") + protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() { + Set<Class<? extends TestExecutionListener>> defaultListenerClasses = new LinkedHashSet<Class<? extends TestExecutionListener>>(); + ClassLoader cl = getClass().getClassLoader(); + for (String className : DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES) { + try { + defaultListenerClasses.add((Class<? extends TestExecutionListener>) ClassUtils.forName(className, cl)); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not load default TestExecutionListener class [" + className + + "]. Specify custom listener classes or make the default listener classes available.", ex); + } + } + } + return defaultListenerClasses; + } + + /** + * Hook for pre-processing a test class <em>before</em> execution of any + * tests within the class. Should be called prior to any framework-specific + * <em>before class methods</em> (e.g., methods annotated with JUnit's + * {@link org.junit.BeforeClass @BeforeClass}). + * <p>An attempt will be made to give each registered + * {@link TestExecutionListener} a chance to pre-process the test class + * execution. If a listener throws an exception, however, the remaining + * registered listeners will <strong>not</strong> be called. + * @throws Exception if a registered TestExecutionListener throws an + * exception + * @see #getTestExecutionListeners() + */ + public void beforeTestClass() throws Exception { + Class<?> testClass = getTestContext().getTestClass(); + if (logger.isTraceEnabled()) { + logger.trace("beforeTestClass(): class [" + testClass.getName() + "]"); + } + getTestContext().updateState(null, null, null); + + for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { + try { + testExecutionListener.beforeTestClass(getTestContext()); + } + catch (Exception ex) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'before class' callback for test class [" + testClass + "]", ex); + throw ex; + } + } + } + + /** + * Hook for preparing a test instance prior to execution of any individual + * test methods, for example for injecting dependencies, etc. Should be + * called immediately after instantiation of the test instance. + * <p>The managed {@link TestContext} will be updated with the supplied + * {@code testInstance}. + * <p>An attempt will be made to give each registered + * {@link TestExecutionListener} a chance to prepare the test instance. If a + * listener throws an exception, however, the remaining registered listeners + * will <strong>not</strong> be called. + * @param testInstance the test instance to prepare (never {@code null}) + * @throws Exception if a registered TestExecutionListener throws an exception + * @see #getTestExecutionListeners() + */ + public void prepareTestInstance(Object testInstance) throws Exception { + Assert.notNull(testInstance, "testInstance must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("prepareTestInstance(): instance [" + testInstance + "]"); + } + getTestContext().updateState(testInstance, null, null); + + for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { + try { + testExecutionListener.prepareTestInstance(getTestContext()); + } + catch (Exception ex) { + logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to prepare test instance [" + testInstance + "]", ex); + throw ex; + } + } + } + + /** + * Hook for pre-processing a test <em>before</em> execution of the supplied + * {@link Method test method}, for example for setting up test fixtures, + * starting a transaction, etc. Should be called prior to any + * framework-specific <em>before methods</em> (e.g., methods annotated with + * JUnit's {@link org.junit.Before @Before}). + * <p>The managed {@link TestContext} will be updated with the supplied + * {@code testInstance} and {@code testMethod}. + * <p>An attempt will be made to give each registered + * {@link TestExecutionListener} a chance to pre-process the test method + * execution. If a listener throws an exception, however, the remaining + * registered listeners will <strong>not</strong> be called. + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which is about to be executed on the + * test instance + * @throws Exception if a registered TestExecutionListener throws an exception + * @see #getTestExecutionListeners() + */ + public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception { + Assert.notNull(testInstance, "Test instance must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("beforeTestMethod(): instance [" + testInstance + "], method [" + testMethod + "]"); + } + getTestContext().updateState(testInstance, testMethod, null); + + for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { + try { + testExecutionListener.beforeTestMethod(getTestContext()); + } + catch (Exception ex) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'before' execution of test method [" + testMethod + "] for test instance [" + + testInstance + "]", ex); + throw ex; + } + } + } + + /** + * Hook for post-processing a test <em>after</em> execution of the supplied + * {@link Method test method}, for example for tearing down test fixtures, + * ending a transaction, etc. Should be called after any framework-specific + * <em>after methods</em> (e.g., methods annotated with JUnit's + * {@link org.junit.After @After}). + * <p>The managed {@link TestContext} will be updated with the supplied + * {@code testInstance}, {@code testMethod}, and + * {@code exception}. + * <p>Each registered {@link TestExecutionListener} will be given a chance to + * post-process the test method execution. If a listener throws an + * exception, the remaining registered listeners will still be called, but + * the first exception thrown will be tracked and rethrown after all + * listeners have executed. Note that registered listeners will be executed + * in the opposite order in which they were registered. + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which has just been executed on the + * test instance + * @param exception the exception that was thrown during execution of the + * test method or by a TestExecutionListener, or {@code null} if none + * was thrown + * @throws Exception if a registered TestExecutionListener throws an exception + * @see #getTestExecutionListeners() + */ + public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception { + Assert.notNull(testInstance, "testInstance must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod + + "], exception [" + exception + "]"); + } + getTestContext().updateState(testInstance, testMethod, exception); + + Exception afterTestMethodException = null; + // Traverse the TestExecutionListeners in reverse order to ensure proper + // "wrapper"-style execution of listeners. + for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { + try { + testExecutionListener.afterTestMethod(getTestContext()); + } + catch (Exception ex) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'after' execution for test: method [" + testMethod + "], instance [" + + testInstance + "], exception [" + exception + "]", ex); + if (afterTestMethodException == null) { + afterTestMethodException = ex; + } + } + } + if (afterTestMethodException != null) { + throw afterTestMethodException; + } + } + + /** + * Hook for post-processing a test class <em>after</em> execution of all + * tests within the class. Should be called after any framework-specific + * <em>after class methods</em> (e.g., methods annotated with JUnit's + * {@link org.junit.AfterClass @AfterClass}). + * <p>Each registered {@link TestExecutionListener} will be given a chance to + * post-process the test class. If a listener throws an exception, the + * remaining registered listeners will still be called, but the first + * exception thrown will be tracked and rethrown after all listeners have + * executed. Note that registered listeners will be executed in the opposite + * order in which they were registered. + * @throws Exception if a registered TestExecutionListener throws an exception + * @see #getTestExecutionListeners() + */ + public void afterTestClass() throws Exception { + Class<?> testClass = getTestContext().getTestClass(); + if (logger.isTraceEnabled()) { + logger.trace("afterTestClass(): class [" + testClass.getName() + "]"); + } + getTestContext().updateState(null, null, null); + + Exception afterTestClassException = null; + // Traverse the TestExecutionListeners in reverse order to ensure proper + // "wrapper"-style execution of listeners. + for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { + try { + testExecutionListener.afterTestClass(getTestContext()); + } + catch (Exception ex) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'after class' callback for test class [" + testClass + "]", ex); + if (afterTestClassException == null) { + afterTestClassException = ex; + } + } + } + if (afterTestClassException != null) { + throw afterTestClassException; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java new file mode 100644 index 00000000..a052523f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +/** + * {@code TestExecutionListener} defines a <em>listener</em> API for reacting to + * test execution events published by the {@link TestContextManager} with which + * the listener is registered. + * <p> + * Concrete implementations must provide a {@code public} no-args constructor, + * so that listeners can be instantiated transparently by tools and configuration + * mechanisms. + * <p> + * Spring provides the following out-of-the-box implementations: + * <ul> + * <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener + * DependencyInjectionTestExecutionListener}</li> + * <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener + * DirtiesContextTestExecutionListener}</li> + * <li>{@link org.springframework.test.context.transaction.TransactionalTestExecutionListener + * TransactionalTestExecutionListener}</li> + * <li>{@link org.springframework.test.context.web.ServletTestExecutionListener + * ServletTestExecutionListener}</li> + * </ul> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + */ +public interface TestExecutionListener { + + /** + * Pre-processes a test class <em>before</em> execution of all tests within + * the class. + * <p> + * This method should be called immediately before framework-specific + * <em>before class</em> lifecycle callbacks. + * <p> + * If a given testing framework (e.g., JUnit 3.8) does not support + * <em>before class</em> lifecycle callbacks, this method will not be called + * for that framework. + * + * @param testContext the test context for the test; never {@code null} + * @throws Exception allows any exception to propagate + */ + void beforeTestClass(TestContext testContext) throws Exception; + + /** + * Prepares the {@link Object test instance} of the supplied + * {@link TestContext test context}, for example by injecting dependencies. + * <p> + * This method should be called immediately after instantiation of the test + * instance but prior to any framework-specific lifecycle callbacks. + * + * @param testContext the test context for the test; never {@code null} + * @throws Exception allows any exception to propagate + */ + void prepareTestInstance(TestContext testContext) throws Exception; + + /** + * Pre-processes a test <em>before</em> execution of the + * {@link java.lang.reflect.Method test method} in the supplied + * {@link TestContext test context}, for example by setting up test + * fixtures. + * <p> + * This method should be called immediately prior to framework-specific + * <em>before</em> lifecycle callbacks. + * + * @param testContext the test context in which the test method will be + * executed; never {@code null} + * @throws Exception allows any exception to propagate + */ + void beforeTestMethod(TestContext testContext) throws Exception; + + /** + * Post-processes a test <em>after</em> execution of the + * {@link java.lang.reflect.Method test method} in the supplied + * {@link TestContext test context}, for example by tearing down test + * fixtures. + * <p> + * This method should be called immediately after framework-specific + * <em>after</em> lifecycle callbacks. + * + * @param testContext the test context in which the test method was + * executed; never {@code null} + * @throws Exception allows any exception to propagate + */ + void afterTestMethod(TestContext testContext) throws Exception; + + /** + * Post-processes a test class <em>after</em> execution of all tests within + * the class. + * <p> + * This method should be called immediately after framework-specific + * <em>after class</em> lifecycle callbacks. + * <p> + * If a given testing framework (e.g., JUnit 3.8) does not support + * <em>after class</em> lifecycle callbacks, this method will not be called + * for that framework. + * + * @param testContext the test context for the test; never {@code null} + * @throws Exception allows any exception to propagate + */ + void afterTestClass(TestContext testContext) throws Exception; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java new file mode 100644 index 00000000..388f66b5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +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 TestExecutionListeners} defines class-level metadata for + * configuring which {@link TestExecutionListener TestExecutionListeners} should + * be registered with a {@link TestContextManager}. + * + * <p>Typically, {@code @TestExecutionListeners} will be used in conjunction with + * {@link ContextConfiguration @ContextConfiguration}. + * + * @author Sam Brannen + * @since 2.5 + * @see TestExecutionListener + * @see TestContextManager + * @see ContextConfiguration + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface TestExecutionListeners { + + /** + * The {@link TestExecutionListener TestExecutionListeners} to register with + * a {@link TestContextManager}. + * + * @see org.springframework.test.context.web.ServletTestExecutionListener + * @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener + * @see org.springframework.test.context.support.DirtiesContextTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + */ + Class<? extends TestExecutionListener>[] listeners() default {}; + + /** + * Alias for {@link #listeners() listeners}. + */ + Class<? extends TestExecutionListener>[] value() default {}; + + /** + * Whether or not {@link #value() TestExecutionListeners} from superclasses + * should be <em>inherited</em>. + * <p> + * The default value is {@code true}, which means that an annotated + * class will <em>inherit</em> the listeners defined by an annotated + * superclass. Specifically, the listeners for an annotated class will be + * appended to the list of listeners defined by an annotated superclass. + * Thus, subclasses have the option of <em>extending</em> the list of + * listeners. In the following example, {@code AbstractBaseTest} will + * be configured with {@code DependencyInjectionTestExecutionListener} + * and {@code DirtiesContextTestExecutionListener}; whereas, + * {@code TransactionalTest} will be configured with + * {@code DependencyInjectionTestExecutionListener}, + * {@code DirtiesContextTestExecutionListener}, <strong>and</strong> + * {@code TransactionalTestExecutionListener}, in that order. + * + * <pre class="code"> + * @TestExecutionListeners({ + * DependencyInjectionTestExecutionListener.class, + * DirtiesContextTestExecutionListener.class + * }) + * public abstract class AbstractBaseTest { + * // ... + * } + * + * @TestExecutionListeners(TransactionalTestExecutionListener.class) + * public class TransactionalTest extends AbstractBaseTest { + * // ... + * }</pre> + * + * <p> + * If {@code inheritListeners} is set to {@code false}, the listeners for the + * annotated class will <em>shadow</em> and effectively replace any listeners + * defined by a superclass. + */ + boolean inheritListeners() default true; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java new file mode 100644 index 00000000..732c1e0b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractJUnit38SpringContextTests.java @@ -0,0 +1,399 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit38; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.annotation.ExpectedException; +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.annotation.ProfileValueSource; +import org.springframework.test.annotation.ProfileValueUtils; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.annotation.Timed; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; + +/** + * <p> + * Abstract base {@link TestCase} which integrates the <i>Spring TestContext + * Framework</i> and explicit {@link ApplicationContext} testing support in a + * <strong>JUnit 3.8</strong> environment. + * </p> + * <p> + * Concrete subclasses: + * </p> + * <ul> + * <li>Typically declare a class-level + * {@link org.springframework.test.context.ContextConfiguration + * @ContextConfiguration} annotation to configure the + * {@link ApplicationContext application context} + * {@link org.springframework.test.context.ContextConfiguration#locations() + * resource locations}. <i>If your test does not need to load an application + * context, you may choose to omit the + * {@link org.springframework.test.context.ContextConfiguration + * @ContextConfiguration} declaration and configure the appropriate + * {@link org.springframework.test.context.TestExecutionListener + * TestExecutionListeners} manually.</i></li> + * <li>Must declare public constructors which match the signatures of + * {@link #AbstractJUnit38SpringContextTests() + * AbstractJUnit38SpringContextTests()} and + * {@link #AbstractJUnit38SpringContextTests(String) + * AbstractJUnit38SpringContextTests(String)} and delegate to + * {@code super();} and {@code super(name);} respectively.</li> + * </ul> + * <p> + * The following list constitutes all annotations currently supported directly + * by {@code AbstractJUnit38SpringContextTests}. <i>(Note that additional + * annotations may be supported by various + * {@link org.springframework.test.context.TestExecutionListener + * TestExecutionListeners})</i> + * </p> + * <ul> + * <li>{@link org.springframework.test.annotation.DirtiesContext + * @DirtiesContext} (via the configured + * {@link DirtiesContextTestExecutionListener}; only supported on methods for + * JUnit 3.8)</li> + * <li> + * {@link org.springframework.test.annotation.ProfileValueSourceConfiguration + * @ProfileValueSourceConfiguration}</li> + * <li>{@link IfProfileValue @IfProfileValue}</li> + * <li>{@link ExpectedException @ExpectedException}</li> + * <li>{@link Timed @Timed}</li> + * <li>{@link Repeat @Repeat}</li> + * </ul> + * <p> + * JUnit 3.8 does not support <i>before class</i> or <i>after class</i> + * lifecycle callbacks. The following + * {@link org.springframework.test.context.TestExecutionListener + * TestExecutionListener} methods are therefore unsupported in a JUnit 3.8 + * environment: + * <ul> + * <li> + * {@link org.springframework.test.context.TestExecutionListener#beforeTestClass(org.springframework.test.context.TestContext) + * beforeTestClass()}</li> + * <li> + * {@link org.springframework.test.context.TestExecutionListener#afterTestClass(org.springframework.test.context.TestContext) + * afterTestClass()}</li> + * </ul> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.test.context.TestContext + * @see org.springframework.test.context.TestContextManager + * @see org.springframework.test.context.TestExecutionListeners + * @see AbstractTransactionalJUnit38SpringContextTests + * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests + * @see org.springframework.test.context.testng.AbstractTestNGSpringContextTests + * @deprecated as of Spring 3.1, in favor of using + * {@link org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests AbstractJUnit4SpringContextTests} + */ +@Deprecated +@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) +public abstract class AbstractJUnit38SpringContextTests extends TestCase implements ApplicationContextAware { + + private static int disabledTestCount = 0; + + + /** + * Return the number of tests disabled in this environment. + */ + public static int getDisabledTestCount() { + return disabledTestCount; + } + + + /** + * Logger available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The {@link ApplicationContext} that was injected into this test instance + * via {@link #setApplicationContext(ApplicationContext)}. + */ + protected ApplicationContext applicationContext; + + /** + * {@link ProfileValueSource} available to subclasses but primarily intended + * for internal use to provide support for {@link IfProfileValue + * @IfProfileValue}. + */ + protected final ProfileValueSource profileValueSource; + + private final TestContextManager testContextManager; + + + /** + * Constructs a new AbstractJUnit38SpringContextTests instance; initializes + * the internal {@link TestContextManager} for the current test; and + * retrieves the configured (or default) {@link ProfileValueSource}. + */ + public AbstractJUnit38SpringContextTests() { + super(); + this.testContextManager = new TestContextManager(getClass()); + this.profileValueSource = ProfileValueUtils.retrieveProfileValueSource(getClass()); + } + + /** + * Constructs a new AbstractJUnit38SpringContextTests instance with the + * supplied {@code name}; initializes the internal + * {@link TestContextManager} for the current test; and retrieves the + * configured (or default) {@link ProfileValueSource}. + * + * @param name the name of the current test to execute + */ + public AbstractJUnit38SpringContextTests(String name) { + super(name); + this.testContextManager = new TestContextManager(getClass()); + this.profileValueSource = ProfileValueUtils.retrieveProfileValueSource(getClass()); + } + + /** + * Sets the {@link ApplicationContext} to be used by this test instance, + * provided via {@link ApplicationContextAware} semantics. + */ + public final void setApplicationContext(final ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Runs the <em>Spring TestContext Framework</em> test sequence. + * <p> + * In addition to standard {@link TestCase#runBare()} semantics, this + * implementation performs the following: + * <ul> + * <li>Calls {@link TestContextManager#prepareTestInstance(Object) + * prepareTestInstance()}, + * {@link TestContextManager#beforeTestMethod(Object,Method) + * beforeTestMethod()}, and + * {@link TestContextManager#afterTestMethod(Object,Method,Throwable) + * afterTestMethod()} on this test's {@link TestContextManager} at the + * appropriate test execution points.</li> + * <li>Provides support for {@link IfProfileValue @IfProfileValue}.</li> + * <li>Provides support for {@link Repeat @Repeat}.</li> + * <li>Provides support for {@link Timed @Timed}.</li> + * <li>Provides support for {@link ExpectedException + * @ExpectedException}.</li> + * </ul> + * + * @see ProfileValueUtils#isTestEnabledInThisEnvironment + */ + @Override + public void runBare() throws Throwable { + this.testContextManager.prepareTestInstance(this); + final Method testMethod = getTestMethod(); + + if (!ProfileValueUtils.isTestEnabledInThisEnvironment(this.profileValueSource, testMethod, getClass())) { + recordDisabled(testMethod); + return; + } + + runTestTimed(new TestExecutionCallback() { + + public void run() throws Throwable { + runManaged(testMethod); + } + }, testMethod); + } + + /** + * Get the current test method. + */ + private Method getTestMethod() { + assertNotNull("TestCase.getName() cannot be null", getName()); + Method testMethod = null; + try { + testMethod = getClass().getMethod(getName(), (Class[]) null); + } + catch (NoSuchMethodException ex) { + fail("Method \"" + getName() + "\" not found"); + } + if (!Modifier.isPublic(testMethod.getModifiers())) { + fail("Method \"" + getName() + "\" should be public"); + } + return testMethod; + } + + /** + * Runs a <em>timed</em> test via the supplied {@link TestExecutionCallback} + * , providing support for the {@link Timed @Timed} annotation. + * + * @param tec the test execution callback to run + * @param testMethod the actual test method: used to retrieve the + * {@code timeout} + * @throws Throwable if any exception is thrown + * @see Timed + * @see #runTest + */ + private void runTestTimed(TestExecutionCallback tec, Method testMethod) throws Throwable { + Timed timed = testMethod.getAnnotation(Timed.class); + if (timed == null) { + runTest(tec, testMethod); + } + else { + long startTime = System.currentTimeMillis(); + try { + runTest(tec, testMethod); + } + finally { + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed > timed.millis()) { + fail("Took " + elapsed + " ms; limit was " + timed.millis()); + } + } + } + } + + /** + * Runs a test via the supplied {@link TestExecutionCallback}, providing + * support for the {@link ExpectedException @ExpectedException} and + * {@link Repeat @Repeat} annotations. + * + * @param tec the test execution callback to run + * @param testMethod the actual test method: used to retrieve the + * {@link ExpectedException @ExpectedException} and {@link Repeat + * @Repeat} annotations + * @throws Throwable if any exception is thrown + * @see ExpectedException + * @see Repeat + */ + private void runTest(TestExecutionCallback tec, Method testMethod) throws Throwable { + ExpectedException expectedExceptionAnnotation = testMethod.getAnnotation(ExpectedException.class); + boolean exceptionIsExpected = (expectedExceptionAnnotation != null && expectedExceptionAnnotation.value() != null); + Class<? extends Throwable> expectedException = (exceptionIsExpected ? expectedExceptionAnnotation.value() + : null); + + Repeat repeat = testMethod.getAnnotation(Repeat.class); + int runs = ((repeat != null) && (repeat.value() > 1)) ? repeat.value() : 1; + + for (int i = 0; i < runs; i++) { + try { + if (runs > 1 && this.logger.isInfoEnabled()) { + this.logger.info("Repetition " + (i + 1) + " of test " + testMethod.getName()); + } + tec.run(); + if (exceptionIsExpected) { + fail("Expected exception: " + expectedException.getName()); + } + } + catch (Throwable ex) { + if (!exceptionIsExpected) { + throw ex; + } + if (!expectedException.isAssignableFrom(ex.getClass())) { + // Wrap the unexpected throwable with an explicit message. + AssertionFailedError assertionError = new AssertionFailedError("Unexpected exception, expected <" + + expectedException.getName() + "> but was <" + ex.getClass().getName() + ">"); + assertionError.initCause(ex); + throw assertionError; + } + } + } + } + + /** + * Calls {@link TestContextManager#beforeTestMethod(Object,Method)} and + * {@link TestContextManager#afterTestMethod(Object,Method,Throwable)} at + * the appropriate test execution points. + * + * @param testMethod the test method to run + * @throws Throwable if any exception is thrown + * @see #runBare() + * @see TestCase#runTest() + */ + private void runManaged(Method testMethod) throws Throwable { + Throwable exception = null; + boolean reachedTest = false; + + try { + this.testContextManager.beforeTestMethod(this, testMethod); + setUp(); + reachedTest = true; + runTest(); + } + catch (Throwable ex) { + exception = ex; + } + finally { + try { + if (reachedTest) { + tearDown(); + } + } + catch (Throwable ex) { + if (exception == null) { + exception = ex; + } + } + finally { + try { + this.testContextManager.afterTestMethod(this, testMethod, exception); + } + catch (Throwable ex) { + if (exception == null) { + exception = ex; + } + } + } + } + + if (exception != null) { + if (exception.getCause() instanceof AssertionError) { + exception = exception.getCause(); + } + throw exception; + } + } + + /** + * Records the supplied test method as <em>disabled</em> in the current + * environment by incrementing the total number of disabled tests and + * logging a debug message. + * + * @param testMethod the test method that is disabled. + * @see #getDisabledTestCount() + */ + protected void recordDisabled(Method testMethod) { + disabledTestCount++; + if (this.logger.isInfoEnabled()) { + this.logger.info("**** " + getClass().getName() + "." + getName() + "() is disabled in this environment. " + + "Total disabled tests = " + getDisabledTestCount()); + } + } + + + /** + * Private inner class that defines a callback analogous to {@link Runnable} + * , just declaring Throwable. + */ + private static interface TestExecutionCallback { + + void run() throws Throwable; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java new file mode 100644 index 00000000..9b7fd03e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit38/AbstractTransactionalJUnit38SpringContextTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit38; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.jdbc.SimpleJdbcTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * <p> + * Abstract {@link Transactional transactional} extension of + * {@link AbstractJUnit38SpringContextTests} which adds convenience + * functionality for JDBC access. Expects a {@link javax.sql.DataSource} bean + * and a {@link PlatformTransactionManager} bean to be defined in the Spring + * {@link ApplicationContext application context}. + * </p> + * <p> + * This class exposes a {@link SimpleJdbcTemplate} and provides an easy way to + * {@link #countRowsInTable(String) count the number of rows in a table} , + * {@link #deleteFromTables(String...) delete from the database} , and + * {@link #executeSqlScript(String, boolean) execute SQL scripts} within a + * transaction. + * </p> + * <p> + * Concrete subclasses must fulfill the same requirements outlined in + * {@link AbstractJUnit38SpringContextTests}. + * </p> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractJUnit38SpringContextTests + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.TestExecutionListeners + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionConfiguration + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see org.springframework.test.context.transaction.BeforeTransaction + * @see org.springframework.test.context.transaction.AfterTransaction + * @see org.springframework.test.jdbc.SimpleJdbcTestUtils + * @see org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests + * @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests + * @deprecated as of Spring 3.1, in favor of using + * {@link org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests AbstractTransactionalJUnit4SpringContextTests} + */ +@Deprecated +@TestExecutionListeners(TransactionalTestExecutionListener.class) +@Transactional +public abstract class AbstractTransactionalJUnit38SpringContextTests extends AbstractJUnit38SpringContextTests { + + /** + * The SimpleJdbcTemplate that this base class manages, available to subclasses. + */ + protected SimpleJdbcTemplate simpleJdbcTemplate; + + private String sqlScriptEncoding; + + + /** + * Constructs a new AbstractTransactionalJUnit38SpringContextTests instance. + */ + public AbstractTransactionalJUnit38SpringContextTests() { + super(); + } + + /** + * Constructs a new AbstractTransactionalJUnit38SpringContextTests instance + * with the supplied {@code name}. + * @param name the name of the current test to execute + */ + public AbstractTransactionalJUnit38SpringContextTests(String name) { + super(name); + } + + + /** + * Set the DataSource, typically provided via Dependency Injection. + * @param dataSource The DataSource to inject + */ + @Autowired + public void setDataSource(DataSource dataSource) { + this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + } + + /** + * Specify the encoding for SQL scripts, if different from the platform encoding. + * @see #executeSqlScript + */ + public void setSqlScriptEncoding(String sqlScriptEncoding) { + this.sqlScriptEncoding = sqlScriptEncoding; + } + + + /** + * Count the rows in the given table. + * @param tableName table name to count rows in + * @return the number of rows in the table + */ + protected int countRowsInTable(String tableName) { + return SimpleJdbcTestUtils.countRowsInTable(this.simpleJdbcTemplate, tableName); + } + + /** + * Convenience method for deleting all rows from the specified tables. + * Use with caution outside of a transaction! + * @param names the names of the tables from which to delete + * @return the total number of rows deleted from all specified tables + */ + protected int deleteFromTables(String... names) { + return SimpleJdbcTestUtils.deleteFromTables(this.simpleJdbcTemplate, names); + } + + /** + * Execute the given SQL script. Use with caution outside of a transaction! + * <p>The script will normally be loaded by classpath. There should be one statement + * per line. Any semicolons will be removed. <b>Do not use this method to execute + * DDL if you expect rollback.</b> + * @param sqlResourcePath the Spring resource path for the SQL script + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and continueOnError was {@code false} + */ + protected void executeSqlScript(String sqlResourcePath, boolean continueOnError) + throws DataAccessException { + + Resource resource = this.applicationContext.getResource(sqlResourcePath); + SimpleJdbcTestUtils.executeSqlScript( + this.simpleJdbcTemplate, new EncodedResource(resource, this.sqlScriptEncoding), continueOnError); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit38/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit38/package-info.java new file mode 100644 index 00000000..22ead8d5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit38/package-info.java @@ -0,0 +1,7 @@ +/** + * <p>Support classes for ApplicationContext-based and transactional + * tests run with JUnit 3.8 and the <em>Spring TestContext Framework</em>.</p> + */ + +package org.springframework.test.context.junit38; + diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java new file mode 100644 index 00000000..05d35d01 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.junit4; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.junit.runner.RunWith; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.springframework.test.context.web.ServletTestExecutionListener; + +/** + * Abstract base test class which integrates the <em>Spring TestContext + * Framework</em> with explicit {@link ApplicationContext} testing support in a + * <strong>JUnit 4.5+</strong> environment. + * + * <p>Concrete subclasses should typically declare a class-level + * {@link ContextConfiguration @ContextConfiguration} annotation to + * configure the {@link ApplicationContext application context} {@link + * ContextConfiguration#locations() resource locations} or {@link + * ContextConfiguration#classes() annotated classes}. <em>If your test does not + * need to load an application context, you may choose to omit the {@link + * ContextConfiguration @ContextConfiguration} declaration and to configure + * the appropriate {@link org.springframework.test.context.TestExecutionListener + * TestExecutionListeners} manually.</em> + * + * <p>The following {@link org.springframework.test.context.TestExecutionListener + * TestExecutionListeners} are configured by default: + * + * <ul> + * <li>{@link org.springframework.test.context.web.ServletTestExecutionListener} + * <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener} + * <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener} + * </ul> + * + * <p>Note: this class serves only as a convenience for extension. If you do not + * wish for your test classes to be tied to a Spring-specific class hierarchy, + * you may configure your own custom test classes by using + * {@link SpringJUnit4ClassRunner}, {@link ContextConfiguration + * @ContextConfiguration}, {@link TestExecutionListeners + * @TestExecutionListeners}, etc. + * + * @author Sam Brannen + * @since 2.5 + * @see ContextConfiguration + * @see TestContext + * @see TestContextManager + * @see TestExecutionListeners + * @see ServletTestExecutionListener + * @see DependencyInjectionTestExecutionListener + * @see DirtiesContextTestExecutionListener + * @see AbstractTransactionalJUnit4SpringContextTests + * @see org.springframework.test.context.testng.AbstractTestNGSpringContextTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners({ ServletTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class }) +public abstract class AbstractJUnit4SpringContextTests implements ApplicationContextAware { + + /** + * Logger available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The {@link ApplicationContext} that was injected into this test instance + * via {@link #setApplicationContext(ApplicationContext)}. + */ + protected ApplicationContext applicationContext; + + + /** + * Set the {@link ApplicationContext} to be used by this test instance, + * provided via {@link ApplicationContextAware} semantics. + */ + public final void setApplicationContext(final ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java new file mode 100644 index 00000000..083b0fae --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.jdbc.JdbcTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * Abstract {@linkplain Transactional transactional} extension of + * {@link AbstractJUnit4SpringContextTests} which adds convenience functionality + * for JDBC access. Expects a {@link DataSource} bean and a + * {@link PlatformTransactionManager} bean to be defined in the Spring + * {@linkplain ApplicationContext application context}. + * + * <p>This class exposes a {@link JdbcTemplate} and provides an easy way to + * {@linkplain #countRowsInTable count the number of rows in a table} + * (potentially {@linkplain #countRowsInTableWhere with a WHERE clause}), + * {@linkplain #deleteFromTables delete from tables}, + * {@linkplain #dropTables drop tables}, and + * {@linkplain #executeSqlScript execute SQL scripts} within a transaction. + * + * <p>Concrete subclasses must fulfill the same requirements outlined in + * {@link AbstractJUnit4SpringContextTests}. + * + * <p>Note: this class serves only as a convenience for extension. If you do not + * wish for your test classes to be tied to a Spring-specific class hierarchy, + * you may configure your own custom test classes by using + * {@link SpringJUnit4ClassRunner}, {@link ContextConfiguration + * @ContextConfiguration}, {@link TestExecutionListeners + * @TestExecutionListeners}, {@link Transactional @Transactional}, + * etc. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractJUnit4SpringContextTests + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.TestExecutionListeners + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionConfiguration + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see org.springframework.test.context.transaction.BeforeTransaction + * @see org.springframework.test.context.transaction.AfterTransaction + * @see org.springframework.test.jdbc.JdbcTestUtils + * @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests + */ +@TestExecutionListeners(TransactionalTestExecutionListener.class) +@Transactional +@SuppressWarnings("deprecation") +public abstract class AbstractTransactionalJUnit4SpringContextTests extends AbstractJUnit4SpringContextTests { + + /** + * The {@code SimpleJdbcTemplate} that this base class manages, available to subclasses. + * @deprecated As of Spring 3.2, use {@link #jdbcTemplate} instead. + */ + @Deprecated + protected SimpleJdbcTemplate simpleJdbcTemplate; + + /** + * The {@code JdbcTemplate} that this base class manages, available to subclasses. + * @since 3.2 + */ + protected JdbcTemplate jdbcTemplate; + + private String sqlScriptEncoding; + + + /** + * Set the {@code DataSource}, typically provided via Dependency Injection. + * <p>This method also instantiates the {@link #simpleJdbcTemplate} and + * {@link #jdbcTemplate} instance variables. + */ + @Autowired + public void setDataSource(DataSource dataSource) { + this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Specify the encoding for SQL scripts, if different from the platform encoding. + * @see #executeSqlScript + */ + public void setSqlScriptEncoding(String sqlScriptEncoding) { + this.sqlScriptEncoding = sqlScriptEncoding; + } + + /** + * Count the rows in the given table. + * @param tableName table name to count rows in + * @return the number of rows in the table + */ + protected int countRowsInTable(String tableName) { + return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); + } + + /** + * Count the rows in the given table, using the provided {@code WHERE} clause. + * <p>See the Javadoc for {@link JdbcTestUtils#countRowsInTableWhere} for details. + * @param tableName the name of the table to count rows in + * @param whereClause the {@code WHERE} clause to append to the query + * @return the number of rows in the table that match the provided + * {@code WHERE} clause + * @since 3.2 + */ + protected int countRowsInTableWhere(String tableName, String whereClause) { + return JdbcTestUtils.countRowsInTableWhere(this.jdbcTemplate, tableName, whereClause); + } + + /** + * Convenience method for deleting all rows from the specified tables. Use + * with caution outside of a transaction! + * @param names the names of the tables from which to delete + * @return the total number of rows deleted from all specified tables + */ + protected int deleteFromTables(String... names) { + return JdbcTestUtils.deleteFromTables(this.jdbcTemplate, names); + } + + /** + * Convenience method for dropping all of the specified tables. Use + * with caution outside of a transaction! + * @param names the names of the tables to drop + * @since 3.2 + */ + protected void dropTables(String... names) { + JdbcTestUtils.dropTables(this.jdbcTemplate, names); + } + + /** + * Execute the given SQL script. Use with caution outside of a transaction! + * <p>The script will normally be loaded by classpath. There should be one + * statement per line. Any semicolons will be removed. <b>Do not use this + * method to execute DDL if you expect rollback.</b> + * @param sqlResourcePath the Spring resource path for the SQL script + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and continueOnError was {@code false} + */ + protected void executeSqlScript(String sqlResourcePath, boolean continueOnError) throws DataAccessException { + Resource resource = this.applicationContext.getResource(sqlResourcePath); + JdbcTestUtils.executeSqlScript(this.jdbcTemplate, new EncodedResource(resource, + this.sqlScriptEncoding), continueOnError); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java new file mode 100644 index 00000000..5496b4ee --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java @@ -0,0 +1,474 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.internal.AssumptionViolatedException; +import org.junit.internal.runners.model.EachTestNotifier; +import org.junit.internal.runners.model.ReflectiveCallable; +import org.junit.internal.runners.statements.ExpectException; +import org.junit.internal.runners.statements.Fail; +import org.junit.internal.runners.statements.FailOnTimeout; +import org.junit.runner.Description; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; +import org.springframework.test.annotation.ExpectedException; +import org.springframework.test.annotation.ProfileValueUtils; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.annotation.Timed; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks; +import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks; +import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks; +import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks; +import org.springframework.test.context.junit4.statements.SpringFailOnTimeout; +import org.springframework.test.context.junit4.statements.SpringRepeat; +import org.springframework.util.ReflectionUtils; + +/** + * <p> + * {@code SpringJUnit4ClassRunner} is a custom extension of + * {@link BlockJUnit4ClassRunner} which provides functionality of the + * <em>Spring TestContext Framework</em> to standard JUnit 4.5+ tests by means + * of the {@link TestContextManager} and associated support classes and + * annotations. + * </p> + * <p> + * The following list constitutes all annotations currently supported directly + * by {@code SpringJUnit4ClassRunner}. + * <em>(Note that additional annotations may be supported by various + * {@link org.springframework.test.context.TestExecutionListener + * TestExecutionListeners})</em> + * </p> + * <ul> + * <li>{@link Test#expected() @Test(expected=...)}</li> + * <li>{@link ExpectedException @ExpectedException}</li> + * <li>{@link Test#timeout() @Test(timeout=...)}</li> + * <li>{@link Timed @Timed}</li> + * <li>{@link Repeat @Repeat}</li> + * <li>{@link Ignore @Ignore}</li> + * <li> + * {@link org.springframework.test.annotation.ProfileValueSourceConfiguration + * @ProfileValueSourceConfiguration}</li> + * <li>{@link org.springframework.test.annotation.IfProfileValue + * @IfProfileValue}</li> + * </ul> + * <p> + * <b>NOTE:</b> As of Spring 3.0, {@code SpringJUnit4ClassRunner} requires + * JUnit 4.5+. + * </p> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see TestContextManager + */ +@SuppressWarnings("deprecation") +public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { + + private static final Log logger = LogFactory.getLog(SpringJUnit4ClassRunner.class); + + private final TestContextManager testContextManager; + + + /** + * Constructs a new {@code SpringJUnit4ClassRunner} and initializes a + * {@link TestContextManager} to provide Spring testing functionality to + * standard JUnit tests. + * @param clazz the test class to be run + * @see #createTestContextManager(Class) + */ + public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError { + super(clazz); + if (logger.isDebugEnabled()) { + logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]."); + } + this.testContextManager = createTestContextManager(clazz); + } + + /** + * Creates a new {@link TestContextManager} for the supplied test class and + * the configured <em>default {@code ContextLoader} class name</em>. + * Can be overridden by subclasses. + * @param clazz the test class to be managed + * @see #getDefaultContextLoaderClassName(Class) + */ + protected TestContextManager createTestContextManager(Class<?> clazz) { + return new TestContextManager(clazz, getDefaultContextLoaderClassName(clazz)); + } + + /** + * Get the {@link TestContextManager} associated with this runner. + */ + protected final TestContextManager getTestContextManager() { + return this.testContextManager; + } + + /** + * Get the name of the default {@code ContextLoader} class to use for + * the supplied test class. The named class will be used if the test class + * does not explicitly declare a {@code ContextLoader} class via the + * {@code @ContextConfiguration} annotation. + * <p>The default implementation returns {@code null}, thus implying use + * of the <em>standard</em> default {@code ContextLoader} class name. + * Can be overridden by subclasses. + * @param clazz the test class + * @return {@code null} + */ + protected String getDefaultContextLoaderClassName(Class<?> clazz) { + return null; + } + + /** + * Returns a description suitable for an ignored test class if the test is + * disabled via {@code @IfProfileValue} at the class-level, and + * otherwise delegates to the parent implementation. + * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class) + */ + @Override + public Description getDescription() { + if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) { + return Description.createSuiteDescription(getTestClass().getJavaClass()); + } + return super.getDescription(); + } + + /** + * Check whether the test is enabled in the first place. This prevents + * classes with a non-matching {@code @IfProfileValue} annotation + * from running altogether, even skipping the execution of + * {@code prepareTestInstance()} {@code TestExecutionListener} + * methods. + * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class) + * @see org.springframework.test.annotation.IfProfileValue + * @see org.springframework.test.context.TestExecutionListener + */ + @Override + public void run(RunNotifier notifier) { + if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) { + notifier.fireTestIgnored(getDescription()); + return; + } + super.run(notifier); + } + + /** + * Wraps the {@link Statement} returned by the parent implementation with a + * {@link RunBeforeTestClassCallbacks} statement, thus preserving the + * default functionality but adding support for the Spring TestContext + * Framework. + * @see RunBeforeTestClassCallbacks + */ + @Override + protected Statement withBeforeClasses(Statement statement) { + Statement junitBeforeClasses = super.withBeforeClasses(statement); + return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager()); + } + + /** + * Wraps the {@link Statement} returned by the parent implementation with a + * {@link RunAfterTestClassCallbacks} statement, thus preserving the default + * functionality but adding support for the Spring TestContext Framework. + * @see RunAfterTestClassCallbacks + */ + @Override + protected Statement withAfterClasses(Statement statement) { + Statement junitAfterClasses = super.withAfterClasses(statement); + return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager()); + } + + /** + * Delegates to the parent implementation for creating the test instance and + * then allows the {@link #getTestContextManager() TestContextManager} to + * prepare the test instance before returning it. + * @see TestContextManager#prepareTestInstance(Object) + */ + @Override + protected Object createTest() throws Exception { + Object testInstance = super.createTest(); + getTestContextManager().prepareTestInstance(testInstance); + return testInstance; + } + + /** + * Performs the same logic as + * {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)}, + * except that tests are determined to be <em>ignored</em> by + * {@link #isTestMethodIgnored(FrameworkMethod)}. + */ + @Override + protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) { + EachTestNotifier eachNotifier = springMakeNotifier(frameworkMethod, notifier); + if (isTestMethodIgnored(frameworkMethod)) { + eachNotifier.fireTestIgnored(); + return; + } + + eachNotifier.fireTestStarted(); + try { + methodBlock(frameworkMethod).evaluate(); + } + catch (AssumptionViolatedException e) { + eachNotifier.addFailedAssumption(e); + } + catch (Throwable e) { + eachNotifier.addFailure(e); + } + finally { + eachNotifier.fireTestFinished(); + } + } + + /** + * {@code springMakeNotifier()} is an exact copy of + * {@link BlockJUnit4ClassRunner BlockJUnit4ClassRunner's} + * {@code makeNotifier()} method, but we have decided to prefix it with + * "spring" and keep it {@code private} in order to avoid the + * compatibility clashes that were introduced in JUnit between versions 4.5, + * 4.6, and 4.7. + */ + private EachTestNotifier springMakeNotifier(FrameworkMethod method, RunNotifier notifier) { + Description description = describeChild(method); + return new EachTestNotifier(notifier, description); + } + + /** + * Augments the default JUnit behavior + * {@link #withPotentialRepeat(FrameworkMethod, Object, Statement) with + * potential repeats} of the entire execution chain. + * <p>Furthermore, support for timeouts has been moved down the execution chain + * in order to include execution of {@link org.junit.Before @Before} + * and {@link org.junit.After @After} methods within the timed + * execution. Note that this differs from the default JUnit behavior of + * executing {@code @Before} and {@code @After} methods + * in the main thread while executing the actual test method in a separate + * thread. Thus, the end effect is that {@code @Before} and + * {@code @After} methods will be executed in the same thread as + * the test method. As a consequence, JUnit-specified timeouts will work + * fine in combination with Spring transactions. Note that JUnit-specific + * timeouts still differ from Spring-specific timeouts in that the former + * execute in a separate thread while the latter simply execute in the main + * thread (like regular tests). + * @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement) + * @see #withBefores(FrameworkMethod, Object, Statement) + * @see #withAfters(FrameworkMethod, Object, Statement) + * @see #withPotentialTimeout(FrameworkMethod, Object, Statement) + * @see #withPotentialRepeat(FrameworkMethod, Object, Statement) + */ + @Override + protected Statement methodBlock(FrameworkMethod frameworkMethod) { + Object testInstance; + try { + testInstance = new ReflectiveCallable() { + + @Override + protected Object runReflectiveCall() throws Throwable { + return createTest(); + } + }.run(); + } + catch (Throwable ex) { + return new Fail(ex); + } + + Statement statement = methodInvoker(frameworkMethod, testInstance); + statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement); + statement = withBefores(frameworkMethod, testInstance, statement); + statement = withAfters(frameworkMethod, testInstance, statement); + statement = withRulesReflectively(frameworkMethod, testInstance, statement); + statement = withPotentialRepeat(frameworkMethod, testInstance, statement); + statement = withPotentialTimeout(frameworkMethod, testInstance, statement); + + return statement; + } + + /** + * Invokes JUnit 4.7's private {@code withRules()} method using + * reflection. This is necessary for backwards compatibility with the JUnit + * 4.5 and 4.6 implementations of {@link BlockJUnit4ClassRunner}. + */ + private Statement withRulesReflectively(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { + Method withRulesMethod = ReflectionUtils.findMethod(getClass(), "withRules", FrameworkMethod.class, + Object.class, Statement.class); + if (withRulesMethod != null) { + // Original JUnit 4.7 code: + // statement = withRules(frameworkMethod, testInstance, statement); + ReflectionUtils.makeAccessible(withRulesMethod); + statement = (Statement) ReflectionUtils.invokeMethod(withRulesMethod, this, frameworkMethod, testInstance, + statement); + } + return statement; + } + + /** + * Returns {@code true} if {@link Ignore @Ignore} is present for + * the supplied {@link FrameworkMethod test method} or if the test method is + * disabled via {@code @IfProfileValue}. + * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class) + */ + protected boolean isTestMethodIgnored(FrameworkMethod frameworkMethod) { + Method method = frameworkMethod.getMethod(); + return (method.isAnnotationPresent(Ignore.class) || !ProfileValueUtils.isTestEnabledInThisEnvironment(method, + getTestClass().getJavaClass())); + } + + /** + * Performs the same logic as + * {@link BlockJUnit4ClassRunner#possiblyExpectingExceptions(FrameworkMethod, Object, Statement)} + * except that the <em>expected exception</em> is retrieved using + * {@link #getExpectedException(FrameworkMethod)}. + */ + @Override + protected Statement possiblyExpectingExceptions(FrameworkMethod frameworkMethod, Object testInstance, Statement next) { + Class<? extends Throwable> expectedException = getExpectedException(frameworkMethod); + return expectedException != null ? new ExpectException(next, expectedException) : next; + } + + /** + * Get the {@code exception} that the supplied {@link FrameworkMethod + * test method} is expected to throw. + * <p>Supports both Spring's {@link ExpectedException @ExpectedException(...)} + * and JUnit's {@link Test#expected() @Test(expected=...)} annotations, but + * not both simultaneously. + * @return the expected exception, or {@code null} if none was specified + */ + protected Class<? extends Throwable> getExpectedException(FrameworkMethod frameworkMethod) { + Test testAnnotation = frameworkMethod.getAnnotation(Test.class); + Class<? extends Throwable> junitExpectedException = (testAnnotation != null + && testAnnotation.expected() != Test.None.class ? testAnnotation.expected() : null); + + ExpectedException expectedExAnn = frameworkMethod.getAnnotation(ExpectedException.class); + Class<? extends Throwable> springExpectedException = (expectedExAnn != null ? expectedExAnn.value() : null); + + if (springExpectedException != null && junitExpectedException != null) { + String msg = "Test method [" + frameworkMethod.getMethod() + + "] has been configured with Spring's @ExpectedException(" + springExpectedException.getName() + + ".class) and JUnit's @Test(expected=" + junitExpectedException.getName() + + ".class) annotations. " + + "Only one declaration of an 'expected exception' is permitted per test method."; + logger.error(msg); + throw new IllegalStateException(msg); + } + + return springExpectedException != null ? springExpectedException : junitExpectedException; + } + + /** + * Supports both Spring's {@link Timed @Timed} and JUnit's + * {@link Test#timeout() @Test(timeout=...)} annotations, but not both + * simultaneously. Returns either a {@link SpringFailOnTimeout}, a + * {@link FailOnTimeout}, or the unmodified, supplied {@link Statement} as + * appropriate. + * @see #getSpringTimeout(FrameworkMethod) + * @see #getJUnitTimeout(FrameworkMethod) + */ + @Override + protected Statement withPotentialTimeout(FrameworkMethod frameworkMethod, Object testInstance, Statement next) { + Statement statement = null; + long springTimeout = getSpringTimeout(frameworkMethod); + long junitTimeout = getJUnitTimeout(frameworkMethod); + if (springTimeout > 0 && junitTimeout > 0) { + String msg = "Test method [" + frameworkMethod.getMethod() + + "] has been configured with Spring's @Timed(millis=" + springTimeout + + ") and JUnit's @Test(timeout=" + junitTimeout + + ") annotations. Only one declaration of a 'timeout' is permitted per test method."; + logger.error(msg); + throw new IllegalStateException(msg); + } + else if (springTimeout > 0) { + statement = new SpringFailOnTimeout(next, springTimeout); + } + else if (junitTimeout > 0) { + statement = new FailOnTimeout(next, junitTimeout); + } + else { + statement = next; + } + + return statement; + } + + /** + * Retrieves the configured JUnit {@code timeout} from the {@link Test + * @Test} annotation on the supplied {@link FrameworkMethod test method}. + * @return the timeout, or {@code 0} if none was specified. + */ + protected long getJUnitTimeout(FrameworkMethod frameworkMethod) { + Test testAnnotation = frameworkMethod.getAnnotation(Test.class); + return (testAnnotation != null && testAnnotation.timeout() > 0 ? testAnnotation.timeout() : 0); + } + + /** + * Retrieves the configured Spring-specific {@code timeout} from the + * {@link Timed @Timed} annotation on the supplied + * {@link FrameworkMethod test method}. + * @return the timeout, or {@code 0} if none was specified. + */ + protected long getSpringTimeout(FrameworkMethod frameworkMethod) { + Timed timedAnnotation = frameworkMethod.getAnnotation(Timed.class); + return (timedAnnotation != null && timedAnnotation.millis() > 0 ? timedAnnotation.millis() : 0); + } + + /** + * Wraps the {@link Statement} returned by the parent implementation with a + * {@link RunBeforeTestMethodCallbacks} statement, thus preserving the + * default functionality but adding support for the Spring TestContext + * Framework. + * @see RunBeforeTestMethodCallbacks + */ + @Override + protected Statement withBefores(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { + Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement); + return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(), + getTestContextManager()); + } + + /** + * Wraps the {@link Statement} returned by the parent implementation with a + * {@link RunAfterTestMethodCallbacks} statement, thus preserving the + * default functionality but adding support for the Spring TestContext + * Framework. + * @see RunAfterTestMethodCallbacks + */ + @Override + protected Statement withAfters(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { + Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement); + return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(), + getTestContextManager()); + } + + /** + * Supports Spring's {@link Repeat @Repeat} annotation by returning a + * {@link SpringRepeat} statement initialized with the configured repeat + * count or {@code 1} if no repeat count is configured. + * @see SpringRepeat + */ + protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Object testInstance, Statement next) { + Repeat repeatAnnotation = frameworkMethod.getAnnotation(Repeat.class); + int repeat = (repeatAnnotation != null ? repeatAnnotation.value() : 1); + return new SpringRepeat(next, frameworkMethod.getMethod(), repeat); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java new file mode 100644 index 00000000..64f30c8f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java @@ -0,0 +1,7 @@ +/** + * <p>Support classes for ApplicationContext-based and transactional + * tests run with JUnit 4.5+ and the <em>Spring TestContext Framework</em>.</p> + */ + +package org.springframework.test.context.junit4; + diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java new file mode 100644 index 00000000..c3d6c8f5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.statements; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.internal.runners.model.MultipleFailureException; +import org.junit.runners.model.Statement; +import org.springframework.test.context.TestContextManager; + +/** + * {@code RunAfterTestClassCallbacks} is a custom JUnit 4.5+ + * {@link Statement} which allows the <em>Spring TestContext Framework</em> to + * be plugged into the JUnit execution chain by calling + * {@link TestContextManager#afterTestClass() afterTestClass()} on the supplied + * {@link TestContextManager}. + * + * @see #evaluate() + * @see RunBeforeTestMethodCallbacks + * @author Sam Brannen + * @since 3.0 + */ +@SuppressWarnings("deprecation") +public class RunAfterTestClassCallbacks extends Statement { + + private final Statement next; + + private final TestContextManager testContextManager; + + + /** + * Constructs a new {@code RunAfterTestClassCallbacks} statement. + * + * @param next the next {@code Statement} in the execution chain + * @param testContextManager the TestContextManager upon which to call + * {@code afterTestClass()} + */ + public RunAfterTestClassCallbacks(Statement next, TestContextManager testContextManager) { + this.next = next; + this.testContextManager = testContextManager; + } + + /** + * Invokes the next {@link Statement} in the execution chain (typically an + * instance of {@link org.junit.internal.runners.statements.RunAfters + * RunAfters}), catching any exceptions thrown, and then calls + * {@link TestContextManager#afterTestClass()}. If the call to + * {@code afterTestClass()} throws an exception, it will also be + * tracked. Multiple exceptions will be combined into a + * {@link MultipleFailureException}. + */ + @Override + public void evaluate() throws Throwable { + List<Throwable> errors = new ArrayList<Throwable>(); + try { + this.next.evaluate(); + } + catch (Throwable e) { + errors.add(e); + } + + try { + this.testContextManager.afterTestClass(); + } + catch (Exception e) { + errors.add(e); + } + + if (errors.isEmpty()) { + return; + } + if (errors.size() == 1) { + throw errors.get(0); + } + throw new MultipleFailureException(errors); + } +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java new file mode 100644 index 00000000..0fa60566 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.statements; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.internal.runners.model.MultipleFailureException; +import org.junit.runners.model.Statement; +import org.springframework.test.context.TestContextManager; + +/** + * {@code RunAfterTestMethodCallbacks} is a custom JUnit 4.5+ + * {@link Statement} which allows the <em>Spring TestContext Framework</em> to + * be plugged into the JUnit execution chain by calling + * {@link TestContextManager#afterTestMethod(Object, Method, Throwable) afterTestMethod()} + * on the supplied {@link TestContextManager}. + * + * @see #evaluate() + * @see RunBeforeTestMethodCallbacks + * @author Sam Brannen + * @since 3.0 + */ +@SuppressWarnings("deprecation") +public class RunAfterTestMethodCallbacks extends Statement { + + private final Statement next; + + private final Object testInstance; + + private final Method testMethod; + + private final TestContextManager testContextManager; + + + /** + * Constructs a new {@code RunAfterTestMethodCallbacks} statement. + * + * @param next the next {@code Statement} in the execution chain + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which has just been executed on the + * test instance + * @param testContextManager the TestContextManager upon which to call + * {@code afterTestMethod()} + */ + public RunAfterTestMethodCallbacks(Statement next, Object testInstance, Method testMethod, + TestContextManager testContextManager) { + this.next = next; + this.testInstance = testInstance; + this.testMethod = testMethod; + this.testContextManager = testContextManager; + } + + /** + * Invokes the next {@link Statement} in the execution chain (typically an + * instance of {@link org.junit.internal.runners.statements.RunAfters + * RunAfters}), catching any exceptions thrown, and then calls + * {@link TestContextManager#afterTestMethod(Object, Method, Throwable)} with the first + * caught exception (if any). If the call to {@code afterTestMethod()} + * throws an exception, it will also be tracked. Multiple exceptions will be + * combined into a {@link MultipleFailureException}. + */ + @Override + public void evaluate() throws Throwable { + Throwable testException = null; + List<Throwable> errors = new ArrayList<Throwable>(); + try { + this.next.evaluate(); + } + catch (Throwable e) { + testException = e; + errors.add(e); + } + + try { + this.testContextManager.afterTestMethod(this.testInstance, this.testMethod, testException); + } + catch (Exception e) { + errors.add(e); + } + + if (errors.isEmpty()) { + return; + } + if (errors.size() == 1) { + throw errors.get(0); + } + throw new MultipleFailureException(errors); + } +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java new file mode 100644 index 00000000..16c6f97d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.statements; + +import org.junit.runners.model.Statement; +import org.springframework.test.context.TestContextManager; + +/** + * {@code RunBeforeTestClassCallbacks} is a custom JUnit 4.5+ + * {@link Statement} which allows the <em>Spring TestContext Framework</em> to + * be plugged into the JUnit execution chain by calling + * {@link TestContextManager#beforeTestClass() beforeTestClass()} on the + * supplied {@link TestContextManager}. + * + * @see #evaluate() + * @see RunAfterTestMethodCallbacks + * @author Sam Brannen + * @since 3.0 + */ +public class RunBeforeTestClassCallbacks extends Statement { + + private final Statement next; + + private final TestContextManager testContextManager; + + + /** + * Constructs a new {@code RunBeforeTestClassCallbacks} statement. + * + * @param next the next {@code Statement} in the execution chain + * @param testContextManager the TestContextManager upon which to call + * {@code beforeTestClass()} + */ + public RunBeforeTestClassCallbacks(Statement next, TestContextManager testContextManager) { + this.next = next; + this.testContextManager = testContextManager; + } + + /** + * Calls {@link TestContextManager#beforeTestClass()} and then invokes the + * next {@link Statement} in the execution chain (typically an instance of + * {@link org.junit.internal.runners.statements.RunBefores RunBefores}). + */ + @Override + public void evaluate() throws Throwable { + this.testContextManager.beforeTestClass(); + this.next.evaluate(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java new file mode 100644 index 00000000..92d32ea5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.statements; + +import java.lang.reflect.Method; + +import org.junit.runners.model.Statement; +import org.springframework.test.context.TestContextManager; + +/** + * {@code RunBeforeTestMethodCallbacks} is a custom JUnit 4.5+ + * {@link Statement} which allows the <em>Spring TestContext Framework</em> to + * be plugged into the JUnit execution chain by calling + * {@link TestContextManager#beforeTestMethod(Object, Method) + * beforeTestMethod()} on the supplied {@link TestContextManager}. + * + * @see #evaluate() + * @see RunAfterTestMethodCallbacks + * @author Sam Brannen + * @since 3.0 + */ +public class RunBeforeTestMethodCallbacks extends Statement { + + private final Statement next; + + private final Object testInstance; + + private final Method testMethod; + + private final TestContextManager testContextManager; + + + /** + * Constructs a new {@code RunBeforeTestMethodCallbacks} statement. + * + * @param next the next {@code Statement} in the execution chain + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which is about to be executed on the + * test instance + * @param testContextManager the TestContextManager upon which to call + * {@code beforeTestMethod()} + */ + public RunBeforeTestMethodCallbacks(Statement next, Object testInstance, Method testMethod, + TestContextManager testContextManager) { + this.next = next; + this.testInstance = testInstance; + this.testMethod = testMethod; + this.testContextManager = testContextManager; + } + + /** + * Calls {@link TestContextManager#beforeTestMethod(Object, Method)} and + * then invokes the next {@link Statement} in the execution chain (typically + * an instance of {@link org.junit.internal.runners.statements.RunBefores + * RunBefores}). + */ + @Override + public void evaluate() throws Throwable { + this.testContextManager.beforeTestMethod(this.testInstance, this.testMethod); + this.next.evaluate(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java new file mode 100644 index 00000000..093ae49b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.statements; + +import java.util.concurrent.TimeoutException; + +import org.junit.runners.model.Statement; +import org.springframework.test.annotation.Timed; + +/** + * {@code SpringFailOnTimeout} is a custom JUnit 4.5+ {@link Statement} + * which adds support for Spring's {@link Timed @Timed} annotation by throwing + * an exception if the next statement in the execution chain takes more than the + * specified number of milliseconds. + * + * @see #evaluate() + * @author Sam Brannen + * @since 3.0 + */ +public class SpringFailOnTimeout extends Statement { + + private final Statement next; + + private final long timeout; + + + /** + * Constructs a new {@code SpringFailOnTimeout} statement. + * + * @param next the next {@code Statement} in the execution chain + * @param timeout the configured {@code timeout} for the current test + * @see Timed#millis() + */ + public SpringFailOnTimeout(Statement next, long timeout) { + this.next = next; + this.timeout = timeout; + } + + /** + * Invokes the next {@link Statement statement} in the execution chain + * (typically an instance of + * {@link org.junit.internal.runners.statements.InvokeMethod InvokeMethod} + * or {@link org.junit.internal.runners.statements.ExpectException + * ExpectException}) and throws an exception if the next + * {@code statement} takes more than the specified {@code timeout} + * . + */ + @Override + public void evaluate() throws Throwable { + long startTime = System.currentTimeMillis(); + try { + this.next.evaluate(); + } + finally { + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed > this.timeout) { + throw new TimeoutException(String.format("Test took %s ms; limit was %s ms.", elapsed, this.timeout)); + } + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java new file mode 100644 index 00000000..0aa21888 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.statements; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.runners.model.Statement; +import org.springframework.test.annotation.Repeat; +import org.springframework.util.ClassUtils; + +/** + * {@code SpringRepeat} is a custom JUnit 4.5+ {@link Statement} which adds + * support for Spring's {@link Repeat @Repeat} annotation by repeating the + * test for the specified number of times. + * + * @see #evaluate() + * @author Sam Brannen + * @since 3.0 + */ +public class SpringRepeat extends Statement { + + protected static final Log logger = LogFactory.getLog(SpringRepeat.class); + + private final Statement next; + + private final Method testMethod; + + private final int repeat; + + + /** + * Constructs a new {@code SpringRepeat} statement. + * + * @param next the next {@code Statement} in the execution chain + * @param testMethod the current test method + * @param repeat the configured repeat count for the current test method + * @see Repeat#value() + */ + public SpringRepeat(Statement next, Method testMethod, int repeat) { + this.next = next; + this.testMethod = testMethod; + this.repeat = Math.max(1, repeat); + } + + /** + * Invokes the next {@link Statement statement} in the execution chain for + * the specified repeat count. + */ + @Override + public void evaluate() throws Throwable { + for (int i = 0; i < this.repeat; i++) { + if (this.repeat > 1 && logger.isInfoEnabled()) { + logger.info(String.format("Repetition %d of test %s#%s()", (i + 1), + ClassUtils.getShortName(this.testMethod.getDeclaringClass()), this.testMethod.getName())); + } + this.next.evaluate(); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java new file mode 100644 index 00000000..116e5fe1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java @@ -0,0 +1,7 @@ +/** + * + * <p>JUnit 4.5 based {@code statements} used in the <em>Spring TestContext Framework</em>.</p> + * + */ +package org.springframework.test.context.junit4.statements; + diff --git a/spring-test/src/main/java/org/springframework/test/context/package-info.java b/spring-test/src/main/java/org/springframework/test/context/package-info.java new file mode 100644 index 00000000..b02e0b44 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/package-info.java @@ -0,0 +1,16 @@ +/** + * <p>This package contains the <em>Spring TestContext Framework</em> which + * provides annotation-driven unit and integration testing support that is + * agnostic of the actual testing framework in use. The same techniques and + * annotation-based configuration used in, for example, a JUnit 4.5+ environment + * can also be applied to tests written with TestNG, etc. + * + * <p>In addition to providing generic and extensible testing infrastructure, + * the Spring TestContext Framework provides out-of-the-box support for + * Spring-specific integration testing functionality such as context management + * and caching, dependency injection of test fixtures, and transactional test + * management with default rollback semantics. + */ + +package org.springframework.test.context; + diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java new file mode 100644 index 00000000..8b497ccc --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java @@ -0,0 +1,286 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.SmartContextLoader; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * Abstract application context loader that provides a basis for all concrete + * implementations of the {@link ContextLoader} SPI. Provides a + * <em>Template Method</em> based approach for {@link #processLocations processing} + * resource locations. + * + * <p>As of Spring 3.1, {@code AbstractContextLoader} also provides a basis + * for all concrete implementations of the {@link SmartContextLoader} SPI. For + * backwards compatibility with the {@code ContextLoader} SPI, + * {@link #processContextConfiguration(ContextConfigurationAttributes)} delegates + * to {@link #processLocations(Class, String...)}. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see #generateDefaultLocations + * @see #modifyLocations + */ +public abstract class AbstractContextLoader implements SmartContextLoader { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static final String SLASH = "/"; + + private static final Log logger = LogFactory.getLog(AbstractContextLoader.class); + + + // --- SmartContextLoader ----------------------------------------------- + + /** + * For backwards compatibility with the {@link ContextLoader} SPI, the + * default implementation simply delegates to {@link #processLocations(Class, String...)}, + * passing it the {@link ContextConfigurationAttributes#getDeclaringClass() + * declaring class} and {@link ContextConfigurationAttributes#getLocations() + * resource locations} retrieved from the supplied + * {@link ContextConfigurationAttributes configuration attributes}. The + * processed locations are then + * {@link ContextConfigurationAttributes#setLocations(String[]) set} in + * the supplied configuration attributes. + * <p>Can be overridden in subclasses — for example, to process + * annotated classes instead of resource locations. + * @since 3.1 + * @see #processLocations(Class, String...) + */ + public void processContextConfiguration(ContextConfigurationAttributes configAttributes) { + String[] processedLocations = processLocations(configAttributes.getDeclaringClass(), configAttributes.getLocations()); + configAttributes.setLocations(processedLocations); + } + + /** + * Prepare the {@link ConfigurableApplicationContext} created by this + * {@code SmartContextLoader} <i>before</i> bean definitions are read. + * <p>The default implementation: + * <ul> + * <li>Sets the <em>active bean definition profiles</em> from the supplied + * {@code MergedContextConfiguration} in the + * {@link org.springframework.core.env.Environment Environment} of the context.</li> + * <li>Determines what (if any) context initializer classes have been supplied + * via the {@code MergedContextConfiguration} and + * {@linkplain ApplicationContextInitializer#initialize invokes each} with the + * given application context.</li> + * </ul> + * <p>Any {@code ApplicationContextInitializers} implementing + * {@link org.springframework.core.Ordered Ordered} or marked with {@link + * org.springframework.core.annotation.Order @Order} will be sorted appropriately. + * @param context the newly created application context + * @param mergedConfig the merged context configuration + * @since 3.2 + * @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext) + * @see #loadContext(MergedContextConfiguration) + * @see ConfigurableApplicationContext#setId + */ + @SuppressWarnings("unchecked") + protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles()); + + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = + mergedConfig.getContextInitializerClasses(); + if (initializerClasses.isEmpty()) { + // no ApplicationContextInitializers have been declared -> nothing to do + return; + } + + List<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances = + new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>(); + Class<?> contextClass = context.getClass(); + + for (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>> initializerClass : initializerClasses) { + Class<?> initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass, + ApplicationContextInitializer.class); + Assert.isAssignable(initializerContextClass, contextClass, String.format( + "Could not add context initializer [%s] since its generic parameter [%s] " + + "is not assignable from the type of application context used by this " + + "context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(), + contextClass.getName())); + initializerInstances.add((ApplicationContextInitializer<ConfigurableApplicationContext>) BeanUtils.instantiateClass(initializerClass)); + } + + AnnotationAwareOrderComparator.sort(initializerInstances); + for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) { + initializer.initialize(context); + } + } + + // --- ContextLoader ------------------------------------------------------- + + /** + * If the supplied {@code locations} are {@code null} or + * <em>empty</em> and {@link #isGenerateDefaultLocations()} returns + * {@code true}, default locations will be + * {@link #generateDefaultLocations(Class) generated} for the specified + * {@link Class class} and the configured + * {@link #getResourceSuffix() resource suffix}; otherwise, the supplied + * {@code locations} will be {@link #modifyLocations modified} if + * necessary and returned. + * @param clazz the class with which the locations are associated: to be + * used when generating default locations + * @param locations the unmodified locations to use for loading the + * application context (can be {@code null} or empty) + * @return a processed array of application context resource locations + * @since 2.5 + * @see #isGenerateDefaultLocations() + * @see #generateDefaultLocations(Class) + * @see #modifyLocations(Class, String...) + * @see org.springframework.test.context.ContextLoader#processLocations(Class, String...) + * @see #processContextConfiguration(ContextConfigurationAttributes) + */ + public final String[] processLocations(Class<?> clazz, String... locations) { + return (ObjectUtils.isEmpty(locations) && isGenerateDefaultLocations()) ? + generateDefaultLocations(clazz) : modifyLocations(clazz, locations); + } + + /** + * Generate the default classpath resource locations array based on the + * supplied class. + * <p>For example, if the supplied class is {@code com.example.MyTest}, + * the generated locations will contain a single string with a value of + * "classpath:/com/example/MyTest{@code <suffix>}", + * where {@code <suffix>} is the value of the + * {@link #getResourceSuffix() resource suffix} string. + * <p>As of Spring 3.1, the implementation of this method adheres to the + * contract defined in the {@link SmartContextLoader} SPI. Specifically, + * this method will <em>preemptively</em> verify that the generated default + * location actually exists. If it does not exist, this method will log a + * warning and return an empty array. + * <p>Subclasses can override this method to implement a different + * <em>default location generation</em> strategy. + * @param clazz the class for which the default locations are to be generated + * @return an array of default application context resource locations + * @since 2.5 + * @see #getResourceSuffix() + */ + protected String[] generateDefaultLocations(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + String suffix = getResourceSuffix(); + Assert.hasText(suffix, "Resource suffix must not be empty"); + String resourcePath = SLASH + ClassUtils.convertClassNameToResourcePath(clazz.getName()) + suffix; + String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath; + ClassPathResource classPathResource = new ClassPathResource(resourcePath, clazz); + + if (classPathResource.exists()) { + if (logger.isInfoEnabled()) { + logger.info(String.format("Detected default resource location \"%s\" for test class [%s]", + prefixedResourcePath, clazz.getName())); + } + return new String[] {prefixedResourcePath}; + } + else { + if (logger.isInfoEnabled()) { + logger.info(String.format("Could not detect default resource locations for test class [%s]: " + + "%s does not exist", clazz.getName(), classPathResource)); + } + return EMPTY_STRING_ARRAY; + } + } + + /** + * Generate a modified version of the supplied locations array and return it. + * <p>A plain path — for example, "context.xml" — will + * be treated as a classpath resource that is relative to the package in which + * the specified class is defined. A path starting with a slash is treated + * as an absolute classpath location, for example: + * "/org/springframework/whatever/foo.xml". A path which + * references a URL (e.g., a path prefixed with + * {@link ResourceUtils#CLASSPATH_URL_PREFIX classpath:}, + * {@link ResourceUtils#FILE_URL_PREFIX file:}, {@code http:}, + * etc.) will be added to the results unchanged. + * <p>Subclasses can override this method to implement a different + * <em>location modification</em> strategy. + * @param clazz the class with which the locations are associated + * @param locations the resource locations to be modified + * @return an array of modified application context resource locations + * @since 2.5 + */ + protected String[] modifyLocations(Class<?> clazz, String... locations) { + String[] modifiedLocations = new String[locations.length]; + for (int i = 0; i < locations.length; i++) { + String path = locations[i]; + if (path.startsWith(SLASH)) { + modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + path; + } + else if (!ResourcePatternUtils.isUrl(path)) { + modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + SLASH + + StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(clazz) + SLASH + path); + } + else { + modifiedLocations[i] = StringUtils.cleanPath(path); + } + } + return modifiedLocations; + } + + /** + * Determine whether or not <em>default</em> resource locations should be + * generated if the {@code locations} provided to + * {@link #processLocations(Class, String...)} are {@code null} or empty. + * <p>As of Spring 3.1, the semantics of this method have been overloaded + * to include detection of either default resource locations or default + * configuration classes. Consequently, this method can also be used to + * determine whether or not <em>default</em> configuration classes should be + * detected if the {@code classes} present in the + * {@link ContextConfigurationAttributes configuration attributes} supplied + * to {@link #processContextConfiguration(ContextConfigurationAttributes)} + * are {@code null} or empty. + * <p>Can be overridden by subclasses to change the default behavior. + * @return always {@code true} by default + * @since 2.5 + */ + protected boolean isGenerateDefaultLocations() { + return true; + } + + /** + * Get the suffix to append to {@link ApplicationContext} resource locations + * when generating default locations. + * <p>Must be implemented by subclasses. + * @return the resource suffix; should not be {@code null} or empty + * @since 2.5 + * @see #generateDefaultLocations(Class) + */ + protected abstract String getResourceSuffix(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java new file mode 100644 index 00000000..edf34158 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java @@ -0,0 +1,286 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.support; + +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.SmartContextLoader; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class + * for implementations of the {@link SmartContextLoader} SPI that delegate to a + * set of <em>candidate</em> SmartContextLoaders (i.e., one that supports XML + * configuration files and one that supports annotated classes) to determine which + * context loader is appropriate for a given test class's configuration. Each + * candidate is given a chance to {@link #processContextConfiguration process} the + * {@link ContextConfigurationAttributes} for each class in the test class hierarchy + * that is annotated with {@link ContextConfiguration @ContextConfiguration}, and + * the candidate that supports the merged, processed configuration will be used to + * actually {@link #loadContext load} the context. + * + * <p>Placing an empty {@code @ContextConfiguration} annotation on a test class signals + * that default resource locations (i.e., XML configuration files) or default + * {@link org.springframework.context.annotation.Configuration configuration classes} + * should be detected. Furthermore, if a specific {@link ContextLoader} or + * {@link SmartContextLoader} is not explicitly declared via + * {@code @ContextConfiguration}, a concrete subclass of + * {@code AbstractDelegatingSmartContextLoader} will be used as the default loader, + * thus providing automatic support for either XML configuration files or annotated + * classes, but not both simultaneously. + * + * <p>As of Spring 3.2, a test class may optionally declare neither XML configuration + * files nor annotated classes and instead declare only {@linkplain + * ContextConfiguration#initializers application context initializers}. In such + * cases, an attempt will still be made to detect defaults, but their absence will + * not result in an exception. + * + * @author Sam Brannen + * @since 3.2 + * @see SmartContextLoader + */ +public abstract class AbstractDelegatingSmartContextLoader implements SmartContextLoader { + + private static final Log logger = LogFactory.getLog(AbstractDelegatingSmartContextLoader.class); + + + /** + * Get the delegate {@code SmartContextLoader} that supports XML configuration files. + */ + protected abstract SmartContextLoader getXmlLoader(); + + /** + * Get the delegate {@code SmartContextLoader} that supports annotated classes. + */ + protected abstract SmartContextLoader getAnnotationConfigLoader(); + + + // --- SmartContextLoader -------------------------------------------------- + + private static String name(SmartContextLoader loader) { + return loader.getClass().getSimpleName(); + } + + private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Delegating to %s to process context configuration %s.", name(loader), + configAttributes)); + } + loader.processContextConfiguration(configAttributes); + } + + private static ApplicationContext delegateLoading(SmartContextLoader loader, MergedContextConfiguration mergedConfig) + throws Exception { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig)); + } + return loader.loadContext(mergedConfig); + } + + private boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) { + if (loader == getAnnotationConfigLoader()) { + return ObjectUtils.isEmpty(mergedConfig.getLocations()) && !ObjectUtils.isEmpty(mergedConfig.getClasses()); + } + else { + return !ObjectUtils.isEmpty(mergedConfig.getLocations()) && ObjectUtils.isEmpty(mergedConfig.getClasses()); + } + } + + /** + * Delegates to candidate {@code SmartContextLoaders} to process the supplied + * {@link ContextConfigurationAttributes}. + * <p>Delegation is based on explicit knowledge of the implementations of the + * default loaders for {@link #getXmlLoader() XML configuration files} and + * {@link #getAnnotationConfigLoader() annotated classes}. Specifically, the + * delegation algorithm is as follows: + * <ul> + * <li>If the resource locations or annotated classes in the supplied + * {@code ContextConfigurationAttributes} are not empty, the appropriate + * candidate loader will be allowed to process the configuration <em>as is</em>, + * without any checks for detection of defaults.</li> + * <li>Otherwise, the XML-based loader will be allowed to process + * the configuration in order to detect default resource locations. If + * the XML-based loader detects default resource locations, + * an {@code info} message will be logged.</li> + * <li>Subsequently, the annotation-based loader will be allowed to + * process the configuration in order to detect default configuration classes. + * If the annotation-based loader detects default configuration + * classes, an {@code info} message will be logged.</li> + * </ul> + * @param configAttributes the context configuration attributes to process + * @throws IllegalArgumentException if the supplied configuration attributes are + * {@code null}, or if the supplied configuration attributes include both + * resource locations and annotated classes + * @throws IllegalStateException if the XML-based loader detects default + * configuration classes; if the annotation-based loader detects default + * resource locations; if neither candidate loader detects defaults for the supplied + * context configuration; or if both candidate loaders detect defaults for the + * supplied context configuration + */ + public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) { + + Assert.notNull(configAttributes, "configAttributes must not be null"); + Assert.isTrue(!(configAttributes.hasLocations() && configAttributes.hasClasses()), String.format( + "Cannot process locations AND classes for context configuration %s; configure one or the other, but not both.", + configAttributes)); + + // If the original locations or classes were not empty, there's no + // need to bother with default detection checks; just let the + // appropriate loader process the configuration. + if (configAttributes.hasLocations()) { + delegateProcessing(getXmlLoader(), configAttributes); + } + else if (configAttributes.hasClasses()) { + delegateProcessing(getAnnotationConfigLoader(), configAttributes); + } + else { + // Else attempt to detect defaults... + + // Let the XML loader process the configuration. + delegateProcessing(getXmlLoader(), configAttributes); + boolean xmlLoaderDetectedDefaults = configAttributes.hasLocations(); + + if (xmlLoaderDetectedDefaults) { + if (logger.isInfoEnabled()) { + logger.info(String.format("%s detected default locations for context configuration %s.", + name(getXmlLoader()), configAttributes)); + } + } + + if (configAttributes.hasClasses()) { + throw new IllegalStateException(String.format( + "%s should NOT have detected default configuration classes for context configuration %s.", + name(getXmlLoader()), configAttributes)); + } + + // Now let the annotation config loader process the configuration. + delegateProcessing(getAnnotationConfigLoader(), configAttributes); + + if (configAttributes.hasClasses()) { + if (logger.isInfoEnabled()) { + logger.info(String.format( + "%s detected default configuration classes for context configuration %s.", + name(getAnnotationConfigLoader()), configAttributes)); + } + } + + if (!xmlLoaderDetectedDefaults && configAttributes.hasLocations()) { + throw new IllegalStateException(String.format( + "%s should NOT have detected default locations for context configuration %s.", + name(getAnnotationConfigLoader()), configAttributes)); + } + + // If neither loader detected defaults and no initializers were declared, + // throw an exception. + if (!configAttributes.hasResources() && ObjectUtils.isEmpty(configAttributes.getInitializers())) { + throw new IllegalStateException(String.format( + "Neither %s nor %s was able to detect defaults, and no ApplicationContextInitializers " + + "were declared for context configuration %s", name(getXmlLoader()), + name(getAnnotationConfigLoader()), configAttributes)); + } + + if (configAttributes.hasLocations() && configAttributes.hasClasses()) { + String message = String.format( + "Configuration error: both default locations AND default configuration classes " + + "were detected for context configuration %s; configure one or the other, but not both.", + configAttributes); + logger.error(message); + throw new IllegalStateException(message); + } + } + } + + /** + * Delegates to an appropriate candidate {@code SmartContextLoader} to load + * an {@link ApplicationContext}. + * <p>Delegation is based on explicit knowledge of the implementations of the + * default loaders for {@link #getXmlLoader() XML configuration files} and + * {@link #getAnnotationConfigLoader() annotated classes}. Specifically, the + * delegation algorithm is as follows: + * <ul> + * <li>If the resource locations in the supplied {@code MergedContextConfiguration} + * are not empty and the annotated classes are empty, + * the XML-based loader will load the {@code ApplicationContext}.</li> + * <li>If the annotated classes in the supplied {@code MergedContextConfiguration} + * are not empty and the resource locations are empty, + * the annotation-based loader will load the {@code ApplicationContext}.</li> + * </ul> + * @param mergedConfig the merged context configuration to use to load the application context + * @throws IllegalArgumentException if the supplied merged configuration is {@code null} + * @throws IllegalStateException if neither candidate loader is capable of loading an + * {@code ApplicationContext} from the supplied merged context configuration + */ + public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { + Assert.notNull(mergedConfig, "mergedConfig must not be null"); + List<SmartContextLoader> candidates = Arrays.asList(getXmlLoader(), getAnnotationConfigLoader()); + + for (SmartContextLoader loader : candidates) { + // Determine if each loader can load a context from the mergedConfig. If it + // can, let it; otherwise, keep iterating. + if (supports(loader, mergedConfig)) { + return delegateLoading(loader, mergedConfig); + } + } + + // If neither of the candidates supports the mergedConfig based on resources but + // ACIs were declared, then delegate to the annotation config loader. + if (!mergedConfig.getContextInitializerClasses().isEmpty()) { + return delegateLoading(getAnnotationConfigLoader(), mergedConfig); + } + + throw new IllegalStateException(String.format( + "Neither %s nor %s was able to load an ApplicationContext from %s.", name(getXmlLoader()), + name(getAnnotationConfigLoader()), mergedConfig)); + } + + + // --- ContextLoader ------------------------------------------------------- + + /** + * {@code AbstractDelegatingSmartContextLoader} does not support the + * {@link ContextLoader#processLocations(Class, String...)} method. Call + * {@link #processContextConfiguration(ContextConfigurationAttributes)} instead. + * @throws UnsupportedOperationException + */ + public final String[] processLocations(Class<?> clazz, String... locations) { + throw new UnsupportedOperationException("DelegatingSmartContextLoaders do not support the ContextLoader SPI. " + + "Call processContextConfiguration(ContextConfigurationAttributes) instead."); + } + + /** + * {@code AbstractDelegatingSmartContextLoader} does not support the + * {@link ContextLoader#loadContext(String...) } method. Call + * {@link #loadContext(MergedContextConfiguration)} instead. + * @throws UnsupportedOperationException + */ + public final ApplicationContext loadContext(String... locations) throws Exception { + throw new UnsupportedOperationException("DelegatingSmartContextLoaders do not support the ContextLoader SPI. " + + "Call loadContext(MergedContextConfiguration) instead."); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java new file mode 100644 index 00000000..bd84068a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java @@ -0,0 +1,266 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.support; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.StringUtils; + +/** + * Abstract, generic extension of {@link AbstractContextLoader} that loads a + * {@link GenericApplicationContext}. + * + * <ul> + * <li>If instances of concrete subclasses are invoked via the + * {@link org.springframework.test.context.ContextLoader ContextLoader} SPI, the + * context will be loaded from the <em>locations</em> provided to + * {@link #loadContext(String...)}.</li> + * <li>If instances of concrete subclasses are invoked via the + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader} + * SPI, the context will be loaded from the {@link MergedContextConfiguration} + * provided to {@link #loadContext(MergedContextConfiguration)}. In such cases, a + * {@code SmartContextLoader} will decide whether to load the context from + * <em>locations</em> or <em>annotated classes</em>.</li> + * </ul> + * + * <p>Concrete subclasses must provide an appropriate implementation of + * {@link #createBeanDefinitionReader createBeanDefinitionReader()}, + * potentially overriding {@link #loadBeanDefinitions loadBeanDefinitions()} + * as well. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see #loadContext(MergedContextConfiguration) + * @see #loadContext(String...) + */ +public abstract class AbstractGenericContextLoader extends AbstractContextLoader { + + protected static final Log logger = LogFactory.getLog(AbstractGenericContextLoader.class); + + + /** + * Load a Spring ApplicationContext from the supplied {@link MergedContextConfiguration}. + * + * <p>Implementation details: + * + * <ul> + * <li>Creates a {@link GenericApplicationContext} instance.</li> + * <li>If the supplied {@code MergedContextConfiguration} references a + * {@linkplain MergedContextConfiguration#getParent() parent configuration}, + * the corresponding {@link MergedContextConfiguration#getParentApplicationContext() + * ApplicationContext} will be retrieved and + * {@linkplain GenericApplicationContext#setParent(ApplicationContext) set as the parent} + * for the context created by this method.</li> + * <li>Calls {@link #prepareContext(GenericApplicationContext)} for backwards + * compatibility with the {@link org.springframework.test.context.ContextLoader + * ContextLoader} SPI.</li> + * <li>Calls {@link #prepareContext(ConfigurableApplicationContext, MergedContextConfiguration)} + * to allow for customizing the context before bean definitions are loaded.</li> + * <li>Calls {@link #customizeBeanFactory(DefaultListableBeanFactory)} to allow for customizing the + * context's {@code DefaultListableBeanFactory}.</li> + * <li>Delegates to {@link #loadBeanDefinitions(GenericApplicationContext, MergedContextConfiguration)} + * to populate the context from the locations or classes in the supplied + * {@code MergedContextConfiguration}.</li> + * <li>Delegates to {@link AnnotationConfigUtils} for + * {@link AnnotationConfigUtils#registerAnnotationConfigProcessors registering} + * annotation configuration processors.</li> + * <li>Calls {@link #customizeContext(GenericApplicationContext)} to allow for customizing the context + * before it is refreshed.</li> + * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the + * context and registers a JVM shutdown hook for it.</li> + * </ul> + * + * @return a new application context + * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration) + * @see GenericApplicationContext + * @since 3.1 + */ + public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Loading ApplicationContext for merged context configuration [%s].", + mergedConfig)); + } + + GenericApplicationContext context = new GenericApplicationContext(); + + ApplicationContext parent = mergedConfig.getParentApplicationContext(); + if (parent != null) { + context.setParent(parent); + } + prepareContext(context); + prepareContext(context, mergedConfig); + customizeBeanFactory(context.getDefaultListableBeanFactory()); + loadBeanDefinitions(context, mergedConfig); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + customizeContext(context); + context.refresh(); + context.registerShutdownHook(); + return context; + } + + /** + * Load a Spring ApplicationContext from the supplied {@code locations}. + * + * <p>Implementation details: + * + * <ul> + * <li>Creates a {@link GenericApplicationContext} instance.</li> + * <li>Calls {@link #prepareContext(GenericApplicationContext)} to allow for customizing the context + * before bean definitions are loaded.</li> + * <li>Calls {@link #customizeBeanFactory(DefaultListableBeanFactory)} to allow for customizing the + * context's {@code DefaultListableBeanFactory}.</li> + * <li>Delegates to {@link #createBeanDefinitionReader(GenericApplicationContext)} to create a + * {@link BeanDefinitionReader} which is then used to populate the context + * from the specified locations.</li> + * <li>Delegates to {@link AnnotationConfigUtils} for + * {@link AnnotationConfigUtils#registerAnnotationConfigProcessors registering} + * annotation configuration processors.</li> + * <li>Calls {@link #customizeContext(GenericApplicationContext)} to allow for customizing the context + * before it is refreshed.</li> + * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the + * context and registers a JVM shutdown hook for it.</li> + * </ul> + * + * <p><b>Note</b>: this method does not provide a means to set active bean definition + * profiles for the loaded context. See {@link #loadContext(MergedContextConfiguration)} + * and {@link AbstractContextLoader#prepareContext(ConfigurableApplicationContext, MergedContextConfiguration)} + * for an alternative. + * + * @return a new application context + * @see org.springframework.test.context.ContextLoader#loadContext + * @see GenericApplicationContext + * @see #loadContext(MergedContextConfiguration) + * @since 2.5 + */ + public final ConfigurableApplicationContext loadContext(String... locations) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Loading ApplicationContext for locations [%s].", + StringUtils.arrayToCommaDelimitedString(locations))); + } + GenericApplicationContext context = new GenericApplicationContext(); + prepareContext(context); + customizeBeanFactory(context.getDefaultListableBeanFactory()); + createBeanDefinitionReader(context).loadBeanDefinitions(locations); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + customizeContext(context); + context.refresh(); + context.registerShutdownHook(); + return context; + } + + /** + * Prepare the {@link GenericApplicationContext} created by this {@code ContextLoader}. + * Called <i>before</i> bean definitions are read. + * + * <p>The default implementation is empty. Can be overridden in subclasses to + * customize {@code GenericApplicationContext}'s standard settings. + * + * @param context the context that should be prepared + * @see #loadContext(MergedContextConfiguration) + * @see #loadContext(String...) + * @see GenericApplicationContext#setAllowBeanDefinitionOverriding + * @see GenericApplicationContext#setResourceLoader + * @see GenericApplicationContext#setId + * @since 2.5 + */ + protected void prepareContext(GenericApplicationContext context) { + } + + /** + * Customize the internal bean factory of the ApplicationContext created by + * this {@code ContextLoader}. + * + * <p>The default implementation is empty but can be overridden in subclasses + * to customize {@code DefaultListableBeanFactory}'s standard settings. + * + * @param beanFactory the bean factory created by this {@code ContextLoader} + * @see #loadContext(MergedContextConfiguration) + * @see #loadContext(String...) + * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see DefaultListableBeanFactory#setAllowEagerClassLoading + * @see DefaultListableBeanFactory#setAllowCircularReferences + * @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping + * @since 2.5 + */ + protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { + } + + /** + * Load bean definitions into the supplied {@link GenericApplicationContext context} + * from the locations or classes in the supplied {@code MergedContextConfiguration}. + * + * <p>The default implementation delegates to the {@link BeanDefinitionReader} + * returned by {@link #createBeanDefinitionReader(GenericApplicationContext)} to + * {@link BeanDefinitionReader#loadBeanDefinitions(String) load} the + * bean definitions. + * + * <p>Subclasses must provide an appropriate implementation of + * {@link #createBeanDefinitionReader(GenericApplicationContext)}. Alternatively subclasses + * may provide a <em>no-op</em> implementation of {@code createBeanDefinitionReader()} + * and override this method to provide a custom strategy for loading or + * registering bean definitions. + * + * @param context the context into which the bean definitions should be loaded + * @param mergedConfig the merged context configuration + * @see #loadContext(MergedContextConfiguration) + * @since 3.1 + */ + protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) { + createBeanDefinitionReader(context).loadBeanDefinitions(mergedConfig.getLocations()); + } + + /** + * Factory method for creating a new {@link BeanDefinitionReader} for loading + * bean definitions into the supplied {@link GenericApplicationContext context}. + * + * @param context the context for which the {@code BeanDefinitionReader} + * should be created + * @return a {@code BeanDefinitionReader} for the supplied context + * @see #loadContext(String...) + * @see #loadBeanDefinitions + * @see BeanDefinitionReader + * @since 2.5 + */ + protected abstract BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context); + + /** + * Customize the {@link GenericApplicationContext} created by this + * {@code ContextLoader} <i>after</i> bean definitions have been + * loaded into the context but <i>before</i> the context is refreshed. + * + * <p>The default implementation is empty but can be overridden in subclasses + * to customize the application context. + * + * @param context the newly created application context + * @see #loadContext(MergedContextConfiguration) + * @see #loadContext(String...) + * @since 2.5 + */ + protected void customizeContext(GenericApplicationContext context) { + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java new file mode 100644 index 00000000..34dfe47e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestExecutionListener.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; + +/** + * Abstract implementation of the {@link TestExecutionListener} interface which + * provides empty method stubs. Subclasses can extend this class and override + * only those methods suitable for the task at hand. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + */ +public abstract class AbstractTestExecutionListener implements TestExecutionListener { + + /** + * The default implementation is <em>empty</em>. Can be overridden by + * subclasses as necessary. + */ + public void beforeTestClass(TestContext testContext) throws Exception { + /* no-op */ + } + + /** + * The default implementation is <em>empty</em>. Can be overridden by + * subclasses as necessary. + */ + public void prepareTestInstance(TestContext testContext) throws Exception { + /* no-op */ + } + + /** + * The default implementation is <em>empty</em>. Can be overridden by + * subclasses as necessary. + */ + public void beforeTestMethod(TestContext testContext) throws Exception { + /* no-op */ + } + + /** + * The default implementation is <em>empty</em>. Can be overridden by + * subclasses as necessary. + */ + public void afterTestMethod(TestContext testContext) throws Exception { + /* no-op */ + } + + /** + * The default implementation is <em>empty</em>. Can be overridden by + * subclasses as necessary. + */ + public void afterTestClass(TestContext testContext) throws Exception { + /* no-op */ + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java new file mode 100644 index 00000000..39e76f6e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.ObjectUtils; + +/** + * Concrete implementation of {@link AbstractGenericContextLoader} that loads + * bean definitions from annotated classes. + * + * <p>See the Javadoc for + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration} + * for a definition of <em>annotated class</em>. + * + * <p>Note: {@code AnnotationConfigContextLoader} supports <em>annotated classes</em> + * rather than the String-based resource locations defined by the legacy + * {@link org.springframework.test.context.ContextLoader ContextLoader} API. Thus, + * although {@code AnnotationConfigContextLoader} extends + * {@code AbstractGenericContextLoader}, {@code AnnotationConfigContextLoader} + * does <em>not</em> support any String-based methods defined by + * {@code AbstractContextLoader} or {@code AbstractGenericContextLoader}. + * Consequently, {@code AnnotationConfigContextLoader} should chiefly be + * considered a {@link org.springframework.test.context.SmartContextLoader SmartContextLoader} + * rather than a {@link org.springframework.test.context.ContextLoader ContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + * @see #processContextConfiguration(ContextConfigurationAttributes) + * @see #detectDefaultConfigurationClasses(Class) + * @see #loadBeanDefinitions(GenericApplicationContext, MergedContextConfiguration) + */ +public class AnnotationConfigContextLoader extends AbstractGenericContextLoader { + + private static final Log logger = LogFactory.getLog(AnnotationConfigContextLoader.class); + + + // --- SmartContextLoader ----------------------------------------------- + + /** + * Process <em>annotated classes</em> in the supplied {@link ContextConfigurationAttributes}. + * + * <p>If the <em>annotated classes</em> are {@code null} or empty and + * {@link #isGenerateDefaultLocations()} returns {@code true}, this + * {@code SmartContextLoader} will attempt to {@link + * #detectDefaultConfigurationClasses detect default configuration classes}. + * If defaults are detected they will be + * {@link ContextConfigurationAttributes#setClasses(Class[]) set} in the + * supplied configuration attributes. Otherwise, properties in the supplied + * configuration attributes will not be modified. + * + * @param configAttributes the context configuration attributes to process + * @see org.springframework.test.context.SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes) + * @see #isGenerateDefaultLocations() + * @see #detectDefaultConfigurationClasses(Class) + */ + public void processContextConfiguration(ContextConfigurationAttributes configAttributes) { + if (ObjectUtils.isEmpty(configAttributes.getClasses()) && isGenerateDefaultLocations()) { + Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses(configAttributes.getDeclaringClass()); + configAttributes.setClasses(defaultConfigClasses); + } + } + + // --- AnnotationConfigContextLoader --------------------------------------- + + /** + * Detect the default configuration classes for the supplied test class. + * + * <p>The default implementation simply delegates to + * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses(Class)}. + * + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @return an array of default configuration classes, potentially empty but + * never {@code null} + * @see AnnotationConfigContextLoaderUtils + */ + protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) { + return AnnotationConfigContextLoaderUtils.detectDefaultConfigurationClasses(declaringClass); + } + + // --- AbstractContextLoader ----------------------------------------------- + + /** + * {@code AnnotationConfigContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see AbstractContextLoader#modifyLocations + * @throws UnsupportedOperationException + */ + @Override + protected String[] modifyLocations(Class<?> clazz, String... locations) { + throw new UnsupportedOperationException( + "AnnotationConfigContextLoader does not support the modifyLocations(Class, String...) method"); + } + + /** + * {@code AnnotationConfigContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see AbstractContextLoader#generateDefaultLocations + * @throws UnsupportedOperationException + */ + @Override + protected String[] generateDefaultLocations(Class<?> clazz) { + throw new UnsupportedOperationException( + "AnnotationConfigContextLoader does not support the generateDefaultLocations(Class) method"); + } + + /** + * {@code AnnotationConfigContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see AbstractContextLoader#getResourceSuffix + * @throws UnsupportedOperationException + */ + @Override + protected String getResourceSuffix() { + throw new UnsupportedOperationException( + "AnnotationConfigContextLoader does not support the getResourceSuffix() method"); + } + + // --- AbstractGenericContextLoader ---------------------------------------- + + /** + * Register classes in the supplied {@link GenericApplicationContext context} + * from the classes in the supplied {@link MergedContextConfiguration}. + * + * <p>Each class must represent an <em>annotated class</em>. An + * {@link AnnotatedBeanDefinitionReader} is used to register the appropriate + * bean definitions. + * + * <p>Note that this method does not call {@link #createBeanDefinitionReader} + * since {@code AnnotatedBeanDefinitionReader} is not an instance of + * {@link BeanDefinitionReader}. + * + * @param context the context in which the annotated classes should be registered + * @param mergedConfig the merged configuration from which the classes should be retrieved + * + * @see AbstractGenericContextLoader#loadBeanDefinitions + */ + @Override + protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) { + Class<?>[] annotatedClasses = mergedConfig.getClasses(); + if (logger.isDebugEnabled()) { + logger.debug("Registering annotated classes: " + ObjectUtils.nullSafeToString(annotatedClasses)); + } + new AnnotatedBeanDefinitionReader(context).register(annotatedClasses); + } + + /** + * {@code AnnotationConfigContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see #loadBeanDefinitions + * @see AbstractGenericContextLoader#createBeanDefinitionReader + * @throws UnsupportedOperationException + */ + @Override + protected BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context) { + throw new UnsupportedOperationException( + "AnnotationConfigContextLoader does not support the createBeanDefinitionReader(GenericApplicationContext) method"); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java new file mode 100644 index 00000000..0cdbe30f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.support; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.SmartContextLoader; +import org.springframework.util.Assert; + +/** + * Utility methods for {@link SmartContextLoader SmartContextLoaders} that deal + * with annotated classes (e.g., {@link Configuration @Configuration} classes). + * + * @author Sam Brannen + * @since 3.2 + */ +public abstract class AnnotationConfigContextLoaderUtils { + + private static final Log logger = LogFactory.getLog(AnnotationConfigContextLoaderUtils.class); + + + /** + * Determine if the supplied {@link Class} meets the criteria for being + * considered a <em>default configuration class</em> candidate. + * <p>Specifically, such candidates: + * <ul> + * <li>must not be {@code null}</li> + * <li>must not be {@code private}</li> + * <li>must not be {@code final}</li> + * <li>must be {@code static}</li> + * <li>must be annotated with {@code @Configuration}</li> + * </ul> + * @param clazz the class to check + * @return {@code true} if the supplied class meets the candidate criteria + */ + private static boolean isDefaultConfigurationClassCandidate(Class<?> clazz) { + return clazz != null && isStaticNonPrivateAndNonFinal(clazz) && clazz.isAnnotationPresent(Configuration.class); + } + + private static boolean isStaticNonPrivateAndNonFinal(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + int modifiers = clazz.getModifiers(); + return (Modifier.isStatic(modifiers) && !Modifier.isPrivate(modifiers) && !Modifier.isFinal(modifiers)); + } + + /** + * Detect the default configuration classes for the supplied test class. + * <p>The returned class array will contain all static inner classes of + * the supplied class that meet the requirements for {@code @Configuration} + * class implementations as specified in the documentation for + * {@link Configuration @Configuration}. + * <p>The implementation of this method adheres to the contract defined in the + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader} + * SPI. Specifically, this method uses introspection to detect default + * configuration classes that comply with the constraints required of + * {@code @Configuration} class implementations. If a potential candidate + * configuration class does not meet these requirements, this method will log a + * debug message, and the potential candidate class will be ignored. + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @return an array of default configuration classes, potentially empty but + * never {@code null} + */ + public static Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) { + Assert.notNull(declaringClass, "Declaring class must not be null"); + List<Class<?>> configClasses = new ArrayList<Class<?>>(); + + for (Class<?> candidate : declaringClass.getDeclaredClasses()) { + if (isDefaultConfigurationClassCandidate(candidate)) { + configClasses.add(candidate); + } + else { + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "Ignoring class [%s]; it must be static, non-private, non-final, and annotated " + + "with @Configuration to be considered a default configuration class.", + candidate.getName())); + } + } + } + + if (configClasses.isEmpty()) { + if (logger.isInfoEnabled()) { + logger.info(String.format("Could not detect default configuration classes for test class [%s]: " + + "%s does not declare any static, non-private, non-final, inner classes " + + "annotated with @Configuration.", declaringClass.getName(), declaringClass.getSimpleName())); + } + } + + return configClasses.toArray(new Class<?>[configClasses.size()]); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java new file mode 100644 index 00000000..32442fe3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.test.context.SmartContextLoader; + +/** + * {@code DelegatingSmartContextLoader} is a concrete implementation of + * {@link AbstractDelegatingSmartContextLoader} that delegates to a + * {@link GenericXmlContextLoader} and an {@link AnnotationConfigContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + * @see SmartContextLoader + * @see AbstractDelegatingSmartContextLoader + * @see GenericXmlContextLoader + * @see AnnotationConfigContextLoader + */ +public class DelegatingSmartContextLoader extends AbstractDelegatingSmartContextLoader { + + private final SmartContextLoader xmlLoader = new GenericXmlContextLoader(); + private final SmartContextLoader annotationConfigLoader = new AnnotationConfigContextLoader(); + + + protected SmartContextLoader getXmlLoader() { + return this.xmlLoader; + } + + protected SmartContextLoader getAnnotationConfigLoader() { + return this.annotationConfigLoader; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java new file mode 100644 index 00000000..cb852237 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.core.Conventions; +import org.springframework.test.context.TestContext; + +/** + * {@code TestExecutionListener} which provides support for dependency + * injection and initialization of test instances. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + */ +public class DependencyInjectionTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Attribute name for a {@link TestContext} attribute which indicates + * whether or not the dependencies of a test instance should be + * <em>reinjected</em> in + * {@link #beforeTestMethod(TestContext) beforeTestMethod()}. Note that + * dependencies will be injected in + * {@link #prepareTestInstance(TestContext) prepareTestInstance()} in any + * case. + * <p>Clients of a {@link TestContext} (e.g., other + * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners}) + * may therefore choose to set this attribute to signal that dependencies + * should be reinjected <em>between</em> execution of individual test + * methods. + * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. + */ + public static final String REINJECT_DEPENDENCIES_ATTRIBUTE = Conventions.getQualifiedAttributeName( + DependencyInjectionTestExecutionListener.class, "reinjectDependencies"); + + private static final Log logger = LogFactory.getLog(DependencyInjectionTestExecutionListener.class); + + + /** + * Performs dependency injection on the + * {@link TestContext#getTestInstance() test instance} of the supplied + * {@link TestContext test context} by + * {@link AutowireCapableBeanFactory#autowireBeanProperties(Object, int, boolean) autowiring} + * and + * {@link AutowireCapableBeanFactory#initializeBean(Object, String) initializing} + * the test instance via its own + * {@link TestContext#getApplicationContext() application context} (without + * checking dependencies). + * <p>The {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} will be subsequently removed + * from the test context, regardless of its value. + */ + @Override + public void prepareTestInstance(final TestContext testContext) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Performing dependency injection for test context [" + testContext + "]."); + } + injectDependencies(testContext); + } + + /** + * If the {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} in the supplied + * {@link TestContext test context} has a value of {@link Boolean#TRUE}, + * this method will have the same effect as + * {@link #prepareTestInstance(TestContext) prepareTestInstance()}; + * otherwise, this method will have no effect. + */ + @Override + public void beforeTestMethod(final TestContext testContext) throws Exception { + if (Boolean.TRUE.equals(testContext.getAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE))) { + if (logger.isDebugEnabled()) { + logger.debug("Reinjecting dependencies for test context [" + testContext + "]."); + } + injectDependencies(testContext); + } + } + + /** + * Performs dependency injection and bean initialization for the supplied + * {@link TestContext} as described in + * {@link #prepareTestInstance(TestContext) prepareTestInstance()}. + * <p>The {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} will be subsequently removed + * from the test context, regardless of its value. + * @param testContext the test context for which dependency injection should + * be performed (never {@code null}) + * @throws Exception allows any exception to propagate + * @see #prepareTestInstance(TestContext) + * @see #beforeTestMethod(TestContext) + */ + protected void injectDependencies(final TestContext testContext) throws Exception { + Object bean = testContext.getTestInstance(); + AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory(); + beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false); + beanFactory.initializeBean(bean, testContext.getTestClass().getName()); + testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java new file mode 100644 index 00000000..f2a45a46 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.support; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.TestContext; +import org.springframework.util.Assert; + +/** + * {@code TestExecutionListener} which provides support for marking the + * {@code ApplicationContext} associated with a test as <em>dirty</em> for + * both test classes and test methods configured with the {@link DirtiesContext + * @DirtiesContext} annotation. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see DirtiesContext + */ +public class DirtiesContextTestExecutionListener extends AbstractTestExecutionListener { + + private static final Log logger = LogFactory.getLog(DirtiesContextTestExecutionListener.class); + + + /** + * Marks the {@linkplain ApplicationContext application context} of the supplied + * {@linkplain TestContext test context} as + * {@linkplain TestContext#markApplicationContextDirty() dirty}, and sets the + * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * in the test context to {@code true}. + * @param testContext the test context whose application context should + * marked as dirty + * @deprecated as of Spring 3.2.2, use {@link #dirtyContext(TestContext, HierarchyMode)} instead. + */ + @Deprecated + protected void dirtyContext(TestContext testContext) { + testContext.markApplicationContextDirty(); + testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, Boolean.TRUE); + } + + /** + * Marks the {@linkplain ApplicationContext application context} of the supplied + * {@linkplain TestContext test context} as + * {@linkplain TestContext#markApplicationContextDirty(DirtiesContext.HierarchyMode) dirty} + * and sets {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * in the test context to {@code true}. + * @param testContext the test context whose application context should + * marked as dirty + * @param hierarchyMode the context cache clearing mode to be applied if the + * context is part of a hierarchy; may be {@code null} + * @since 3.2.2 + */ + protected void dirtyContext(TestContext testContext, HierarchyMode hierarchyMode) { + testContext.markApplicationContextDirty(hierarchyMode); + testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, Boolean.TRUE); + } + + /** + * If the current test method of the supplied {@linkplain TestContext test + * context} is annotated with {@link DirtiesContext @DirtiesContext}, + * or if the test class is annotated with {@link DirtiesContext + * @DirtiesContext} and the {@linkplain DirtiesContext#classMode() class + * mode} is set to {@link ClassMode#AFTER_EACH_TEST_METHOD + * AFTER_EACH_TEST_METHOD}, the {@linkplain ApplicationContext application + * context} of the test context will be + * {@linkplain TestContext#markApplicationContextDirty() marked as dirty} and the + * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * in the test context will be set to {@code true}. + */ + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + Class<?> testClass = testContext.getTestClass(); + Assert.notNull(testClass, "The test class of the supplied TestContext must not be null"); + Method testMethod = testContext.getTestMethod(); + Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null"); + + final Class<DirtiesContext> annotationType = DirtiesContext.class; + + boolean methodDirtiesContext = testMethod.isAnnotationPresent(annotationType); + boolean classDirtiesContext = testClass.isAnnotationPresent(annotationType); + DirtiesContext classDirtiesContextAnnotation = testClass.getAnnotation(annotationType); + ClassMode classMode = classDirtiesContext ? classDirtiesContextAnnotation.classMode() : null; + + if (logger.isDebugEnabled()) { + logger.debug("After test method: context [" + testContext + "], class dirties context [" + + classDirtiesContext + "], class mode [" + classMode + "], method dirties context [" + + methodDirtiesContext + "]."); + } + + if (methodDirtiesContext || (classDirtiesContext && classMode == ClassMode.AFTER_EACH_TEST_METHOD)) { + HierarchyMode hierarchyMode = methodDirtiesContext ? testMethod.getAnnotation(annotationType).hierarchyMode() + : classDirtiesContextAnnotation.hierarchyMode(); + dirtyContext(testContext, hierarchyMode); + } + } + + /** + * If the test class of the supplied {@linkplain TestContext test context} is + * annotated with {@link DirtiesContext @DirtiesContext}, the + * {@linkplain ApplicationContext application context} of the test context will + * be {@linkplain TestContext#markApplicationContextDirty() marked as dirty} , + * and the + * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE + * REINJECT_DEPENDENCIES_ATTRIBUTE} in the test context will be set to + * {@code true}. + */ + @Override + public void afterTestClass(TestContext testContext) throws Exception { + Class<?> testClass = testContext.getTestClass(); + Assert.notNull(testClass, "The test class of the supplied TestContext must not be null"); + + final Class<DirtiesContext> annotationType = DirtiesContext.class; + + boolean dirtiesContext = testClass.isAnnotationPresent(annotationType); + if (logger.isDebugEnabled()) { + logger.debug("After test class: context [" + testContext + "], dirtiesContext [" + dirtiesContext + "]."); + } + if (dirtiesContext) { + HierarchyMode hierarchyMode = testClass.getAnnotation(annotationType).hierarchyMode(); + dirtyContext(testContext, hierarchyMode); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java new file mode 100644 index 00000000..82368879 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/GenericPropertiesContextLoader.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import java.util.Properties; + +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; + +/** + * Concrete implementation of {@link AbstractGenericContextLoader} that reads + * bean definitions from Java {@link Properties} resources. + * + * @author Sam Brannen + * @since 2.5 + */ +public class GenericPropertiesContextLoader extends AbstractGenericContextLoader { + + /** + * Creates a new {@link PropertiesBeanDefinitionReader}. + * @return a new PropertiesBeanDefinitionReader + * @see PropertiesBeanDefinitionReader + */ + @Override + protected BeanDefinitionReader createBeanDefinitionReader(final GenericApplicationContext context) { + return new PropertiesBeanDefinitionReader(context); + } + + /** + * Returns "{@code -context.properties}". + */ + @Override + public String getResourceSuffix() { + return "-context.properties"; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java new file mode 100644 index 00000000..9c636e0e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; + +/** + * Concrete implementation of {@link AbstractGenericContextLoader} that reads + * bean definitions from XML resources. + * + * @author Sam Brannen + * @since 2.5 + */ +public class GenericXmlContextLoader extends AbstractGenericContextLoader { + + /** + * Create a new {@link XmlBeanDefinitionReader}. + * @return a new XmlBeanDefinitionReader + * @see XmlBeanDefinitionReader + */ + @Override + protected BeanDefinitionReader createBeanDefinitionReader(final GenericApplicationContext context) { + return new XmlBeanDefinitionReader(context); + } + + /** + * Returns "{@code -context.xml}". + */ + @Override + public String getResourceSuffix() { + return "-context.xml"; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/package-info.java b/spring-test/src/main/java/org/springframework/test/context/support/package-info.java new file mode 100644 index 00000000..0212c4a0 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/package-info.java @@ -0,0 +1,6 @@ +/** + * <p>Support classes for the <em>Spring TestContext Framework</em>.</p> + */ + +package org.springframework.test.context.support; + diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java new file mode 100644 index 00000000..80c74a5b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.testng; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.springframework.test.context.web.ServletTestExecutionListener; + +import org.testng.IHookCallBack; +import org.testng.IHookable; +import org.testng.ITestResult; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; + +/** + * Abstract base test class which integrates the <em>Spring TestContext Framework</em> + * with explicit {@link ApplicationContext} testing support in a <strong>TestNG</strong> + * environment. + * + * <p>Concrete subclasses: + * <ul> + * <li>Typically declare a class-level {@link ContextConfiguration + * @ContextConfiguration} annotation to configure the {@link ApplicationContext + * application context} {@link ContextConfiguration#locations() resource locations} + * or {@link ContextConfiguration#classes() annotated classes}. <em>If your test + * does not need to load an application context, you may choose to omit the + * {@link ContextConfiguration @ContextConfiguration} declaration and to + * configure the appropriate + * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners} + * manually.</em></li> + * <li>Must have constructors which either implicitly or explicitly delegate to + * {@code super();}.</li> + * </ul> + * + * <p>The following {@link org.springframework.test.context.TestExecutionListener + * TestExecutionListeners} are configured by default: + * + * <ul> + * <li>{@link org.springframework.test.context.web.ServletTestExecutionListener} + * <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener} + * <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener} + * </ul> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ContextConfiguration + * @see TestContext + * @see TestContextManager + * @see TestExecutionListeners + * @see ServletTestExecutionListener + * @see DependencyInjectionTestExecutionListener + * @see DirtiesContextTestExecutionListener + * @see AbstractTransactionalTestNGSpringContextTests + * @see org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests + */ +@TestExecutionListeners({ ServletTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class }) +public abstract class AbstractTestNGSpringContextTests implements IHookable, ApplicationContextAware { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The {@link ApplicationContext} that was injected into this test instance + * via {@link #setApplicationContext(ApplicationContext)}. + */ + protected ApplicationContext applicationContext; + + private final TestContextManager testContextManager; + + private Throwable testException; + + + /** + * Construct a new AbstractTestNGSpringContextTests instance and initialize + * the internal {@link TestContextManager} for the current test. + */ + public AbstractTestNGSpringContextTests() { + this.testContextManager = new TestContextManager(getClass()); + } + + /** + * Set the {@link ApplicationContext} to be used by this test instance, + * provided via {@link ApplicationContextAware} semantics. + * + * @param applicationContext the applicationContext to set + */ + public final void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Delegates to the configured {@link TestContextManager} to call + * {@link TestContextManager#beforeTestClass() 'before test class'} + * callbacks. + * + * @throws Exception if a registered TestExecutionListener throws an + * exception + */ + @BeforeClass(alwaysRun = true) + protected void springTestContextBeforeTestClass() throws Exception { + this.testContextManager.beforeTestClass(); + } + + /** + * Delegates to the configured {@link TestContextManager} to + * {@link TestContextManager#prepareTestInstance(Object) prepare} this test + * instance prior to execution of any individual tests, for example for + * injecting dependencies, etc. + * + * @throws Exception if a registered TestExecutionListener throws an + * exception + */ + @BeforeClass(alwaysRun = true, dependsOnMethods = "springTestContextBeforeTestClass") + protected void springTestContextPrepareTestInstance() throws Exception { + this.testContextManager.prepareTestInstance(this); + } + + /** + * Delegates to the configured {@link TestContextManager} to + * {@link TestContextManager#beforeTestMethod(Object,Method) pre-process} + * the test method before the actual test is executed. + * + * @param testMethod the test method which is about to be executed. + * @throws Exception allows all exceptions to propagate. + */ + @BeforeMethod(alwaysRun = true) + protected void springTestContextBeforeTestMethod(Method testMethod) throws Exception { + this.testContextManager.beforeTestMethod(this, testMethod); + } + + /** + * Delegates to the {@link IHookCallBack#runTestMethod(ITestResult) test + * method} in the supplied {@code callback} to execute the actual test + * and then tracks the exception thrown during test execution, if any. + * + * @see org.testng.IHookable#run(org.testng.IHookCallBack, + * org.testng.ITestResult) + */ + public void run(IHookCallBack callBack, ITestResult testResult) { + callBack.runTestMethod(testResult); + + Throwable testResultException = testResult.getThrowable(); + if (testResultException instanceof InvocationTargetException) { + testResultException = ((InvocationTargetException) testResultException).getCause(); + } + this.testException = testResultException; + } + + /** + * Delegates to the configured {@link TestContextManager} to + * {@link TestContextManager#afterTestMethod(Object, Method, Throwable) + * post-process} the test method after the actual test has executed. + * + * @param testMethod the test method which has just been executed on the + * test instance + * @throws Exception allows all exceptions to propagate + */ + @AfterMethod(alwaysRun = true) + protected void springTestContextAfterTestMethod(Method testMethod) throws Exception { + try { + this.testContextManager.afterTestMethod(this, testMethod, this.testException); + } + finally { + this.testException = null; + } + } + + /** + * Delegates to the configured {@link TestContextManager} to call + * {@link TestContextManager#afterTestClass() 'after test class'} callbacks. + * + * @throws Exception if a registered TestExecutionListener throws an + * exception + */ + @AfterClass(alwaysRun = true) + protected void springTestContextAfterTestClass() throws Exception { + this.testContextManager.afterTestClass(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java new file mode 100644 index 00000000..41e70a70 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.testng; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.jdbc.JdbcTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * Abstract {@linkplain Transactional transactional} extension of + * {@link AbstractTestNGSpringContextTests} which adds convenience functionality + * for JDBC access. Expects a {@link DataSource} bean and a + * {@link PlatformTransactionManager} bean to be defined in the Spring + * {@linkplain ApplicationContext application context}. + * + * <p>This class exposes a {@link JdbcTemplate} and provides an easy way to + * {@linkplain #countRowsInTable count the number of rows in a table} + * (potentially {@linkplain #countRowsInTableWhere with a WHERE clause}), + * {@linkplain #deleteFromTables delete from tables}, + * {@linkplain #dropTables drop tables}, and + * {@linkplain #executeSqlScript execute SQL scripts} within a transaction. + * + * <p>Concrete subclasses must fulfill the same requirements outlined in + * {@link AbstractTestNGSpringContextTests}. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractTestNGSpringContextTests + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.TestExecutionListeners + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + * @see org.springframework.test.context.transaction.TransactionConfiguration + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see org.springframework.test.context.transaction.BeforeTransaction + * @see org.springframework.test.context.transaction.AfterTransaction + * @see org.springframework.test.jdbc.JdbcTestUtils + * @see org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests + */ +@TestExecutionListeners(TransactionalTestExecutionListener.class) +@Transactional +@SuppressWarnings("deprecation") +public abstract class AbstractTransactionalTestNGSpringContextTests extends AbstractTestNGSpringContextTests { + + /** + * The {@code SimpleJdbcTemplate} that this base class manages, available to subclasses. + * @deprecated As of Spring 3.2, use {@link #jdbcTemplate} instead. + */ + @Deprecated + protected SimpleJdbcTemplate simpleJdbcTemplate; + + /** + * The {@code JdbcTemplate} that this base class manages, available to subclasses. + * @since 3.2 + */ + protected JdbcTemplate jdbcTemplate; + + private String sqlScriptEncoding; + + + /** + * Set the {@code DataSource}, typically provided via Dependency Injection. + * <p>This method also instantiates the {@link #simpleJdbcTemplate} and + * {@link #jdbcTemplate} instance variables. + */ + @Autowired + public void setDataSource(DataSource dataSource) { + this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Specify the encoding for SQL scripts, if different from the platform encoding. + * @see #executeSqlScript + */ + public void setSqlScriptEncoding(String sqlScriptEncoding) { + this.sqlScriptEncoding = sqlScriptEncoding; + } + + /** + * Count the rows in the given table. + * @param tableName table name to count rows in + * @return the number of rows in the table + */ + protected int countRowsInTable(String tableName) { + return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); + } + + /** + * Count the rows in the given table, using the provided {@code WHERE} clause. + * <p>See the Javadoc for {@link JdbcTestUtils#countRowsInTableWhere} for details. + * @param tableName the name of the table to count rows in + * @param whereClause the {@code WHERE} clause to append to the query + * @return the number of rows in the table that match the provided + * {@code WHERE} clause + * @since 3.2 + */ + protected int countRowsInTableWhere(String tableName, String whereClause) { + return JdbcTestUtils.countRowsInTableWhere(this.jdbcTemplate, tableName, whereClause); + } + + /** + * Convenience method for deleting all rows from the specified tables. Use + * with caution outside of a transaction! + * @param names the names of the tables from which to delete + * @return the total number of rows deleted from all specified tables + */ + protected int deleteFromTables(String... names) { + return JdbcTestUtils.deleteFromTables(this.jdbcTemplate, names); + } + + /** + * Convenience method for dropping all of the specified tables. Use + * with caution outside of a transaction! + * @param names the names of the tables to drop + * @since 3.2 + */ + protected void dropTables(String... names) { + JdbcTestUtils.dropTables(this.jdbcTemplate, names); + } + + /** + * Execute the given SQL script. Use with caution outside of a transaction! + * <p>The script will normally be loaded by classpath. There should be one + * statement per line. Any semicolons will be removed. <b>Do not use this + * method to execute DDL if you expect rollback.</b> + * @param sqlResourcePath the Spring resource path for the SQL script + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and continueOnError was {@code false} + */ + protected void executeSqlScript(String sqlResourcePath, boolean continueOnError) throws DataAccessException { + Resource resource = this.applicationContext.getResource(sqlResourcePath); + JdbcTestUtils.executeSqlScript(this.jdbcTemplate, new EncodedResource(resource, + this.sqlScriptEncoding), continueOnError); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java b/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java new file mode 100644 index 00000000..3d2045a4 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java @@ -0,0 +1,7 @@ +/** + * <p>Support classes for ApplicationContext-based and transactional + * tests run with TestNG and the <em>Spring TestContext Framework</em>.</p> + */ + +package org.springframework.test.context.testng; + diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java new file mode 100644 index 00000000..bb25f1f8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.transaction; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <p> + * Test annotation to indicate that the annotated {@code public void} + * method should be executed <em>after</em> a transaction is ended for test + * methods configured to run within a transaction via the + * {@code @Transactional} annotation. + * </p> + * <p> + * The {@code @AfterTransaction} methods of superclasses will be + * executed after those of the current class. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see org.springframework.transaction.annotation.Transactional + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AfterTransaction { +} diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java new file mode 100644 index 00000000..d3625009 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.transaction; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <p> + * Test annotation to indicate that the annotated {@code public void} + * method should be executed <em>before</em> a transaction is started for test + * methods configured to run within a transaction via the + * {@code @Transactional} annotation. + * </p> + * <p> + * The {@code @BeforeTransaction} methods of superclasses will be + * executed before those of the current class. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see org.springframework.transaction.annotation.Transactional + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface BeforeTransaction { +} diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java new file mode 100644 index 00000000..7d91bcba --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.transaction; + +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 TransactionConfiguration} defines class-level metadata for configuring + * transactional tests. + * + * @author Sam Brannen + * @since 2.5 + * @see TransactionalTestExecutionListener + * @see org.springframework.test.context.ContextConfiguration + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface TransactionConfiguration { + + /** + * The bean name of the {@link org.springframework.transaction.PlatformTransactionManager + * PlatformTransactionManager} that should be used to drive transactions. + * + * <p>This attribute is not required and only needs to be declared if there + * are multiple beans of type {@code PlatformTransactionManager} in the test's + * {@code ApplicationContext} <em>and</em> if one of the following is true. + * <ul> + * <li>the bean name of the desired {@code PlatformTransactionManager} is not + * "transactionManager"</li> + * <li>{@link org.springframework.transaction.annotation.TransactionManagementConfigurer + * TransactionManagementConfigurer} was not implemented to specify which + * {@code PlatformTransactionManager} bean should be used for annotation-driven + * transaction management + * </ul> + * + * <p><b>NOTE:</b> The XML {@code <tx:annotation-driven>} element also refers + * to a bean named "transactionManager" by default. If you are using both + * features in combination, make sure to point to the same transaction manager + * bean - here in {@code @TransactionConfiguration} and also in + * {@code <tx:annotation-driven transaction-manager="...">}. + */ + String transactionManager() default "transactionManager"; + + /** + * Should transactions be rolled back by default? + */ + boolean defaultRollback() default true; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java new file mode 100644 index 00000000..c6124703 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfigurationAttributes.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2008 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 + * + * http://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.transaction; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.Assert; + +/** + * Configuration attributes for configuring transactional tests. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see TransactionConfiguration + */ +public class TransactionConfigurationAttributes { + + private final String transactionManagerName; + + private final boolean defaultRollback; + + + /** + * Construct a new TransactionConfigurationAttributes instance from the + * supplied arguments. + * @param transactionManagerName the bean name of the + * {@link PlatformTransactionManager} that is to be used to drive transactions + * @param defaultRollback whether or not transactions should be rolled back by default + */ + public TransactionConfigurationAttributes(String transactionManagerName, boolean defaultRollback) { + Assert.notNull(transactionManagerName, "transactionManagerName can not be null"); + this.transactionManagerName = transactionManagerName; + this.defaultRollback = defaultRollback; + } + + + /** + * Get the bean name of the {@link PlatformTransactionManager} that is to + * be used to drive transactions. + */ + public final String getTransactionManagerName() { + return this.transactionManagerName; + } + + /** + * Whether or not transactions should be rolled back by default. + * @return the <em>default rollback</em> flag + */ + public final boolean isDefaultRollback() { + return this.defaultRollback; + } + + + @Override + public String toString() { + return new ToStringCreator(this) + .append("transactionManagerName", this.transactionManagerName) + .append("defaultRollback", this.defaultRollback) + .toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java new file mode 100644 index 00000000..5535fa97 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -0,0 +1,596 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.transaction; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.annotation.TransactionManagementConfigurer; +import org.springframework.transaction.interceptor.DelegatingTransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@code TestExecutionListener} that provides support for executing tests + * within transactions by honoring the + * {@link org.springframework.transaction.annotation.Transactional @Transactional} + * and {@link NotTransactional @NotTransactional} annotations. Expects a + * {@link PlatformTransactionManager} bean to be defined in the Spring + * {@link ApplicationContext} for the test. + * + * <p>Changes to the database during a test that is run with {@code @Transactional} + * will be run within a transaction that will, by default, be automatically + * <em>rolled back</em> after completion of the test; whereas, changes to the + * database during a test that is run with {@code @NotTransactional} will + * <strong>not</strong> be run within a transaction. Test methods that are not + * annotated with {@code @Transactional} (at the class or method level) will not + * be run within a transaction. + * + * <p>Transactional commit and rollback behavior can be configured via the + * class-level {@link TransactionConfiguration @TransactionConfiguration} and + * method-level {@link Rollback @Rollback} annotations. + * + * <p>In case there are multiple instances of {@code PlatformTransactionManager} + * within the test's {@code ApplicationContext}, {@code @TransactionConfiguration} + * supports configuring the bean name of the {@code PlatformTransactionManager} + * that should be used to drive transactions. Alternatively, + * {@link TransactionManagementConfigurer} can be implemented in an + * {@link org.springframework.context.annotation.Configuration @Configuration} + * class. + * + * <p>When executing transactional tests, it is sometimes useful to be able to + * execute certain <em>set up</em> or <em>tear down</em> code outside of a + * transaction. {@code TransactionalTestExecutionListener} provides such + * support for methods annotated with + * {@link BeforeTransaction @BeforeTransaction} and + * {@link AfterTransaction @AfterTransaction}. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see TransactionConfiguration + * @see TransactionManagementConfigurer + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see org.springframework.test.annotation.Rollback + * @see BeforeTransaction + * @see AfterTransaction + */ +@SuppressWarnings("deprecation") +public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { + + private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class); + + private static final String DEFAULT_TRANSACTION_MANAGER_NAME = (String) AnnotationUtils.getDefaultValue( + TransactionConfiguration.class, "transactionManager"); + + private static final Boolean DEFAULT_DEFAULT_ROLLBACK = (Boolean) AnnotationUtils.getDefaultValue( + TransactionConfiguration.class, "defaultRollback"); + + protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(); + + private final Map<Method, TransactionContext> transactionContextCache = + new ConcurrentHashMap<Method, TransactionContext>(8); + + private TransactionConfigurationAttributes configurationAttributes; + + private volatile int transactionsStarted = 0; + + + /** + * If the test method of the supplied {@link TestContext test context} is + * configured to run within a transaction, this method will run + * {@link BeforeTransaction @BeforeTransaction methods} and start a new + * transaction. + * <p>Note that if a {@code @BeforeTransaction} method fails, any remaining + * {@code @BeforeTransaction} methods will not be invoked, and a transaction + * will not be started. + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.annotation.NotTransactional + * @see #getTransactionManager(TestContext, String) + */ + @SuppressWarnings("serial") + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + final Method testMethod = testContext.getTestMethod(); + Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null"); + + if (this.transactionContextCache.remove(testMethod) != null) { + throw new IllegalStateException("Cannot start new transaction without ending existing transaction: " + + "Invoke endTransaction() before startNewTransaction()."); + } + + if (testMethod.isAnnotationPresent(NotTransactional.class)) { + return; + } + + PlatformTransactionManager tm = null; + TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, + testContext.getTestClass()); + + if (transactionAttribute != null) { + transactionAttribute = new DelegatingTransactionAttribute(transactionAttribute) { + + public String getName() { + return testMethod.getName(); + } + }; + + if (logger.isDebugEnabled()) { + logger.debug("Explicit transaction definition [" + transactionAttribute + "] found for test context " + + testContext); + } + tm = getTransactionManager(testContext, transactionAttribute.getQualifier()); + } + + if (tm != null) { + TransactionContext txContext = new TransactionContext(tm, transactionAttribute); + runBeforeTransactionMethods(testContext); + startNewTransaction(testContext, txContext); + this.transactionContextCache.put(testMethod, txContext); + } + } + + /** + * If a transaction is currently active for the test method of the supplied + * {@link TestContext test context}, this method will end the transaction + * and run {@link AfterTransaction @AfterTransaction methods}. + * <p>{@code @AfterTransaction} methods are guaranteed to be + * invoked even if an error occurs while ending the transaction. + */ + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + Method testMethod = testContext.getTestMethod(); + Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null"); + + // If the transaction is still active... + TransactionContext txContext = this.transactionContextCache.remove(testMethod); + if (txContext != null && !txContext.transactionStatus.isCompleted()) { + try { + endTransaction(testContext, txContext); + } + finally { + runAfterTransactionMethods(testContext); + } + } + } + + /** + * Run all {@link BeforeTransaction @BeforeTransaction methods} for the + * specified {@link TestContext test context}. If one of the methods fails, + * however, the caught exception will be rethrown in a wrapped + * {@link RuntimeException}, and the remaining methods will <strong>not</strong> + * be given a chance to execute. + * @param testContext the current test context + */ + protected void runBeforeTransactionMethods(TestContext testContext) throws Exception { + try { + List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class); + Collections.reverse(methods); + for (Method method : methods) { + if (logger.isDebugEnabled()) { + logger.debug("Executing @BeforeTransaction method [" + method + "] for test context " + testContext); + } + method.invoke(testContext.getTestInstance()); + } + } + catch (InvocationTargetException ex) { + logger.error("Exception encountered while executing @BeforeTransaction methods for test context " + + testContext + ".", ex.getTargetException()); + ReflectionUtils.rethrowException(ex.getTargetException()); + } + } + + /** + * Run all {@link AfterTransaction @AfterTransaction methods} for the + * specified {@link TestContext test context}. If one of the methods fails, + * the caught exception will be logged as an error, and the remaining + * methods will be given a chance to execute. After all methods have + * executed, the first caught exception, if any, will be rethrown. + * @param testContext the current test context + */ + protected void runAfterTransactionMethods(TestContext testContext) throws Exception { + Throwable afterTransactionException = null; + + List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class); + for (Method method : methods) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext); + } + method.invoke(testContext.getTestInstance()); + } + catch (InvocationTargetException ex) { + Throwable targetException = ex.getTargetException(); + if (afterTransactionException == null) { + afterTransactionException = targetException; + } + logger.error("Exception encountered while executing @AfterTransaction method [" + method + + "] for test context " + testContext, targetException); + } + catch (Exception ex) { + if (afterTransactionException == null) { + afterTransactionException = ex; + } + logger.error("Exception encountered while executing @AfterTransaction method [" + method + + "] for test context " + testContext, ex); + } + } + + if (afterTransactionException != null) { + ReflectionUtils.rethrowException(afterTransactionException); + } + } + + /** + * Start a new transaction for the supplied {@link TestContext test context}. + * <p>Only call this method if {@link #endTransaction} has been called or if no + * transaction has been previously started. + * @param testContext the current test context + * @throws TransactionException if starting the transaction fails + * @throws Exception if an error occurs while retrieving the transaction manager + */ + private void startNewTransaction(TestContext testContext, TransactionContext txContext) throws Exception { + txContext.startTransaction(); + ++this.transactionsStarted; + if (logger.isInfoEnabled()) { + logger.info("Began transaction (" + this.transactionsStarted + "): transaction manager [" + + txContext.transactionManager + "]; rollback [" + isRollback(testContext) + "]"); + } + } + + /** + * Immediately force a <em>commit</em> or <em>rollback</em> of the + * transaction for the supplied {@link TestContext test context}, according + * to the commit and rollback flags. + * @param testContext the current test context + * @throws Exception if an error occurs while retrieving the transaction manager + */ + private void endTransaction(TestContext testContext, TransactionContext txContext) throws Exception { + boolean rollback = isRollback(testContext); + if (logger.isTraceEnabled()) { + logger.trace("Ending transaction for test context " + testContext + "; transaction manager [" + + txContext.transactionStatus + "]; rollback [" + rollback + "]"); + } + txContext.endTransaction(rollback); + if (logger.isInfoEnabled()) { + logger.info((rollback ? "Rolled back" : "Committed") + + " transaction after test execution for test context " + testContext); + } + } + + /** + * Get the {@link PlatformTransactionManager transaction manager} to use + * for the supplied {@link TestContext test context} and {@code qualifier}. + * <p>Delegates to {@link #getTransactionManager(TestContext)} if the + * supplied {@code qualifier} is {@code null} or empty. + * @param testContext the test context for which the transaction manager + * should be retrieved + * @param qualifier the qualifier for selecting between multiple bean matches; + * may be {@code null} or empty + * @return the transaction manager to use, or {@code null} if not found + * @throws BeansException if an error occurs while retrieving the transaction manager + * @see #getTransactionManager(TestContext) + */ + protected final PlatformTransactionManager getTransactionManager(TestContext testContext, String qualifier) { + // look up by type and qualifier from @Transactional + if (StringUtils.hasText(qualifier)) { + try { + // Use autowire-capable factory in order to support extended qualifier + // matching (only exposed on the internal BeanFactory, not on the + // ApplicationContext). + BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); + + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier); + } catch (RuntimeException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Caught exception while retrieving transaction manager for test context " + testContext + + " and qualifier [" + qualifier + "]", ex); + } + throw ex; + } + } + + // else + return getTransactionManager(testContext); + } + + /** + * Get the {@link PlatformTransactionManager transaction manager} to use + * for the supplied {@link TestContext test context}. + * @param testContext the test context for which the transaction manager + * should be retrieved + * @return the transaction manager to use, or {@code null} if not found + * @throws BeansException if an error occurs while retrieving the transaction manager + * @see #getTransactionManager(TestContext, String) + */ + protected final PlatformTransactionManager getTransactionManager(TestContext testContext) { + BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); + String tmName = retrieveConfigurationAttributes(testContext).getTransactionManagerName(); + + try { + // look up by type and explicit name from @TransactionConfiguration + if (StringUtils.hasText(tmName) && !DEFAULT_TRANSACTION_MANAGER_NAME.equals(tmName)) { + return bf.getBean(tmName, PlatformTransactionManager.class); + } + + if (bf instanceof ListableBeanFactory) { + ListableBeanFactory lbf = (ListableBeanFactory) bf; + + // look up single bean by type + Map<String, PlatformTransactionManager> txMgrs = BeanFactoryUtils.beansOfTypeIncludingAncestors( + lbf, PlatformTransactionManager.class); + if (txMgrs.size() == 1) { + return txMgrs.values().iterator().next(); + } + + // look up single TransactionManagementConfigurer + Map<String, TransactionManagementConfigurer> configurers = BeanFactoryUtils.beansOfTypeIncludingAncestors( + lbf, TransactionManagementConfigurer.class); + if (configurers.size() > 1) { + throw new IllegalStateException( + "Only one TransactionManagementConfigurer may exist in the ApplicationContext"); + } + if (configurers.size() == 1) { + return configurers.values().iterator().next().annotationDrivenTransactionManager(); + } + } + + // look up by type and default name from @TransactionConfiguration + return bf.getBean(DEFAULT_TRANSACTION_MANAGER_NAME, PlatformTransactionManager.class); + + } catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Caught exception while retrieving transaction manager for test context " + testContext, ex); + } + throw ex; + } + } + + /** + * Determine whether or not to rollback transactions by default for the + * supplied {@link TestContext test context}. + * @param testContext the test context for which the default rollback flag + * should be retrieved + * @return the <em>default rollback</em> flag for the supplied test context + * @throws Exception if an error occurs while determining the default rollback flag + */ + protected final boolean isDefaultRollback(TestContext testContext) throws Exception { + return retrieveConfigurationAttributes(testContext).isDefaultRollback(); + } + + /** + * Determine whether or not to rollback transactions for the supplied + * {@link TestContext test context} by taking into consideration the + * {@link #isDefaultRollback(TestContext) default rollback} flag and a + * possible method-level override via the {@link Rollback} annotation. + * @param testContext the test context for which the rollback flag + * should be retrieved + * @return the <em>rollback</em> flag for the supplied test context + * @throws Exception if an error occurs while determining the rollback flag + */ + protected final boolean isRollback(TestContext testContext) throws Exception { + boolean rollback = isDefaultRollback(testContext); + Rollback rollbackAnnotation = testContext.getTestMethod().getAnnotation(Rollback.class); + if (rollbackAnnotation != null) { + boolean rollbackOverride = rollbackAnnotation.value(); + if (logger.isDebugEnabled()) { + logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback [" + rollback + + "] for test context " + testContext); + } + rollback = rollbackOverride; + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No method-level @Rollback override: using default rollback [" + rollback + + "] for test context " + testContext); + } + } + return rollback; + } + + /** + * Gets all superclasses of the supplied {@link Class class}, including the + * class itself. The ordering of the returned list will begin with the + * supplied class and continue up the class hierarchy. + * <p>Note: This code has been borrowed from + * {@link org.junit.internal.runners.TestClass#getSuperClasses(Class)} and + * adapted. + * @param clazz the class for which to retrieve the superclasses. + * @return all superclasses of the supplied class. + */ + private List<Class<?>> getSuperClasses(Class<?> clazz) { + ArrayList<Class<?>> results = new ArrayList<Class<?>>(); + Class<?> current = clazz; + while (current != null) { + results.add(current); + current = current.getSuperclass(); + } + return results; + } + + /** + * Gets all methods in the supplied {@link Class class} and its superclasses + * which are annotated with the supplied {@code annotationType} but + * which are not <em>shadowed</em> by methods overridden in subclasses. + * <p>Note: This code has been borrowed from + * {@link org.junit.internal.runners.TestClass#getAnnotatedMethods(Class)} + * and adapted. + * @param clazz the class for which to retrieve the annotated methods + * @param annotationType the annotation type for which to search + * @return all annotated methods in the supplied class and its superclasses + */ + private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) { + List<Method> results = new ArrayList<Method>(); + for (Class<?> eachClass : getSuperClasses(clazz)) { + Method[] methods = eachClass.getDeclaredMethods(); + for (Method eachMethod : methods) { + Annotation annotation = eachMethod.getAnnotation(annotationType); + if (annotation != null && !isShadowed(eachMethod, results)) { + results.add(eachMethod); + } + } + } + return results; + } + + /** + * Determines if the supplied {@link Method method} is <em>shadowed</em> + * by a method in supplied {@link List list} of previous methods. + * <p>Note: This code has been borrowed from + * {@link org.junit.internal.runners.TestClass#isShadowed(Method, List)}. + * @param method the method to check for shadowing + * @param previousMethods the list of methods which have previously been processed + * @return {@code true} if the supplied method is shadowed by a + * method in the {@code previousMethods} list + */ + private boolean isShadowed(Method method, List<Method> previousMethods) { + for (Method each : previousMethods) { + if (isShadowed(method, each)) { + return true; + } + } + return false; + } + + /** + * Determines if the supplied {@link Method current method} is + * <em>shadowed</em> by a {@link Method previous method}. + * <p>Note: This code has been borrowed from + * {@link org.junit.internal.runners.TestClass#isShadowed(Method, Method)}. + * @param current the current method + * @param previous the previous method + * @return {@code true} if the previous method shadows the current one + */ + private boolean isShadowed(Method current, Method previous) { + if (!previous.getName().equals(current.getName())) { + return false; + } + if (previous.getParameterTypes().length != current.getParameterTypes().length) { + return false; + } + for (int i = 0; i < previous.getParameterTypes().length; i++) { + if (!previous.getParameterTypes()[i].equals(current.getParameterTypes()[i])) { + return false; + } + } + return true; + } + + /** + * Retrieves the {@link TransactionConfigurationAttributes} for the + * specified {@link Class class} which may optionally declare or inherit + * {@link TransactionConfiguration @TransactionConfiguration}. If + * {@code @TransactionConfiguration} is not present for the supplied + * class, the <em>default values</em> for attributes defined in + * {@code @TransactionConfiguration} will be used instead. + * @param testContext the test context for which the configuration + * attributes should be retrieved + * @return a new TransactionConfigurationAttributes instance + */ + private TransactionConfigurationAttributes retrieveConfigurationAttributes(TestContext testContext) { + if (this.configurationAttributes == null) { + Class<?> clazz = testContext.getTestClass(); + TransactionConfiguration config = clazz.getAnnotation(TransactionConfiguration.class); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved @TransactionConfiguration [" + config + "] for test class [" + clazz + "]"); + } + + String transactionManagerName; + boolean defaultRollback; + if (config != null) { + transactionManagerName = config.transactionManager(); + defaultRollback = config.defaultRollback(); + } + else { + transactionManagerName = DEFAULT_TRANSACTION_MANAGER_NAME; + defaultRollback = DEFAULT_DEFAULT_ROLLBACK; + } + + TransactionConfigurationAttributes configAttributes = new TransactionConfigurationAttributes( + transactionManagerName, defaultRollback); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved TransactionConfigurationAttributes " + configAttributes + " for class [" + + clazz + "]"); + } + this.configurationAttributes = configAttributes; + } + return this.configurationAttributes; + } + + + /** + * Internal context holder for a specific test method. + */ + private static class TransactionContext { + + private final PlatformTransactionManager transactionManager; + + private final TransactionDefinition transactionDefinition; + + private TransactionStatus transactionStatus; + + + public TransactionContext(PlatformTransactionManager transactionManager, + TransactionDefinition transactionDefinition) { + this.transactionManager = transactionManager; + this.transactionDefinition = transactionDefinition; + } + + public void startTransaction() { + this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition); + } + + public void endTransaction(boolean rollback) { + if (rollback) { + this.transactionManager.rollback(this.transactionStatus); + } + else { + this.transactionManager.commit(this.transactionStatus); + } + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java b/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java new file mode 100644 index 00000000..3c685db1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java @@ -0,0 +1,7 @@ +/** + * <p>Transactional support classes for the <em>Spring TestContext + * Framework</em>.</p> + */ + +package org.springframework.test.context.transaction; + diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java new file mode 100644 index 00000000..75c363dd --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java @@ -0,0 +1,267 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.web; + +import javax.servlet.ServletContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.FileSystemResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.support.AbstractContextLoader; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.GenericWebApplicationContext; + +/** + * Abstract, generic extension of {@link AbstractContextLoader} that loads a + * {@link GenericWebApplicationContext}. + * + * <p>If instances of concrete subclasses are invoked via the + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader} + * SPI, the context will be loaded from the {@link MergedContextConfiguration} + * provided to {@link #loadContext(MergedContextConfiguration)}. In such cases, a + * {@code SmartContextLoader} will decide whether to load the context from + * <em>locations</em> or <em>annotated classes</em>. Note that {@code + * AbstractGenericWebContextLoader} does not support the {@code + * loadContext(String... locations)} method from the legacy + * {@link org.springframework.test.context.ContextLoader ContextLoader} SPI. + * + * <p>Concrete subclasses must provide an appropriate implementation of + * {@link #loadBeanDefinitions}. + * + * @author Sam Brannen + * @since 3.2 + * @see #loadContext(MergedContextConfiguration) + * @see #loadContext(String...) + */ +public abstract class AbstractGenericWebContextLoader extends AbstractContextLoader { + + private static final Log logger = LogFactory.getLog(AbstractGenericWebContextLoader.class); + + + // --- SmartContextLoader ----------------------------------------------- + + /** + * Load a Spring {@link WebApplicationContext} from the supplied + * {@link MergedContextConfiguration}. + * + * <p>Implementation details: + * + * <ul> + * <li>Creates a {@link GenericWebApplicationContext} instance.</li> + * <li>If the supplied {@code MergedContextConfiguration} references a + * {@linkplain MergedContextConfiguration#getParent() parent configuration}, + * the corresponding {@link MergedContextConfiguration#getParentApplicationContext() + * ApplicationContext} will be retrieved and + * {@linkplain GenericWebApplicationContext#setParent(ApplicationContext) set as the parent} + * for the context created by this method.</li> + * <li>Delegates to {@link #configureWebResources} to create the + * {@link MockServletContext} and set it in the {@code WebApplicationContext}.</li> + * <li>Calls {@link #prepareContext} to allow for customizing the context + * before bean definitions are loaded.</li> + * <li>Calls {@link #customizeBeanFactory} to allow for customizing the + * context's {@code DefaultListableBeanFactory}.</li> + * <li>Delegates to {@link #loadBeanDefinitions} to populate the context + * from the locations or classes in the supplied {@code MergedContextConfiguration}.</li> + * <li>Delegates to {@link AnnotationConfigUtils} for + * {@linkplain AnnotationConfigUtils#registerAnnotationConfigProcessors registering} + * annotation configuration processors.</li> + * <li>Calls {@link #customizeContext} to allow for customizing the context + * before it is refreshed.</li> + * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the + * context and registers a JVM shutdown hook for it.</li> + * </ul> + * + * @return a new web application context + * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration) + * @see GenericWebApplicationContext + */ + public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { + + if (!(mergedConfig instanceof WebMergedContextConfiguration)) { + throw new IllegalArgumentException(String.format( + "Cannot load WebApplicationContext from non-web merged context configuration %s. " + + "Consider annotating your test class with @WebAppConfiguration.", mergedConfig)); + } + WebMergedContextConfiguration webMergedConfig = (WebMergedContextConfiguration) mergedConfig; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Loading WebApplicationContext for merged context configuration %s.", + webMergedConfig)); + } + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + + ApplicationContext parent = mergedConfig.getParentApplicationContext(); + if (parent != null) { + context.setParent(parent); + } + configureWebResources(context, webMergedConfig); + prepareContext(context, webMergedConfig); + customizeBeanFactory(context.getDefaultListableBeanFactory(), webMergedConfig); + loadBeanDefinitions(context, webMergedConfig); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + customizeContext(context, webMergedConfig); + context.refresh(); + context.registerShutdownHook(); + return context; + } + + /** + * Configures web resources for the supplied web application context (WAC). + * + * <h4>Implementation Details</h4> + * + * <p>If the supplied WAC has no parent or its parent is not a WAC, the + * supplied WAC will be configured as the Root WAC (see "<em>Root WAC + * Configuration</em>" below). + * + * <p>Otherwise the context hierarchy of the supplied WAC will be traversed + * to find the top-most WAC (i.e., the root); and the {@link ServletContext} + * of the Root WAC will be set as the {@code ServletContext} for the supplied + * WAC. + * + * <h4>Root WAC Configuration</h4> + * + * <ul> + * <li>The resource base path is retrieved from the supplied + * {@code WebMergedContextConfiguration}.</li> + * <li>A {@link ResourceLoader} is instantiated for the {@link MockServletContext}: + * if the resource base path is prefixed with "{@code classpath:}", a + * {@link DefaultResourceLoader} will be used; otherwise, a + * {@link FileSystemResourceLoader} will be used.</li> + * <li>A {@code MockServletContext} will be created using the resource base + * path and resource loader.</li> + * <li>The supplied {@link GenericWebApplicationContext} is then stored in + * the {@code MockServletContext} under the + * {@link WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} key.</li> + * <li>Finally, the {@code MockServletContext} is set in the + * {@code WebApplicationContext}.</li> + * + * @param context the web application context for which to configure the web + * resources + * @param webMergedConfig the merged context configuration to use to load the + * web application context + */ + protected void configureWebResources(GenericWebApplicationContext context, + WebMergedContextConfiguration webMergedConfig) { + + ApplicationContext parent = context.getParent(); + + // if the WAC has no parent or the parent is not a WAC, set the WAC as + // the Root WAC: + if (parent == null || (!(parent instanceof WebApplicationContext))) { + String resourceBasePath = webMergedConfig.getResourceBasePath(); + ResourceLoader resourceLoader = resourceBasePath.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX) ? new DefaultResourceLoader() + : new FileSystemResourceLoader(); + + ServletContext servletContext = new MockServletContext(resourceBasePath, resourceLoader); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); + context.setServletContext(servletContext); + } + else { + ServletContext servletContext = null; + + // find the Root WAC + while (parent != null) { + if (parent instanceof WebApplicationContext && !(parent.getParent() instanceof WebApplicationContext)) { + servletContext = ((WebApplicationContext) parent).getServletContext(); + break; + } + parent = parent.getParent(); + } + Assert.state(servletContext != null, "Failed to find Root WebApplicationContext in the context hierarchy"); + context.setServletContext(servletContext); + } + } + + /** + * Customize the internal bean factory of the {@code WebApplicationContext} + * created by this context loader. + * + * <p>The default implementation is empty but can be overridden in subclasses + * to customize {@code DefaultListableBeanFactory}'s standard settings. + * + * @param beanFactory the bean factory created by this context loader + * @param webMergedConfig the merged context configuration to use to load the + * web application context + * @see #loadContext(MergedContextConfiguration) + * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see DefaultListableBeanFactory#setAllowEagerClassLoading + * @see DefaultListableBeanFactory#setAllowCircularReferences + * @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping + */ + protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory, + WebMergedContextConfiguration webMergedConfig) { + } + + /** + * Load bean definitions into the supplied {@link GenericWebApplicationContext context} + * from the locations or classes in the supplied {@code WebMergedContextConfiguration}. + * + * <p>Concrete subclasses must provide an appropriate implementation. + * + * @param context the context into which the bean definitions should be loaded + * @param webMergedConfig the merged context configuration to use to load the + * web application context + * @see #loadContext(MergedContextConfiguration) + */ + protected abstract void loadBeanDefinitions(GenericWebApplicationContext context, + WebMergedContextConfiguration webMergedConfig); + + /** + * Customize the {@link GenericWebApplicationContext} created by this context + * loader <i>after</i> bean definitions have been loaded into the context but + * <i>before</i> the context is refreshed. + * + * <p>The default implementation is empty but can be overridden in subclasses + * to customize the web application context. + * + * @param context the newly created web application context + * @param webMergedConfig the merged context configuration to use to load the + * web application context + * @see #loadContext(MergedContextConfiguration) + */ + protected void customizeContext(GenericWebApplicationContext context, WebMergedContextConfiguration webMergedConfig) { + } + + // --- ContextLoader ------------------------------------------------------- + + /** + * {@code AbstractGenericWebContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see org.springframework.test.context.ContextLoader#loadContext(java.lang.String[]) + * @throws UnsupportedOperationException + */ + public final ApplicationContext loadContext(String... locations) throws Exception { + throw new UnsupportedOperationException( + "AbstractGenericWebContextLoader does not support the loadContext(String... locations) method"); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java new file mode 100644 index 00000000..05acb463 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.web.context.support.GenericWebApplicationContext; + +/** + * Concrete implementation of {@link AbstractGenericWebContextLoader} that loads + * bean definitions from annotated classes. + * + * <p>See the Javadoc for + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration} + * for a definition of <em>annotated class</em>. + * + * <p>Note: {@code AnnotationConfigWebContextLoader} supports <em>annotated classes</em> + * rather than the String-based resource locations defined by the legacy + * {@link org.springframework.test.context.ContextLoader ContextLoader} API. Thus, + * although {@code AnnotationConfigWebContextLoader} extends + * {@code AbstractGenericWebContextLoader}, {@code AnnotationConfigWebContextLoader} + * does <em>not</em> support any String-based methods defined by + * {@link org.springframework.test.context.support.AbstractContextLoader + * AbstractContextLoader} or {@code AbstractGenericWebContextLoader}. + * Consequently, {@code AnnotationConfigWebContextLoader} should chiefly be + * considered a {@link org.springframework.test.context.SmartContextLoader SmartContextLoader} + * rather than a {@link org.springframework.test.context.ContextLoader ContextLoader}. + * + * @author Sam Brannen + * @since 3.2 + * @see #processContextConfiguration(ContextConfigurationAttributes) + * @see #detectDefaultConfigurationClasses(Class) + * @see #loadBeanDefinitions(GenericWebApplicationContext, WebMergedContextConfiguration) + */ +public class AnnotationConfigWebContextLoader extends AbstractGenericWebContextLoader { + + private static final Log logger = LogFactory.getLog(AnnotationConfigWebContextLoader.class); + + + // --- SmartContextLoader ----------------------------------------------- + + /** + * Process <em>annotated classes</em> in the supplied {@link ContextConfigurationAttributes}. + * + * <p>If the <em>annotated classes</em> are {@code null} or empty and + * {@link #isGenerateDefaultLocations()} returns {@code true}, this + * {@code SmartContextLoader} will attempt to {@linkplain + * #detectDefaultConfigurationClasses detect default configuration classes}. + * If defaults are detected they will be + * {@linkplain ContextConfigurationAttributes#setClasses(Class[]) set} in the + * supplied configuration attributes. Otherwise, properties in the supplied + * configuration attributes will not be modified. + * + * @param configAttributes the context configuration attributes to process + * @see org.springframework.test.context.SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes) + * @see #isGenerateDefaultLocations() + * @see #detectDefaultConfigurationClasses(Class) + */ + public void processContextConfiguration(ContextConfigurationAttributes configAttributes) { + if (ObjectUtils.isEmpty(configAttributes.getClasses()) && isGenerateDefaultLocations()) { + Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses(configAttributes.getDeclaringClass()); + configAttributes.setClasses(defaultConfigClasses); + } + } + + /** + * Detect the default configuration classes for the supplied test class. + * + * <p>The default implementation simply delegates to + * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses(Class)}. + * + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @return an array of default configuration classes, potentially empty but + * never {@code null} + * @see AnnotationConfigContextLoaderUtils + */ + protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) { + return AnnotationConfigContextLoaderUtils.detectDefaultConfigurationClasses(declaringClass); + } + + // --- AbstractContextLoader ----------------------------------------------- + + /** + * {@code AnnotationConfigWebContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see org.springframework.test.context.support.AbstractContextLoader#modifyLocations + * @throws UnsupportedOperationException + */ + @Override + protected String[] modifyLocations(Class<?> clazz, String... locations) { + throw new UnsupportedOperationException( + "AnnotationConfigWebContextLoader does not support the modifyLocations(Class, String...) method"); + } + + /** + * {@code AnnotationConfigWebContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see org.springframework.test.context.support.AbstractContextLoader#generateDefaultLocations + * @throws UnsupportedOperationException + */ + @Override + protected String[] generateDefaultLocations(Class<?> clazz) { + throw new UnsupportedOperationException( + "AnnotationConfigWebContextLoader does not support the generateDefaultLocations(Class) method"); + } + + /** + * {@code AnnotationConfigWebContextLoader} should be used as a + * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, + * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}. + * Consequently, this method is not supported. + * + * @see org.springframework.test.context.support.AbstractContextLoader#getResourceSuffix + * @throws UnsupportedOperationException + */ + @Override + protected String getResourceSuffix() { + throw new UnsupportedOperationException( + "AnnotationConfigWebContextLoader does not support the getResourceSuffix() method"); + } + + // --- AbstractGenericWebContextLoader ------------------------------------- + + /** + * Register classes in the supplied {@linkplain GenericWebApplicationContext context} + * from the classes in the supplied {@link WebMergedContextConfiguration}. + * + * <p>Each class must represent an <em>annotated class</em>. An + * {@link AnnotatedBeanDefinitionReader} is used to register the appropriate + * bean definitions. + * + * @param context the context in which the annotated classes should be registered + * @param webMergedConfig the merged configuration from which the classes should be retrieved + * + * @see AbstractGenericWebContextLoader#loadBeanDefinitions + */ + @Override + protected void loadBeanDefinitions(GenericWebApplicationContext context, + WebMergedContextConfiguration webMergedConfig) { + Class<?>[] annotatedClasses = webMergedConfig.getClasses(); + if (logger.isDebugEnabled()) { + logger.debug("Registering annotated classes: " + ObjectUtils.nullSafeToString(annotatedClasses)); + } + new AnnotatedBeanDefinitionReader(context).register(annotatedClasses); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java new file mode 100644 index 00000000..0bf295e0 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.web.context.support.GenericWebApplicationContext; + +/** + * Concrete implementation of {@link AbstractGenericWebContextLoader} that loads + * bean definitions from XML resources. + * + * @author Sam Brannen + * @since 3.2 + */ +public class GenericXmlWebContextLoader extends AbstractGenericWebContextLoader { + + /** + * Loads bean definitions using an {@link XmlBeanDefinitionReader}. + * @see AbstractGenericWebContextLoader#loadBeanDefinitions + */ + @Override + protected void loadBeanDefinitions(GenericWebApplicationContext context, + WebMergedContextConfiguration webMergedConfig) { + new XmlBeanDefinitionReader(context).loadBeanDefinitions(webMergedConfig.getLocations()); + } + + /** + * Returns "{@code -context.xml}". + */ + protected String getResourceSuffix() { + return "-context.xml"; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java new file mode 100644 index 00000000..5166f8ce --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.web; + +import javax.servlet.ServletContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.Conventions; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * {@code TestExecutionListener} which provides mock Servlet API support to + * {@link WebApplicationContext WebApplicationContexts} loaded by the <em>Spring + * TestContext Framework</em>. + * + * <p>Specifically, {@code ServletTestExecutionListener} sets up thread-local + * state via Spring Web's {@link RequestContextHolder} during {@linkplain + * #prepareTestInstance(TestContext) test instance preparation} and {@linkplain + * #beforeTestMethod(TestContext) before each test method} and creates a {@link + * MockHttpServletRequest}, {@link MockHttpServletResponse}, and + * {@link ServletWebRequest} based on the {@link MockServletContext} present in + * the {@code WebApplicationContext}. This listener also ensures that the + * {@code MockHttpServletResponse} and {@code ServletWebRequest} can be injected + * into the test instance, and once the test is complete this listener {@linkplain + * #afterTestMethod(TestContext) cleans up} thread-local state. + * + * <p>Note that {@code ServletTestExecutionListener} is enabled by default but + * generally takes no action if the {@linkplain TestContext#getTestClass() test + * class} is not annotated with {@link WebAppConfiguration @WebAppConfiguration}. + * See the Javadoc for individual methods in this class for details. + * + * @author Sam Brannen + * @since 3.2 + */ +public class ServletTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Attribute name for a {@link TestContext} attribute which indicates + * whether or not the {@code ServletTestExecutionListener} should {@linkplain + * RequestContextHolder#resetRequestAttributes() reset} Spring Web's + * {@code RequestContextHolder} in {@link #afterTestMethod(TestContext)}. + * + * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. + */ + public static final String RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName( + ServletTestExecutionListener.class, "resetRequestContextHolder"); + + /** + * Attribute name for a {@link TestContext} attribute which indicates that + * {@code ServletTestExecutionListener} has already populated Spring Web's + * {@code RequestContextHolder}. + * + * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. + */ + public static final String POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName( + ServletTestExecutionListener.class, "populatedRequestContextHolder"); + + private static final Log logger = LogFactory.getLog(ServletTestExecutionListener.class); + + + /** + * Sets up thread-local state during the <em>test instance preparation</em> + * callback phase via Spring Web's {@link RequestContextHolder}, but only if + * the {@linkplain TestContext#getTestClass() test class} is annotated with + * {@link WebAppConfiguration @WebAppConfiguration}. + * + * @see TestExecutionListener#prepareTestInstance(TestContext) + * @see #setUpRequestContextIfNecessary(TestContext) + */ + @SuppressWarnings("javadoc") + public void prepareTestInstance(TestContext testContext) throws Exception { + setUpRequestContextIfNecessary(testContext); + } + + /** + * Sets up thread-local state before each test method via Spring Web's + * {@link RequestContextHolder}, but only if the + * {@linkplain TestContext#getTestClass() test class} is annotated with + * {@link WebAppConfiguration @WebAppConfiguration}. + * + * @see TestExecutionListener#beforeTestMethod(TestContext) + * @see #setUpRequestContextIfNecessary(TestContext) + */ + @SuppressWarnings("javadoc") + public void beforeTestMethod(TestContext testContext) throws Exception { + setUpRequestContextIfNecessary(testContext); + } + + /** + * If the {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} in the supplied + * {@code TestContext} has a value of {@link Boolean#TRUE}, this method will + * (1) clean up thread-local state after each test method by {@linkplain + * RequestContextHolder#resetRequestAttributes() resetting} Spring Web's + * {@code RequestContextHolder} and (2) ensure that new mocks are injected + * into the test instance for subsequent tests by setting the + * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * in the test context to {@code true}. + * + * <p>The {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} and + * {@link #POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} will be subsequently + * removed from the test context, regardless of their values. + * + * @see TestExecutionListener#afterTestMethod(TestContext) + */ + public void afterTestMethod(TestContext testContext) throws Exception { + if (Boolean.TRUE.equals(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE))) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Resetting RequestContextHolder for test context %s.", testContext)); + } + RequestContextHolder.resetRequestAttributes(); + testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, + Boolean.TRUE); + } + testContext.removeAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE); + testContext.removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE); + } + + private boolean notAnnotatedWithWebAppConfiguration(TestContext testContext) { + return AnnotationUtils.findAnnotation(testContext.getTestClass(), WebAppConfiguration.class) == null; + } + + private boolean alreadyPopulatedRequestContextHolder(TestContext testContext) { + return Boolean.TRUE.equals(testContext.getAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE)); + } + + private void setUpRequestContextIfNecessary(TestContext testContext) { + if (notAnnotatedWithWebAppConfiguration(testContext) || alreadyPopulatedRequestContextHolder(testContext)) { + return; + } + + ApplicationContext context = testContext.getApplicationContext(); + + if (context instanceof WebApplicationContext) { + WebApplicationContext wac = (WebApplicationContext) context; + ServletContext servletContext = wac.getServletContext(); + Assert.state(servletContext instanceof MockServletContext, String.format( + "The WebApplicationContext for test context %s must be configured with a MockServletContext.", + testContext)); + + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "Setting up MockHttpServletRequest, MockHttpServletResponse, ServletWebRequest, and RequestContextHolder for test context %s.", + testContext)); + } + + MockServletContext mockServletContext = (MockServletContext) servletContext; + MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext); + MockHttpServletResponse response = new MockHttpServletResponse(); + ServletWebRequest servletWebRequest = new ServletWebRequest(request, response); + + RequestContextHolder.setRequestAttributes(servletWebRequest); + testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + + if (wac instanceof ConfigurableApplicationContext) { + @SuppressWarnings("resource") + ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) wac; + ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory(); + bf.registerResolvableDependency(MockHttpServletResponse.class, response); + bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest); + } + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java new file mode 100644 index 00000000..7f24c266 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +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 @WebAppConfiguration} is a class-level annotation that is used to + * declare that the {@code ApplicationContext} loaded for an integration test + * should be a {@link org.springframework.web.context.WebApplicationContext + * WebApplicationContext}. + * + * <p>The mere presence of {@code @WebAppConfiguration} on a test class ensures + * that a {@code WebApplicationContext} will be loaded for the test using a default + * for the path to the root of the web application. To override the default, + * specify an explicit resource path via the {@link #value} attribute. + * + * <p>Note that {@code @WebAppConfiguration} must be used in conjunction with + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}, + * either within a single test class or within a test class hierarchy. + * + * @author Sam Brannen + * @since 3.2 + * @see org.springframework.web.context.WebApplicationContext + * @see org.springframework.test.context.ContextConfiguration + * @see ServletTestExecutionListener + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface WebAppConfiguration { + + /** + * The resource path to the root directory of the web application. + * + * <p>A path that does not include a Spring resource prefix (e.g., {@code classpath:}, + * {@code file:}, etc.) will be interpreted as a file system resource, and a + * path should not end with a slash. + * + * <p>Defaults to {@code "src/main/webapp"} as a file system resource. Note + * that this is the standard directory for the root of a web application in + * a project that follows the standard Maven project layout for a WAR. + */ + String value() default "src/main/webapp"; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java new file mode 100644 index 00000000..fa561c00 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import org.springframework.test.context.SmartContextLoader; +import org.springframework.test.context.support.AbstractDelegatingSmartContextLoader; + +/** + * {@code WebDelegatingSmartContextLoader} is a concrete implementation of + * {@link AbstractDelegatingSmartContextLoader} that delegates to a + * {@link GenericXmlWebContextLoader} and an {@link AnnotationConfigWebContextLoader}. + * + * @author Sam Brannen + * @since 3.2 + * @see SmartContextLoader + * @see AbstractDelegatingSmartContextLoader + * @see GenericXmlWebContextLoader + * @see AnnotationConfigWebContextLoader + */ +public class WebDelegatingSmartContextLoader extends AbstractDelegatingSmartContextLoader { + + private final SmartContextLoader xmlLoader = new GenericXmlWebContextLoader(); + private final SmartContextLoader annotationConfigLoader = new AnnotationConfigWebContextLoader(); + + + protected SmartContextLoader getXmlLoader() { + return this.xmlLoader; + } + + protected SmartContextLoader getAnnotationConfigLoader() { + return this.annotationConfigLoader; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java new file mode 100644 index 00000000..58e8b386 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.web; + +import java.util.Set; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@code WebMergedContextConfiguration} encapsulates the <em>merged</em> + * context configuration declared on a test class and all of its superclasses + * via {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}, + * {@link WebAppConfiguration @WebAppConfiguration}, and + * {@link org.springframework.test.context.ActiveProfiles @ActiveProfiles}. + * + * <p>{@code WebMergedContextConfiguration} extends the contract of + * {@link MergedContextConfiguration} by adding support for the {@link + * #getResourceBasePath() resource base path} configured via {@code @WebAppConfiguration}. + * This allows the {@link org.springframework.test.context.TestContext TestContext} + * to properly cache the corresponding {@link + * org.springframework.web.context.WebApplicationContext WebApplicationContext} + * that was loaded using properties of this {@code WebMergedContextConfiguration}. + * + * @author Sam Brannen + * @since 3.2 + * @see WebAppConfiguration + * @see MergedContextConfiguration + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.ActiveProfiles + * @see org.springframework.test.context.ContextConfigurationAttributes + * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration) + */ +public class WebMergedContextConfiguration extends MergedContextConfiguration { + + private static final long serialVersionUID = 7323361588604247458L; + + private final String resourceBasePath; + + + /** + * Create a new {@code WebMergedContextConfiguration} instance for the + * supplied test class, resource locations, annotated classes, context + * initializers, active profiles, resource base path, and {@code ContextLoader}. + * + * <p>If a {@code null} value is supplied for {@code locations}, + * {@code classes}, or {@code activeProfiles} an empty array will + * be stored instead. If a {@code null} value is supplied for the + * {@code contextInitializerClasses} an empty set will be stored instead. + * If an <em>empty</em> value is supplied for the {@code resourceBasePath} + * an empty string will be used. Furthermore, active profiles will be sorted, + * and duplicate profiles will be removed. + * + * @param testClass the test class for which the configuration was merged + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param resourceBasePath the resource path to the root directory of the web application + * @param contextLoader the resolved {@code ContextLoader} + * @see #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration) + * @deprecated as of Spring 3.2.2, use + * {@link #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} instead. + */ + @Deprecated + public WebMergedContextConfiguration( + Class<?> testClass, + String[] locations, + Class<?>[] classes, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses, + String[] activeProfiles, String resourceBasePath, ContextLoader contextLoader) { + + this(testClass, locations, classes, contextInitializerClasses, activeProfiles, resourceBasePath, contextLoader, + null, null); + } + + /** + * Create a new {@code WebMergedContextConfiguration} instance for the + * supplied test class, resource locations, annotated classes, context + * initializers, active profiles, resource base path, and {@code ContextLoader}. + * + * <p>If a {@code null} value is supplied for {@code locations}, + * {@code classes}, or {@code activeProfiles} an empty array will + * be stored instead. If a {@code null} value is supplied for the + * {@code contextInitializerClasses} an empty set will be stored instead. + * If an <em>empty</em> value is supplied for the {@code resourceBasePath} + * an empty string will be used. Furthermore, active profiles will be sorted, + * and duplicate profiles will be removed. + * + * @param testClass the test class for which the configuration was merged + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param resourceBasePath the resource path to the root directory of the web application + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate a cache-aware context loader + * delegate with which to retrieve the parent context + * @param parent the parent configuration or {@code null} if there is no parent + * @since 3.2.2 + */ + public WebMergedContextConfiguration( + Class<?> testClass, + String[] locations, + Class<?>[] classes, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses, + String[] activeProfiles, String resourceBasePath, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) { + + super(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader, + cacheAwareContextLoaderDelegate, parent); + + this.resourceBasePath = !StringUtils.hasText(resourceBasePath) ? "" : resourceBasePath; + } + + /** + * Get the resource path to the root directory of the web application for the + * {@linkplain #getTestClass() test class}, configured via {@code @WebAppConfiguration}. + * @see WebAppConfiguration + */ + public String getResourceBasePath() { + return this.resourceBasePath; + } + + /** + * Generate a unique hash code for all properties of this + * {@code WebMergedContextConfiguration} excluding the + * {@linkplain #getTestClass() test class}. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + resourceBasePath.hashCode(); + return result; + } + + /** + * Determine if the supplied object is equal to this {@code WebMergedContextConfiguration} + * instance by comparing both object's {@linkplain #getLocations() locations}, + * {@linkplain #getClasses() annotated classes}, + * {@linkplain #getContextInitializerClasses() context initializer classes}, + * {@linkplain #getActiveProfiles() active profiles}, + * {@linkplain #getResourceBasePath() resource base path}, + * {@linkplain #getParent() parents}, and the fully qualified names of their + * {@link #getContextLoader() ContextLoaders}. + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (!(obj instanceof WebMergedContextConfiguration)) { + return false; + } + + final WebMergedContextConfiguration that = (WebMergedContextConfiguration) obj; + + return super.equals(that) && this.getResourceBasePath().equals(that.getResourceBasePath()); + } + + /** + * Provide a String representation of the {@linkplain #getTestClass() test class}, + * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes}, + * {@linkplain #getContextInitializerClasses() context initializer classes}, + * {@linkplain #getActiveProfiles() active profiles}, + * {@linkplain #getResourceBasePath() resource base path}, the name of the + * {@link #getContextLoader() ContextLoader}, and the + * {@linkplain #getParent() parent configuration}. + */ + @Override + public String toString() { + return new ToStringCreator(this)// + .append("testClass", getTestClass())// + .append("locations", ObjectUtils.nullSafeToString(getLocations()))// + .append("classes", ObjectUtils.nullSafeToString(getClasses()))// + .append("contextInitializerClasses", ObjectUtils.nullSafeToString(getContextInitializerClasses()))// + .append("activeProfiles", ObjectUtils.nullSafeToString(getActiveProfiles()))// + .append("resourceBasePath", getResourceBasePath())// + .append("contextLoader", nullSafeToString(getContextLoader()))// + .append("parent", getParent())// + .toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/package-info.java b/spring-test/src/main/java/org/springframework/test/context/web/package-info.java new file mode 100644 index 00000000..ebf00203 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/package-info.java @@ -0,0 +1,6 @@ +/** + * Web support classes for the <em>Spring TestContext Framework</em>. + */ + +package org.springframework.test.context.web; + diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java new file mode 100644 index 00000000..383b20df --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java @@ -0,0 +1,419 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.jdbc; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.util.StringUtils; + +/** + * {@code JdbcTestUtils} is a collection of JDBC related utility functions + * intended to simplify standard database testing scenarios. + * + * <p>As of Spring 3.1.3, {@code JdbcTestUtils} supersedes {@link SimpleJdbcTestUtils}. + * + * @author Thomas Risberg + * @author Sam Brannen + * @author Juergen Hoeller + * @author Phillip Webb + * @since 2.5.4 + */ +public class JdbcTestUtils { + + private static final Log logger = LogFactory.getLog(JdbcTestUtils.class); + + private static final String DEFAULT_COMMENT_PREFIX = "--"; + + private static final char DEFAULT_STATEMENT_SEPARATOR = ';'; + + + /** + * Count the rows in the given table. + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param tableName name of the table to count rows in + * @return the number of rows in the table + */ + public static int countRowsInTable(JdbcTemplate jdbcTemplate, String tableName) { + return jdbcTemplate.queryForObject("SELECT COUNT(0) FROM " + tableName, Integer.class); + } + + /** + * Count the rows in the given table, using the provided {@code WHERE} clause. + * <p>If the provided {@code WHERE} clause contains text, it will be prefixed + * with {@code " WHERE "} and then appended to the generated {@code SELECT} + * statement. For example, if the provided table name is {@code "person"} and + * the provided where clause is {@code "name = 'Bob' and age > 25"}, the + * resulting SQL statement to execute will be + * {@code "SELECT COUNT(0) FROM person WHERE name = 'Bob' and age > 25"}. + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param tableName the name of the table to count rows in + * @param whereClause the {@code WHERE} clause to append to the query + * @return the number of rows in the table that match the provided + * {@code WHERE} clause + */ + public static int countRowsInTableWhere(JdbcTemplate jdbcTemplate, String tableName, String whereClause) { + String sql = "SELECT COUNT(0) FROM " + tableName; + if (StringUtils.hasText(whereClause)) { + sql += " WHERE " + whereClause; + } + return jdbcTemplate.queryForObject(sql, Integer.class); + } + + /** + * Delete all rows from the specified tables. + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param tableNames the names of the tables to delete from + * @return the total number of rows deleted from all specified tables + */ + public static int deleteFromTables(JdbcTemplate jdbcTemplate, String... tableNames) { + int totalRowCount = 0; + for (String tableName : tableNames) { + int rowCount = jdbcTemplate.update("DELETE FROM " + tableName); + totalRowCount += rowCount; + if (logger.isInfoEnabled()) { + logger.info("Deleted " + rowCount + " rows from table " + tableName); + } + } + return totalRowCount; + } + + /** + * Delete rows from the given table, using the provided {@code WHERE} clause. + * <p>If the provided {@code WHERE} clause contains text, it will be prefixed + * with {@code " WHERE "} and then appended to the generated {@code DELETE} + * statement. For example, if the provided table name is {@code "person"} and + * the provided where clause is {@code "name = 'Bob' and age > 25"}, the + * resulting SQL statement to execute will be + * {@code "DELETE FROM person WHERE name = 'Bob' and age > 25"}. + * <p>As an alternative to hard-coded values, the {@code "?"} placeholder can + * be used within the {@code WHERE} clause, binding to the given arguments. + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param tableName the name of the table to delete rows from + * @param whereClause the {@code WHERE} clause to append to the query + * @param args arguments to bind to the query (leaving it to the PreparedStatement + * to guess the corresponding SQL type); may also contain {@link SqlParameterValue} + * objects which indicate not only the argument value but also the SQL type and + * optionally the scale. + * @return the number of rows deleted from the table + */ + public static int deleteFromTableWhere(JdbcTemplate jdbcTemplate, String tableName, + String whereClause, Object... args) { + String sql = "DELETE FROM " + tableName; + if (StringUtils.hasText(whereClause)) { + sql += " WHERE " + whereClause; + } + int rowCount = (args != null && args.length > 0 ? jdbcTemplate.update(sql, args) + : jdbcTemplate.update(sql)); + if (logger.isInfoEnabled()) { + logger.info("Deleted " + rowCount + " rows from table " + tableName); + } + return rowCount; + } + + /** + * Drop the specified tables. + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param tableNames the names of the tables to drop + */ + public static void dropTables(JdbcTemplate jdbcTemplate, String... tableNames) { + for (String tableName : tableNames) { + jdbcTemplate.execute("DROP TABLE " + tableName); + if (logger.isInfoEnabled()) { + logger.info("Dropped table " + tableName); + } + } + } + + /** + * Execute the given SQL script. + * <p>The script will typically be loaded from the classpath. There should + * be one statement per line. Any semicolons and line comments will be removed. + * <p><b>Do not use this method to execute DDL if you expect rollback.</b> + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param resourceLoader the resource loader with which to load the SQL script + * @param sqlResourcePath the Spring resource path for the SQL script + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and {@code continueOnError} is {@code false} + * @see ResourceDatabasePopulator + * @see #executeSqlScript(JdbcTemplate, Resource, boolean) + */ + public static void executeSqlScript(JdbcTemplate jdbcTemplate, ResourceLoader resourceLoader, + String sqlResourcePath, boolean continueOnError) throws DataAccessException { + Resource resource = resourceLoader.getResource(sqlResourcePath); + executeSqlScript(jdbcTemplate, resource, continueOnError); + } + + /** + * Execute the given SQL script. + * <p>The script will typically be loaded from the classpath. Statements + * should be delimited with a semicolon. If statements are not delimited with + * a semicolon then there should be one statement per line. Statements are + * allowed to span lines only if they are delimited with a semicolon. Any + * line comments will be removed. + * <p><b>Do not use this method to execute DDL if you expect rollback.</b> + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param resource the resource to load the SQL script from + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and {@code continueOnError} is {@code false} + * @see ResourceDatabasePopulator + * @see #executeSqlScript(JdbcTemplate, EncodedResource, boolean) + */ + public static void executeSqlScript(JdbcTemplate jdbcTemplate, Resource resource, boolean continueOnError) + throws DataAccessException { + executeSqlScript(jdbcTemplate, new EncodedResource(resource), continueOnError); + } + + /** + * Execute the given SQL script. + * <p>The script will typically be loaded from the classpath. There should + * be one statement per line. Any semicolons and line comments will be removed. + * <p><b>Do not use this method to execute DDL if you expect rollback.</b> + * @param jdbcTemplate the JdbcTemplate with which to perform JDBC operations + * @param resource the resource (potentially associated with a specific encoding) + * to load the SQL script from + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and {@code continueOnError} is {@code false} + * @see ResourceDatabasePopulator + */ + public static void executeSqlScript(JdbcTemplate jdbcTemplate, EncodedResource resource, boolean continueOnError) + throws DataAccessException { + + if (logger.isInfoEnabled()) { + logger.info("Executing SQL script from " + resource); + } + long startTime = System.currentTimeMillis(); + List<String> statements = new LinkedList<String>(); + LineNumberReader reader = null; + try { + reader = new LineNumberReader(resource.getReader()); + String script = readScript(reader); + char delimiter = DEFAULT_STATEMENT_SEPARATOR; + if (!containsSqlScriptDelimiters(script, delimiter)) { + delimiter = '\n'; + } + splitSqlScript(script, delimiter, statements); + int lineNumber = 0; + for (String statement : statements) { + lineNumber++; + try { + int rowsAffected = jdbcTemplate.update(statement); + if (logger.isDebugEnabled()) { + logger.debug(rowsAffected + " rows affected by SQL: " + statement); + } + } + catch (DataAccessException ex) { + if (continueOnError) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to execute SQL script statement at line " + lineNumber + + " of resource " + resource + ": " + statement, ex); + } + } + else { + throw ex; + } + } + } + long elapsedTime = System.currentTimeMillis() - startTime; + if (logger.isInfoEnabled()) { + logger.info(String.format("Executed SQL script from %s in %s ms.", resource, elapsedTime)); + } + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Failed to open SQL script from " + resource, ex); + } + finally { + try { + if (reader != null) { + reader.close(); + } + } + catch (IOException ex) { + // ignore + } + } + } + + /** + * Read a script from the provided {@code LineNumberReader}, using + * "{@code --}" as the comment prefix, and build a {@code String} containing + * the lines. + * @param lineNumberReader the {@code LineNumberReader} containing the script + * to be processed + * @return a {@code String} containing the script lines + * @see #readScript(LineNumberReader, String) + */ + public static String readScript(LineNumberReader lineNumberReader) throws IOException { + return readScript(lineNumberReader, DEFAULT_COMMENT_PREFIX); + } + + /** + * Read a script from the provided {@code LineNumberReader}, using the supplied + * comment prefix, and build a {@code String} containing the lines. + * <p>Lines <em>beginning</em> with the comment prefix are excluded from the + * results; however, line comments anywhere else — for example, within + * a statement — will be included in the results. + * @param lineNumberReader the {@code LineNumberReader} containing the script + * to be processed + * @param commentPrefix the prefix that identifies comments in the SQL script — typically "--" + * @return a {@code String} containing the script lines + */ + public static String readScript(LineNumberReader lineNumberReader, String commentPrefix) throws IOException { + String currentStatement = lineNumberReader.readLine(); + StringBuilder scriptBuilder = new StringBuilder(); + while (currentStatement != null) { + if (StringUtils.hasText(currentStatement) + && (commentPrefix != null && !currentStatement.startsWith(commentPrefix))) { + if (scriptBuilder.length() > 0) { + scriptBuilder.append('\n'); + } + scriptBuilder.append(currentStatement); + } + currentStatement = lineNumberReader.readLine(); + } + return scriptBuilder.toString(); + } + + /** + * Determine if the provided SQL script contains the specified delimiter. + * @param script the SQL script + * @param delim character delimiting each statement — typically a ';' character + * @return {@code true} if the script contains the delimiter; {@code false} otherwise + */ + public static boolean containsSqlScriptDelimiters(String script, char delim) { + boolean inLiteral = false; + char[] content = script.toCharArray(); + for (int i = 0; i < script.length(); i++) { + if (content[i] == '\'') { + inLiteral = !inLiteral; + } + if (content[i] == delim && !inLiteral) { + return true; + } + } + return false; + } + + /** + * Split an SQL script into separate statements delimited by the provided + * delimiter character. Each individual statement will be added to the + * provided {@code List}. + * <p>Within a statement, "{@code --}" will be used as the comment prefix; + * any text beginning with the comment prefix and extending to the end of + * the line will be omitted from the statement. In addition, multiple adjacent + * whitespace characters will be collapsed into a single space. + * @param script the SQL script + * @param delim character delimiting each statement — typically a ';' character + * @param statements the list that will contain the individual statements + */ + public static void splitSqlScript(String script, char delim, List<String> statements) { + splitSqlScript(script, "" + delim, DEFAULT_COMMENT_PREFIX, statements); + } + + /** + * Split an SQL script into separate statements delimited by the provided + * delimiter string. Each individual statement will be added to the provided + * {@code List}. + * <p>Within a statement, the provided {@code commentPrefix} will be honored; + * any text beginning with the comment prefix and extending to the end of the + * line will be omitted from the statement. In addition, multiple adjacent + * whitespace characters will be collapsed into a single space. + * @param script the SQL script + * @param delim character delimiting each statement — typically a ';' character + * @param commentPrefix the prefix that identifies line comments in the SQL script — typically "--" + * @param statements the List that will contain the individual statements + */ + private static void splitSqlScript(String script, String delim, String commentPrefix, List<String> statements) { + StringBuilder sb = new StringBuilder(); + boolean inLiteral = false; + boolean inEscape = false; + char[] content = script.toCharArray(); + for (int i = 0; i < script.length(); i++) { + char c = content[i]; + if (inEscape) { + inEscape = false; + sb.append(c); + continue; + } + // MySQL style escapes + if (c == '\\') { + inEscape = true; + sb.append(c); + continue; + } + if (c == '\'') { + inLiteral = !inLiteral; + } + if (!inLiteral) { + if (script.startsWith(delim, i)) { + // we've reached the end of the current statement + if (sb.length() > 0) { + statements.add(sb.toString()); + sb = new StringBuilder(); + } + i += delim.length() - 1; + continue; + } + else if (script.startsWith(commentPrefix, i)) { + // skip over any content from the start of the comment to the EOL + int indexOfNextNewline = script.indexOf("\n", i); + if (indexOfNextNewline > i) { + i = indexOfNextNewline; + continue; + } + else { + // if there's no newline after the comment, we must be at the end + // of the script, so stop here. + break; + } + } + else if (c == ' ' || c == '\n' || c == '\t') { + // avoid multiple adjacent whitespace characters + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + c = ' '; + } + else { + continue; + } + } + } + sb.append(c); + } + if (StringUtils.hasText(sb)) { + statements.add(sb.toString()); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java b/spring-test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java new file mode 100644 index 00000000..24cee9ad --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/jdbc/SimpleJdbcTestUtils.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.jdbc; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; + +/** + * A Java-5-based collection of JDBC related utility functions intended to + * simplify standard database testing scenarios. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @author Thomas Risberg + * @since 2.5 + * @deprecated as of Spring 3.1.3; use {@link JdbcTestUtils} instead. + */ +@Deprecated +public abstract class SimpleJdbcTestUtils { + + private static final Log logger = LogFactory.getLog(SimpleJdbcTestUtils.class); + + + /** + * Count the rows in the given table. + * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations + * @param tableName table name to count rows in + * @return the number of rows in the table + */ + public static int countRowsInTable(SimpleJdbcTemplate simpleJdbcTemplate, String tableName) { + return simpleJdbcTemplate.queryForInt("SELECT COUNT(0) FROM " + tableName); + } + + /** + * Delete all rows from the specified tables. + * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations + * @param tableNames the names of the tables from which to delete + * @return the total number of rows deleted from all specified tables + */ + public static int deleteFromTables(SimpleJdbcTemplate simpleJdbcTemplate, String... tableNames) { + int totalRowCount = 0; + for (String tableName : tableNames) { + int rowCount = simpleJdbcTemplate.update("DELETE FROM " + tableName); + totalRowCount += rowCount; + if (logger.isInfoEnabled()) { + logger.info("Deleted " + rowCount + " rows from table " + tableName); + } + } + return totalRowCount; + } + + /** + * Execute the given SQL script. + * <p>The script will normally be loaded by classpath. There should be one statement + * per line. Any semicolons will be removed. <b>Do not use this method to execute + * DDL if you expect rollback.</b> + * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations + * @param resourceLoader the resource loader (with which to load the SQL script + * @param sqlResourcePath the Spring resource path for the SQL script + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error + * @throws DataAccessException if there is an error executing a statement + * and continueOnError was {@code false} + */ + public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, ResourceLoader resourceLoader, + String sqlResourcePath, boolean continueOnError) throws DataAccessException { + + Resource resource = resourceLoader.getResource(sqlResourcePath); + executeSqlScript(simpleJdbcTemplate, resource, continueOnError); + } + + /** + * Execute the given SQL script. The script will normally be loaded by classpath. + * <p>Statements should be delimited with a semicolon. If statements are not delimited with + * a semicolon then there should be one statement per line. Statements are allowed to span + * lines only if they are delimited with a semicolon. + * <p><b>Do not use this method to execute DDL if you expect rollback.</b> + * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations + * @param resource the resource to load the SQL script from. + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error. + * @throws DataAccessException if there is an error executing a statement + * and continueOnError was {@code false} + */ + public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, Resource resource, + boolean continueOnError) throws DataAccessException { + + executeSqlScript(simpleJdbcTemplate, new EncodedResource(resource), continueOnError); + } + + /** + * Execute the given SQL script. + * <p>The script will normally be loaded by classpath. There should be one statement + * per line. Any semicolons will be removed. <b>Do not use this method to execute + * DDL if you expect rollback.</b> + * @param simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations + * @param resource the resource (potentially associated with a specific encoding) + * to load the SQL script from. + * @param continueOnError whether or not to continue without throwing an + * exception in the event of an error. + * @throws DataAccessException if there is an error executing a statement + * and continueOnError was {@code false} + */ + public static void executeSqlScript(SimpleJdbcTemplate simpleJdbcTemplate, EncodedResource resource, + boolean continueOnError) throws DataAccessException { + + if (logger.isInfoEnabled()) { + logger.info("Executing SQL script from " + resource); + } + + long startTime = System.currentTimeMillis(); + List<String> statements = new LinkedList<String>(); + LineNumberReader reader = null; + try { + reader = new LineNumberReader(resource.getReader()); + String script = JdbcTestUtils.readScript(reader); + char delimiter = ';'; + if (!JdbcTestUtils.containsSqlScriptDelimiters(script, delimiter)) { + delimiter = '\n'; + } + JdbcTestUtils.splitSqlScript(script, delimiter, statements); + for (String statement : statements) { + try { + int rowsAffected = simpleJdbcTemplate.update(statement); + if (logger.isDebugEnabled()) { + logger.debug(rowsAffected + " rows affected by SQL: " + statement); + } + } + catch (DataAccessException ex) { + if (continueOnError) { + if (logger.isWarnEnabled()) { + logger.warn("SQL: " + statement + " failed", ex); + } + } + else { + throw ex; + } + } + } + long elapsedTime = System.currentTimeMillis() - startTime; + if (logger.isInfoEnabled()) { + logger.info("Done executing SQL scriptBuilder from " + resource + " in " + elapsedTime + " ms."); + } + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Failed to open SQL script from " + resource, ex); + } + finally { + try { + if (reader != null) { + reader.close(); + } + } + catch (IOException ex) { + // ignore + } + + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java b/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java new file mode 100644 index 00000000..4587aa46 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java @@ -0,0 +1,6 @@ +/** + * Support classes for tests based on JDBC. + */ + +package org.springframework.test.jdbc; + diff --git a/spring-test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java b/spring-test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java new file mode 100644 index 00000000..dd55d0e7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/jpa/AbstractAspectjJpaTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.jpa; + +import org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter; + +import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader; + +/** + * Subclass of {@link AbstractJpaTests} that activates AspectJ load-time weaving and + * allows for specifying a custom location for AspectJ's {@code aop.xml} file. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests}) + */ +@Deprecated +public abstract class AbstractAspectjJpaTests extends AbstractJpaTests { + + /** + * Default location of the {@code aop.xml} file in the class path: + * "META-INF/aop.xml" + */ + public static final String DEFAULT_AOP_XML_LOCATION = "META-INF/aop.xml"; + + + @Override + protected void customizeResourceOverridingShadowingClassLoader(ClassLoader shadowingClassLoader) { + ResourceOverridingShadowingClassLoader orxl = (ResourceOverridingShadowingClassLoader) shadowingClassLoader; + orxl.override(DEFAULT_AOP_XML_LOCATION, getActualAopXmlLocation()); + orxl.addTransformer(new ClassPreProcessorAgentAdapter()); + } + + /** + * Return the actual location of the {@code aop.xml} file + * in the class path. The default is "META-INF/aop.xml". + * <p>Override this method to point to a specific {@code aop.xml} + * file within your test suite, allowing for different config files + * to co-exist within the same class path. + */ + protected String getActualAopXmlLocation() { + return DEFAULT_AOP_XML_LOCATION; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java b/spring-test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java new file mode 100644 index 00000000..005c19e3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/jpa/AbstractJpaTests.java @@ -0,0 +1,386 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.jpa; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import junit.framework.TestCase; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader; +import org.springframework.instrument.classloading.ShadowingClassLoader; +import org.springframework.orm.jpa.ExtendedEntityManagerCreator; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.SharedEntityManagerCreator; +import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; +import org.springframework.test.annotation.AbstractAnnotationAwareTransactionalTests; +import org.springframework.util.StringUtils; + +/** + * Convenient support class for JPA-related tests. Offers the same contract as + * AbstractTransactionalDataSourceSpringContextTests and equally good performance, + * even when performing the instrumentation required by the JPA specification. + * + * <p>Exposes an EntityManagerFactory and a shared EntityManager. + * Requires an EntityManagerFactory to be injected, plus the DataSource and + * JpaTransactionManager through the superclass. + * + * <p>When using Xerces, make sure a post 2.0.2 version is available on the classpath + * to avoid a critical + * <a href="http://nagoya.apache.org/bugzilla/show_bug.cgi?id=16014"/>bug</a> + * that leads to StackOverflow. Maven users are likely to encounter this problem since + * 2.0.2 is used by default. + * + * <p>A workaround is to explicitly specify the Xerces version inside the Maven POM: + * <pre> + * <dependency> + * <groupId>xerces</groupId> + * <artifactId>xercesImpl</artifactId> + * <version>2.8.1</version> + * </dependency> + * </pre> + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests}) + */ +@Deprecated +public abstract class AbstractJpaTests extends AbstractAnnotationAwareTransactionalTests { + + private static final String DEFAULT_ORM_XML_LOCATION = "META-INF/orm.xml"; + + /** + * Map from String defining unique combination of config locations, to ApplicationContext. + * Values are intentionally not strongly typed, to avoid potential class cast exceptions + * through use between different class loaders. + */ + private static Map<String, Object> contextCache = new HashMap<String, Object>(); + + private static Map<String, ClassLoader> classLoaderCache = new HashMap<String, ClassLoader>(); + + protected EntityManagerFactory entityManagerFactory; + + /** + * If this instance is in a shadow loader, this variable + * will contain the parent instance of the subclass. + * The class will not be the same as the class of the + * shadow instance, as it was loaded by a different class loader, + * but it can be invoked reflectively. The shadowParent + * and the shadow loader can communicate reflectively + * but not through direct invocation. + */ + private Object shadowParent; + + /** + * Subclasses can use this in test cases. + * It will participate in any current transaction. + */ + protected EntityManager sharedEntityManager; + + + public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + this.sharedEntityManager = SharedEntityManagerCreator.createSharedEntityManager(this.entityManagerFactory); + } + + /** + * Create an EntityManager that will always automatically enlist itself in current + * transactions, in contrast to an EntityManager returned by + * {@code EntityManagerFactory.createEntityManager()} + * (which requires an explicit {@code joinTransaction()} call). + */ + protected EntityManager createContainerManagedEntityManager() { + return ExtendedEntityManagerCreator.createContainerManagedEntityManager(this.entityManagerFactory); + } + + /** + * Subclasses should override this method if they wish to disable shadow class loading. + * <p>The default implementation deactivates shadow class loading if Spring's + * InstrumentationSavingAgent has been configured on VM startup. + */ + protected boolean shouldUseShadowLoader() { + return !InstrumentationLoadTimeWeaver.isInstrumentationAvailable(); + } + + @Override + public void setDirty() { + super.setDirty(); + contextCache.remove(cacheKeys()); + classLoaderCache.remove(cacheKeys()); + + // If we are a shadow loader, we need to invoke + // the shadow parent to set it dirty, as + // it is the shadow parent that maintains the cache state, + // not the child + if (this.shadowParent != null) { + try { + Method m = shadowParent.getClass().getMethod("setDirty", (Class[]) null); + m.invoke(shadowParent, (Object[]) null); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } + + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void runBare() throws Throwable { + + // getName will return the name of the method being run. + if (isDisabledInThisEnvironment(getName())) { + // Let superclass log that we didn't run the test. + super.runBare(); + return; + } + + final Method testMethod = getTestMethod(); + + if (isDisabledInThisEnvironment(testMethod)) { + recordDisabled(); + this.logger.info("**** " + getClass().getName() + "." + getName() + " is disabled in this environment: " + + "Total disabled tests=" + getDisabledTestCount()); + return; + } + + if (!shouldUseShadowLoader()) { + super.runBare(); + return; + } + + String combinationOfContextLocationsForThisTestClass = cacheKeys(); + ClassLoader classLoaderForThisTestClass = getClass().getClassLoader(); + // save the TCCL + ClassLoader initialClassLoader = Thread.currentThread().getContextClassLoader(); + + if (this.shadowParent != null) { + Thread.currentThread().setContextClassLoader(classLoaderForThisTestClass); + super.runBare(); + } + + else { + ShadowingClassLoader shadowingClassLoader = (ShadowingClassLoader) classLoaderCache.get(combinationOfContextLocationsForThisTestClass); + + if (shadowingClassLoader == null) { + shadowingClassLoader = (ShadowingClassLoader) createShadowingClassLoader(classLoaderForThisTestClass); + classLoaderCache.put(combinationOfContextLocationsForThisTestClass, shadowingClassLoader); + } + try { + Thread.currentThread().setContextClassLoader(shadowingClassLoader); + String[] configLocations = getConfigLocations(); + + // Do not strongly type, to avoid ClassCastException. + Object cachedContext = contextCache.get(combinationOfContextLocationsForThisTestClass); + + if (cachedContext == null) { + + // Create the LoadTimeWeaver. + Class shadowingLoadTimeWeaverClass = shadowingClassLoader.loadClass(ShadowingLoadTimeWeaver.class.getName()); + Constructor constructor = shadowingLoadTimeWeaverClass.getConstructor(ClassLoader.class); + constructor.setAccessible(true); + Object ltw = constructor.newInstance(shadowingClassLoader); + + // Create the BeanFactory. + Class beanFactoryClass = shadowingClassLoader.loadClass(DefaultListableBeanFactory.class.getName()); + Object beanFactory = BeanUtils.instantiateClass(beanFactoryClass); + + // Create the BeanDefinitionReader. + Class beanDefinitionReaderClass = shadowingClassLoader.loadClass(XmlBeanDefinitionReader.class.getName()); + Class beanDefinitionRegistryClass = shadowingClassLoader.loadClass(BeanDefinitionRegistry.class.getName()); + Object reader = beanDefinitionReaderClass.getConstructor(beanDefinitionRegistryClass).newInstance(beanFactory); + + // Load the bean definitions into the BeanFactory. + Method loadBeanDefinitions = beanDefinitionReaderClass.getMethod("loadBeanDefinitions", String[].class); + loadBeanDefinitions.invoke(reader, new Object[] {configLocations}); + + // Create LoadTimeWeaver-injecting BeanPostProcessor. + Class loadTimeWeaverInjectingBeanPostProcessorClass = shadowingClassLoader.loadClass(LoadTimeWeaverInjectingBeanPostProcessor.class.getName()); + Class loadTimeWeaverClass = shadowingClassLoader.loadClass(LoadTimeWeaver.class.getName()); + Constructor bppConstructor = loadTimeWeaverInjectingBeanPostProcessorClass.getConstructor(loadTimeWeaverClass); + bppConstructor.setAccessible(true); + Object beanPostProcessor = bppConstructor.newInstance(ltw); + + // Add LoadTimeWeaver-injecting BeanPostProcessor. + Class beanPostProcessorClass = shadowingClassLoader.loadClass(BeanPostProcessor.class.getName()); + Method addBeanPostProcessor = beanFactoryClass.getMethod("addBeanPostProcessor", beanPostProcessorClass); + addBeanPostProcessor.invoke(beanFactory, beanPostProcessor); + + // Create the GenericApplicationContext. + Class genericApplicationContextClass = shadowingClassLoader.loadClass(GenericApplicationContext.class.getName()); + Class defaultListableBeanFactoryClass = shadowingClassLoader.loadClass(DefaultListableBeanFactory.class.getName()); + cachedContext = genericApplicationContextClass.getConstructor(defaultListableBeanFactoryClass).newInstance(beanFactory); + + // Invoke the context's "refresh" method. + genericApplicationContextClass.getMethod("refresh").invoke(cachedContext); + + // Store the context reference in the cache. + contextCache.put(combinationOfContextLocationsForThisTestClass, cachedContext); + } + // create the shadowed test + Class shadowedTestClass = shadowingClassLoader.loadClass(getClass().getName()); + + // So long as JUnit is excluded from shadowing we + // can minimize reflective invocation here + TestCase shadowedTestCase = (TestCase) BeanUtils.instantiateClass(shadowedTestClass); + + /* shadowParent = this */ + Class thisShadowedClass = shadowingClassLoader.loadClass(AbstractJpaTests.class.getName()); + Field shadowed = thisShadowedClass.getDeclaredField("shadowParent"); + shadowed.setAccessible(true); + shadowed.set(shadowedTestCase, this); + + /* AbstractSpringContextTests.addContext(Object, ApplicationContext) */ + Class applicationContextClass = shadowingClassLoader.loadClass(ConfigurableApplicationContext.class.getName()); + Method addContextMethod = shadowedTestClass.getMethod("addContext", Object.class, applicationContextClass); + addContextMethod.invoke(shadowedTestCase, configLocations, cachedContext); + + // Invoke tests on shadowed test case + shadowedTestCase.setName(getName()); + shadowedTestCase.runBare(); + } + catch (InvocationTargetException ex) { + // Unwrap this for better exception reporting + // when running tests + throw ex.getTargetException(); + } + finally { + Thread.currentThread().setContextClassLoader(initialClassLoader); + } + } + } + + protected String cacheKeys() { + return StringUtils.arrayToCommaDelimitedString(getConfigLocations()); + } + + /** + * NB: This method must <b>not</b> have a return type of ShadowingClassLoader as that would cause that + * class to be loaded eagerly when this test case loads, creating verify errors at runtime. + */ + protected ClassLoader createShadowingClassLoader(ClassLoader classLoader) { + OrmXmlOverridingShadowingClassLoader orxl = new OrmXmlOverridingShadowingClassLoader(classLoader, + getActualOrmXmlLocation()); + customizeResourceOverridingShadowingClassLoader(orxl); + return orxl; + } + + /** + * Customize the shadowing class loader. + * @param shadowingClassLoader this parameter is actually of type + * ResourceOverridingShadowingClassLoader, and can safely to be cast to + * that type. However, the signature must not be of that type as that + * would cause the present class loader to load that type. + */ + protected void customizeResourceOverridingShadowingClassLoader(ClassLoader shadowingClassLoader) { + // empty + } + + /** + * Subclasses can override this to return the real location path for + * orm.xml or null if they do not wish to find any orm.xml + * @return orm.xml path or null to hide any such file + */ + protected String getActualOrmXmlLocation() { + return DEFAULT_ORM_XML_LOCATION; + } + + + private static class LoadTimeWeaverInjectingBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter { + + private final LoadTimeWeaver ltw; + + @SuppressWarnings("unused") + public LoadTimeWeaverInjectingBeanPostProcessor(LoadTimeWeaver ltw) { + this.ltw = ltw; + } + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof LocalContainerEntityManagerFactoryBean) { + ((LocalContainerEntityManagerFactoryBean) bean).setLoadTimeWeaver(this.ltw); + } + if (bean instanceof DefaultPersistenceUnitManager) { + ((DefaultPersistenceUnitManager) bean).setLoadTimeWeaver(this.ltw); + } + return bean; + } + } + + + private static class ShadowingLoadTimeWeaver implements LoadTimeWeaver { + + private final ClassLoader shadowingClassLoader; + + @SuppressWarnings("unused") + public ShadowingLoadTimeWeaver(ClassLoader shadowingClassLoader) { + this.shadowingClassLoader = shadowingClassLoader; + } + + public void addTransformer(ClassFileTransformer transformer) { + try { + Method addClassFileTransformer = + this.shadowingClassLoader.getClass().getMethod("addTransformer", ClassFileTransformer.class); + addClassFileTransformer.setAccessible(true); + addClassFileTransformer.invoke(this.shadowingClassLoader, transformer); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public ClassLoader getInstrumentableClassLoader() { + return this.shadowingClassLoader; + } + + public ClassLoader getThrowawayClassLoader() { + // Be sure to copy the same resource overrides and same class file transformers: + // We want the throwaway class loader to behave like the instrumentable class loader. + ResourceOverridingShadowingClassLoader roscl = + new ResourceOverridingShadowingClassLoader(getClass().getClassLoader()); + if (this.shadowingClassLoader instanceof ShadowingClassLoader) { + roscl.copyTransformers((ShadowingClassLoader) this.shadowingClassLoader); + } + if (this.shadowingClassLoader instanceof ResourceOverridingShadowingClassLoader) { + roscl.copyOverrides((ResourceOverridingShadowingClassLoader) this.shadowingClassLoader); + } + return roscl; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java b/spring-test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java new file mode 100644 index 00000000..9cf1ded1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/jpa/OrmXmlOverridingShadowingClassLoader.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.jpa; + +import org.springframework.instrument.classloading.ResourceOverridingShadowingClassLoader; + +/** + * Subclass of ShadowingClassLoader that overrides attempts to + * locate {@code orm.xml}. + * + * <p>This class must <b>not</b> be an inner class of AbstractJpaTests + * to avoid it being loaded until first used. + * + * @author Rod Johnson + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +class OrmXmlOverridingShadowingClassLoader extends ResourceOverridingShadowingClassLoader { + + /** + * Default location of the {@code orm.xml} file in the class path: + * "META-INF/orm.xml" + */ + public static final String DEFAULT_ORM_XML_LOCATION = "META-INF/orm.xml"; + + + public OrmXmlOverridingShadowingClassLoader(ClassLoader loader, String realOrmXmlLocation) { + super(loader); + + // Automatically exclude classes from these well-known persistence providers. + // Do NOT exclude Hibernate classes -- + // this causes class casts due to use of CGLIB by Hibernate. + // Same goes for OpenJPA which will not enhance the domain classes. + excludePackage("oracle.toplink.essentials"); + excludePackage("junit"); + + override(DEFAULT_ORM_XML_LOCATION, realOrmXmlLocation); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/jpa/package-info.java b/spring-test/src/main/java/org/springframework/test/jpa/package-info.java new file mode 100644 index 00000000..58ef717a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/jpa/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * As of Spring 3.0, this package has been deprecated in favor of using the listener-based + * <em>Spring TestContext Framework</em>. + * + */ +package org.springframework.test.jpa; + diff --git a/spring-test/src/main/java/org/springframework/test/package-info.java b/spring-test/src/main/java/org/springframework/test/package-info.java new file mode 100644 index 00000000..9ec02f96 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * This package contains the legacy JUnit 3.8 class hierarchy, which as of Spring 3.0 + * has been deprecated in favor of using the listener-based <em>Spring TestContext Framework</em>. + * + */ +package org.springframework.test; + diff --git a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java new file mode 100644 index 00000000..4f2dbab7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.util; + +import org.springframework.util.ObjectUtils; + +/** + * JUnit independent assertion class. + * + * @author Lukas Krecan + * @author Arjen Poutsma + * @since 3.2 + */ +public abstract class AssertionErrors { + + + private AssertionErrors() { + } + + /** + * Fails a test with the given message. + * + * @param message describes the reason for the failure + */ + public static void fail(String message) { + throw new AssertionError(message); + } + + /** + * Fails a test with the given message passing along expected and actual + * values to be added to the message. + * + * <p>For example given: + * <pre> + * assertEquals("Response header [" + name + "]", actual, expected); + * </pre> + * <p>The resulting message is: + * <pre> + * Response header [Accept] expected:<application/json> but was:<text/plain> + * </pre> + * + * @param message describes the value that failed the match + * @param expected expected value + * @param actual actual value + */ + public static void fail(String message, Object expected, Object actual) { + throw new AssertionError(message + " expected:<" + expected + "> but was:<" + actual + ">"); + } + + /** + * Assert the given condition is {@code true} and raise an + * {@link AssertionError} if it is not. + * + * @param message the message + * @param condition the condition to test for + */ + public static void assertTrue(String message, boolean condition) { + if (!condition) { + fail(message); + } + } + + /** + * Assert two objects are equal raise an {@link AssertionError} if not. + * <p>For example: + * <pre> + * assertEquals("Response header [" + name + "]", actual, expected); + * </pre> + * + * @param message describes the value being checked + * @param expected the expected value + * @param actual the actual value + */ + public static void assertEquals(String message, Object expected, Object actual) { + if (!ObjectUtils.nullSafeEquals(expected, actual)) { + fail(message, expected, actual); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java new file mode 100644 index 00000000..6b54d547 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java @@ -0,0 +1,315 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.util.MethodInvoker; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@code ReflectionTestUtils} is a collection of reflection-based utility + * methods for use in unit and integration testing scenarios. + * + * <p>There are often times when it would be beneficial to be able to set a + * non-{@code public} field, invoke a non-{@code public} setter method, or + * invoke a non-{@code public} <em>configuration</em> or <em>lifecycle</em> + * callback method when testing code involving, for example: + * + * <ul> + * <li>ORM frameworks such as JPA and Hibernate which condone the usage of + * {@code private} or {@code protected} field access as opposed to + * {@code public} setter methods for properties in a domain entity.</li> + * <li>Spring's support for annotations such as + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} and + * {@link javax.annotation.Resource @Resource} which provides dependency + * injection for {@code private} or {@code protected} fields, setter methods, + * and configuration methods.</li> + * <li>Use of annotations such as {@link javax.annotation.PostConstruct @PostConstruct} + * and {@link javax.annotation.PreDestroy @PreDestroy} for lifecycle callback + * methods.</li> + * </ul> + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ReflectionUtils + */ +public class ReflectionTestUtils { + + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final Log logger = LogFactory.getLog(ReflectionTestUtils.class); + + + /** + * Set the {@link Field field} with the given {@code name} on the provided + * {@link Object target object} to the supplied {@code value}. + * + * <p>This method traverses the class hierarchy in search of the desired field. + * In addition, an attempt will be made to make non-{@code public} fields + * <em>accessible</em>, thus allowing one to set {@code protected}, + * {@code private}, and <em>package-private</em> fields. + * + * @param target the target object on which to set the field + * @param name the name of the field to set + * @param value the value to set + * @see ReflectionUtils#findField(Class, String, Class) + * @see ReflectionUtils#makeAccessible(Field) + * @see ReflectionUtils#setField(Field, Object, Object) + */ + public static void setField(Object target, String name, Object value) { + setField(target, name, value, null); + } + + /** + * Set the {@link Field field} with the given {@code name} on the provided + * {@link Object target object} to the supplied {@code value}. + * + * <p>This method traverses the class hierarchy in search of the desired + * field. In addition, an attempt will be made to make non-{@code public} + * fields <em>accessible</em>, thus allowing one to set {@code protected}, + * {@code private}, and <em>package-private</em> fields. + * + * @param target the target object on which to set the field + * @param name the name of the field to set + * @param value the value to set + * @param type the type of the field (may be {@code null}) + * @see ReflectionUtils#findField(Class, String, Class) + * @see ReflectionUtils#makeAccessible(Field) + * @see ReflectionUtils#setField(Field, Object, Object) + */ + public static void setField(Object target, String name, Object value, Class<?> type) { + Assert.notNull(target, "Target object must not be null"); + Field field = ReflectionUtils.findField(target.getClass(), name, type); + + // SPR-9571: inline Assert.notNull() in order to avoid accidentally invoking + // toString() on a non-null target. + if (field == null) { + throw new IllegalArgumentException(String.format("Could not find field [%s] of type [%s] on target [%s]", + name, type, target)); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Setting field [%s] of type [%s] on target [%s] to value [%s]", name, type, + target, + value)); + } + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, target, value); + } + + /** + * Get the field with the given {@code name} from the provided target object. + * + * <p>This method traverses the class hierarchy in search of the desired + * field. In addition, an attempt will be made to make non-{@code public} + * fields <em>accessible</em>, thus allowing one to get {@code protected}, + * {@code private}, and <em>package-private</em> fields. + * + * @param target the target object on which to set the field + * @param name the name of the field to get + * @return the field's current value + * @see ReflectionUtils#findField(Class, String, Class) + * @see ReflectionUtils#makeAccessible(Field) + * @see ReflectionUtils#setField(Field, Object, Object) + */ + public static Object getField(Object target, String name) { + Assert.notNull(target, "Target object must not be null"); + Field field = ReflectionUtils.findField(target.getClass(), name); + Assert.notNull(field, "Could not find field [" + name + "] on target [" + target + "]"); + + if (logger.isDebugEnabled()) { + logger.debug("Getting field [" + name + "] from target [" + target + "]"); + } + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, target); + } + + /** + * Invoke the setter method with the given {@code name} on the supplied + * target object with the supplied {@code value}. + * + * <p>This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-{@code public} + * methods <em>accessible</em>, thus allowing one to invoke {@code protected}, + * {@code private}, and <em>package-private</em> setter methods. + * + * <p>In addition, this method supports JavaBean-style <em>property</em> + * names. For example, if you wish to set the {@code name} property on the + * target object, you may pass either "name" or + * "setName" as the method name. + * + * @param target the target object on which to invoke the specified setter + * method + * @param name the name of the setter method to invoke or the corresponding + * property name + * @param value the value to provide to the setter method + * @see ReflectionUtils#findMethod(Class, String, Class[]) + * @see ReflectionUtils#makeAccessible(Method) + * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) + */ + public static void invokeSetterMethod(Object target, String name, Object value) { + invokeSetterMethod(target, name, value, null); + } + + /** + * Invoke the setter method with the given {@code name} on the supplied + * target object with the supplied {@code value}. + * + * <p>This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-{@code public} + * methods <em>accessible</em>, thus allowing one to invoke {@code protected}, + * {@code private}, and <em>package-private</em> setter methods. + * + * <p>In addition, this method supports JavaBean-style <em>property</em> + * names. For example, if you wish to set the {@code name} property on the + * target object, you may pass either "name" or + * "setName" as the method name. + * + * @param target the target object on which to invoke the specified setter + * method + * @param name the name of the setter method to invoke or the corresponding + * property name + * @param value the value to provide to the setter method + * @param type the formal parameter type declared by the setter method + * @see ReflectionUtils#findMethod(Class, String, Class[]) + * @see ReflectionUtils#makeAccessible(Method) + * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) + */ + public static void invokeSetterMethod(Object target, String name, Object value, Class<?> type) { + Assert.notNull(target, "Target object must not be null"); + Assert.hasText(name, "Method name must not be empty"); + Class<?>[] paramTypes = (type != null ? new Class<?>[] { type } : null); + + String setterMethodName = name; + if (!name.startsWith(SETTER_PREFIX)) { + setterMethodName = SETTER_PREFIX + StringUtils.capitalize(name); + } + Method method = ReflectionUtils.findMethod(target.getClass(), setterMethodName, paramTypes); + if (method == null && !setterMethodName.equals(name)) { + setterMethodName = name; + method = ReflectionUtils.findMethod(target.getClass(), setterMethodName, paramTypes); + } + Assert.notNull(method, "Could not find setter method [" + setterMethodName + "] on target [" + target + + "] with parameter type [" + type + "]"); + + if (logger.isDebugEnabled()) { + logger.debug("Invoking setter method [" + setterMethodName + "] on target [" + target + "]"); + } + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, target, new Object[] { value }); + } + + /** + * Invoke the getter method with the given {@code name} on the supplied + * target object with the supplied {@code value}. + * + * <p>This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-{@code public} + * methods <em>accessible</em>, thus allowing one to invoke {@code protected}, + * {@code private}, and <em>package-private</em> getter methods. + * + * <p>In addition, this method supports JavaBean-style <em>property</em> + * names. For example, if you wish to get the {@code name} property on the + * target object, you may pass either "name" or + * "getName" as the method name. + * + * @param target the target object on which to invoke the specified getter + * method + * @param name the name of the getter method to invoke or the corresponding + * property name + * @return the value returned from the invocation + * @see ReflectionUtils#findMethod(Class, String, Class[]) + * @see ReflectionUtils#makeAccessible(Method) + * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) + */ + public static Object invokeGetterMethod(Object target, String name) { + Assert.notNull(target, "Target object must not be null"); + Assert.hasText(name, "Method name must not be empty"); + + String getterMethodName = name; + if (!name.startsWith(GETTER_PREFIX)) { + getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + } + Method method = ReflectionUtils.findMethod(target.getClass(), getterMethodName); + if (method == null && !getterMethodName.equals(name)) { + getterMethodName = name; + method = ReflectionUtils.findMethod(target.getClass(), getterMethodName); + } + Assert.notNull(method, "Could not find getter method [" + getterMethodName + "] on target [" + target + "]"); + + if (logger.isDebugEnabled()) { + logger.debug("Invoking getter method [" + getterMethodName + "] on target [" + target + "]"); + } + ReflectionUtils.makeAccessible(method); + return ReflectionUtils.invokeMethod(method, target); + } + + /** + * Invoke the method with the given {@code name} on the supplied target + * object with the supplied arguments. + * + * <p>This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-{@code public} + * methods <em>accessible</em>, thus allowing one to invoke {@code protected}, + * {@code private}, and <em>package-private</em> methods. + * + * @param target the target object on which to invoke the specified method + * @param name the name of the method to invoke + * @param args the arguments to provide to the method + * @return the invocation result, if any + * @see MethodInvoker + * @see ReflectionUtils#makeAccessible(Method) + * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) + * @see ReflectionUtils#handleReflectionException(Exception) + */ + @SuppressWarnings("unchecked") + public static <T> T invokeMethod(Object target, String name, Object... args) { + Assert.notNull(target, "Target object must not be null"); + Assert.hasText(name, "Method name must not be empty"); + + try { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(target); + methodInvoker.setTargetMethod(name); + methodInvoker.setArguments(args); + methodInvoker.prepare(); + + if (logger.isDebugEnabled()) { + logger.debug("Invoking method [" + name + "] on target [" + target + "] with arguments [" + + ObjectUtils.nullSafeToString(args) + "]"); + } + + return (T) methodInvoker.invoke(); + } + catch (Exception e) { + ReflectionUtils.handleReflectionException(e); + } + + throw new IllegalStateException("Should never get here"); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/util/package-info.java b/spring-test/src/main/java/org/springframework/test/util/package-info.java new file mode 100644 index 00000000..7d9e7496 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/util/package-info.java @@ -0,0 +1,5 @@ +/** + * General utility classes for use in unit and integration tests. + */ + +package org.springframework.test.util;
\ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java b/spring-test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java new file mode 100644 index 00000000..fefc31e1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/AbstractModelAndViewTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.springframework.web.servlet.ModelAndView; + +/** + * Convenient JUnit 3.8 base class for tests dealing with Spring Web MVC + * {@link org.springframework.web.servlet.ModelAndView ModelAndView} objects. + * + * <p>All {@code assert*()} methods throw {@link AssertionFailedError}s. + * + * <p>Consider the use of {@link ModelAndViewAssert} with JUnit 4 and TestNG. + * + * @author Alef Arendsen + * @author Bram Smeets + * @author Sam Brannen + * @since 2.0 + * @see org.springframework.web.servlet.ModelAndView + * @see ModelAndViewAssert + * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework + * ({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests}) + * or {@link ModelAndViewAssert} with JUnit 4 and TestNG. + */ +@Deprecated +public abstract class AbstractModelAndViewTests extends TestCase { + + /** + * Checks whether the model value under the given {@code modelName} + * exists and checks it type, based on the {@code expectedType}. If + * the model entry exists and the type matches, the model value is returned. + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedType expected type of the model value + * @return the model value + */ + protected <T> T assertAndReturnModelAttributeOfType(ModelAndView mav, String modelName, Class<T> expectedType) { + try { + return ModelAndViewAssert.assertAndReturnModelAttributeOfType(mav, modelName, expectedType); + } + catch (AssertionError e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + /** + * Compare each individual entry in a list, without first sorting the lists. + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedList the expected list + */ + @SuppressWarnings("rawtypes") + protected void assertCompareListModelAttribute(ModelAndView mav, String modelName, List expectedList) { + try { + ModelAndViewAssert.assertCompareListModelAttribute(mav, modelName, expectedList); + } + catch (AssertionError e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + /** + * Assert whether or not a model attribute is available. + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + */ + protected void assertModelAttributeAvailable(ModelAndView mav, String modelName) { + try { + ModelAndViewAssert.assertModelAttributeAvailable(mav, modelName); + } + catch (AssertionError e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + /** + * Compare a given {@code expectedValue} to the value from the model + * bound under the given {@code modelName}. + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedValue the model value + */ + protected void assertModelAttributeValue(ModelAndView mav, String modelName, Object expectedValue) { + try { + ModelAndViewAssert.assertModelAttributeValue(mav, modelName, expectedValue); + } + catch (AssertionError e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + /** + * Inspect the {@code expectedModel} to see if all elements in the + * model appear and are equal. + * @param mav ModelAndView to test against (never {@code null}) + * @param expectedModel the expected model + */ + protected void assertModelAttributeValues(ModelAndView mav, Map<String, Object> expectedModel) { + try { + ModelAndViewAssert.assertModelAttributeValues(mav, expectedModel); + } + catch (AssertionError e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + /** + * Compare each individual entry in a list after having sorted both lists + * (optionally using a comparator). + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedList the expected list + * @param comparator the comparator to use (may be {@code null}). If + * not specifying the comparator, both lists will be sorted not using + * any comparator. + */ + @SuppressWarnings("rawtypes") + protected void assertSortAndCompareListModelAttribute( + ModelAndView mav, String modelName, List expectedList, Comparator comparator) { + try { + ModelAndViewAssert.assertSortAndCompareListModelAttribute(mav, modelName, expectedList, comparator); + } + catch (AssertionError e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + /** + * Check to see if the view name in the ModelAndView matches the given + * {@code expectedName}. + * @param mav ModelAndView to test against (never {@code null}) + * @param expectedName the name of the model value + */ + protected void assertViewName(ModelAndView mav, String expectedName) { + try { + ModelAndViewAssert.assertViewName(mav, expectedName); + } + catch (AssertionError e) { + throw new AssertionFailedError(e.getMessage()); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java new file mode 100644 index 00000000..62677fdb --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import static org.springframework.test.util.AssertionErrors.*; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.ObjectUtils; +import org.springframework.web.servlet.ModelAndView; + +/** + * A collection of assertions intended to simplify testing scenarios dealing + * with Spring Web MVC {@link org.springframework.web.servlet.ModelAndView + * ModelAndView} objects. + * <p> + * Intended for use with JUnit 4 and TestNG. All {@code assert*()} methods + * throw {@link AssertionError}s. + * + * @author Sam Brannen + * @author Alef Arendsen + * @author Bram Smeets + * @since 2.5 + * @see org.springframework.web.servlet.ModelAndView + */ +public abstract class ModelAndViewAssert { + + /** + * Checks whether the model value under the given {@code modelName} + * exists and checks it type, based on the {@code expectedType}. If the + * model entry exists and the type matches, the model value is returned. + * + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedType expected type of the model value + * @return the model value + */ + @SuppressWarnings("unchecked") + public static <T> T assertAndReturnModelAttributeOfType(ModelAndView mav, String modelName, Class<T> expectedType) { + assertTrue("ModelAndView is null", mav != null); + assertTrue("Model is null", mav.getModel() != null); + Object obj = mav.getModel().get(modelName); + assertTrue("Model attribute with name '" + modelName + "' is null", obj != null); + assertTrue("Model attribute is not of expected type '" + expectedType.getName() + "' but rather of type '" + + obj.getClass().getName() + "'", expectedType.isAssignableFrom(obj.getClass())); + return (T) obj; + } + + /** + * Compare each individual entry in a list, without first sorting the lists. + * + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedList the expected list + */ + @SuppressWarnings("rawtypes") + public static void assertCompareListModelAttribute(ModelAndView mav, String modelName, List expectedList) { + assertTrue("ModelAndView is null", mav != null); + List modelList = assertAndReturnModelAttributeOfType(mav, modelName, List.class); + assertTrue( + "Size of model list is '" + modelList.size() + "' while size of expected list is '" + expectedList.size() + + "'", expectedList.size() == modelList.size()); + assertTrue("List in model under name '" + modelName + "' is not equal to the expected list.", + expectedList.equals(modelList)); + } + + /** + * Assert whether or not a model attribute is available. + * + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + */ + public static void assertModelAttributeAvailable(ModelAndView mav, String modelName) { + assertTrue("ModelAndView is null", mav != null); + assertTrue("Model is null", mav.getModel() != null); + assertTrue("Model attribute with name '" + modelName + "' is not available", + mav.getModel().containsKey(modelName)); + } + + /** + * Compare a given {@code expectedValue} to the value from the model + * bound under the given {@code modelName}. + * + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedValue the model value + */ + public static void assertModelAttributeValue(ModelAndView mav, String modelName, Object expectedValue) { + assertTrue("ModelAndView is null", mav != null); + Object modelValue = assertAndReturnModelAttributeOfType(mav, modelName, Object.class); + assertTrue("Model value with name '" + modelName + "' is not the same as the expected value which was '" + + expectedValue + "'", modelValue.equals(expectedValue)); + } + + /** + * Inspect the {@code expectedModel} to see if all elements in the + * model appear and are equal. + * + * @param mav ModelAndView to test against (never {@code null}) + * @param expectedModel the expected model + */ + public static void assertModelAttributeValues(ModelAndView mav, Map<String, Object> expectedModel) { + assertTrue("ModelAndView is null", mav != null); + assertTrue("Model is null", mav.getModel() != null); + + if (!mav.getModel().keySet().equals(expectedModel.keySet())) { + StringBuilder sb = new StringBuilder("Keyset of expected model does not match.\n"); + appendNonMatchingSetsErrorMessage(expectedModel.keySet(), mav.getModel().keySet(), sb); + fail(sb.toString()); + } + + StringBuilder sb = new StringBuilder(); + for (String modelName : mav.getModel().keySet()) { + Object assertionValue = expectedModel.get(modelName); + Object mavValue = mav.getModel().get(modelName); + if (!assertionValue.equals(mavValue)) { + sb.append("Value under name '").append(modelName).append("' differs, should have been '").append( + assertionValue).append("' but was '").append(mavValue).append("'\n"); + } + } + + if (sb.length() != 0) { + sb.insert(0, "Values of expected model do not match.\n"); + fail(sb.toString()); + } + } + + /** + * Compare each individual entry in a list after having sorted both lists + * (optionally using a comparator). + * + * @param mav ModelAndView to test against (never {@code null}) + * @param modelName name of the object to add to the model (never + * {@code null}) + * @param expectedList the expected list + * @param comparator the comparator to use (may be {@code null}). If + * not specifying the comparator, both lists will be sorted not using any + * comparator. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static void assertSortAndCompareListModelAttribute(ModelAndView mav, String modelName, List expectedList, + Comparator comparator) { + + assertTrue("ModelAndView is null", mav != null); + List modelList = assertAndReturnModelAttributeOfType(mav, modelName, List.class); + + assertTrue( + "Size of model list is '" + modelList.size() + "' while size of expected list is '" + expectedList.size() + + "'", expectedList.size() == modelList.size()); + + if (comparator != null) { + Collections.sort(modelList, comparator); + Collections.sort(expectedList, comparator); + } + else { + Collections.sort(modelList); + Collections.sort(expectedList); + } + + assertTrue("List in model under name '" + modelName + "' is not equal to the expected list.", + expectedList.equals(modelList)); + } + + /** + * Check to see if the view name in the ModelAndView matches the given + * {@code expectedName}. + * + * @param mav ModelAndView to test against (never {@code null}) + * @param expectedName the name of the model value + */ + public static void assertViewName(ModelAndView mav, String expectedName) { + assertTrue("ModelAndView is null", mav != null); + assertTrue("View name is not equal to '" + expectedName + "' but was '" + mav.getViewName() + "'", + ObjectUtils.nullSafeEquals(expectedName, mav.getViewName())); + } + + private static void appendNonMatchingSetsErrorMessage(Set<String> assertionSet, Set<String> incorrectSet, + StringBuilder sb) { + + Set<String> tempSet = new HashSet<String>(); + tempSet.addAll(incorrectSet); + tempSet.removeAll(assertionSet); + + if (tempSet.size() > 0) { + sb.append("Set has too many elements:\n"); + for (Object element : tempSet) { + sb.append('-'); + sb.append(element); + sb.append('\n'); + } + } + + tempSet = new HashSet<String>(); + tempSet.addAll(assertionSet); + tempSet.removeAll(incorrectSet); + + if (tempSet.size() > 0) { + sb.append("Set is missing elements:\n"); + for (Object element : tempSet) { + sb.append('-'); + sb.append(element); + sb.append('\n'); + } + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/package-info.java b/spring-test/src/main/java/org/springframework/test/web/package-info.java new file mode 100644 index 00000000..00853208 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/package-info.java @@ -0,0 +1,6 @@ +/** + * Helper classes for unit tests based on Spring's web support. + */ + +package org.springframework.test.web; + diff --git a/spring-test/src/main/java/overview.html b/spring-test/src/main/java/overview.html new file mode 100644 index 00000000..96bde489 --- /dev/null +++ b/spring-test/src/main/java/overview.html @@ -0,0 +1,7 @@ +<html> +<body> +<p> +Spring's TestContext framework. Also includes common Servlet and Portlet API mocks. +</p> +</body> +</html> diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockFilterChainTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockFilterChainTests.java new file mode 100644 index 00000000..61240cb4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/mock/web/MockFilterChainTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + +/** + * Test fixture for {@link MockFilterChain}. + * + * @author Rob Winch + */ +public class MockFilterChainTests { + + private ServletRequest request; + + private ServletResponse response; + + @Before + public void setup() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test(expected=IllegalArgumentException.class) + public void constructorNullServlet() { + new MockFilterChain((Servlet) null); + } + + @Test(expected=IllegalArgumentException.class) + public void constructorNullFilter() { + new MockFilterChain(mock(Servlet.class), (Filter) null); + } + + @Test(expected = IllegalArgumentException.class) + public void doFilterNullRequest() throws Exception { + MockFilterChain chain = new MockFilterChain(); + chain.doFilter(null, this.response); + } + + @Test(expected = IllegalArgumentException.class) + public void doFilterNullResponse() throws Exception { + MockFilterChain chain = new MockFilterChain(); + chain.doFilter(this.request, null); + } + + @Test + public void doFilterEmptyChain() throws Exception { + MockFilterChain chain = new MockFilterChain(); + chain.doFilter(this.request, this.response); + + assertThat(chain.getRequest(), is(request)); + assertThat(chain.getResponse(), is(response)); + + try { + chain.doFilter(this.request, this.response); + fail("Expected Exception"); + } + catch(IllegalStateException ex) { + assertEquals("This FilterChain has already been called!", ex.getMessage()); + } + } + + @Test + public void doFilterWithServlet() throws Exception { + Servlet servlet = mock(Servlet.class); + MockFilterChain chain = new MockFilterChain(servlet); + chain.doFilter(this.request, this.response); + verify(servlet).service(this.request, this.response); + try { + chain.doFilter(this.request, this.response); + fail("Expected Exception"); + } + catch(IllegalStateException ex) { + assertEquals("This FilterChain has already been called!", ex.getMessage()); + } + } + + @Test + public void doFilterWithServletAndFilters() throws Exception { + Servlet servlet = mock(Servlet.class); + + MockFilter filter2 = new MockFilter(servlet); + MockFilter filter1 = new MockFilter(null); + MockFilterChain chain = new MockFilterChain(servlet, filter1, filter2); + + chain.doFilter(this.request, this.response); + + assertTrue(filter1.invoked); + assertTrue(filter2.invoked); + + verify(servlet).service(this.request, this.response); + + try { + chain.doFilter(this.request, this.response); + fail("Expected Exception"); + } + catch(IllegalStateException ex) { + assertEquals("This FilterChain has already been called!", ex.getMessage()); + } + } + + + private static class MockFilter implements Filter { + + private final Servlet servlet; + + private boolean invoked; + + public MockFilter(Servlet servlet) { + this.servlet = servlet; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + this.invoked = true; + + if (this.servlet != null) { + this.servlet.service(request, response); + } + else { + chain.doFilter(request, response); + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void destroy() { + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java new file mode 100644 index 00000000..a237bb00 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit tests for {@link MockHttpServletRequest}. + * + * @author Rick Evans + * @author Mark Fisher + * @author Rossen Stoyanchev + * @author Sam Brannen + */ +public class MockHttpServletRequestTests { + + private MockHttpServletRequest request = new MockHttpServletRequest(); + + + @Test + public void setContentType() { + String contentType = "test/plain"; + request.setContentType(contentType); + assertEquals(contentType, request.getContentType()); + assertEquals(contentType, request.getHeader("Content-Type")); + assertNull(request.getCharacterEncoding()); + } + + @Test + public void setContentTypeUTF8() { + String contentType = "test/plain;charset=UTF-8"; + request.setContentType(contentType); + assertEquals(contentType, request.getContentType()); + assertEquals(contentType, request.getHeader("Content-Type")); + assertEquals("UTF-8", request.getCharacterEncoding()); + } + + @Test + public void contentTypeHeader() { + String contentType = "test/plain"; + request.addHeader("Content-Type", contentType); + assertEquals(contentType, request.getContentType()); + assertEquals(contentType, request.getHeader("Content-Type")); + assertNull(request.getCharacterEncoding()); + } + + @Test + public void contentTypeHeaderUTF8() { + String contentType = "test/plain;charset=UTF-8"; + request.addHeader("Content-Type", contentType); + assertEquals(contentType, request.getContentType()); + assertEquals(contentType, request.getHeader("Content-Type")); + assertEquals("UTF-8", request.getCharacterEncoding()); + } + + @Test + public void setContentTypeThenCharacterEncoding() { + request.setContentType("test/plain"); + request.setCharacterEncoding("UTF-8"); + assertEquals("test/plain", request.getContentType()); + assertEquals("test/plain;charset=UTF-8", request.getHeader("Content-Type")); + assertEquals("UTF-8", request.getCharacterEncoding()); + } + + @Test + public void setCharacterEncodingThenContentType() { + request.setCharacterEncoding("UTF-8"); + request.setContentType("test/plain"); + assertEquals("test/plain", request.getContentType()); + assertEquals("test/plain;charset=UTF-8", request.getHeader("Content-Type")); + assertEquals("UTF-8", request.getCharacterEncoding()); + } + + @Test + public void httpHeaderNameCasingIsPreserved() throws Exception { + String headerName = "Header1"; + request.addHeader(headerName, "value1"); + Enumeration<String> requestHeaders = request.getHeaderNames(); + assertNotNull(requestHeaders); + assertEquals("HTTP header casing not being preserved", headerName, requestHeaders.nextElement()); + } + + @Test + public void nullParameterName() { + assertNull(request.getParameter(null)); + assertNull(request.getParameterValues(null)); + } + + @Test + public void setMultipleParameters() { + request.setParameter("key1", "value1"); + request.setParameter("key2", "value2"); + Map<String, Object> params = new HashMap<String, Object>(2); + params.put("key1", "newValue1"); + params.put("key3", new String[] { "value3A", "value3B" }); + request.setParameters(params); + String[] values1 = request.getParameterValues("key1"); + assertEquals(1, values1.length); + assertEquals("newValue1", request.getParameter("key1")); + assertEquals("value2", request.getParameter("key2")); + String[] values3 = request.getParameterValues("key3"); + assertEquals(2, values3.length); + assertEquals("value3A", values3[0]); + assertEquals("value3B", values3[1]); + } + + @Test + public void addMultipleParameters() { + request.setParameter("key1", "value1"); + request.setParameter("key2", "value2"); + Map<String, Object> params = new HashMap<String, Object>(2); + params.put("key1", "newValue1"); + params.put("key3", new String[] { "value3A", "value3B" }); + request.addParameters(params); + String[] values1 = request.getParameterValues("key1"); + assertEquals(2, values1.length); + assertEquals("value1", values1[0]); + assertEquals("newValue1", values1[1]); + assertEquals("value2", request.getParameter("key2")); + String[] values3 = request.getParameterValues("key3"); + assertEquals(2, values3.length); + assertEquals("value3A", values3[0]); + assertEquals("value3B", values3[1]); + } + + @Test + public void removeAllParameters() { + request.setParameter("key1", "value1"); + Map<String, Object> params = new HashMap<String, Object>(2); + params.put("key2", "value2"); + params.put("key3", new String[] { "value3A", "value3B" }); + request.addParameters(params); + assertEquals(3, request.getParameterMap().size()); + request.removeAllParameters(); + assertEquals(0, request.getParameterMap().size()); + } + + @Test + public void defaultLocale() { + Locale originalDefaultLocale = Locale.getDefault(); + try { + Locale newDefaultLocale = originalDefaultLocale.equals(Locale.GERMANY) ? Locale.FRANCE : Locale.GERMANY; + Locale.setDefault(newDefaultLocale); + // Create the request after changing the default locale. + MockHttpServletRequest request = new MockHttpServletRequest(); + assertFalse(newDefaultLocale.equals(request.getLocale())); + assertEquals(Locale.ENGLISH, request.getLocale()); + } + finally { + Locale.setDefault(originalDefaultLocale); + } + } + + @Test(expected = IllegalArgumentException.class) + public void setPreferredLocalesWithNullList() { + request.setPreferredLocales(null); + } + + @Test(expected = IllegalArgumentException.class) + public void setPreferredLocalesWithEmptyList() { + request.setPreferredLocales(new ArrayList<Locale>()); + } + + @Test + public void setPreferredLocales() { + List<Locale> preferredLocales = Arrays.asList(Locale.ITALY, Locale.CHINA); + request.setPreferredLocales(preferredLocales); + assertEqualEnumerations(Collections.enumeration(preferredLocales), request.getLocales()); + } + + private void assertEqualEnumerations(Enumeration<?> enum1, Enumeration<?> enum2) { + assertNotNull(enum1); + assertNotNull(enum2); + int count = 0; + while (enum1.hasMoreElements()) { + assertTrue("enumerations must be equal in length", enum2.hasMoreElements()); + assertEquals("enumeration element #" + ++count, enum1.nextElement(), enum2.nextElement()); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java new file mode 100644 index 00000000..867b86ad --- /dev/null +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.mock.web; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; + +import org.springframework.web.util.WebUtils; + +import static org.junit.Assert.*; + +/** + * Unit tests for {@link MockHttpServletResponse}. + * + * @author Juergen Hoeller + * @author Rick Evans + * @author Rossen Stoyanchev + * @author Rob Winch + * @author Sam Brannen + * @since 19.02.2006 + */ +public class MockHttpServletResponseTests { + + private MockHttpServletResponse response = new MockHttpServletResponse(); + + + @Test + public void setContentType() { + String contentType = "test/plain"; + response.setContentType(contentType); + assertEquals(contentType, response.getContentType()); + assertEquals(contentType, response.getHeader("Content-Type")); + assertEquals(WebUtils.DEFAULT_CHARACTER_ENCODING, response.getCharacterEncoding()); + } + + @Test + public void setContentTypeUTF8() { + String contentType = "test/plain;charset=UTF-8"; + response.setContentType(contentType); + assertEquals("UTF-8", response.getCharacterEncoding()); + assertEquals(contentType, response.getContentType()); + assertEquals(contentType, response.getHeader("Content-Type")); + } + + @Test + public void contentTypeHeader() { + String contentType = "test/plain"; + response.addHeader("Content-Type", contentType); + assertEquals(contentType, response.getContentType()); + assertEquals(contentType, response.getHeader("Content-Type")); + assertEquals(WebUtils.DEFAULT_CHARACTER_ENCODING, response.getCharacterEncoding()); + + response = new MockHttpServletResponse(); + response.setHeader("Content-Type", contentType); + assertEquals(contentType, response.getContentType()); + assertEquals(contentType, response.getHeader("Content-Type")); + assertEquals(WebUtils.DEFAULT_CHARACTER_ENCODING, response.getCharacterEncoding()); + } + + @Test + public void contentTypeHeaderUTF8() { + String contentType = "test/plain;charset=UTF-8"; + response.setHeader("Content-Type", contentType); + assertEquals(contentType, response.getContentType()); + assertEquals(contentType, response.getHeader("Content-Type")); + assertEquals("UTF-8", response.getCharacterEncoding()); + + response = new MockHttpServletResponse(); + response.addHeader("Content-Type", contentType); + assertEquals(contentType, response.getContentType()); + assertEquals(contentType, response.getHeader("Content-Type")); + assertEquals("UTF-8", response.getCharacterEncoding()); + } + + @Test + public void setContentTypeThenCharacterEncoding() { + response.setContentType("test/plain"); + response.setCharacterEncoding("UTF-8"); + assertEquals("test/plain", response.getContentType()); + assertEquals("test/plain;charset=UTF-8", response.getHeader("Content-Type")); + assertEquals("UTF-8", response.getCharacterEncoding()); + } + + @Test + public void setCharacterEncodingThenContentType() { + response.setCharacterEncoding("UTF-8"); + response.setContentType("test/plain"); + assertEquals("test/plain", response.getContentType()); + assertEquals("test/plain;charset=UTF-8", response.getHeader("Content-Type")); + assertEquals("UTF-8", response.getCharacterEncoding()); + } + + @Test + public void contentLength() { + response.setContentLength(66); + assertEquals(66, response.getContentLength()); + assertEquals("66", response.getHeader("Content-Length")); + } + + @Test + public void contentLengthHeader() { + response.addHeader("Content-Length", "66"); + assertEquals(66, response.getContentLength()); + assertEquals("66", response.getHeader("Content-Length")); + } + + @Test + public void httpHeaderNameCasingIsPreserved() throws Exception { + final String headerName = "Header1"; + response.addHeader(headerName, "value1"); + Collection<String> responseHeaders = response.getHeaderNames(); + assertNotNull(responseHeaders); + assertEquals(1, responseHeaders.size()); + assertEquals("HTTP header casing not being preserved", headerName, responseHeaders.iterator().next()); + } + + @Test + public void servletOutputStreamCommittedWhenBufferSizeExceeded() throws IOException { + assertFalse(response.isCommitted()); + response.getOutputStream().write('X'); + assertFalse(response.isCommitted()); + int size = response.getBufferSize(); + response.getOutputStream().write(new byte[size]); + assertTrue(response.isCommitted()); + assertEquals(size + 1, response.getContentAsByteArray().length); + } + + @Test + public void servletOutputStreamCommittedOnFlushBuffer() throws IOException { + assertFalse(response.isCommitted()); + response.getOutputStream().write('X'); + assertFalse(response.isCommitted()); + response.flushBuffer(); + assertTrue(response.isCommitted()); + assertEquals(1, response.getContentAsByteArray().length); + } + + @Test + public void servletWriterCommittedWhenBufferSizeExceeded() throws IOException { + assertFalse(response.isCommitted()); + response.getWriter().write("X"); + assertFalse(response.isCommitted()); + int size = response.getBufferSize(); + char[] data = new char[size]; + Arrays.fill(data, 'p'); + response.getWriter().write(data); + assertTrue(response.isCommitted()); + assertEquals(size + 1, response.getContentAsByteArray().length); + } + + @Test + public void servletOutputStreamCommittedOnOutputStreamFlush() throws IOException { + assertFalse(response.isCommitted()); + response.getOutputStream().write('X'); + assertFalse(response.isCommitted()); + response.getOutputStream().flush(); + assertTrue(response.isCommitted()); + assertEquals(1, response.getContentAsByteArray().length); + } + + @Test + public void servletWriterCommittedOnWriterFlush() throws IOException { + assertFalse(response.isCommitted()); + response.getWriter().write("X"); + assertFalse(response.isCommitted()); + response.getWriter().flush(); + assertTrue(response.isCommitted()); + assertEquals(1, response.getContentAsByteArray().length); + } + + @Test + public void servletWriterAutoFlushedForString() throws IOException { + response.getWriter().write("X"); + assertEquals("X", response.getContentAsString()); + } + + @Test + public void servletWriterAutoFlushedForChar() throws IOException { + response.getWriter().write('X'); + assertEquals("X", response.getContentAsString()); + } + + @Test + public void servletWriterAutoFlushedForCharArray() throws IOException { + response.getWriter().write("XY".toCharArray()); + assertEquals("XY", response.getContentAsString()); + } + + @Test + public void sendRedirect() throws IOException { + String redirectUrl = "/redirect"; + response.sendRedirect(redirectUrl); + assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, response.getStatus()); + assertEquals(redirectUrl, response.getHeader("Location")); + assertEquals(redirectUrl, response.getRedirectedUrl()); + assertTrue(response.isCommitted()); + } + + @Test + public void locationHeaderUpdatesGetRedirectedUrl() { + String redirectUrl = "/redirect"; + response.setHeader("Location", redirectUrl); + assertEquals(redirectUrl, response.getRedirectedUrl()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpSessionTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpSessionTests.java new file mode 100644 index 00000000..0379ea78 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpSessionTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link MockHttpSession}. + * + * @author Sam Brannen + * @since 3.2 + */ +public class MockHttpSessionTests { + + private MockHttpSession session = new MockHttpSession(); + + + @Test + public void invalidateOnce() { + assertFalse(session.isInvalid()); + session.invalidate(); + assertTrue(session.isInvalid()); + } + + @Test(expected = IllegalStateException.class) + public void invalidateTwice() { + session.invalidate(); + session.invalidate(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockPageContextTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockPageContextTests.java new file mode 100644 index 00000000..493cdc75 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/mock/web/MockPageContextTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import junit.framework.TestCase; + +import javax.servlet.jsp.PageContext; + +/** + * Unit tests for the {@code MockPageContext} class. + * + * @author Rick Evans + */ +public final class MockPageContextTests extends TestCase { + + public void testSetAttributeWithNoScopeUsesPageScope() throws Exception { + String key = "foo"; + String value = "bar"; + + MockPageContext ctx = new MockPageContext(); + ctx.setAttribute(key, value); + assertEquals(value, ctx.getAttribute(key, PageContext.PAGE_SCOPE)); + assertNull(ctx.getAttribute(key, PageContext.APPLICATION_SCOPE)); + assertNull(ctx.getAttribute(key, PageContext.REQUEST_SCOPE)); + assertNull(ctx.getAttribute(key, PageContext.SESSION_SCOPE)); + } + + public void testRemoveAttributeWithNoScopeSpecifiedRemovesValueFromAllScopes() throws Exception { + String key = "foo"; + String value = "bar"; + + MockPageContext ctx = new MockPageContext(); + ctx.setAttribute(key, value, PageContext.APPLICATION_SCOPE); + ctx.removeAttribute(key); + + assertNull(ctx.getAttribute(key, PageContext.PAGE_SCOPE)); + assertNull(ctx.getAttribute(key, PageContext.APPLICATION_SCOPE)); + assertNull(ctx.getAttribute(key, PageContext.REQUEST_SCOPE)); + assertNull(ctx.getAttribute(key, PageContext.SESSION_SCOPE)); + } + +} diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockServletContextTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockServletContextTests.java new file mode 100644 index 00000000..f647ec7e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/mock/web/MockServletContextTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.mock.web; + +import static org.junit.Assert.*; + +import java.util.Set; + +import javax.servlet.RequestDispatcher; + +import org.junit.Test; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @since 19.02.2006 + */ +public class MockServletContextTests { + + private final MockServletContext sc = new MockServletContext("org/springframework/mock"); + + + @Test + public void listFiles() { + Set<String> paths = sc.getResourcePaths("/web"); + assertNotNull(paths); + assertTrue(paths.contains("/web/MockServletContextTests.class")); + } + + @Test + public void listSubdirectories() { + Set<String> paths = sc.getResourcePaths("/"); + assertNotNull(paths); + assertTrue(paths.contains("/web/")); + } + + @Test + public void listNonDirectory() { + Set<String> paths = sc.getResourcePaths("/web/MockServletContextTests.class"); + assertNull(paths); + } + + @Test + public void listInvalidPath() { + Set<String> paths = sc.getResourcePaths("/web/invalid"); + assertNull(paths); + } + + @Test + public void registerContextAndGetContext() { + MockServletContext sc2 = new MockServletContext(); + sc.setContextPath("/"); + sc.registerContext("/second", sc2); + assertSame(sc, sc.getContext("/")); + assertSame(sc2, sc.getContext("/second")); + } + + @Test + public void getMimeType() { + assertEquals("text/html", sc.getMimeType("test.html")); + assertEquals("image/gif", sc.getMimeType("test.gif")); + } + + @Test + public void minorVersion() { + assertEquals(5, sc.getMinorVersion()); + sc.setMinorVersion(4); + assertEquals(4, sc.getMinorVersion()); + } + + @Test + public void registerAndUnregisterNamedDispatcher() throws Exception { + final String name = "test-servlet"; + final String url = "/test"; + + assertNull(sc.getNamedDispatcher(name)); + + sc.registerNamedDispatcher(name, new MockRequestDispatcher(url)); + RequestDispatcher namedDispatcher = sc.getNamedDispatcher(name); + assertNotNull(namedDispatcher); + MockHttpServletResponse response = new MockHttpServletResponse(); + namedDispatcher.forward(new MockHttpServletRequest(sc), response); + assertEquals(url, response.getForwardedUrl()); + + sc.unregisterNamedDispatcher(name); + assertNull(sc.getNamedDispatcher(name)); + } + + @Test + public void getNamedDispatcherForDefaultServlet() throws Exception { + final String name = "default"; + RequestDispatcher namedDispatcher = sc.getNamedDispatcher(name); + assertNotNull(namedDispatcher); + + MockHttpServletResponse response = new MockHttpServletResponse(); + namedDispatcher.forward(new MockHttpServletRequest(sc), response); + assertEquals(name, response.getForwardedUrl()); + } + + @Test + public void setDefaultServletName() throws Exception { + final String originalDefault = "default"; + final String newDefault = "test"; + assertNotNull(sc.getNamedDispatcher(originalDefault)); + + sc.setDefaultServletName(newDefault); + assertEquals(newDefault, sc.getDefaultServletName()); + assertNull(sc.getNamedDispatcher(originalDefault)); + + RequestDispatcher namedDispatcher = sc.getNamedDispatcher(newDefault); + assertNotNull(namedDispatcher); + MockHttpServletResponse response = new MockHttpServletResponse(); + namedDispatcher.forward(new MockHttpServletRequest(sc), response); + assertEquals(newDefault, response.getForwardedUrl()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/AbstractSpr3350SingleSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/AbstractSpr3350SingleSpringContextTests.java new file mode 100644 index 00000000..ba8c81c2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/AbstractSpr3350SingleSpringContextTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; + +/** + * Abstract JUnit 3.8 based unit test which verifies new functionality requested + * in <a href="http://opensource.atlassian.com/projects/spring/browse/SPR-3550" + * target="_blank">SPR-3350</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@SuppressWarnings("deprecation") +public abstract class AbstractSpr3350SingleSpringContextTests extends AbstractDependencyInjectionSpringContextTests { + + private Pet cat; + + + public AbstractSpr3350SingleSpringContextTests() { + super(); + } + + public AbstractSpr3350SingleSpringContextTests(String name) { + super(name); + } + + public final void setCat(final Pet cat) { + this.cat = cat; + } + + /** + * Forcing concrete subclasses to provide a config path appropriate to the + * configured + * {@link #createBeanDefinitionReader(org.springframework.context.support.GenericApplicationContext) + * BeanDefinitionReader}. + * + * @see org.springframework.test.AbstractSingleSpringContextTests#getConfigPath() + */ + @Override + protected abstract String getConfigPath(); + + /** + * <p> + * Test which addresses the following issue raised in SPR-3350: + * </p> + * <p> + * {@link AbstractSingleSpringContextTests} always uses an + * {@link XmlBeanDefinitionReader} internally when creating the + * {@link ApplicationContext} inside + * {@link #createApplicationContext(String[])}. It would be nice to have the + * bean definition reader creation in a separate method so that subclasses + * can choose that individually without having to copy-n-paste code from + * createApplicationContext() to do the context creation and refresh. + * Consider JavaConfig where an Annotation based reader can be plugged in. + * </p> + */ + public final void testApplicationContextNotAutoCreated() { + assertNotNull("The cat field should have been autowired.", this.cat); + assertEquals("Garfield", this.cat.getName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/PropertiesBasedSpr3350SingleSpringContextTests-context.properties b/spring-test/src/test/java/org/springframework/test/PropertiesBasedSpr3350SingleSpringContextTests-context.properties new file mode 100644 index 00000000..7b96503a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/PropertiesBasedSpr3350SingleSpringContextTests-context.properties @@ -0,0 +1,2 @@ +cat.(class)=org.springframework.tests.sample.beans.Pet +cat.$0=Garfield diff --git a/spring-test/src/test/java/org/springframework/test/PropertiesBasedSpr3350SingleSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/PropertiesBasedSpr3350SingleSpringContextTests.java new file mode 100644 index 00000000..07b0cf4b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/PropertiesBasedSpr3350SingleSpringContextTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; + +/** + * Concrete implementation of {@link AbstractSpr3350SingleSpringContextTests} + * which configures a {@link PropertiesBeanDefinitionReader} instead of the + * default {@link XmlBeanDefinitionReader}. + * + * @author Sam Brannen + * @since 2.5 + */ +public class PropertiesBasedSpr3350SingleSpringContextTests extends AbstractSpr3350SingleSpringContextTests { + + public PropertiesBasedSpr3350SingleSpringContextTests() { + super(); + } + + public PropertiesBasedSpr3350SingleSpringContextTests(String name) { + super(name); + } + + /** + * Creates a new {@link PropertiesBeanDefinitionReader}. + * + * @see org.springframework.test.AbstractSingleSpringContextTests#createBeanDefinitionReader(org.springframework.context.support.GenericApplicationContext) + */ + @Override + protected final BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context) { + return new PropertiesBeanDefinitionReader(context); + } + + /** + * Returns + * "PropertiesBasedSpr3350SingleSpringContextTests-context.properties". + */ + @Override + protected final String getConfigPath() { + return "PropertiesBasedSpr3350SingleSpringContextTests-context.properties"; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/Spr3264DependencyInjectionSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/Spr3264DependencyInjectionSpringContextTests.java new file mode 100644 index 00000000..08523c79 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/Spr3264DependencyInjectionSpringContextTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +/** + * JUnit 3.8 based unit test which verifies new functionality requested in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3264" + * target="_blank">SPR-3264</a>. + * + * @author Sam Brannen + * @since 2.5 + * @see Spr3264SingleSpringContextTests + */ +@SuppressWarnings("deprecation") +public class Spr3264DependencyInjectionSpringContextTests extends AbstractDependencyInjectionSpringContextTests { + + public Spr3264DependencyInjectionSpringContextTests() { + super(); + } + + public Spr3264DependencyInjectionSpringContextTests(String name) { + super(name); + } + + /** + * <p> + * Test which addresses the following issue raised in SPR-3264: + * </p> + * <p> + * AbstractDependencyInjectionSpringContextTests will try to apply + * auto-injection; this can be disabled but it has to be done manually + * inside the onSetUp... + * </p> + */ + public void testInjectDependenciesThrowsIllegalStateException() { + + // Re-assert issues covered by Spr3264SingleSpringContextTests as a + // safety net. + assertNull("The ApplicationContext should NOT be automatically created if no 'locations' are defined.", + this.applicationContext); + assertEquals("Verifying the ApplicationContext load count.", 0, super.getLoadCount()); + + // Assert changes to AbstractDependencyInjectionSpringContextTests: + new AssertThrows(IllegalStateException.class) { + + @Override + public void test() throws Exception { + Spr3264DependencyInjectionSpringContextTests.super.injectDependencies(); + } + }.runTest(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/Spr3264SingleSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/Spr3264SingleSpringContextTests.java new file mode 100644 index 00000000..4b3223f4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/Spr3264SingleSpringContextTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +/** + * JUnit 3.8 based unit test which verifies new functionality requested in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3264" + * target="_blank">SPR-3264</a>. + * + * @author Sam Brannen + * @since 2.5 + * @see Spr3264DependencyInjectionSpringContextTests + */ +@SuppressWarnings("deprecation") +public class Spr3264SingleSpringContextTests extends AbstractSingleSpringContextTests { + + public Spr3264SingleSpringContextTests() { + super(); + } + + public Spr3264SingleSpringContextTests(String name) { + super(name); + } + + /** + * <p> + * Test which addresses the following issue raised in SPR-3264: + * </p> + * <p> + * AbstractSingleSpringContextTests always expects an application context to + * be created even if no files/locations are specified which can lead to NPE + * problems or force an appCtx to be instantiated even if not needed. + * </p> + */ + public void testApplicationContextNotAutoCreated() { + assertNull("The ApplicationContext should NOT be automatically created if no 'locations' are defined.", + super.applicationContext); + assertEquals("Verifying the ApplicationContext load count.", 0, super.getLoadCount()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/XmlBasedSpr3350SingleSpringContextTests-context.xml b/spring-test/src/test/java/org/springframework/test/XmlBasedSpr3350SingleSpringContextTests-context.xml new file mode 100644 index 00000000..b1939de1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/XmlBasedSpr3350SingleSpringContextTests-context.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> + + <bean id="cat" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Garfield" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/XmlBasedSpr3350SingleSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/XmlBasedSpr3350SingleSpringContextTests.java new file mode 100644 index 00000000..192d005f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/XmlBasedSpr3350SingleSpringContextTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; + +/** + * Concrete implementation of {@link AbstractSpr3350SingleSpringContextTests} + * which is based on the default {@link XmlBeanDefinitionReader}. + * + * @author Sam Brannen + * @since 2.5 + */ +public class XmlBasedSpr3350SingleSpringContextTests extends AbstractSpr3350SingleSpringContextTests { + + public XmlBasedSpr3350SingleSpringContextTests() { + super(); + } + + public XmlBasedSpr3350SingleSpringContextTests(String name) { + super(name); + } + + /** + * Returns "XmlBasedSpr3350SingleSpringContextTests-context.xml". + */ + @Override + protected final String getConfigPath() { + return "XmlBasedSpr3350SingleSpringContextTests-context.xml"; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueAnnotationAwareTransactionalTests-context.xml b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueAnnotationAwareTransactionalTests-context.xml new file mode 100644 index 00000000..3c4fc976 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueAnnotationAwareTransactionalTests-context.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password="" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueAnnotationAwareTransactionalTests.java b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueAnnotationAwareTransactionalTests.java new file mode 100644 index 00000000..1568b6fb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueAnnotationAwareTransactionalTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import junit.framework.TestCase; +import junit.framework.TestResult; + +/** + * Verifies proper handling of {@link IfProfileValue @IfProfileValue} and + * {@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration} in + * conjunction with {@link AbstractAnnotationAwareTransactionalTests}. + * + * @author Sam Brannen + * @since 2.5 + */ +public class ProfileValueAnnotationAwareTransactionalTests extends TestCase { + + private static final String NAME = "ProfileValueAnnotationAwareTransactionalTests.profile_value.name"; + + private static final String VALUE = "enigma"; + + + public ProfileValueAnnotationAwareTransactionalTests() { + System.setProperty(NAME, VALUE); + } + + private void runTestAndAssertCounters(Class<? extends DefaultProfileValueSourceTestCase> testCaseType, + String testName, int expectedInvocationCount, int expectedErrorCount, int expectedFailureCount) + throws Exception { + + DefaultProfileValueSourceTestCase testCase = testCaseType.newInstance(); + testCase.setName(testName); + TestResult testResult = testCase.run(); + assertEquals("Verifying number of invocations for test method [" + testName + "].", expectedInvocationCount, + testCase.invocationCount); + assertEquals("Verifying number of errors for test method [" + testName + "].", expectedErrorCount, + testResult.errorCount()); + assertEquals("Verifying number of failures for test method [" + testName + "].", expectedFailureCount, + testResult.failureCount()); + } + + private void runTests(Class<? extends DefaultProfileValueSourceTestCase> testCaseType) throws Exception { + runTestAndAssertCounters(testCaseType, "testIfProfileValueEmpty", 0, 0, 0); + runTestAndAssertCounters(testCaseType, "testIfProfileValueDisabledViaWrongName", 0, 0, 0); + runTestAndAssertCounters(testCaseType, "testIfProfileValueDisabledViaWrongValue", 0, 0, 0); + runTestAndAssertCounters(testCaseType, "testIfProfileValueEnabledViaSingleValue", 1, 0, 0); + runTestAndAssertCounters(testCaseType, "testIfProfileValueEnabledViaMultipleValues", 1, 0, 0); + runTestAndAssertCounters(testCaseType, "testIfProfileValueNotConfigured", 1, 0, 0); + } + + public void testDefaultProfileValueSource() throws Exception { + assertEquals("Verifying the type of the configured ProfileValueSource.", SystemProfileValueSource.class, + new DefaultProfileValueSourceTestCase().getProfileValueSource().getClass()); + runTests(DefaultProfileValueSourceTestCase.class); + } + + public void testHardCodedProfileValueSource() throws Exception { + assertEquals("Verifying the type of the configured ProfileValueSource.", HardCodedProfileValueSource.class, + new HardCodedProfileValueSourceTestCase().getProfileValueSource().getClass()); + runTests(HardCodedProfileValueSourceTestCase.class); + } + + + @SuppressWarnings("deprecation") + public static class DefaultProfileValueSourceTestCase extends AbstractAnnotationAwareTransactionalTests { + + int invocationCount = 0; + + + public DefaultProfileValueSourceTestCase() { + } + + public ProfileValueSource getProfileValueSource() { + return super.profileValueSource; + } + + @Override + protected String getConfigPath() { + return "ProfileValueAnnotationAwareTransactionalTests-context.xml"; + } + + @NotTransactional + @IfProfileValue(name = NAME) + public void testIfProfileValueEmpty() { + this.invocationCount++; + fail("The body of a disabled test should never be executed!"); + } + + @NotTransactional + @IfProfileValue(name = NAME + "X", value = VALUE) + public void testIfProfileValueDisabledViaWrongName() { + this.invocationCount++; + fail("The body of a disabled test should never be executed!"); + } + + @NotTransactional + @IfProfileValue(name = NAME, value = VALUE + "X") + public void testIfProfileValueDisabledViaWrongValue() { + this.invocationCount++; + fail("The body of a disabled test should never be executed!"); + } + + @NotTransactional + @IfProfileValue(name = NAME, value = VALUE) + public void testIfProfileValueEnabledViaSingleValue() { + this.invocationCount++; + } + + @NotTransactional + @IfProfileValue(name = NAME, values = { "foo", VALUE, "bar" }) + public void testIfProfileValueEnabledViaMultipleValues() { + this.invocationCount++; + } + + @NotTransactional + public void testIfProfileValueNotConfigured() { + this.invocationCount++; + } + } + + @ProfileValueSourceConfiguration(HardCodedProfileValueSource.class) + public static class HardCodedProfileValueSourceTestCase extends DefaultProfileValueSourceTestCase { + } + + public static class HardCodedProfileValueSource implements ProfileValueSource { + + @Override + public String get(String key) { + return (key.equals(NAME) ? VALUE : null); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java new file mode 100644 index 00000000..bb715753 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.annotation; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Unit tests for {@link ProfileValueUtils}. + * + * @author Sam Brannen + * @since 3.0 + */ +public class ProfileValueUtilsTests { + + private static final String NON_ANNOTATED_METHOD = "nonAnnotatedMethod"; + private static final String ENABLED_ANNOTATED_METHOD = "enabledAnnotatedMethod"; + private static final String DISABLED_ANNOTATED_METHOD = "disabledAnnotatedMethod"; + + private static final String NAME = "ProfileValueUtilsTests.profile_value.name"; + private static final String VALUE = "enigma"; + + + @BeforeClass + public static void setProfileValue() { + System.setProperty(NAME, VALUE); + } + + private void assertClassIsEnabled(Class<?> testClass) throws Exception { + assertTrue("Test class [" + testClass + "] should be enabled.", + ProfileValueUtils.isTestEnabledInThisEnvironment(testClass)); + } + + private void assertClassIsDisabled(Class<?> testClass) throws Exception { + assertFalse("Test class [" + testClass + "] should be disbled.", + ProfileValueUtils.isTestEnabledInThisEnvironment(testClass)); + } + + private void assertMethodIsEnabled(String methodName, Class<?> testClass) throws Exception { + Method testMethod = testClass.getMethod(methodName); + assertTrue("Test method [" + testMethod + "] should be enabled.", + ProfileValueUtils.isTestEnabledInThisEnvironment(testMethod, testClass)); + } + + private void assertMethodIsDisabled(String methodName, Class<?> testClass) throws Exception { + Method testMethod = testClass.getMethod(methodName); + assertFalse("Test method [" + testMethod + "] should be disabled.", + ProfileValueUtils.isTestEnabledInThisEnvironment(testMethod, testClass)); + } + + private void assertMethodIsEnabled(ProfileValueSource profileValueSource, String methodName, Class<?> testClass) + throws Exception { + Method testMethod = testClass.getMethod(methodName); + assertTrue("Test method [" + testMethod + "] should be enabled for ProfileValueSource [" + profileValueSource + + "].", ProfileValueUtils.isTestEnabledInThisEnvironment(profileValueSource, testMethod, testClass)); + } + + private void assertMethodIsDisabled(ProfileValueSource profileValueSource, String methodName, Class<?> testClass) + throws Exception { + Method testMethod = testClass.getMethod(methodName); + assertFalse("Test method [" + testMethod + "] should be disabled for ProfileValueSource [" + profileValueSource + + "].", ProfileValueUtils.isTestEnabledInThisEnvironment(profileValueSource, testMethod, testClass)); + } + + // ------------------------------------------------------------------- + + @Test + public void isTestEnabledInThisEnvironmentForProvidedClass() throws Exception { + assertClassIsEnabled(NonAnnotated.class); + assertClassIsEnabled(EnabledAnnotatedSingleValue.class); + assertClassIsEnabled(EnabledAnnotatedMultiValue.class); + assertClassIsDisabled(DisabledAnnotatedSingleValue.class); + assertClassIsDisabled(DisabledAnnotatedMultiValue.class); + } + + @Test + public void isTestEnabledInThisEnvironmentForProvidedMethodAndClass() throws Exception { + assertMethodIsEnabled(NON_ANNOTATED_METHOD, NonAnnotated.class); + + assertMethodIsEnabled(NON_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); + assertMethodIsEnabled(ENABLED_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); + assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); + + assertMethodIsEnabled(NON_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); + assertMethodIsEnabled(ENABLED_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); + assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); + + assertMethodIsDisabled(NON_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(ENABLED_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); + + assertMethodIsDisabled(NON_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); + assertMethodIsDisabled(ENABLED_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); + assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); + } + + @Test + public void isTestEnabledInThisEnvironmentForProvidedProfileValueSourceMethodAndClass() throws Exception { + + ProfileValueSource profileValueSource = SystemProfileValueSource.getInstance(); + + assertMethodIsEnabled(profileValueSource, NON_ANNOTATED_METHOD, NonAnnotated.class); + + assertMethodIsEnabled(profileValueSource, NON_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); + assertMethodIsEnabled(profileValueSource, ENABLED_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); + assertMethodIsDisabled(profileValueSource, DISABLED_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); + + assertMethodIsEnabled(profileValueSource, NON_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); + assertMethodIsEnabled(profileValueSource, ENABLED_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); + assertMethodIsDisabled(profileValueSource, DISABLED_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); + + assertMethodIsDisabled(profileValueSource, NON_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(profileValueSource, ENABLED_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(profileValueSource, DISABLED_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); + + assertMethodIsDisabled(profileValueSource, NON_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); + assertMethodIsDisabled(profileValueSource, ENABLED_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); + assertMethodIsDisabled(profileValueSource, DISABLED_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); + } + + + // ------------------------------------------------------------------- + + @SuppressWarnings("unused") + private static class NonAnnotated { + + public void nonAnnotatedMethod() { + } + } + + @SuppressWarnings("unused") + @IfProfileValue(name = NAME, value = VALUE) + private static class EnabledAnnotatedSingleValue { + + public void nonAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE) + public void enabledAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE + "X") + public void disabledAnnotatedMethod() { + } + } + + @SuppressWarnings("unused") + @IfProfileValue(name = NAME, values = { "foo", VALUE, "bar" }) + private static class EnabledAnnotatedMultiValue { + + public void nonAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE) + public void enabledAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE + "X") + public void disabledAnnotatedMethod() { + } + } + + @SuppressWarnings("unused") + @IfProfileValue(name = NAME, value = VALUE + "X") + private static class DisabledAnnotatedSingleValue { + + public void nonAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE) + public void enabledAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE + "X") + public void disabledAnnotatedMethod() { + } + } + + @SuppressWarnings("unused") + @IfProfileValue(name = NAME, values = { "foo", "bar" }) + private static class DisabledAnnotatedMultiValue { + + public void nonAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE) + public void enabledAnnotatedMethod() { + } + + @IfProfileValue(name = NAME, value = VALUE + "X") + public void disabledAnnotatedMethod() { + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java new file mode 100644 index 00000000..0be28a87 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.JUnitCore; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.TrackingRunListener; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; + +/** + * JUnit 4 based integration test which verifies correct {@linkplain ContextCache + * application context caching} in conjunction with the + * {@link SpringJUnit4ClassRunner} and the {@link DirtiesContext + * @DirtiesContext} annotation at the class level. + * + * @author Sam Brannen + * @since 3.0 + */ +@RunWith(JUnit4.class) +public class ClassLevelDirtiesContextTests { + + private static final AtomicInteger cacheHits = new AtomicInteger(0); + private static final AtomicInteger cacheMisses = new AtomicInteger(0); + + + /** + * Asserts the statistics of the supplied context cache. + * + * @param usageScenario the scenario in which the statistics are used + * @param expectedSize the expected number of contexts in the cache + * @param expectedHitCount the expected hit count + * @param expectedMissCount the expected miss count + */ + private static final void assertCacheStats(String usageScenario, int expectedSize, int expectedHitCount, + int expectedMissCount) { + + ContextCache contextCache = TestContextManager.contextCache; + assertEquals("Verifying number of contexts in cache (" + usageScenario + ").", expectedSize, + contextCache.size()); + assertEquals("Verifying number of cache hits (" + usageScenario + ").", expectedHitCount, + contextCache.getHitCount()); + assertEquals("Verifying number of cache misses (" + usageScenario + ").", expectedMissCount, + contextCache.getMissCount()); + } + + private static final void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) { + final int expectedTestFailureCount = 0; + final int expectedTestStartedCount = expectedTestCount; + final int expectedTestFinishedCount = expectedTestCount; + + TrackingRunListener listener = new TrackingRunListener(); + JUnitCore jUnitCore = new JUnitCore(); + jUnitCore.addListener(listener); + jUnitCore.run(testClass); + + assertEquals("Verifying number of failures for test class [" + testClass + "].", expectedTestFailureCount, + listener.getTestFailureCount()); + assertEquals("Verifying number of tests started for test class [" + testClass + "].", expectedTestStartedCount, + listener.getTestStartedCount()); + assertEquals("Verifying number of tests finished for test class [" + testClass + "].", + expectedTestFinishedCount, listener.getTestFinishedCount()); + } + + @BeforeClass + public static void verifyInitialCacheState() { + ContextCache contextCache = TestContextManager.contextCache; + contextCache.clear(); + contextCache.clearStatistics(); + cacheHits.set(0); + cacheMisses.set(0); + assertCacheStats("BeforeClass", 0, cacheHits.get(), cacheMisses.get()); + } + + @Test + public void verifyDirtiesContextBehavior() throws Exception { + + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1); + assertCacheStats("after class-level @DirtiesContext with clean test method and default class mode", 0, + cacheHits.incrementAndGet(), cacheMisses.get()); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1); + assertCacheStats("after inherited class-level @DirtiesContext with clean test method and default class mode", + 0, cacheHits.incrementAndGet(), cacheMisses.get()); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1); + assertCacheStats("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode", 0, + cacheHits.incrementAndGet(), cacheMisses.get()); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1); + assertCacheStats("after inherited class-level @DirtiesContext with clean test method and AFTER_CLASS mode", 0, + cacheHits.incrementAndGet(), cacheMisses.get()); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3); + assertCacheStats("after class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0, + cacheHits.incrementAndGet(), cacheMisses.addAndGet(2)); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3); + assertCacheStats( + "after inherited class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0, + cacheHits.incrementAndGet(), cacheMisses.addAndGet(2)); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1); + assertCacheStats("after class-level @DirtiesContext with dirty test method", 0, cacheHits.incrementAndGet(), + cacheMisses.get()); + runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1); + assertCacheStats("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(), + cacheMisses.incrementAndGet()); + runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1); + assertCacheStats("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(), + cacheMisses.incrementAndGet()); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1); + assertCacheStats("after inherited class-level @DirtiesContext with dirty test method", 0, + cacheHits.incrementAndGet(), cacheMisses.get()); + runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1); + assertCacheStats("after inherited class-level @DirtiesContext with dirty test method", 0, cacheHits.get(), + cacheMisses.incrementAndGet()); + runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1); + assertCacheStats("after inherited class-level @DirtiesContext with dirty test method", 0, cacheHits.get(), + cacheMisses.incrementAndGet()); + assertBehaviorForCleanTestCase(); + + runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1); + assertCacheStats("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode", 0, + cacheHits.incrementAndGet(), cacheMisses.get()); + } + + private void assertBehaviorForCleanTestCase() { + runTestClassAndAssertStats(CleanTestCase.class, 1); + assertCacheStats("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet()); + } + + @AfterClass + public static void verifyFinalCacheState() { + assertCacheStats("AfterClass", 0, cacheHits.get(), cacheMisses.get()); + } + + + // ------------------------------------------------------------------- + + @RunWith(SpringJUnit4ClassRunner.class) + @TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class }) + @ContextConfiguration + public static abstract class BaseTestCase { + + @Configuration + static class Config { + /* no beans */ + } + + @Autowired + protected ApplicationContext applicationContext; + + + protected void assertApplicationContextWasAutowired() { + assertNotNull("The application context should have been autowired.", this.applicationContext); + } + } + + public static final class CleanTestCase extends BaseTestCase { + + @Test + public void verifyContextWasAutowired() { + assertApplicationContextWasAutowired(); + } + + } + + @DirtiesContext + public static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase { + + @Test + public void verifyContextWasAutowired() { + assertApplicationContextWasAutowired(); + } + } + + public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends + ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase { + } + + @DirtiesContext(classMode = ClassMode.AFTER_CLASS) + public static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase { + + @Test + public void verifyContextWasAutowired() { + assertApplicationContextWasAutowired(); + } + } + + public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends + ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase { + } + + @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) + public static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase { + + @Test + public void verifyContextWasAutowired1() { + assertApplicationContextWasAutowired(); + } + + @Test + public void verifyContextWasAutowired2() { + assertApplicationContextWasAutowired(); + } + + @Test + public void verifyContextWasAutowired3() { + assertApplicationContextWasAutowired(); + } + } + + public static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends + ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase { + } + + @DirtiesContext + public static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase { + + @Test + @DirtiesContext + public void dirtyContext() { + assertApplicationContextWasAutowired(); + } + } + + public static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends + ClassLevelDirtiesContextWithDirtyMethodsTestCase { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java new file mode 100644 index 00000000..eb1c39a9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java @@ -0,0 +1,329 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.*; +import static org.springframework.test.context.SpringRunnerContextCacheTests.*; + +/** + * Integration tests for verifying proper behavior of the {@link ContextCache} in + * conjunction with cache keys used in {@link TestContext}. + * + * @author Sam Brannen + * @since 3.1 + * @see SpringRunnerContextCacheTests + */ +public class ContextCacheTests { + + private ContextCache contextCache = new ContextCache(); + + + @Before + public void initialCacheState() { + assertContextCacheStatistics(contextCache, "initial state", 0, 0, 0); + assertParentContextCount(0); + } + + private void assertParentContextCount(int expected) { + assertEquals("parent context count", expected, contextCache.getParentContextCount()); + } + + private MergedContextConfiguration getMergedContextConfiguration(TestContext testContext) { + return (MergedContextConfiguration) ReflectionTestUtils.getField(testContext, "mergedContextConfiguration"); + } + + private ApplicationContext loadContext(Class<?> testClass) { + TestContext testContext = new TestContext(testClass, contextCache); + return testContext.getApplicationContext(); + } + + private void loadCtxAndAssertStats(Class<?> testClass, int expectedSize, int expectedHitCount, int expectedMissCount) { + assertNotNull(loadContext(testClass)); + assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedHitCount, + expectedMissCount); + } + + @Test + public void verifyCacheKeyIsBasedOnContextLoader() { + loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 0, 1); + loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 1, 1); + loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 2); + loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 2, 2); + loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 2, 3, 2); + loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 4, 2); + } + + @Test + public void verifyCacheKeyIsBasedOnActiveProfiles() { + loadCtxAndAssertStats(FooBarProfilesTestCase.class, 1, 0, 1); + loadCtxAndAssertStats(FooBarProfilesTestCase.class, 1, 1, 1); + // Profiles {foo, bar} should hash to the same as {bar,foo} + loadCtxAndAssertStats(BarFooProfilesTestCase.class, 1, 2, 1); + loadCtxAndAssertStats(FooBarProfilesTestCase.class, 1, 3, 1); + loadCtxAndAssertStats(FooBarProfilesTestCase.class, 1, 4, 1); + loadCtxAndAssertStats(BarFooProfilesTestCase.class, 1, 5, 1); + } + + @Test + public void verifyCacheBehaviorForContextHierarchies() { + int size = 0; + int hits = 0; + int misses = 0; + + // Level 1 + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel1TestCase.class, ++size, hits, ++misses); + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel1TestCase.class, size, ++hits, misses); + + // Level 2 + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, ++size /* L2 */, ++hits /* L1 */, + ++misses /* L2 */); + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, size, ++hits /* L2 */, misses); + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, size, ++hits /* L2 */, misses); + + // Level 3-A + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3aTestCase.class, ++size /* L3A */, ++hits /* L2 */, + ++misses /* L3A */); + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3aTestCase.class, size, ++hits /* L3A */, misses); + + // Level 3-B + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3bTestCase.class, ++size /* L3B */, ++hits /* L2 */, + ++misses /* L3B */); + loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3bTestCase.class, size, ++hits /* L3B */, misses); + } + + @Test + public void removeContextHierarchyCacheLevel1() { + + // Load Level 3-A + TestContext testContext3a = new TestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); + testContext3a.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); + assertParentContextCount(2); + + // Load Level 3-B + TestContext testContext3b = new TestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); + testContext3b.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); + assertParentContextCount(2); + + // Remove Level 1 + // Should also remove Levels 2, 3-A, and 3-B, leaving nothing. + contextCache.remove(getMergedContextConfiguration(testContext3a).getParent().getParent(), + HierarchyMode.CURRENT_LEVEL); + assertContextCacheStatistics(contextCache, "removed level 1", 0, 1, 4); + assertParentContextCount(0); + } + + @Test + public void removeContextHierarchyCacheLevel1WithExhaustiveMode() { + + // Load Level 3-A + TestContext testContext3a = new TestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); + testContext3a.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); + assertParentContextCount(2); + + // Load Level 3-B + TestContext testContext3b = new TestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); + testContext3b.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); + assertParentContextCount(2); + + // Remove Level 1 + // Should also remove Levels 2, 3-A, and 3-B, leaving nothing. + contextCache.remove(getMergedContextConfiguration(testContext3a).getParent().getParent(), + HierarchyMode.EXHAUSTIVE); + assertContextCacheStatistics(contextCache, "removed level 1", 0, 1, 4); + assertParentContextCount(0); + } + + @Test + public void removeContextHierarchyCacheLevel2() { + + // Load Level 3-A + TestContext testContext3a = new TestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); + testContext3a.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); + assertParentContextCount(2); + + // Load Level 3-B + TestContext testContext3b = new TestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); + testContext3b.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); + assertParentContextCount(2); + + // Remove Level 2 + // Should also remove Levels 3-A and 3-B, leaving only Level 1 as a context in the + // cache but also removing the Level 1 hierarchy since all children have been + // removed. + contextCache.remove(getMergedContextConfiguration(testContext3a).getParent(), HierarchyMode.CURRENT_LEVEL); + assertContextCacheStatistics(contextCache, "removed level 2", 1, 1, 4); + assertParentContextCount(0); + } + + @Test + public void removeContextHierarchyCacheLevel2WithExhaustiveMode() { + + // Load Level 3-A + TestContext testContext3a = new TestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); + testContext3a.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); + assertParentContextCount(2); + + // Load Level 3-B + TestContext testContext3b = new TestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); + testContext3b.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); + assertParentContextCount(2); + + // Remove Level 2 + // Should wipe the cache + contextCache.remove(getMergedContextConfiguration(testContext3a).getParent(), HierarchyMode.EXHAUSTIVE); + assertContextCacheStatistics(contextCache, "removed level 2", 0, 1, 4); + assertParentContextCount(0); + } + + @Test + public void removeContextHierarchyCacheLevel3Then2() { + + // Load Level 3-A + TestContext testContext3a = new TestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); + testContext3a.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); + assertParentContextCount(2); + + // Load Level 3-B + TestContext testContext3b = new TestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); + testContext3b.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); + assertParentContextCount(2); + + // Remove Level 3-A + contextCache.remove(getMergedContextConfiguration(testContext3a), HierarchyMode.CURRENT_LEVEL); + assertContextCacheStatistics(contextCache, "removed level 3-A", 3, 1, 4); + assertParentContextCount(2); + + // Remove Level 2 + // Should also remove Level 3-B, leaving only Level 1. + contextCache.remove(getMergedContextConfiguration(testContext3b).getParent(), HierarchyMode.CURRENT_LEVEL); + assertContextCacheStatistics(contextCache, "removed level 2", 1, 1, 4); + assertParentContextCount(0); + } + + @Test + public void removeContextHierarchyCacheLevel3Then2WithExhaustiveMode() { + + // Load Level 3-A + TestContext testContext3a = new TestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); + testContext3a.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); + assertParentContextCount(2); + + // Load Level 3-B + TestContext testContext3b = new TestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); + testContext3b.getApplicationContext(); + assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); + assertParentContextCount(2); + + // Remove Level 3-A + // Should wipe the cache. + contextCache.remove(getMergedContextConfiguration(testContext3a), HierarchyMode.EXHAUSTIVE); + assertContextCacheStatistics(contextCache, "removed level 3-A", 0, 1, 4); + assertParentContextCount(0); + + // Remove Level 2 + // Should not actually do anything since the cache was cleared in the + // previous step. So the stats should remain the same. + contextCache.remove(getMergedContextConfiguration(testContext3b).getParent(), HierarchyMode.EXHAUSTIVE); + assertContextCacheStatistics(contextCache, "removed level 2", 0, 1, 4); + assertParentContextCount(0); + } + + + @Configuration + static class Config { + } + + @ContextConfiguration(classes = Config.class, loader = AnnotationConfigContextLoader.class) + private static class AnnotationConfigContextLoaderTestCase { + } + + @ContextConfiguration(classes = Config.class, loader = CustomAnnotationConfigContextLoader.class) + private static class CustomAnnotationConfigContextLoaderTestCase { + } + + private static class CustomAnnotationConfigContextLoader extends AnnotationConfigContextLoader { + } + + @ActiveProfiles({ "foo", "bar" }) + @ContextConfiguration(classes = Config.class, loader = AnnotationConfigContextLoader.class) + private static class FooBarProfilesTestCase { + } + + @ActiveProfiles({ "bar", "foo" }) + @ContextConfiguration(classes = Config.class, loader = AnnotationConfigContextLoader.class) + private static class BarFooProfilesTestCase { + } + + @ContextHierarchy({ @ContextConfiguration }) + private static class ClassHierarchyContextHierarchyLevel1TestCase { + + @Configuration + static class Level1Config { + + } + } + + @ContextHierarchy({ @ContextConfiguration }) + private static class ClassHierarchyContextHierarchyLevel2TestCase extends + ClassHierarchyContextHierarchyLevel1TestCase { + + @Configuration + static class Level2Config { + + } + } + + @ContextHierarchy({ @ContextConfiguration }) + private static class ClassHierarchyContextHierarchyLevel3aTestCase extends + ClassHierarchyContextHierarchyLevel2TestCase { + + @Configuration + static class Level3aConfig { + + } + } + + @ContextHierarchy({ @ContextConfiguration }) + private static class ClassHierarchyContextHierarchyLevel3bTestCase extends + ClassHierarchyContextHierarchyLevel2TestCase { + + @Configuration + static class Level3bConfig { + + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java new file mode 100644 index 00000000..3026e3a0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import org.junit.After; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.JUnitCore; +import org.junit.runner.Result; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +/** + * Integration tests that verify proper behavior of {@link DirtiesContext @DirtiesContext} + * in conjunction with context hierarchies configured via {@link ContextHierarchy @ContextHierarchy}. + * + * @author Sam Brannen + * @author Tadaya Tsuyukubo + * @since 3.2.2 + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class ContextHierarchyDirtiesContextTests { + + private static ApplicationContext context; + + + @After + public void cleanUp() { + ContextHierarchyDirtiesContextTests.context = null; + } + + @Test + public void classLevelDirtiesContextWithCurrentLevelHierarchyMode() { + runTestAndVerifyHierarchies(ClassLevelDirtiesContextWithCurrentLevelModeTestCase.class, true, true, false); + } + + @Test + public void classLevelDirtiesContextWithExhaustiveHierarchyMode() { + runTestAndVerifyHierarchies(ClassLevelDirtiesContextWithExhaustiveModeTestCase.class, false, false, false); + } + + @Test + public void methodLevelDirtiesContextWithCurrentLevelHierarchyMode() { + runTestAndVerifyHierarchies(MethodLevelDirtiesContextWithCurrentLevelModeTestCase.class, true, true, false); + } + + @Test + public void methodLevelDirtiesContextWithExhaustiveHierarchyMode() { + runTestAndVerifyHierarchies(MethodLevelDirtiesContextWithExhaustiveModeTestCase.class, false, false, false); + } + + private void runTestAndVerifyHierarchies(Class<? extends FooTestCase> testClass, boolean isFooContextActive, + boolean isBarContextActive, boolean isBazContextActive) { + + JUnitCore jUnitCore = new JUnitCore(); + Result result = jUnitCore.run(testClass); + assertTrue("all tests passed", result.wasSuccessful()); + + assertThat(ContextHierarchyDirtiesContextTests.context, notNullValue()); + + ConfigurableApplicationContext bazContext = (ConfigurableApplicationContext) ContextHierarchyDirtiesContextTests.context; + assertEquals("baz", bazContext.getBean("bean", String.class)); + assertThat("bazContext#isActive()", bazContext.isActive(), is(isBazContextActive)); + + ConfigurableApplicationContext barContext = (ConfigurableApplicationContext) bazContext.getParent(); + assertThat(barContext, notNullValue()); + assertEquals("bar", barContext.getBean("bean", String.class)); + assertThat("barContext#isActive()", barContext.isActive(), is(isBarContextActive)); + + ConfigurableApplicationContext fooContext = (ConfigurableApplicationContext) barContext.getParent(); + assertThat(fooContext, notNullValue()); + assertEquals("foo", fooContext.getBean("bean", String.class)); + assertThat("fooContext#isActive()", fooContext.isActive(), is(isFooContextActive)); + } + + + // ------------------------------------------------------------------------- + + @RunWith(SpringJUnit4ClassRunner.class) + @ContextHierarchy(@ContextConfiguration(name = "foo")) + static abstract class FooTestCase implements ApplicationContextAware { + + @Configuration + static class Config { + + @Bean + public String bean() { + return "foo"; + } + } + + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ContextHierarchyDirtiesContextTests.context = applicationContext; + } + } + + @ContextHierarchy(@ContextConfiguration(name = "bar")) + static abstract class BarTestCase extends FooTestCase { + + @Configuration + static class Config { + + @Bean + public String bean() { + return "bar"; + } + } + } + + @ContextHierarchy(@ContextConfiguration(name = "baz")) + static abstract class BazTestCase extends BarTestCase { + + @Configuration + static class Config { + + @Bean + public String bean() { + return "baz"; + } + } + } + + // ------------------------------------------------------------------------- + + /** + * {@link DirtiesContext} is declared at the class level, without specifying + * the {@link DirtiesContext.HierarchyMode}. + * <p>After running this test class, the context cache should be <em>exhaustively</em> + * cleared beginning from the current context hierarchy, upwards to the highest + * parent context, and then back down through all subhierarchies of the parent + * context. + */ + @DirtiesContext + public static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { + + @Test + public void test() { + } + } + + /** + * {@link DirtiesContext} is declared at the class level, specifying the + * {@link DirtiesContext.HierarchyMode#CURRENT_LEVEL CURRENT_LEVEL} hierarchy mode. + * <p>After running this test class, the context cache should be cleared + * beginning from the current context hierarchy and down through all subhierarchies. + */ + @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) + public static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { + + @Test + public void test() { + } + } + + /** + * {@link DirtiesContext} is declared at the method level, without specifying + * the {@link DirtiesContext.HierarchyMode}. + * <p>After running this test class, the context cache should be <em>exhaustively</em> + * cleared beginning from the current context hierarchy, upwards to the highest + * parent context, and then back down through all subhierarchies of the parent + * context. + */ + public static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { + + @Test + @DirtiesContext + public void test() { + } + } + + /** + * {@link DirtiesContext} is declared at the method level, specifying the + * {@link DirtiesContext.HierarchyMode#CURRENT_LEVEL CURRENT_LEVEL} hierarchy mode. + * <p>After running this test class, the context cache should be cleared + * beginning from the current context hierarchy and down through all subhierarchies. + */ + public static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { + + @Test + @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) + public void test() { + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsTests$BareAnnotations-context.xml b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsTests$BareAnnotations-context.xml new file mode 100644 index 00000000..44d11048 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsTests$BareAnnotations-context.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <!-- intentionally empty: only needed so that the ContextLoader can find this file --> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsTests.java new file mode 100644 index 00000000..9553efc8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsTests.java @@ -0,0 +1,911 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.test.context.ContextLoaderUtils.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.context.support.DelegatingSmartContextLoader; +import org.springframework.test.context.support.GenericPropertiesContextLoader; +import org.springframework.web.context.support.GenericWebApplicationContext; + +/** + * Unit tests for {@link ContextLoaderUtils}. + * + * @author Sam Brannen + * @since 3.1 + */ +public class ContextLoaderUtilsTests { + + private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0]; + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> EMPTY_INITIALIZER_CLASSES = // + Collections.<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> emptySet(); + + + private void assertAttributes(ContextConfigurationAttributes attributes, Class<?> expectedDeclaringClass, + String[] expectedLocations, Class<?>[] expectedClasses, + Class<? extends ContextLoader> expectedContextLoaderClass, boolean expectedInheritLocations) { + assertEquals(expectedDeclaringClass, attributes.getDeclaringClass()); + assertArrayEquals(expectedLocations, attributes.getLocations()); + assertArrayEquals(expectedClasses, attributes.getClasses()); + assertEquals(expectedInheritLocations, attributes.isInheritLocations()); + assertEquals(expectedContextLoaderClass, attributes.getContextLoaderClass()); + } + + private void assertLocationsFooAttributes(ContextConfigurationAttributes attributes) { + assertAttributes(attributes, LocationsFoo.class, new String[] { "/foo.xml" }, EMPTY_CLASS_ARRAY, + ContextLoader.class, false); + } + + private void assertClassesFooAttributes(ContextConfigurationAttributes attributes) { + assertAttributes(attributes, ClassesFoo.class, EMPTY_STRING_ARRAY, new Class<?>[] { FooConfig.class }, + ContextLoader.class, false); + } + + private void assertLocationsBarAttributes(ContextConfigurationAttributes attributes) { + assertAttributes(attributes, LocationsBar.class, new String[] { "/bar.xml" }, EMPTY_CLASS_ARRAY, + AnnotationConfigContextLoader.class, true); + } + + private void assertClassesBarAttributes(ContextConfigurationAttributes attributes) { + assertAttributes(attributes, ClassesBar.class, EMPTY_STRING_ARRAY, new Class<?>[] { BarConfig.class }, + AnnotationConfigContextLoader.class, true); + } + + private void assertMergedConfig(MergedContextConfiguration mergedConfig, Class<?> expectedTestClass, + String[] expectedLocations, Class<?>[] expectedClasses, + Class<? extends ContextLoader> expectedContextLoaderClass) { + assertMergedConfig(mergedConfig, expectedTestClass, expectedLocations, expectedClasses, + EMPTY_INITIALIZER_CLASSES, expectedContextLoaderClass); + } + + private void assertMergedConfig( + MergedContextConfiguration mergedConfig, + Class<?> expectedTestClass, + String[] expectedLocations, + Class<?>[] expectedClasses, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> expectedInitializerClasses, + Class<? extends ContextLoader> expectedContextLoaderClass) { + assertNotNull(mergedConfig); + assertEquals(expectedTestClass, mergedConfig.getTestClass()); + assertNotNull(mergedConfig.getLocations()); + assertArrayEquals(expectedLocations, mergedConfig.getLocations()); + assertNotNull(mergedConfig.getClasses()); + assertArrayEquals(expectedClasses, mergedConfig.getClasses()); + assertNotNull(mergedConfig.getActiveProfiles()); + assertEquals(expectedContextLoaderClass, mergedConfig.getContextLoader().getClass()); + assertNotNull(mergedConfig.getContextInitializerClasses()); + assertEquals(expectedInitializerClasses, mergedConfig.getContextInitializerClasses()); + } + + private void debugConfigAttributes(List<ContextConfigurationAttributes> configAttributesList) { + // for (ContextConfigurationAttributes configAttributes : configAttributesList) { + // System.err.println(configAttributes); + // } + } + + @Test(expected = IllegalStateException.class) + public void resolveContextHierarchyAttributesForSingleTestClassWithContextConfigurationAndContextHierarchy() { + resolveContextHierarchyAttributes(SingleTestClassWithContextConfigurationAndContextHierarchy.class); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithImplicitSingleLevelContextHierarchy() { + List<List<ContextConfigurationAttributes>> hierarchyAttributes = resolveContextHierarchyAttributes(BareAnnotations.class); + assertEquals(1, hierarchyAttributes.size()); + List<ContextConfigurationAttributes> configAttributesList = hierarchyAttributes.get(0); + assertEquals(1, configAttributesList.size()); + debugConfigAttributes(configAttributesList); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithSingleLevelContextHierarchy() { + List<List<ContextConfigurationAttributes>> hierarchyAttributes = resolveContextHierarchyAttributes(SingleTestClassWithSingleLevelContextHierarchy.class); + assertEquals(1, hierarchyAttributes.size()); + List<ContextConfigurationAttributes> configAttributesList = hierarchyAttributes.get(0); + assertEquals(1, configAttributesList.size()); + debugConfigAttributes(configAttributesList); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithTripleLevelContextHierarchy() { + List<List<ContextConfigurationAttributes>> hierarchyAttributes = resolveContextHierarchyAttributes(SingleTestClassWithTripleLevelContextHierarchy.class); + assertEquals(1, hierarchyAttributes.size()); + List<ContextConfigurationAttributes> configAttributesList = hierarchyAttributes.get(0); + assertEquals(3, configAttributesList.size()); + debugConfigAttributes(configAttributesList); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithSingleLevelContextHierarchies() { + List<List<ContextConfigurationAttributes>> hierarchyAttributes = resolveContextHierarchyAttributes(TestClass3WithSingleLevelContextHierarchy.class); + assertEquals(3, hierarchyAttributes.size()); + + List<ContextConfigurationAttributes> configAttributesListClassLevel1 = hierarchyAttributes.get(0); + debugConfigAttributes(configAttributesListClassLevel1); + assertEquals(1, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("one.xml")); + + List<ContextConfigurationAttributes> configAttributesListClassLevel2 = hierarchyAttributes.get(1); + debugConfigAttributes(configAttributesListClassLevel2); + assertEquals(1, configAttributesListClassLevel2.size()); + assertArrayEquals(new String[] { "two-A.xml", "two-B.xml" }, + configAttributesListClassLevel2.get(0).getLocations()); + + List<ContextConfigurationAttributes> configAttributesListClassLevel3 = hierarchyAttributes.get(2); + debugConfigAttributes(configAttributesListClassLevel3); + assertEquals(1, configAttributesListClassLevel3.size()); + assertThat(configAttributesListClassLevel3.get(0).getLocations()[0], equalTo("three.xml")); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithBareContextConfigurationInSubclass() { + List<List<ContextConfigurationAttributes>> hierarchyAttributes = resolveContextHierarchyAttributes(TestClass2WithBareContextConfigurationInSubclass.class); + assertEquals(2, hierarchyAttributes.size()); + + List<ContextConfigurationAttributes> configAttributesListClassLevel1 = hierarchyAttributes.get(0); + debugConfigAttributes(configAttributesListClassLevel1); + assertEquals(1, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("one.xml")); + + List<ContextConfigurationAttributes> configAttributesListClassLevel2 = hierarchyAttributes.get(1); + debugConfigAttributes(configAttributesListClassLevel2); + assertEquals(1, configAttributesListClassLevel2.size()); + assertThat(configAttributesListClassLevel2.get(0).getLocations()[0], equalTo("two.xml")); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithBareContextConfigurationInSuperclass() { + List<List<ContextConfigurationAttributes>> hierarchyAttributes = resolveContextHierarchyAttributes(TestClass2WithBareContextConfigurationInSuperclass.class); + assertEquals(2, hierarchyAttributes.size()); + + List<ContextConfigurationAttributes> configAttributesListClassLevel1 = hierarchyAttributes.get(0); + debugConfigAttributes(configAttributesListClassLevel1); + assertEquals(1, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("one.xml")); + + List<ContextConfigurationAttributes> configAttributesListClassLevel2 = hierarchyAttributes.get(1); + debugConfigAttributes(configAttributesListClassLevel2); + assertEquals(1, configAttributesListClassLevel2.size()); + assertThat(configAttributesListClassLevel2.get(0).getLocations()[0], equalTo("two.xml")); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithMultiLevelContextHierarchies() { + List<List<ContextConfigurationAttributes>> hierarchyAttributes = resolveContextHierarchyAttributes(TestClass3WithMultiLevelContextHierarchy.class); + assertEquals(3, hierarchyAttributes.size()); + + List<ContextConfigurationAttributes> configAttributesListClassLevel1 = hierarchyAttributes.get(0); + debugConfigAttributes(configAttributesListClassLevel1); + assertEquals(2, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("1-A.xml")); + assertThat(configAttributesListClassLevel1.get(1).getLocations()[0], equalTo("1-B.xml")); + + List<ContextConfigurationAttributes> configAttributesListClassLevel2 = hierarchyAttributes.get(1); + debugConfigAttributes(configAttributesListClassLevel2); + assertEquals(2, configAttributesListClassLevel2.size()); + assertThat(configAttributesListClassLevel2.get(0).getLocations()[0], equalTo("2-A.xml")); + assertThat(configAttributesListClassLevel2.get(1).getLocations()[0], equalTo("2-B.xml")); + + List<ContextConfigurationAttributes> configAttributesListClassLevel3 = hierarchyAttributes.get(2); + debugConfigAttributes(configAttributesListClassLevel3); + assertEquals(3, configAttributesListClassLevel3.size()); + assertThat(configAttributesListClassLevel3.get(0).getLocations()[0], equalTo("3-A.xml")); + assertThat(configAttributesListClassLevel3.get(1).getLocations()[0], equalTo("3-B.xml")); + assertThat(configAttributesListClassLevel3.get(2).getLocations()[0], equalTo("3-C.xml")); + } + + @Test + public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHierarchies() { + Map<String, List<ContextConfigurationAttributes>> map = buildContextHierarchyMap(TestClass3WithMultiLevelContextHierarchy.class); + + assertThat(map.size(), is(3)); + assertThat(map.keySet(), hasItems("alpha", "beta", "gamma")); + + List<ContextConfigurationAttributes> alphaConfig = map.get("alpha"); + assertThat(alphaConfig.size(), is(3)); + assertThat(alphaConfig.get(0).getLocations()[0], is("1-A.xml")); + assertThat(alphaConfig.get(1).getLocations()[0], is("2-A.xml")); + assertThat(alphaConfig.get(2).getLocations()[0], is("3-A.xml")); + + List<ContextConfigurationAttributes> betaConfig = map.get("beta"); + assertThat(betaConfig.size(), is(3)); + assertThat(betaConfig.get(0).getLocations()[0], is("1-B.xml")); + assertThat(betaConfig.get(1).getLocations()[0], is("2-B.xml")); + assertThat(betaConfig.get(2).getLocations()[0], is("3-B.xml")); + + List<ContextConfigurationAttributes> gammaConfig = map.get("gamma"); + assertThat(gammaConfig.size(), is(1)); + assertThat(gammaConfig.get(0).getLocations()[0], is("3-C.xml")); + } + + @Test + public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHierarchiesAndUnnamedConfig() { + Map<String, List<ContextConfigurationAttributes>> map = buildContextHierarchyMap(TestClass3WithMultiLevelContextHierarchyAndUnnamedConfig.class); + + String level1 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 1; + String level2 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 2; + String level3 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 3; + String level4 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 4; + String level5 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 5; + String level6 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 6; + String level7 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 7; + + assertThat(map.size(), is(7)); + assertThat(map.keySet(), hasItems(level1, level2, level3, level4, level5, level6, level7)); + + List<ContextConfigurationAttributes> level1Config = map.get(level1); + assertThat(level1Config.size(), is(1)); + assertThat(level1Config.get(0).getLocations()[0], is("1-A.xml")); + + List<ContextConfigurationAttributes> level2Config = map.get(level2); + assertThat(level2Config.size(), is(1)); + assertThat(level2Config.get(0).getLocations()[0], is("1-B.xml")); + + List<ContextConfigurationAttributes> level3Config = map.get(level3); + assertThat(level3Config.size(), is(1)); + assertThat(level3Config.get(0).getLocations()[0], is("2-A.xml")); + + // ... + + List<ContextConfigurationAttributes> level7Config = map.get(level7); + assertThat(level7Config.size(), is(1)); + assertThat(level7Config.get(0).getLocations()[0], is("3-C.xml")); + } + + @Test + public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHierarchiesAndPartiallyNamedConfig() { + Map<String, List<ContextConfigurationAttributes>> map = buildContextHierarchyMap(TestClass2WithMultiLevelContextHierarchyAndPartiallyNamedConfig.class); + + String level1 = "parent"; + String level2 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 2; + String level3 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 3; + + assertThat(map.size(), is(3)); + assertThat(map.keySet(), hasItems(level1, level2, level3)); + Iterator<String> levels = map.keySet().iterator(); + assertThat(levels.next(), is(level1)); + assertThat(levels.next(), is(level2)); + assertThat(levels.next(), is(level3)); + + List<ContextConfigurationAttributes> level1Config = map.get(level1); + assertThat(level1Config.size(), is(2)); + assertThat(level1Config.get(0).getLocations()[0], is("1-A.xml")); + assertThat(level1Config.get(1).getLocations()[0], is("2-A.xml")); + + List<ContextConfigurationAttributes> level2Config = map.get(level2); + assertThat(level2Config.size(), is(1)); + assertThat(level2Config.get(0).getLocations()[0], is("1-B.xml")); + + List<ContextConfigurationAttributes> level3Config = map.get(level3); + assertThat(level3Config.size(), is(1)); + assertThat(level3Config.get(0).getLocations()[0], is("2-C.xml")); + } + + private void assertContextConfigEntriesAreNotUnique(Class<?> testClass) { + try { + buildContextHierarchyMap(testClass); + fail("Should throw an IllegalStateException"); + } + catch (IllegalStateException e) { + String msg = String.format( + "The @ContextConfiguration elements configured via @ContextHierarchy in test class [%s] and its superclasses must define unique contexts per hierarchy level.", + testClass.getName()); + assertEquals(msg, e.getMessage()); + } + } + + @Test + public void buildContextHierarchyMapForSingleTestClassWithMultiLevelContextHierarchyWithEmptyContextConfig() { + assertContextConfigEntriesAreNotUnique(SingleTestClassWithMultiLevelContextHierarchyWithEmptyContextConfig.class); + } + + @Test + public void buildContextHierarchyMapForSingleTestClassWithMultiLevelContextHierarchyWithDuplicatedContextConfig() { + assertContextConfigEntriesAreNotUnique(SingleTestClassWithMultiLevelContextHierarchyWithDuplicatedContextConfig.class); + } + + /** + * Used to reproduce bug reported in https://jira.springsource.org/browse/SPR-10997 + */ + @Test + public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHierarchiesAndOverriddenInitializers() { + Map<String, List<ContextConfigurationAttributes>> map = buildContextHierarchyMap(TestClass2WithMultiLevelContextHierarchyWithOverriddenInitializers.class); + + assertThat(map.size(), is(2)); + assertThat(map.keySet(), hasItems("alpha", "beta")); + + List<ContextConfigurationAttributes> alphaConfig = map.get("alpha"); + assertThat(alphaConfig.size(), is(2)); + assertThat(alphaConfig.get(0).getLocations().length, is(1)); + assertThat(alphaConfig.get(0).getLocations()[0], is("1-A.xml")); + assertThat(alphaConfig.get(0).getInitializers().length, is(0)); + assertThat(alphaConfig.get(1).getLocations().length, is(0)); + assertThat(alphaConfig.get(1).getInitializers().length, is(1)); + assertEquals(DummyApplicationContextInitializer.class, alphaConfig.get(1).getInitializers()[0]); + + List<ContextConfigurationAttributes> betaConfig = map.get("beta"); + assertThat(betaConfig.size(), is(2)); + assertThat(betaConfig.get(0).getLocations().length, is(1)); + assertThat(betaConfig.get(0).getLocations()[0], is("1-B.xml")); + assertThat(betaConfig.get(0).getInitializers().length, is(0)); + assertThat(betaConfig.get(1).getLocations().length, is(0)); + assertThat(betaConfig.get(1).getInitializers().length, is(1)); + assertEquals(DummyApplicationContextInitializer.class, betaConfig.get(1).getInitializers()[0]); + } + + @Test(expected = IllegalStateException.class) + public void resolveConfigAttributesWithConflictingLocations() { + resolveContextConfigurationAttributes(ConflictingLocations.class); + } + + @Test + public void resolveConfigAttributesWithBareAnnotations() { + List<ContextConfigurationAttributes> attributesList = resolveContextConfigurationAttributes(BareAnnotations.class); + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertAttributes(attributesList.get(0), BareAnnotations.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, + ContextLoader.class, true); + } + + @Test + public void resolveConfigAttributesWithLocalAnnotationAndLocations() { + List<ContextConfigurationAttributes> attributesList = resolveContextConfigurationAttributes(LocationsFoo.class); + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertLocationsFooAttributes(attributesList.get(0)); + } + + @Test + public void resolveConfigAttributesWithLocalAnnotationAndClasses() { + List<ContextConfigurationAttributes> attributesList = resolveContextConfigurationAttributes(ClassesFoo.class); + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertClassesFooAttributes(attributesList.get(0)); + } + + @Test + public void resolveConfigAttributesWithLocalAndInheritedAnnotationsAndLocations() { + List<ContextConfigurationAttributes> attributesList = resolveContextConfigurationAttributes(LocationsBar.class); + assertNotNull(attributesList); + assertEquals(2, attributesList.size()); + assertLocationsBarAttributes(attributesList.get(0)); + assertLocationsFooAttributes(attributesList.get(1)); + } + + @Test + public void resolveConfigAttributesWithLocalAndInheritedAnnotationsAndClasses() { + List<ContextConfigurationAttributes> attributesList = resolveContextConfigurationAttributes(ClassesBar.class); + assertNotNull(attributesList); + assertEquals(2, attributesList.size()); + assertClassesBarAttributes(attributesList.get(0)); + assertClassesFooAttributes(attributesList.get(1)); + } + + @Test(expected = IllegalArgumentException.class) + public void buildMergedConfigWithoutAnnotation() { + buildMergedContextConfiguration(Enigma.class, null, null); + } + + @Test + public void buildMergedConfigWithBareAnnotations() { + Class<BareAnnotations> testClass = BareAnnotations.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + + assertMergedConfig( + mergedConfig, + testClass, + new String[] { "classpath:/org/springframework/test/context/ContextLoaderUtilsTests$BareAnnotations-context.xml" }, + EMPTY_CLASS_ARRAY, DelegatingSmartContextLoader.class); + } + + @Test + public void buildMergedConfigWithLocalAnnotationAndLocations() { + Class<?> testClass = LocationsFoo.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + + assertMergedConfig(mergedConfig, testClass, new String[] { "classpath:/foo.xml" }, EMPTY_CLASS_ARRAY, + DelegatingSmartContextLoader.class); + } + + @Test + public void buildMergedConfigWithLocalAnnotationAndClasses() { + Class<?> testClass = ClassesFoo.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, new Class<?>[] { FooConfig.class }, + DelegatingSmartContextLoader.class); + } + + @Test + public void buildMergedConfigWithLocalAnnotationAndOverriddenContextLoaderAndLocations() { + Class<?> testClass = LocationsFoo.class; + Class<? extends ContextLoader> expectedContextLoaderClass = GenericPropertiesContextLoader.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, + expectedContextLoaderClass.getName(), null); + + assertMergedConfig(mergedConfig, testClass, new String[] { "classpath:/foo.xml" }, EMPTY_CLASS_ARRAY, + expectedContextLoaderClass); + } + + @Test + public void buildMergedConfigWithLocalAnnotationAndOverriddenContextLoaderAndClasses() { + Class<?> testClass = ClassesFoo.class; + Class<? extends ContextLoader> expectedContextLoaderClass = GenericPropertiesContextLoader.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, + expectedContextLoaderClass.getName(), null); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, new Class<?>[] { FooConfig.class }, + expectedContextLoaderClass); + } + + @Test + public void buildMergedConfigWithLocalAndInheritedAnnotationsAndLocations() { + Class<?> testClass = LocationsBar.class; + String[] expectedLocations = new String[] { "/foo.xml", "/bar.xml" }; + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, expectedLocations, EMPTY_CLASS_ARRAY, + AnnotationConfigContextLoader.class); + } + + @Test + public void buildMergedConfigWithLocalAndInheritedAnnotationsAndClasses() { + Class<?> testClass = ClassesBar.class; + Class<?>[] expectedClasses = new Class<?>[] { FooConfig.class, BarConfig.class }; + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + AnnotationConfigContextLoader.class); + } + + @Test + public void buildMergedConfigWithAnnotationsAndOverriddenLocations() { + Class<?> testClass = OverriddenLocationsBar.class; + String[] expectedLocations = new String[] { "/bar.xml" }; + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, expectedLocations, EMPTY_CLASS_ARRAY, + AnnotationConfigContextLoader.class); + } + + @Test + public void buildMergedConfigWithAnnotationsAndOverriddenClasses() { + Class<?> testClass = OverriddenClassesBar.class; + Class<?>[] expectedClasses = new Class<?>[] { BarConfig.class }; + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + AnnotationConfigContextLoader.class); + } + + @Test + public void buildMergedConfigWithLocalInitializer() { + Class<?> testClass = InitializersFoo.class; + Class<?>[] expectedClasses = new Class<?>[] { FooConfig.class }; + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> expectedInitializerClasses// + = new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + expectedInitializerClasses.add(FooInitializer.class); + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, + DelegatingSmartContextLoader.class); + } + + @Test + public void buildMergedConfigWithLocalAndInheritedInitializer() { + Class<?> testClass = InitializersBar.class; + Class<?>[] expectedClasses = new Class<?>[] { FooConfig.class, BarConfig.class }; + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> expectedInitializerClasses// + = new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + expectedInitializerClasses.add(FooInitializer.class); + expectedInitializerClasses.add(BarInitializer.class); + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, + DelegatingSmartContextLoader.class); + } + + @Test + public void buildMergedConfigWithOverriddenInitializers() { + Class<?> testClass = OverriddenInitializersBar.class; + Class<?>[] expectedClasses = new Class<?>[] { FooConfig.class, BarConfig.class }; + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> expectedInitializerClasses// + = new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + expectedInitializerClasses.add(BarInitializer.class); + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, + DelegatingSmartContextLoader.class); + } + + @Test + public void buildMergedConfigWithOverriddenInitializersAndClasses() { + Class<?> testClass = OverriddenInitializersAndClassesBar.class; + Class<?>[] expectedClasses = new Class<?>[] { BarConfig.class }; + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> expectedInitializerClasses// + = new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + expectedInitializerClasses.add(BarInitializer.class); + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, + DelegatingSmartContextLoader.class); + } + + @Test + public void resolveActiveProfilesWithoutAnnotation() { + String[] profiles = resolveActiveProfiles(Enigma.class); + assertArrayEquals(EMPTY_STRING_ARRAY, profiles); + } + + @Test + public void resolveActiveProfilesWithNoProfilesDeclared() { + String[] profiles = resolveActiveProfiles(BareAnnotations.class); + assertArrayEquals(EMPTY_STRING_ARRAY, profiles); + } + + @Test + public void resolveActiveProfilesWithEmptyProfiles() { + String[] profiles = resolveActiveProfiles(EmptyProfiles.class); + assertArrayEquals(EMPTY_STRING_ARRAY, profiles); + } + + @Test + public void resolveActiveProfilesWithDuplicatedProfiles() { + String[] profiles = resolveActiveProfiles(DuplicatedProfiles.class); + assertNotNull(profiles); + assertEquals(3, profiles.length); + + List<String> list = Arrays.asList(profiles); + assertTrue(list.contains("foo")); + assertTrue(list.contains("bar")); + assertTrue(list.contains("baz")); + } + + @Test + public void resolveActiveProfilesWithLocalAnnotation() { + String[] profiles = resolveActiveProfiles(LocationsFoo.class); + assertNotNull(profiles); + assertArrayEquals(new String[] { "foo" }, profiles); + } + + @Test + public void resolveActiveProfilesWithInheritedAnnotationAndLocations() { + String[] profiles = resolveActiveProfiles(InheritedLocationsFoo.class); + assertNotNull(profiles); + assertArrayEquals(new String[] { "foo" }, profiles); + } + + @Test + public void resolveActiveProfilesWithInheritedAnnotationAndClasses() { + String[] profiles = resolveActiveProfiles(InheritedClassesFoo.class); + assertNotNull(profiles); + assertArrayEquals(new String[] { "foo" }, profiles); + } + + @Test + public void resolveActiveProfilesWithLocalAndInheritedAnnotations() { + String[] profiles = resolveActiveProfiles(LocationsBar.class); + assertNotNull(profiles); + assertEquals(2, profiles.length); + + List<String> list = Arrays.asList(profiles); + assertTrue(list.contains("foo")); + assertTrue(list.contains("bar")); + } + + @Test + public void resolveActiveProfilesWithOverriddenAnnotation() { + String[] profiles = resolveActiveProfiles(Animals.class); + assertNotNull(profiles); + assertEquals(2, profiles.length); + + List<String> list = Arrays.asList(profiles); + assertTrue(list.contains("dog")); + assertTrue(list.contains("cat")); + } + + + // ------------------------------------------------------------------------- + + private static class Enigma { + } + + @ContextConfiguration(value = "x", locations = "y") + private static class ConflictingLocations { + } + + @ContextConfiguration + @ActiveProfiles + private static class BareAnnotations { + } + + @ActiveProfiles({ " ", "\t" }) + private static class EmptyProfiles { + } + + @ActiveProfiles({ "foo", "bar", " foo", "bar ", "baz" }) + private static class DuplicatedProfiles { + } + + @Configuration + private static class FooConfig { + } + + @ContextConfiguration(locations = "/foo.xml", inheritLocations = false) + @ActiveProfiles(profiles = "foo") + private static class LocationsFoo { + } + + @ContextConfiguration(classes = FooConfig.class, inheritLocations = false) + @ActiveProfiles(profiles = "foo") + private static class ClassesFoo { + } + + private static class InheritedLocationsFoo extends LocationsFoo { + } + + private static class InheritedClassesFoo extends ClassesFoo { + } + + @Configuration + private static class BarConfig { + } + + @ContextConfiguration(locations = "/bar.xml", inheritLocations = true, loader = AnnotationConfigContextLoader.class) + @ActiveProfiles("bar") + private static class LocationsBar extends LocationsFoo { + } + + @ContextConfiguration(locations = "/bar.xml", inheritLocations = false, loader = AnnotationConfigContextLoader.class) + @ActiveProfiles("bar") + private static class OverriddenLocationsBar extends LocationsFoo { + } + + @ContextConfiguration(classes = BarConfig.class, inheritLocations = true, loader = AnnotationConfigContextLoader.class) + @ActiveProfiles("bar") + private static class ClassesBar extends ClassesFoo { + } + + @ContextConfiguration(classes = BarConfig.class, inheritLocations = false, loader = AnnotationConfigContextLoader.class) + @ActiveProfiles("bar") + private static class OverriddenClassesBar extends ClassesFoo { + } + + @ActiveProfiles(profiles = { "dog", "cat" }, inheritProfiles = false) + private static class Animals extends LocationsBar { + } + + private static class FooInitializer implements ApplicationContextInitializer<GenericApplicationContext> { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + } + } + + private static class BarInitializer implements ApplicationContextInitializer<GenericWebApplicationContext> { + + @Override + public void initialize(GenericWebApplicationContext applicationContext) { + } + } + + @ContextConfiguration(classes = FooConfig.class, initializers = FooInitializer.class) + private static class InitializersFoo { + } + + @ContextConfiguration(classes = BarConfig.class, initializers = BarInitializer.class) + private static class InitializersBar extends InitializersFoo { + } + + @ContextConfiguration(classes = BarConfig.class, initializers = BarInitializer.class, inheritInitializers = false) + private static class OverriddenInitializersBar extends InitializersFoo { + } + + @ContextConfiguration(classes = BarConfig.class, inheritLocations = false, initializers = BarInitializer.class, inheritInitializers = false) + private static class OverriddenInitializersAndClassesBar extends InitializersFoo { + } + + @ContextConfiguration("foo.xml") + @ContextHierarchy(@ContextConfiguration("bar.xml")) + private static class SingleTestClassWithContextConfigurationAndContextHierarchy { + } + + @ContextHierarchy(@ContextConfiguration("A.xml")) + private static class SingleTestClassWithSingleLevelContextHierarchy { + } + + @ContextHierarchy({// + // + @ContextConfiguration("A.xml"),// + @ContextConfiguration("B.xml"),// + @ContextConfiguration("C.xml") // + }) + private static class SingleTestClassWithTripleLevelContextHierarchy { + } + + @ContextHierarchy(@ContextConfiguration("one.xml")) + private static class TestClass1WithSingleLevelContextHierarchy { + } + + @ContextHierarchy(@ContextConfiguration({ "two-A.xml", "two-B.xml" })) + private static class TestClass2WithSingleLevelContextHierarchy extends TestClass1WithSingleLevelContextHierarchy { + } + + @ContextHierarchy(@ContextConfiguration("three.xml")) + private static class TestClass3WithSingleLevelContextHierarchy extends TestClass2WithSingleLevelContextHierarchy { + } + + @ContextConfiguration("one.xml") + private static class TestClass1WithBareContextConfigurationInSuperclass { + } + + @ContextHierarchy(@ContextConfiguration("two.xml")) + private static class TestClass2WithBareContextConfigurationInSuperclass extends + TestClass1WithBareContextConfigurationInSuperclass { + } + + @ContextHierarchy(@ContextConfiguration("one.xml")) + private static class TestClass1WithBareContextConfigurationInSubclass { + } + + @ContextConfiguration("two.xml") + private static class TestClass2WithBareContextConfigurationInSubclass extends + TestClass1WithBareContextConfigurationInSuperclass { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "1-A.xml", name = "alpha"),// + @ContextConfiguration(locations = "1-B.xml", name = "beta") // + }) + private static class TestClass1WithMultiLevelContextHierarchy { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "2-A.xml", name = "alpha"),// + @ContextConfiguration(locations = "2-B.xml", name = "beta") // + }) + private static class TestClass2WithMultiLevelContextHierarchy extends TestClass1WithMultiLevelContextHierarchy { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "3-A.xml", name = "alpha"),// + @ContextConfiguration(locations = "3-B.xml", name = "beta"),// + @ContextConfiguration(locations = "3-C.xml", name = "gamma") // + }) + private static class TestClass3WithMultiLevelContextHierarchy extends TestClass2WithMultiLevelContextHierarchy { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "1-A.xml"),// + @ContextConfiguration(locations = "1-B.xml") // + }) + private static class TestClass1WithMultiLevelContextHierarchyAndUnnamedConfig { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "2-A.xml"),// + @ContextConfiguration(locations = "2-B.xml") // + }) + private static class TestClass2WithMultiLevelContextHierarchyAndUnnamedConfig extends + TestClass1WithMultiLevelContextHierarchyAndUnnamedConfig { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "3-A.xml"),// + @ContextConfiguration(locations = "3-B.xml"),// + @ContextConfiguration(locations = "3-C.xml") // + }) + private static class TestClass3WithMultiLevelContextHierarchyAndUnnamedConfig extends + TestClass2WithMultiLevelContextHierarchyAndUnnamedConfig { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "1-A.xml", name = "parent"),// + @ContextConfiguration(locations = "1-B.xml") // + }) + private static class TestClass1WithMultiLevelContextHierarchyAndPartiallyNamedConfig { + } + + @ContextHierarchy({// + // + @ContextConfiguration(locations = "2-A.xml", name = "parent"),// + @ContextConfiguration(locations = "2-C.xml") // + }) + private static class TestClass2WithMultiLevelContextHierarchyAndPartiallyNamedConfig extends + TestClass1WithMultiLevelContextHierarchyAndPartiallyNamedConfig { + } + + @ContextHierarchy({ + // + @ContextConfiguration,// + @ContextConfiguration // + }) + private static class SingleTestClassWithMultiLevelContextHierarchyWithEmptyContextConfig { + } + + @ContextHierarchy({ + // + @ContextConfiguration("foo.xml"),// + @ContextConfiguration(classes = BarConfig.class),// duplicate! + @ContextConfiguration("baz.xml"),// + @ContextConfiguration(classes = BarConfig.class),// duplicate! + @ContextConfiguration(loader = AnnotationConfigContextLoader.class) // + }) + private static class SingleTestClassWithMultiLevelContextHierarchyWithDuplicatedContextConfig { + } + + /** + * Used to reproduce bug reported in https://jira.springsource.org/browse/SPR-10997 + */ + @ContextHierarchy({// + // + @ContextConfiguration(name = "alpha", locations = "1-A.xml"),// + @ContextConfiguration(name = "beta", locations = "1-B.xml") // + }) + private static class TestClass1WithMultiLevelContextHierarchyWithUniqueContextConfig { + } + + /** + * Used to reproduce bug reported in https://jira.springsource.org/browse/SPR-10997 + */ + @ContextHierarchy({// + // + @ContextConfiguration(name = "alpha", initializers = DummyApplicationContextInitializer.class),// + @ContextConfiguration(name = "beta", initializers = DummyApplicationContextInitializer.class) // + }) + private static class TestClass2WithMultiLevelContextHierarchyWithOverriddenInitializers extends + TestClass1WithMultiLevelContextHierarchyWithUniqueContextConfig { + } + + /** + * Used to reproduce bug reported in https://jira.springsource.org/browse/SPR-10997 + */ + private static class DummyApplicationContextInitializer implements + ApplicationContextInitializer<ConfigurableApplicationContext> { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + /* no-op */ + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java new file mode 100644 index 00000000..863fd9da --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java @@ -0,0 +1,450 @@ +/* + * Copyright 2002-2013 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 + * + * http://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; + +import static org.junit.Assert.*; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.context.support.GenericXmlContextLoader; + +/** + * Unit tests for {@link MergedContextConfiguration}. + * + * <p>These tests primarily exist to ensure that {@code MergedContextConfiguration} + * can safely be used as the cache key for {@link ContextCache}. + * + * @author Sam Brannen + * @since 3.1 + */ +public class MergedContextConfigurationTests { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0]; + + private final GenericXmlContextLoader loader = new GenericXmlContextLoader(); + + + @Test + public void hashCodeWithNulls() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(null, null, null, null, null); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(null, null, null, null, null); + assertTrue(mergedConfig1.hashCode() > 0); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithNullArrays() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), null, null, null, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), null, null, null, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithEmptyArrays() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithEmptyArraysAndDifferentLoaders() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, new AnnotationConfigContextLoader()); + assertNotEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithSameLocations() { + String[] locations = new String[] { "foo", "bar}" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), locations, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), locations, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithDifferentLocations() { + String[] locations1 = new String[] { "foo", "bar}" }; + String[] locations2 = new String[] { "baz", "quux}" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), locations1, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), locations2, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + assertNotEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithSameConfigClasses() { + Class<?>[] classes = new Class<?>[] { String.class, Integer.class }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithDifferentConfigClasses() { + Class<?>[] classes1 = new Class<?>[] { String.class, Integer.class }; + Class<?>[] classes2 = new Class<?>[] { Boolean.class, Number.class }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes1, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes2, EMPTY_STRING_ARRAY, loader); + assertNotEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithSameProfiles() { + String[] activeProfiles = new String[] { "catbert", "dogbert" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithSameProfilesReversed() { + String[] activeProfiles1 = new String[] { "catbert", "dogbert" }; + String[] activeProfiles2 = new String[] { "dogbert", "catbert" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles1, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles2, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithSameDuplicateProfiles() { + String[] activeProfiles1 = new String[] { "catbert", "dogbert" }; + String[] activeProfiles2 = new String[] { "catbert", "dogbert", "catbert", "dogbert", "catbert" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles1, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles2, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithDifferentProfiles() { + String[] activeProfiles1 = new String[] { "catbert", "dogbert" }; + String[] activeProfiles2 = new String[] { "X", "Y" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles1, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles2, loader); + assertNotEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithSameInitializers() { + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses1 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses1.add(FooInitializer.class); + initializerClasses1.add(BarInitializer.class); + + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses2 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses2.add(BarInitializer.class); + initializerClasses2.add(FooInitializer.class); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses1, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses2, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void hashCodeWithDifferentInitializers() { + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses1 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses1.add(FooInitializer.class); + + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses2 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses2.add(BarInitializer.class); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses1, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses2, EMPTY_STRING_ARRAY, loader); + assertNotEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + /** + * @since 3.2.2 + */ + @Test + public void hashCodeWithSameParent() { + MergedContextConfiguration parent = new MergedContextConfiguration(getClass(), new String[] { "foo", "bar}" }, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent); + assertEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + /** + * @since 3.2.2 + */ + @Test + public void hashCodeWithDifferentParents() { + MergedContextConfiguration parent1 = new MergedContextConfiguration(getClass(), new String[] { "foo", "bar}" }, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration parent2 = new MergedContextConfiguration(getClass(), new String[] { "baz", "quux" }, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent1); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent2); + assertNotEquals(mergedConfig1.hashCode(), mergedConfig2.hashCode()); + } + + @Test + public void equalsBasics() { + MergedContextConfiguration mergedConfig = new MergedContextConfiguration(null, null, null, null, null); + assertEquals(mergedConfig, mergedConfig); + assertNotEquals(mergedConfig, null); + assertNotEquals(mergedConfig, new Integer(1)); + } + + @Test + public void equalsWithNulls() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(null, null, null, null, null); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(null, null, null, null, null); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithNullArrays() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), null, null, null, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), null, null, null, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithEmptyArrays() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithEmptyArraysAndDifferentLoaders() { + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, new AnnotationConfigContextLoader()); + assertNotEquals(mergedConfig1, mergedConfig2); + assertNotEquals(mergedConfig2, mergedConfig1); + } + + @Test + public void equalsWithSameLocations() { + String[] locations = new String[] { "foo", "bar}" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), locations, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), locations, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithDifferentLocations() { + String[] locations1 = new String[] { "foo", "bar}" }; + String[] locations2 = new String[] { "baz", "quux}" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), locations1, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), locations2, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + assertNotEquals(mergedConfig1, mergedConfig2); + assertNotEquals(mergedConfig2, mergedConfig1); + } + + @Test + public void equalsWithSameConfigClasses() { + Class<?>[] classes = new Class<?>[] { String.class, Integer.class }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithDifferentConfigClasses() { + Class<?>[] classes1 = new Class<?>[] { String.class, Integer.class }; + Class<?>[] classes2 = new Class<?>[] { Boolean.class, Number.class }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes1, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + classes2, EMPTY_STRING_ARRAY, loader); + assertNotEquals(mergedConfig1, mergedConfig2); + assertNotEquals(mergedConfig2, mergedConfig1); + } + + @Test + public void equalsWithSameProfiles() { + String[] activeProfiles = new String[] { "catbert", "dogbert" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithSameProfilesReversed() { + String[] activeProfiles1 = new String[] { "catbert", "dogbert" }; + String[] activeProfiles2 = new String[] { "dogbert", "catbert" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles1, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles2, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithSameDuplicateProfiles() { + String[] activeProfiles1 = new String[] { "catbert", "dogbert" }; + String[] activeProfiles2 = new String[] { "catbert", "dogbert", "catbert", "dogbert", "catbert" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles1, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles2, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithDifferentProfiles() { + String[] activeProfiles1 = new String[] { "catbert", "dogbert" }; + String[] activeProfiles2 = new String[] { "X", "Y" }; + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles1, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, activeProfiles2, loader); + assertNotEquals(mergedConfig1, mergedConfig2); + assertNotEquals(mergedConfig2, mergedConfig1); + } + + @Test + public void equalsWithSameInitializers() { + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses1 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses1.add(FooInitializer.class); + initializerClasses1.add(BarInitializer.class); + + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses2 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses2.add(BarInitializer.class); + initializerClasses2.add(FooInitializer.class); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses1, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses2, EMPTY_STRING_ARRAY, loader); + assertEquals(mergedConfig1, mergedConfig2); + } + + @Test + public void equalsWithDifferentInitializers() { + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses1 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses1.add(FooInitializer.class); + + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses2 = // + new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>(); + initializerClasses2.add(BarInitializer.class); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses1, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, initializerClasses2, EMPTY_STRING_ARRAY, loader); + assertNotEquals(mergedConfig1, mergedConfig2); + assertNotEquals(mergedConfig2, mergedConfig1); + } + + /** + * @since 3.2.2 + */ + @Test + public void equalsWithSameParent() { + MergedContextConfiguration parent = new MergedContextConfiguration(getClass(), new String[] { "foo", "bar}" }, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent); + assertEquals(mergedConfig1, mergedConfig2); + assertEquals(mergedConfig2, mergedConfig1); + } + + /** + * @since 3.2.2 + */ + @Test + public void equalsWithDifferentParents() { + MergedContextConfiguration parent1 = new MergedContextConfiguration(getClass(), new String[] { "foo", "bar}" }, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + MergedContextConfiguration parent2 = new MergedContextConfiguration(getClass(), new String[] { "baz", "quux" }, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + + MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent1); + MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, loader, null, parent2); + assertNotEquals(mergedConfig1, mergedConfig2); + assertNotEquals(mergedConfig2, mergedConfig1); + } + + + private static class FooInitializer implements ApplicationContextInitializer<GenericApplicationContext> { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + } + } + + private static class BarInitializer implements ApplicationContextInitializer<GenericApplicationContext> { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java new file mode 100644 index 00000000..07f73831 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import static org.junit.Assert.*; + +import java.util.Comparator; +import java.util.List; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.SpringRunnerContextCacheTests.OrderedMethodsSpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; + +/** + * JUnit 4 based unit test which verifies correct {@link ContextCache + * application context caching} in conjunction with the + * {@link SpringJUnit4ClassRunner} and the {@link DirtiesContext + * @DirtiesContext} annotation at the method level. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ContextCacheTests + */ +@RunWith(OrderedMethodsSpringJUnit4ClassRunner.class) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) +@ContextConfiguration("junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml") +public class SpringRunnerContextCacheTests { + + private static ApplicationContext dirtiedApplicationContext; + + @Autowired + protected ApplicationContext applicationContext; + + + /** + * Asserts the statistics of the context cache in {@link TestContextManager}. + * + * @param usageScenario the scenario in which the statistics are used + * @param expectedSize the expected number of contexts in the cache + * @param expectedHitCount the expected hit count + * @param expectedMissCount the expected miss count + */ + private static final void assertContextCacheStatistics(String usageScenario, int expectedSize, + int expectedHitCount, int expectedMissCount) { + assertContextCacheStatistics(TestContextManager.contextCache, usageScenario, expectedSize, expectedHitCount, + expectedMissCount); + } + + /** + * Asserts the statistics of the supplied context cache. + * + * @param contextCache the cache to assert against + * @param usageScenario the scenario in which the statistics are used + * @param expectedSize the expected number of contexts in the cache + * @param expectedHitCount the expected hit count + * @param expectedMissCount the expected miss count + */ + public static final void assertContextCacheStatistics(ContextCache contextCache, String usageScenario, + int expectedSize, int expectedHitCount, int expectedMissCount) { + + assertEquals("Verifying number of contexts in cache (" + usageScenario + ").", expectedSize, + contextCache.size()); + assertEquals("Verifying number of cache hits (" + usageScenario + ").", expectedHitCount, + contextCache.getHitCount()); + assertEquals("Verifying number of cache misses (" + usageScenario + ").", expectedMissCount, + contextCache.getMissCount()); + } + + @BeforeClass + public static void verifyInitialCacheState() { + dirtiedApplicationContext = null; + ContextCache contextCache = TestContextManager.contextCache; + contextCache.clear(); + contextCache.clearStatistics(); + assertContextCacheStatistics("BeforeClass", 0, 0, 0); + } + + @AfterClass + public static void verifyFinalCacheState() { + assertContextCacheStatistics("AfterClass", 1, 1, 2); + } + + @Test + @DirtiesContext + public void dirtyContext() { + assertContextCacheStatistics("dirtyContext()", 1, 0, 1); + assertNotNull("The application context should have been autowired.", this.applicationContext); + SpringRunnerContextCacheTests.dirtiedApplicationContext = this.applicationContext; + } + + @Test + public void verifyContextDirty() { + assertContextCacheStatistics("verifyContextWasDirtied()", 1, 0, 2); + assertNotNull("The application context should have been autowired.", this.applicationContext); + assertNotSame("The application context should have been 'dirtied'.", + SpringRunnerContextCacheTests.dirtiedApplicationContext, this.applicationContext); + SpringRunnerContextCacheTests.dirtiedApplicationContext = this.applicationContext; + } + + @Test + public void verifyContextNotDirty() { + assertContextCacheStatistics("verifyContextWasNotDirtied()", 1, 1, 2); + assertNotNull("The application context should have been autowired.", this.applicationContext); + assertSame("The application context should NOT have been 'dirtied'.", + SpringRunnerContextCacheTests.dirtiedApplicationContext, this.applicationContext); + } + + + /** + * @since 3.2 + */ + public static class OrderedMethodsSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner { + + public OrderedMethodsSpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError { + super(clazz); + } + + @Override + protected List<FrameworkMethod> computeTestMethods() { + List<FrameworkMethod> testMethods = super.computeTestMethods(); + + java.util.Collections.sort(testMethods, new Comparator<FrameworkMethod>() { + + @Override + public int compare(FrameworkMethod method1, FrameworkMethod method2) { + return method1.getName().compareTo(method2.getName()); + } + }); + + return testMethods; + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/TestContextManagerTests.java b/spring-test/src/test/java/org/springframework/test/context/TestContextManagerTests.java new file mode 100644 index 00000000..97f303b8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextManagerTests.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.core.style.ToStringCreator; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * JUnit 4 based unit test for {@link TestContextManager}, which verifies proper + * <em>execution order</em> of registered {@link TestExecutionListener + * TestExecutionListeners}. + * + * @author Sam Brannen + * @since 2.5 + */ +public class TestContextManagerTests { + + private static final String FIRST = "veni"; + private static final String SECOND = "vidi"; + private static final String THIRD = "vici"; + + private static final List<String> afterTestMethodCalls = new ArrayList<String>(); + private static final List<String> beforeTestMethodCalls = new ArrayList<String>(); + + protected static final Log logger = LogFactory.getLog(TestContextManagerTests.class); + + private final TestContextManager testContextManager = new TestContextManager(ExampleTestCase.class); + + + private Method getTestMethod() throws NoSuchMethodException { + return ExampleTestCase.class.getDeclaredMethod("exampleTestMethod", (Class<?>[]) null); + } + + /** + * Asserts the <em>execution order</em> of 'before' and 'after' test method + * calls on {@link TestExecutionListener listeners} registered for the + * configured {@link TestContextManager}. + * + * @see #beforeTestMethodCalls + * @see #afterTestMethodCalls + */ + private static void assertExecutionOrder(List<String> expectedBeforeTestMethodCalls, + List<String> expectedAfterTestMethodCalls, final String usageContext) { + + if (expectedBeforeTestMethodCalls == null) { + expectedBeforeTestMethodCalls = new ArrayList<String>(); + } + if (expectedAfterTestMethodCalls == null) { + expectedAfterTestMethodCalls = new ArrayList<String>(); + } + + if (logger.isDebugEnabled()) { + for (String listenerName : beforeTestMethodCalls) { + logger.debug("'before' listener [" + listenerName + "] (" + usageContext + ")."); + } + for (String listenerName : afterTestMethodCalls) { + logger.debug("'after' listener [" + listenerName + "] (" + usageContext + ")."); + } + } + + assertTrue("Verifying execution order of 'before' listeners' (" + usageContext + ").", + expectedBeforeTestMethodCalls.equals(beforeTestMethodCalls)); + assertTrue("Verifying execution order of 'after' listeners' (" + usageContext + ").", + expectedAfterTestMethodCalls.equals(afterTestMethodCalls)); + } + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + beforeTestMethodCalls.clear(); + afterTestMethodCalls.clear(); + assertExecutionOrder(null, null, "BeforeClass"); + } + + /** + * Verifies the expected {@link TestExecutionListener} + * <em>execution order</em> after all test methods have completed. + */ + @AfterClass + public static void verifyListenerExecutionOrderAfterClass() throws Exception { + assertExecutionOrder(Arrays.<String> asList(FIRST, SECOND, THIRD), + Arrays.<String> asList(THIRD, SECOND, FIRST), "AfterClass"); + } + + @Before + public void setUpTestContextManager() throws Throwable { + assertEquals("Verifying the number of registered TestExecutionListeners.", 3, + this.testContextManager.getTestExecutionListeners().size()); + + this.testContextManager.beforeTestMethod(new ExampleTestCase(), getTestMethod()); + } + + /** + * Verifies the expected {@link TestExecutionListener} + * <em>execution order</em> within a test method. + * + * @see #verifyListenerExecutionOrderAfterClass() + */ + @Test + public void verifyListenerExecutionOrderWithinTestMethod() { + assertExecutionOrder(Arrays.<String> asList(FIRST, SECOND, THIRD), null, "Test"); + } + + @After + public void tearDownTestContextManager() throws Throwable { + this.testContextManager.afterTestMethod(new ExampleTestCase(), getTestMethod(), null); + } + + + @TestExecutionListeners({ FirstTel.class, SecondTel.class, ThirdTel.class }) + private static class ExampleTestCase { + + @SuppressWarnings("unused") + public void exampleTestMethod() { + assertTrue(true); + } + } + + private static class NamedTestExecutionListener extends AbstractTestExecutionListener { + + private final String name; + + + public NamedTestExecutionListener(final String name) { + this.name = name; + } + + @Override + public void beforeTestMethod(final TestContext testContext) { + beforeTestMethodCalls.add(this.name); + } + + @Override + public void afterTestMethod(final TestContext testContext) { + afterTestMethodCalls.add(this.name); + } + + @Override + public String toString() { + return new ToStringCreator(this).append("name", this.name).toString(); + } + } + + private static class FirstTel extends NamedTestExecutionListener { + + public FirstTel() { + super(FIRST); + } + } + + private static class SecondTel extends NamedTestExecutionListener { + + public SecondTel() { + super(SECOND); + } + } + + private static class ThirdTel extends NamedTestExecutionListener { + + public ThirdTel() { + super(THIRD); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java new file mode 100644 index 00000000..5596e6ea --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2012 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 + * + * http://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; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * <p> + * JUnit 4 based unit test for the {@link TestExecutionListeners + * @TestExecutionListeners} annotation, which verifies: + * </p> + * <ul> + * <li>Proper registering of {@link TestExecutionListener listeners} in + * conjunction with a {@link TestContextManager}</li> + * <li><em>Inherited</em> functionality proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a></li> + * </ul> + * + * @author Sam Brannen + * @since 2.5 + */ +public class TestExecutionListenersTests { + + @Test + public void verifyNumDefaultListenersRegistered() throws Exception { + TestContextManager testContextManager = new TestContextManager(DefaultListenersExampleTestCase.class); + assertEquals("Verifying the number of registered TestExecutionListeners for DefaultListenersExampleTest.", 4, + testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumNonInheritedDefaultListenersRegistered() throws Exception { + TestContextManager testContextManager = new TestContextManager( + NonInheritedDefaultListenersExampleTestCase.class); + assertEquals( + "Verifying the number of registered TestExecutionListeners for NonInheritedDefaultListenersExampleTest.", + 1, testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumInheritedDefaultListenersRegistered() throws Exception { + TestContextManager testContextManager = new TestContextManager(InheritedDefaultListenersExampleTestCase.class); + assertEquals( + "Verifying the number of registered TestExecutionListeners for InheritedDefaultListenersExampleTest.", 1, + testContextManager.getTestExecutionListeners().size()); + + testContextManager = new TestContextManager(SubInheritedDefaultListenersExampleTestCase.class); + assertEquals( + "Verifying the number of registered TestExecutionListeners for SubInheritedDefaultListenersExampleTest.", + 1, testContextManager.getTestExecutionListeners().size()); + + testContextManager = new TestContextManager(SubSubInheritedDefaultListenersExampleTestCase.class); + assertEquals( + "Verifying the number of registered TestExecutionListeners for SubSubInheritedDefaultListenersExampleTest.", + 2, testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumListenersRegistered() throws Exception { + TestContextManager testContextManager = new TestContextManager(ExampleTestCase.class); + assertEquals("Verifying the number of registered TestExecutionListeners for ExampleTest.", 3, + testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumNonInheritedListenersRegistered() throws Exception { + TestContextManager testContextManager = new TestContextManager(NonInheritedListenersExampleTestCase.class); + assertEquals("Verifying the number of registered TestExecutionListeners for NonInheritedListenersExampleTest.", + 1, testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumInheritedListenersRegistered() throws Exception { + TestContextManager testContextManager = new TestContextManager(InheritedListenersExampleTestCase.class); + assertEquals("Verifying the number of registered TestExecutionListeners for InheritedListenersExampleTest.", 4, + testContextManager.getTestExecutionListeners().size()); + } + + @Test(expected = IllegalStateException.class) + public void verifyDuplicateListenersConfigThrowsException() throws Exception { + new TestContextManager(DuplicateListenersConfigExampleTestCase.class); + } + + + static class DefaultListenersExampleTestCase { + } + + @TestExecutionListeners(QuuxTestExecutionListener.class) + static class InheritedDefaultListenersExampleTestCase extends DefaultListenersExampleTestCase { + } + + static class SubInheritedDefaultListenersExampleTestCase extends InheritedDefaultListenersExampleTestCase { + } + + @TestExecutionListeners(EnigmaTestExecutionListener.class) + static class SubSubInheritedDefaultListenersExampleTestCase extends SubInheritedDefaultListenersExampleTestCase { + } + + @TestExecutionListeners(listeners = { QuuxTestExecutionListener.class }, inheritListeners = false) + static class NonInheritedDefaultListenersExampleTestCase extends InheritedDefaultListenersExampleTestCase { + } + + @TestExecutionListeners( { FooTestExecutionListener.class, BarTestExecutionListener.class, + BazTestExecutionListener.class }) + static class ExampleTestCase { + } + + @TestExecutionListeners(QuuxTestExecutionListener.class) + static class InheritedListenersExampleTestCase extends ExampleTestCase { + } + + @TestExecutionListeners(listeners = QuuxTestExecutionListener.class, inheritListeners = false) + static class NonInheritedListenersExampleTestCase extends InheritedListenersExampleTestCase { + } + + @TestExecutionListeners(listeners = FooTestExecutionListener.class, value = BarTestExecutionListener.class) + static class DuplicateListenersConfigExampleTestCase { + } + + static class FooTestExecutionListener extends AbstractTestExecutionListener { + } + + static class BarTestExecutionListener extends AbstractTestExecutionListener { + } + + static class BazTestExecutionListener extends AbstractTestExecutionListener { + } + + static class QuuxTestExecutionListener extends AbstractTestExecutionListener { + } + + static class EnigmaTestExecutionListener extends AbstractTestExecutionListener { + } + +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests-context.properties b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests-context.properties new file mode 100644 index 00000000..45d36076 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests-context.properties @@ -0,0 +1,5 @@ +dog.(class)=org.springframework.tests.sample.beans.Pet +dog.$0=Fido + +testString2.(class)=java.lang.String +testString2.$0=Test String #2 diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests.java new file mode 100644 index 00000000..3a976747 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.configuration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.junit4.PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests; + +/** + * Integration tests which verify that the same custom {@link ContextLoader} can + * be used at all levels within a test class hierarchy when the + * {@code loader} is <i>inherited</i> (i.e., not explicitly declared) via + * {@link ContextConfiguration @ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.0 + * @see PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests + * @see ContextConfigurationWithPropertiesExtendingPropertiesTests + */ +@ContextConfiguration +public class ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests extends + PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests { + + @Autowired + private Pet dog; + + @Autowired + private String testString2; + + + @Test + public void verifyExtendedAnnotationAutowiredFields() { + assertNotNull("The dog field should have been autowired.", this.dog); + assertEquals("Fido", this.dog.getName()); + + assertNotNull("The testString2 field should have been autowired.", this.testString2); + assertEquals("Test String #2", this.testString2); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesTests-context.properties b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesTests-context.properties new file mode 100644 index 00000000..45d36076 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesTests-context.properties @@ -0,0 +1,5 @@ +dog.(class)=org.springframework.tests.sample.beans.Pet +dog.$0=Fido + +testString2.(class)=java.lang.String +testString2.$0=Test String #2 diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesTests.java b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesTests.java new file mode 100644 index 00000000..77f079ae --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/ContextConfigurationWithPropertiesExtendingPropertiesTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.configuration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.junit4.PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests; +import org.springframework.test.context.support.GenericPropertiesContextLoader; + +/** + * Integration tests which verify that the same custom {@link ContextLoader} can + * be used at all levels within a test class hierarchy when the + * {@code loader} is explicitly declared via {@link ContextConfiguration + * @ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.0 + * @see PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests + * @see ContextConfigurationWithPropertiesExtendingPropertiesAndInheritedLoaderTests + */ +@ContextConfiguration(loader = GenericPropertiesContextLoader.class) +public class ContextConfigurationWithPropertiesExtendingPropertiesTests extends + PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests { + + @Autowired + private Pet dog; + + @Autowired + private String testString2; + + + @Test + public void verifyExtendedAnnotationAutowiredFields() { + assertNotNull("The dog field should have been autowired.", this.dog); + assertEquals("Fido", this.dog.getName()); + + assertNotNull("The testString2 field should have been autowired.", this.testString2); + assertEquals("Test String #2", this.testString2); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/expression/ExpressionUsageTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/expression/ExpressionUsageTests-context.xml new file mode 100644 index 00000000..38def23c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/expression/ExpressionUsageTests-context.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="properties" + class="org.springframework.beans.factory.config.PropertiesFactoryBean"> + <property name="properties"> + <props> + <prop key="user.name">Dave</prop> + <prop key="username">Andy</prop> + </props> + </property> + </bean> + + <!-- spr5906 --> + + <bean id="derived" + class="org.springframework.beans.factory.config.PropertiesFactoryBean"> + <property name="properties"> + <props> + <prop key="user.name">#{properties['user.name']}</prop> + <prop key="username">#{properties['username']}</prop> + <prop key="username.no.quotes">#{properties[username]}</prop> + <prop key="username.no.brackets">#{properties.username}</prop> + <prop key="#{properties['user.name']}">exists</prop> + <prop key="#{properties.username}">exists also</prop> + </props> + </property> + </bean> + + <!-- spr5847 --> + + <bean id="andy" + class="org.springframework.test.context.expression.ExpressionUsageTests$Foo"> + <property name="name" value="#{properties.username}" /> + </bean> + + <bean id="andy2" + class="org.springframework.test.context.expression.ExpressionUsageTests$Foo"> + <property name="name" value="#{properties.username }" /><!-- space in expression --> + </bean> + +</beans>
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/expression/ExpressionUsageTests.java b/spring-test/src/test/java/org/springframework/test/context/expression/ExpressionUsageTests.java new file mode 100644 index 00000000..94d44e0e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/expression/ExpressionUsageTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.expression; + +import static org.junit.Assert.assertEquals; + +import java.util.Properties; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Andy Clement + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class ExpressionUsageTests { + + @Autowired + @Qualifier("derived") + private Properties props; + + @Autowired + @Qualifier("andy2") + private Foo andy2; + + @Autowired + @Qualifier("andy") + private Foo andy; + + + @Test + public void testSpr5906() throws Exception { + // verify the property values have been evaluated as expressions + assertEquals("Dave", props.getProperty("user.name")); + assertEquals("Andy", props.getProperty("username")); + + // verify the property keys have been evaluated as expressions + assertEquals("exists", props.getProperty("Dave")); + assertEquals("exists also", props.getProperty("Andy")); + } + + @Test + public void testSpr5847() throws Exception { + assertEquals("Andy", andy2.getName()); + assertEquals("Andy", andy.getName()); + } + + + public static class Foo { + + private String name; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithMergedConfigLevelOneTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithMergedConfigLevelOneTests.java new file mode 100644 index 00000000..716228c7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithMergedConfigLevelOneTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy({ +// + @ContextConfiguration(name = "parent", classes = ClassHierarchyWithMergedConfigLevelOneTests.AppConfig.class),// + @ContextConfiguration(name = "child", classes = ClassHierarchyWithMergedConfigLevelOneTests.UserConfig.class) // +}) +public class ClassHierarchyWithMergedConfigLevelOneTests { + + @Configuration + static class AppConfig { + + @Bean + public String parent() { + return "parent"; + } + } + + @Configuration + static class UserConfig { + + @Autowired + private AppConfig appConfig; + + + @Bean + public String user() { + return appConfig.parent() + " + user"; + } + + @Bean + public String beanFromUserConfig() { + return "from UserConfig"; + } + } + + + @Autowired + protected String parent; + + @Autowired + protected String user; + + @Autowired(required = false) + @Qualifier("beanFromUserConfig") + protected String beanFromUserConfig; + + @Autowired + protected ApplicationContext context; + + + @Test + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + assertEquals("parent", parent); + assertEquals("parent + user", user); + assertEquals("from UserConfig", beanFromUserConfig); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithMergedConfigLevelTwoTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithMergedConfigLevelTwoTests.java new file mode 100644 index 00000000..42a63413 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithMergedConfigLevelTwoTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration(name = "child", classes = ClassHierarchyWithMergedConfigLevelTwoTests.OrderConfig.class)) +public class ClassHierarchyWithMergedConfigLevelTwoTests extends ClassHierarchyWithMergedConfigLevelOneTests { + + @Configuration + static class OrderConfig { + + @Autowired + private ClassHierarchyWithMergedConfigLevelOneTests.UserConfig userConfig; + + @Bean + public String order() { + return userConfig.user() + " + order"; + } + } + + + @Autowired + private String order; + + + @Test + @Override + public void loadContextHierarchy() { + super.loadContextHierarchy(); + assertEquals("parent + user + order", order); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithOverriddenConfigLevelTwoTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithOverriddenConfigLevelTwoTests.java new file mode 100644 index 00000000..1f5f69a0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/ClassHierarchyWithOverriddenConfigLevelTwoTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration(name = "child", classes = ClassHierarchyWithOverriddenConfigLevelTwoTests.TestUserConfig.class, inheritLocations = false)) +public class ClassHierarchyWithOverriddenConfigLevelTwoTests extends ClassHierarchyWithMergedConfigLevelOneTests { + + @Configuration + static class TestUserConfig { + + @Autowired + private ClassHierarchyWithMergedConfigLevelOneTests.AppConfig appConfig; + + + @Bean + public String user() { + return appConfig.parent() + " + test user"; + } + + @Bean + public String beanFromTestUserConfig() { + return "from TestUserConfig"; + } + } + + + @Autowired + private String beanFromTestUserConfig; + + + @Test + @Override + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + assertEquals("parent", parent); + assertEquals("parent + test user", user); + assertEquals("from TestUserConfig", beanFromTestUserConfig); + assertNull("Bean from UserConfig should not be present.", beanFromUserConfig); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/DirtiesContextWithContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/DirtiesContextWithContextHierarchyTests.java new file mode 100644 index 00000000..95757b2c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/DirtiesContextWithContextHierarchyTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.*; + +/** + * Integration tests that verify support for {@link DirtiesContext.HierarchyMode} + * in conjunction with context hierarchies configured via {@link ContextHierarchy}. + * + * <p>Note that correct method execution order is essential, thus the use of + * {@link FixMethodOrder}. + * + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy({ @ContextConfiguration(classes = DirtiesContextWithContextHierarchyTests.ParentConfig.class), + @ContextConfiguration(classes = DirtiesContextWithContextHierarchyTests.ChildConfig.class) }) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class DirtiesContextWithContextHierarchyTests { + + @Configuration + static class ParentConfig { + + @Bean + public StringBuffer foo() { + return new StringBuffer("foo"); + } + + @Bean + public StringBuffer baz() { + return new StringBuffer("baz-parent"); + } + } + + @Configuration + static class ChildConfig { + + @Bean + public StringBuffer baz() { + return new StringBuffer("baz-child"); + } + } + + + @Autowired + private StringBuffer foo; + + @Autowired + private StringBuffer baz; + + @Autowired + private ApplicationContext context; + + + // ------------------------------------------------------------------------- + + private void reverseStringBuffers() { + foo.reverse(); + baz.reverse(); + } + + private void assertOriginalState() { + assertCleanParentContext(); + assertCleanChildContext(); + } + + private void assertCleanParentContext() { + assertEquals("foo", foo.toString()); + } + + private void assertCleanChildContext() { + assertEquals("baz-child", baz.toString()); + } + + private void assertDirtyParentContext() { + assertEquals("oof", foo.toString()); + } + + private void assertDirtyChildContext() { + assertEquals("dlihc-zab", baz.toString()); + } + + // ------------------------------------------------------------------------- + + @Before + public void verifyContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + } + + @Test + public void test1_verifyOriginalStateAndDirtyContexts() { + assertOriginalState(); + reverseStringBuffers(); + } + + @Test + @DirtiesContext + public void test2_verifyContextsWereDirtiedAndTriggerExhaustiveCacheClearing() { + assertDirtyParentContext(); + assertDirtyChildContext(); + } + + @Test + @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) + public void test3_verifyOriginalStateWasReinstatedAndDirtyContextsAndTriggerCurrentLevelCacheClearing() { + assertOriginalState(); + reverseStringBuffers(); + } + + @Test + public void test4_verifyParentContextIsStillDirtyButChildContextHasBeenReinstated() { + assertDirtyParentContext(); + assertCleanChildContext(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithSingleLevelContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithSingleLevelContextHierarchyTests.java new file mode 100644 index 00000000..b111f152 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithSingleLevelContextHierarchyTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration) +public class SingleTestClassWithSingleLevelContextHierarchyTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "foo"; + } + } + + + @Autowired + private String foo; + + @Autowired + private ApplicationContext context; + + + @Test + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNull("parent ApplicationContext", context.getParent()); + assertEquals("foo", foo); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests.java new file mode 100644 index 00000000..5591b915 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy({ + @ContextConfiguration(classes = SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests.ParentConfig.class), + @ContextConfiguration("SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests-ChildConfig.xml") }) +public class SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests { + + @Configuration + static class ParentConfig { + + @Bean + public String foo() { + return "foo"; + } + + @Bean + public String baz() { + return "baz-parent"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private String baz; + + @Autowired + private ApplicationContext context; + + + @Test + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + assertEquals("foo", foo); + assertEquals("bar", bar); + assertEquals("baz-child", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyTests.java new file mode 100644 index 00000000..0333e5fb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy({ + @ContextConfiguration(classes = SingleTestClassWithTwoLevelContextHierarchyTests.ParentConfig.class), + @ContextConfiguration(classes = SingleTestClassWithTwoLevelContextHierarchyTests.ChildConfig.class) }) +public class SingleTestClassWithTwoLevelContextHierarchyTests { + + @Configuration + static class ParentConfig { + + @Bean + public String foo() { + return "foo"; + } + + @Bean + public String baz() { + return "baz-parent"; + } + } + + @Configuration + static class ChildConfig { + + @Bean + public String bar() { + return "bar"; + } + + @Bean + public String baz() { + return "baz-child"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private String baz; + + @Autowired + private ApplicationContext context; + + + @Test + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + assertEquals("foo", foo); + assertEquals("bar", bar); + assertEquals("baz-child", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithBareContextConfigurationInSubclassTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithBareContextConfigurationInSubclassTests.java new file mode 100644 index 00000000..3dbf5794 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithBareContextConfigurationInSubclassTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration) +public class TestHierarchyLevelOneWithBareContextConfigurationInSubclassTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "foo-level-1"; + } + + @Bean + public String bar() { + return "bar"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private ApplicationContext context; + + + @Test + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNull("parent ApplicationContext", context.getParent()); + assertEquals("foo-level-1", foo); + assertEquals("bar", bar); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithBareContextConfigurationInSuperclassTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithBareContextConfigurationInSuperclassTests.java new file mode 100644 index 00000000..16b04c7f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithBareContextConfigurationInSuperclassTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class TestHierarchyLevelOneWithBareContextConfigurationInSuperclassTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "foo-level-1"; + } + + @Bean + public String bar() { + return "bar"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private ApplicationContext context; + + + @Test + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNull("parent ApplicationContext", context.getParent()); + assertEquals("foo-level-1", foo); + assertEquals("bar", bar); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithSingleLevelContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithSingleLevelContextHierarchyTests.java new file mode 100644 index 00000000..3e91f375 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelOneWithSingleLevelContextHierarchyTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration) +public class TestHierarchyLevelOneWithSingleLevelContextHierarchyTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "foo-level-1"; + } + + @Bean + public String bar() { + return "bar"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private ApplicationContext context; + + + @Test + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNull("parent ApplicationContext", context.getParent()); + assertEquals("foo-level-1", foo); + assertEquals("bar", bar); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithBareContextConfigurationInSubclassTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithBareContextConfigurationInSubclassTests.java new file mode 100644 index 00000000..2054f450 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithBareContextConfigurationInSubclassTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class TestHierarchyLevelTwoWithBareContextConfigurationInSubclassTests extends + TestHierarchyLevelOneWithBareContextConfigurationInSubclassTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "foo-level-2"; + } + + @Bean + public String baz() { + return "baz"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private String baz; + + @Autowired + private ApplicationContext context; + + + @Test + @Override + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + assertEquals("foo-level-2", foo); + assertEquals("bar", bar); + assertEquals("baz", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithBareContextConfigurationInSuperclassTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithBareContextConfigurationInSuperclassTests.java new file mode 100644 index 00000000..bc38d2bb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithBareContextConfigurationInSuperclassTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration) +public class TestHierarchyLevelTwoWithBareContextConfigurationInSuperclassTests extends + TestHierarchyLevelOneWithBareContextConfigurationInSuperclassTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "foo-level-2"; + } + + @Bean + public String baz() { + return "baz"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private String baz; + + @Autowired + private ApplicationContext context; + + + @Test + @Override + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + assertEquals("foo-level-2", foo); + assertEquals("bar", bar); + assertEquals("baz", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyAndMixedConfigTypesTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyAndMixedConfigTypesTests.java new file mode 100644 index 00000000..c6edd3af --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyAndMixedConfigTypesTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration) +public class TestHierarchyLevelTwoWithSingleLevelContextHierarchyAndMixedConfigTypesTests extends + TestHierarchyLevelOneWithSingleLevelContextHierarchyTests { + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private String baz; + + @Autowired + private ApplicationContext context; + + + @Test + @Override + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertNull("grandparent ApplicationContext", context.getParent().getParent()); + assertEquals("foo-level-2", foo); + assertEquals("bar", bar); + assertEquals("baz", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyTests.java new file mode 100644 index 00000000..edc8fb33 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.standard; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextHierarchy(@ContextConfiguration) +public class TestHierarchyLevelTwoWithSingleLevelContextHierarchyTests extends + TestHierarchyLevelOneWithSingleLevelContextHierarchyTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "foo-level-2"; + } + + @Bean + public String baz() { + return "baz"; + } + } + + + @Autowired + private String foo; + + @Autowired + private String bar; + + @Autowired + private String baz; + + @Autowired + private ApplicationContext context; + + + @Test + @Override + public void loadContextHierarchy() { + assertNotNull("child ApplicationContext", context); + assertNotNull("parent ApplicationContext", context.getParent()); + assertEquals("foo-level-2", foo); + assertEquals("bar", bar); + assertEquals("baz", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/ControllerIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/ControllerIntegrationTests.java new file mode 100644 index 00000000..a6282218 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/ControllerIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.web; + +import javax.servlet.ServletContext; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.hierarchies.web.ControllerIntegrationTests.AppConfig; +import org.springframework.test.context.hierarchies.web.ControllerIntegrationTests.WebConfig; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.Assert.*; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextHierarchy({ + // + @ContextConfiguration(name = "root", classes = AppConfig.class), + @ContextConfiguration(name = "dispatcher", classes = WebConfig.class) // +}) +public class ControllerIntegrationTests { + + @Configuration + static class AppConfig { + + @Bean + public String foo() { + return "foo"; + } + } + + @Configuration + static class WebConfig { + + @Bean + public String bar() { + return "bar"; + } + } + + + // ------------------------------------------------------------------------- + + @Autowired + private WebApplicationContext wac; + + @Autowired + private String foo; + + @Autowired + private String bar; + + + @Test + public void verifyRootWacSupport() { + assertEquals("foo", foo); + assertEquals("bar", bar); + + ApplicationContext parent = wac.getParent(); + assertNotNull(parent); + assertTrue(parent instanceof WebApplicationContext); + WebApplicationContext root = (WebApplicationContext) parent; + assertFalse(root.getBeansOfType(String.class).containsKey("bar")); + + ServletContext childServletContext = wac.getServletContext(); + assertNotNull(childServletContext); + ServletContext rootServletContext = root.getServletContext(); + assertNotNull(rootServletContext); + assertSame(childServletContext, rootServletContext); + + assertSame(root, rootServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)); + assertSame(root, childServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/DispatcherWacRootWacEarTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/DispatcherWacRootWacEarTests.java new file mode 100644 index 00000000..622547c4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/DispatcherWacRootWacEarTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.web; + +import javax.servlet.ServletContext; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.Assert.*; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@ContextHierarchy(@ContextConfiguration) +public class DispatcherWacRootWacEarTests extends RootWacEarTests { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private String ear; + + @Autowired + private String root; + + @Autowired + private String dispatcher; + + + @Test + @Override + public void verifyEarConfig() { + /* no-op */ + } + + @Test + @Override + public void verifyRootWacConfig() { + /* no-op */ + } + + @Test + public void verifyDispatcherWacConfig() { + ApplicationContext parent = wac.getParent(); + assertNotNull(parent); + assertTrue(parent instanceof WebApplicationContext); + + ApplicationContext grandParent = parent.getParent(); + assertNotNull(grandParent); + assertFalse(grandParent instanceof WebApplicationContext); + + ServletContext dispatcherServletContext = wac.getServletContext(); + assertNotNull(dispatcherServletContext); + ServletContext rootServletContext = ((WebApplicationContext) parent).getServletContext(); + assertNotNull(rootServletContext); + assertSame(dispatcherServletContext, rootServletContext); + + assertSame(parent, + rootServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)); + assertSame(parent, + dispatcherServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)); + + assertEquals("ear", ear); + assertEquals("root", root); + assertEquals("dispatcher", dispatcher); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/EarTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/EarTests.java new file mode 100644 index 00000000..8cd7eaf1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/EarTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.web; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.Assert.*; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class EarTests { + + @Configuration + static class EarConfig { + + @Bean + public String ear() { + return "ear"; + } + } + + + // ------------------------------------------------------------------------- + + @Autowired + private ApplicationContext context; + + @Autowired + private String ear; + + + @Test + public void verifyEarConfig() { + assertFalse(context instanceof WebApplicationContext); + assertNull(context.getParent()); + assertEquals("ear", ear); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/RootWacEarTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/RootWacEarTests.java new file mode 100644 index 00000000..dbd3e93a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/web/RootWacEarTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.hierarchies.web; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.Assert.*; + +/** + * @author Sam Brannen + * @since 3.2.2 + */ +@WebAppConfiguration +@ContextHierarchy(@ContextConfiguration) +public class RootWacEarTests extends EarTests { + + @Configuration + static class RootWacConfig { + + @Bean + public String root() { + return "root"; + } + } + + + // ------------------------------------------------------------------------- + + @Autowired + private WebApplicationContext wac; + + @Autowired + private String ear; + + @Autowired + private String root; + + + @Test + @Override + public void verifyEarConfig() { + /* no-op */ + } + + @Test + public void verifyRootWacConfig() { + ApplicationContext parent = wac.getParent(); + assertNotNull(parent); + assertFalse(parent instanceof WebApplicationContext); + assertEquals("ear", ear); + assertEquals("root", root); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit38/ConcreteTransactionalJUnit38SpringContextTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit38/ConcreteTransactionalJUnit38SpringContextTests-context.xml new file mode 100644 index 00000000..9f2fdef4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit38/ConcreteTransactionalJUnit38SpringContextTests-context.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Fido" /> + </bean> + + <bean id="foo" class="java.lang.String"> + <constructor-arg value="Foo" /> + </bean> + + <bean id="bar" class="java.lang.String"> + <constructor-arg value="Bar" /> + </bean> + + <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password="" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.ConcreteTransactionalJUnit4SpringContextTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit38/ConcreteTransactionalJUnit38SpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/junit38/ConcreteTransactionalJUnit38SpringContextTests.java new file mode 100644 index 00000000..735f96a0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit38/ConcreteTransactionalJUnit38SpringContextTests.java @@ -0,0 +1,231 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit38; + +import java.util.ArrayList; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.internal.runners.JUnit38ClassRunner; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.annotation.ExpectedException; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.annotation.Timed; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.test.jdbc.SimpleJdbcTestUtils; + +/** + * Combined integration test for {@link AbstractJUnit38SpringContextTests} and + * {@link AbstractTransactionalJUnit38SpringContextTests}. + * + * @author Sam Brannen + * @since 2.5 + */ +@SuppressWarnings("deprecation") +@RunWith(JUnit38ClassRunner.class) +@ContextConfiguration +public class ConcreteTransactionalJUnit38SpringContextTests extends AbstractTransactionalJUnit38SpringContextTests + implements BeanNameAware, InitializingBean { + + protected static final String BOB = "bob"; + protected static final String JANE = "jane"; + protected static final String SUE = "sue"; + protected static final String YODA = "yoda"; + + private boolean beanInitialized = false; + + private String beanName = "replace me with [" + getClass().getName() + "]"; + + private Employee employee; + + @Autowired + private Pet pet; + + @Autowired(required = false) + protected Long nonrequiredLong; + + @Resource() + protected String foo; + + protected String bar; + + private boolean inTransaction = false; + + + public ConcreteTransactionalJUnit38SpringContextTests() throws Exception { + this(null); + } + + public ConcreteTransactionalJUnit38SpringContextTests(final String name) throws Exception { + super(name); + } + + protected static int clearPersonTable(final SimpleJdbcTemplate simpleJdbcTemplate) { + return SimpleJdbcTestUtils.deleteFromTables(simpleJdbcTemplate, "person"); + } + + protected static void createPersonTable(final SimpleJdbcTemplate simpleJdbcTemplate) { + try { + simpleJdbcTemplate.update("CREATE TABLE person (name VARCHAR(20) NOT NULL, PRIMARY KEY(name))"); + } + catch (final BadSqlGrammarException bsge) { + /* ignore */ + } + } + + protected static int countRowsInPersonTable(final SimpleJdbcTemplate simpleJdbcTemplate) { + return SimpleJdbcTestUtils.countRowsInTable(simpleJdbcTemplate, "person"); + } + + protected static int addPerson(final SimpleJdbcTemplate simpleJdbcTemplate, final String name) { + return simpleJdbcTemplate.update("INSERT INTO person VALUES(?)", name); + } + + protected static int deletePerson(final SimpleJdbcTemplate simpleJdbcTemplate, final String name) { + return simpleJdbcTemplate.update("DELETE FROM person WHERE name=?", name); + } + + @Override + public final void afterPropertiesSet() throws Exception { + this.beanInitialized = true; + } + + @Override + public final void setBeanName(final String beanName) { + this.beanName = beanName; + } + + @Autowired + protected final void setEmployee(final Employee employee) { + this.employee = employee; + } + + @Resource + protected final void setBar(final String bar) { + this.bar = bar; + } + + @NotTransactional + @Timed(millis = 10000) + public void testNoOpShouldNotTimeOut() throws Exception { + /* no-op */ + } + + @NotTransactional + @ExpectedException(IndexOutOfBoundsException.class) + public void testExpectedExceptionAnnotation() { + new ArrayList<Object>().get(1); + } + + @NotTransactional + public void testApplicationContextSet() { + assertNotNull("The application context should have been set due to ApplicationContextAware semantics.", + super.applicationContext); + } + + @NotTransactional + public void testBeanInitialized() { + assertTrue("This test bean should have been initialized due to InitializingBean semantics.", + this.beanInitialized); + } + + @NotTransactional + public void testBeanNameSet() { + assertEquals("The bean name of this test instance should have been set to the fully qualified class name " + + "due to BeanNameAware semantics.", getClass().getName(), this.beanName); + } + + @NotTransactional + public void testAnnotationAutowiredFields() { + assertNull("The nonrequiredLong property should NOT have been autowired.", this.nonrequiredLong); + assertNotNull("The pet field should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } + + @NotTransactional + public void testAnnotationAutowiredMethods() { + assertNotNull("The employee setter method should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } + + @NotTransactional + public void testResourceAnnotationWiredFields() { + assertEquals("The foo field should have been wired via @Resource.", "Foo", this.foo); + } + + @NotTransactional + public void testResourceAnnotationWiredMethods() { + assertEquals("The bar method should have been wired via @Resource.", "Bar", this.bar); + } + + @BeforeTransaction + public void beforeTransaction() { + this.inTransaction = true; + assertEquals("Verifying the number of rows in the person table before a transactional test method.", 1, + countRowsInPersonTable(super.simpleJdbcTemplate)); + assertEquals("Adding yoda", 1, addPerson(super.simpleJdbcTemplate, YODA)); + } + + @Override + public void setUp() throws Exception { + assertEquals("Verifying the number of rows in the person table before a test method.", (this.inTransaction ? 2 + : 1), countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + public void testModifyTestDataWithinTransaction() { + assertEquals("Adding jane", 1, addPerson(super.simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(super.simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within transactionalMethod2().", 4, + countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + @Override + public void tearDown() throws Exception { + assertEquals("Verifying the number of rows in the person table after a test method.", (this.inTransaction ? 4 + : 1), countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals("Deleting yoda", 1, deletePerson(super.simpleJdbcTemplate, YODA)); + assertEquals("Verifying the number of rows in the person table after a transactional test method.", 1, + countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Autowired + void setDataSource(final DataSource dataSource) { + final SimpleJdbcTemplate simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + clearPersonTable(simpleJdbcTemplate); + addPerson(simpleJdbcTemplate, BOB); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit38/FailingBeforeAndAfterMethodsTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit38/FailingBeforeAndAfterMethodsTests-context.xml new file mode 100644 index 00000000..3c4fc976 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit38/FailingBeforeAndAfterMethodsTests-context.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password="" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit38/FailingBeforeAndAfterMethodsTests.java b/spring-test/src/test/java/org/springframework/test/context/junit38/FailingBeforeAndAfterMethodsTests.java new file mode 100644 index 00000000..192702ab --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit38/FailingBeforeAndAfterMethodsTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit38; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collection; + +import junit.framework.TestCase; +import junit.framework.TestResult; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; + +/** + * <p> + * JUnit 4 based integration test for verifying that '<em>before</em>' and '<em>after</em>' + * methods of {@link TestExecutionListener TestExecutionListeners} as well as + * {@link BeforeTransaction @BeforeTransaction} and + * {@link AfterTransaction @AfterTransaction} methods can fail a test in a JUnit + * 3.8 environment, as requested in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3960" + * target="_blank">SPR-3960</a>. + * </p> + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(Parameterized.class) +public class FailingBeforeAndAfterMethodsTests { + + protected final Class<?> clazz; + + + public FailingBeforeAndAfterMethodsTests(final Class<?> clazz) { + this.clazz = clazz; + } + + @Parameters + public static Collection<Object[]> testData() { + return Arrays.asList(new Object[][] { + + { AlwaysFailingBeforeTestMethodTestCase.class }, + + { AlwaysFailingAfterTestMethodTestCase.class }, + + { FailingBeforeTransactionalTestCase.class }, + + { FailingAfterTransactionalTestCase.class } + + }); + } + + @Test + public void runTestAndAssertCounters() throws Exception { + final String testName = "testNothing"; + final TestCase testCase = (TestCase) this.clazz.newInstance(); + testCase.setName(testName); + TestResult testResult = testCase.run(); + assertEquals("Verifying number of errors for test method [" + testName + "] and class [" + this.clazz + "].", + 0, testResult.errorCount()); + assertEquals("Verifying number of failures for test method [" + testName + "] and class [" + this.clazz + "].", + 1, testResult.failureCount()); + } + + + static class AlwaysFailingBeforeTestMethodTestExecutionListener extends AbstractTestExecutionListener { + + @Override + @SuppressWarnings("deprecation") + public void beforeTestMethod(TestContext testContext) { + junit.framework.Assert.fail("always failing beforeTestMethod()"); + } + } + + static class AlwaysFailingAfterTestMethodTestExecutionListener extends AbstractTestExecutionListener { + + @Override + @SuppressWarnings("deprecation") + public void afterTestMethod(TestContext testContext) { + junit.framework.Assert.fail("always failing afterTestMethod()"); + } + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @SuppressWarnings("deprecation") + @TestExecutionListeners(listeners = AlwaysFailingBeforeTestMethodTestExecutionListener.class, inheritListeners = false) + public static class AlwaysFailingBeforeTestMethodTestCase extends AbstractJUnit38SpringContextTests { + + public void testNothing() { + } + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @SuppressWarnings("deprecation") + @TestExecutionListeners(listeners = AlwaysFailingAfterTestMethodTestExecutionListener.class, inheritListeners = false) + public static class AlwaysFailingAfterTestMethodTestCase extends AbstractJUnit38SpringContextTests { + + public void testNothing() { + } + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @SuppressWarnings("deprecation") + @ContextConfiguration("FailingBeforeAndAfterMethodsTests-context.xml") + public static class FailingBeforeTransactionalTestCase extends AbstractTransactionalJUnit38SpringContextTests { + + public void testNothing() { + } + + @BeforeTransaction + public void beforeTransaction() { + fail("always failing beforeTransaction()"); + } + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @SuppressWarnings("deprecation") + @ContextConfiguration("FailingBeforeAndAfterMethodsTests-context.xml") + public static class FailingAfterTransactionalTestCase extends AbstractTransactionalJUnit38SpringContextTests { + + public void testNothing() { + } + + @AfterTransaction + public void afterTransaction() { + fail("always failing afterTransaction()"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit38/ProfileValueJUnit38SpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/junit38/ProfileValueJUnit38SpringContextTests.java new file mode 100644 index 00000000..6cecd0c2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit38/ProfileValueJUnit38SpringContextTests.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit38; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; +import junit.framework.TestResult; + +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.annotation.ProfileValueSource; +import org.springframework.test.annotation.ProfileValueSourceConfiguration; +import org.springframework.test.annotation.SystemProfileValueSource; +import org.springframework.test.context.TestExecutionListeners; + +/** + * Verifies proper handling of {@link IfProfileValue @IfProfileValue} and + * {@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration} + * in conjunction with {@link AbstractJUnit38SpringContextTests}. + * + * @author Sam Brannen + * @since 2.5 + */ +public class ProfileValueJUnit38SpringContextTests extends TestCase { + + private static final String EMPTY = "testIfProfileValueEmpty"; + private static final String DISABLED_VIA_WRONG_NAME = "testIfProfileValueDisabledViaWrongName"; + private static final String DISABLED_VIA_WRONG_VALUE = "testIfProfileValueDisabledViaWrongValue"; + private static final String ENABLED_VIA_MULTIPLE_VALUES = "testIfProfileValueEnabledViaMultipleValues"; + private static final String ENABLED_VIA_SINGLE_VALUE = "testIfProfileValueEnabledViaSingleValue"; + private static final String NOT_CONFIGURED = "testIfProfileValueNotConfigured"; + + private static final String NAME = "ProfileValueAnnotationAwareTransactionalTests.profile_value.name"; + private static final String VALUE = "enigma"; + + private final Map<String, Integer> expectedInvocationCounts = new HashMap<String, Integer>(); + + + public ProfileValueJUnit38SpringContextTests() { + System.setProperty(NAME, VALUE); + } + + @Override + protected void setUp() throws Exception { + this.expectedInvocationCounts.put(EMPTY, 0); + this.expectedInvocationCounts.put(DISABLED_VIA_WRONG_NAME, 0); + this.expectedInvocationCounts.put(DISABLED_VIA_WRONG_VALUE, 0); + this.expectedInvocationCounts.put(ENABLED_VIA_SINGLE_VALUE, 1); + this.expectedInvocationCounts.put(ENABLED_VIA_MULTIPLE_VALUES, 1); + this.expectedInvocationCounts.put(NOT_CONFIGURED, 1); + } + + private void configureDisabledClassExpectations() { + this.expectedInvocationCounts.put(ENABLED_VIA_SINGLE_VALUE, 0); + this.expectedInvocationCounts.put(ENABLED_VIA_MULTIPLE_VALUES, 0); + this.expectedInvocationCounts.put(NOT_CONFIGURED, 0); + } + + private void runTestAndAssertCounters(Class<? extends DefaultProfileValueSourceTestCase> testCaseType, + String testName, int expectedInvocationCount, int expectedErrorCount, int expectedFailureCount) + throws Exception { + + DefaultProfileValueSourceTestCase testCase = testCaseType.newInstance(); + testCase.setName(testName); + TestResult testResult = testCase.run(); + assertEquals("Verifying number of invocations for test method [" + testName + "].", expectedInvocationCount, + testCase.invocationCount); + assertEquals("Verifying number of errors for test method [" + testName + "].", expectedErrorCount, + testResult.errorCount()); + assertEquals("Verifying number of failures for test method [" + testName + "].", expectedFailureCount, + testResult.failureCount()); + } + + private void runTests(final Class<? extends DefaultProfileValueSourceTestCase> testCaseType) throws Exception { + runTestAndAssertCounters(testCaseType, EMPTY, expectedInvocationCounts.get(EMPTY), 0, 0); + runTestAndAssertCounters(testCaseType, DISABLED_VIA_WRONG_NAME, + expectedInvocationCounts.get(DISABLED_VIA_WRONG_NAME), 0, 0); + runTestAndAssertCounters(testCaseType, DISABLED_VIA_WRONG_VALUE, + expectedInvocationCounts.get(DISABLED_VIA_WRONG_VALUE), 0, 0); + runTestAndAssertCounters(testCaseType, ENABLED_VIA_SINGLE_VALUE, + expectedInvocationCounts.get(ENABLED_VIA_SINGLE_VALUE), 0, 0); + runTestAndAssertCounters(testCaseType, ENABLED_VIA_MULTIPLE_VALUES, + expectedInvocationCounts.get(ENABLED_VIA_MULTIPLE_VALUES), 0, 0); + runTestAndAssertCounters(testCaseType, NOT_CONFIGURED, expectedInvocationCounts.get(NOT_CONFIGURED), 0, 0); + } + + public void testDefaultProfileValueSource() throws Exception { + assertEquals("Verifying the type of the configured ProfileValueSource.", SystemProfileValueSource.class, + new DefaultProfileValueSourceTestCase().getProfileValueSource().getClass()); + runTests(DefaultProfileValueSourceTestCase.class); + } + + public void testHardCodedProfileValueSource() throws Exception { + assertEquals("Verifying the type of the configured ProfileValueSource.", HardCodedProfileValueSource.class, + new HardCodedProfileValueSourceTestCase().getProfileValueSource().getClass()); + runTests(HardCodedProfileValueSourceTestCase.class); + } + + public void testClassLevelIfProfileValueEnabledSingleValue() throws Exception { + runTests(ClassLevelIfProfileValueEnabledSingleValueTestCase.class); + } + + public void testClassLevelIfProfileValueDisabledSingleValue() throws Exception { + configureDisabledClassExpectations(); + runTests(ClassLevelIfProfileValueDisabledSingleValueTestCase.class); + } + + public void testClassLevelIfProfileValueEnabledMultiValue() throws Exception { + runTests(ClassLevelIfProfileValueEnabledMultiValueTestCase.class); + } + + public void testClassLevelIfProfileValueDisabledMultiValue() throws Exception { + configureDisabledClassExpectations(); + runTests(ClassLevelIfProfileValueDisabledMultiValueTestCase.class); + } + + + // ------------------------------------------------------------------- + + /** + * Note that {@link TestExecutionListeners @TestExecutionListeners} is + * explicitly configured with an empty list, thus disabling all default + * listeners. + */ + @SuppressWarnings("deprecation") + @TestExecutionListeners(listeners = {}, inheritListeners = false) + public static class DefaultProfileValueSourceTestCase extends AbstractJUnit38SpringContextTests { + + int invocationCount = 0; + + + public ProfileValueSource getProfileValueSource() { + return super.profileValueSource; + } + + @IfProfileValue(name = NAME, value = "") + public void testIfProfileValueEmpty() { + this.invocationCount++; + fail("An empty profile value should throw an IllegalArgumentException."); + } + + @IfProfileValue(name = NAME + "X", value = VALUE) + public void testIfProfileValueDisabledViaWrongName() { + this.invocationCount++; + fail("The body of a disabled test should never be executed!"); + } + + @IfProfileValue(name = NAME, value = VALUE + "X") + public void testIfProfileValueDisabledViaWrongValue() { + this.invocationCount++; + fail("The body of a disabled test should never be executed!"); + } + + @IfProfileValue(name = NAME, value = VALUE) + public void testIfProfileValueEnabledViaSingleValue() { + this.invocationCount++; + } + + @IfProfileValue(name = NAME, values = { "foo", VALUE, "bar" }) + public void testIfProfileValueEnabledViaMultipleValues() { + this.invocationCount++; + } + + public void testIfProfileValueNotConfigured() { + this.invocationCount++; + } + } + + @ProfileValueSourceConfiguration(HardCodedProfileValueSource.class) + public static class HardCodedProfileValueSourceTestCase extends DefaultProfileValueSourceTestCase { + } + + public static class HardCodedProfileValueSource implements ProfileValueSource { + + @Override + public String get(final String key) { + return (key.equals(NAME) ? VALUE : null); + } + } + + @IfProfileValue(name = NAME, value = VALUE) + public static class ClassLevelIfProfileValueEnabledSingleValueTestCase extends DefaultProfileValueSourceTestCase { + } + + @IfProfileValue(name = NAME, value = VALUE + "X") + public static class ClassLevelIfProfileValueDisabledSingleValueTestCase extends DefaultProfileValueSourceTestCase { + } + + @IfProfileValue(name = NAME, values = { "foo", VALUE, "bar" }) + public static class ClassLevelIfProfileValueEnabledMultiValueTestCase extends DefaultProfileValueSourceTestCase { + } + + @IfProfileValue(name = NAME, values = { "foo", "bar", "baz" }) + public static class ClassLevelIfProfileValueDisabledMultiValueTestCase extends DefaultProfileValueSourceTestCase { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit38/RepeatedJUnit38SpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/junit38/RepeatedJUnit38SpringContextTests.java new file mode 100644 index 00000000..e33f290a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit38/RepeatedJUnit38SpringContextTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit38; + +import junit.framework.TestCase; + +import org.springframework.test.annotation.Repeat; +import org.springframework.test.context.TestExecutionListeners; + +/** + * Unit test for {@link AbstractJUnit38SpringContextTests} which focuses on + * proper support of the {@link Repeat @Repeat} annotation. + * + * @author Sam Brannen + * @since 2.5 + */ +public class RepeatedJUnit38SpringContextTests extends TestCase { + + public RepeatedJUnit38SpringContextTests() throws Exception { + super(); + } + + public RepeatedJUnit38SpringContextTests(final String name) throws Exception { + super(name); + } + + private void assertRepetitions(final String testName, final int expectedNumInvocations) throws Exception { + final RepeatedTestCase repeatedTestCase = new RepeatedTestCase(testName); + repeatedTestCase.run(); + assertEquals("Verifying number of invocations for test method [" + testName + "].", expectedNumInvocations, + repeatedTestCase.invocationCount); + } + + public void testRepeatAnnotationSupport() throws Exception { + assertRepetitions("testNonAnnotated", 1); + assertRepetitions("testNegativeRepeatValue", 1); + assertRepetitions("testDefaultRepeatValue", 1); + assertRepetitions("testRepeatedFiveTimes", 5); + } + + + /** + * Note that {@link TestExecutionListeners @TestExecutionListeners} is + * explicitly configured with an empty list, thus disabling all default + * listeners. + */ + @SuppressWarnings("deprecation") + @TestExecutionListeners(listeners = {}, inheritListeners = false) + public static class RepeatedTestCase extends AbstractJUnit38SpringContextTests { + + int invocationCount = 0; + + public RepeatedTestCase(final String name) throws Exception { + super(name); + } + + @Override + protected void setUp() throws Exception { + this.invocationCount++; + } + + public void testNonAnnotated() { + /* no-op */ + } + + @Repeat(-5) + public void testNegativeRepeatValue() { + /* no-op */ + } + + @Repeat + public void testDefaultRepeatValue() { + /* no-op */ + } + + @Repeat(5) + public void testRepeatedFiveTimes() { + /* no-op */ + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..0e53eef4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; + +/** + * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * we can specify an explicit, <em>absolute path</em> location for our + * application context. + * + * @author Sam Brannen + * @since 2.5 + * @see SpringJUnit4ClassRunnerAppCtxTests + * @see ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests + * @see RelativePathSpringJUnit4ClassRunnerAppCtxTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH }, inheritLocations = false) +public class AbsolutePathSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { + /* all tests are in the parent class. */ +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/AbstractTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/AbstractTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..1469cc35 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/AbstractTransactionalSpringRunnerTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; + +/** + * Abstract base class for verifying support of Spring's {@link Transactional + * @Transactional} and {@link NotTransactional @NotTransactional} + * annotations. + * + * @author Sam Brannen + * @since 2.5 + * @see ClassLevelTransactionalSpringRunnerTests + * @see MethodLevelTransactionalSpringRunnerTests + * @see Transactional + * @see NotTransactional + */ +@SuppressWarnings("deprecation") +@ContextConfiguration("transactionalTests-context.xml") +public abstract class AbstractTransactionalSpringRunnerTests { + + protected static final String BOB = "bob"; + protected static final String JANE = "jane"; + protected static final String SUE = "sue"; + protected static final String LUKE = "luke"; + protected static final String LEIA = "leia"; + protected static final String YODA = "yoda"; + + + protected static int clearPersonTable(SimpleJdbcTemplate simpleJdbcTemplate) { + return simpleJdbcTemplate.update("DELETE FROM person"); + } + + protected static void createPersonTable(SimpleJdbcTemplate simpleJdbcTemplate) { + try { + simpleJdbcTemplate.update("CREATE TABLE person (name VARCHAR(20) NOT NULL, PRIMARY KEY(name))"); + } + catch (DataAccessException dae) { + // ignore + } + } + + protected static int countRowsInPersonTable(SimpleJdbcTemplate simpleJdbcTemplate) { + return simpleJdbcTemplate.queryForInt("SELECT COUNT(0) FROM person"); + } + + protected static int addPerson(SimpleJdbcTemplate simpleJdbcTemplate, String name) { + return simpleJdbcTemplate.update("INSERT INTO person VALUES(?)", name); + } + + protected static int deletePerson(SimpleJdbcTemplate simpleJdbcTemplate, String name) { + return simpleJdbcTemplate.update("DELETE FROM person WHERE name=?", name); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/BeforeAndAfterTransactionAnnotationTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/BeforeAndAfterTransactionAnnotationTests-context.xml new file mode 100644 index 00000000..666f357b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/BeforeAndAfterTransactionAnnotationTests-context.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.BeforeAndAfterTransactionAnnotationTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/BeforeAndAfterTransactionAnnotationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/BeforeAndAfterTransactionAnnotationTests.java new file mode 100644 index 00000000..7dc2af2c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/BeforeAndAfterTransactionAnnotationTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2008 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.Transactional; + +/** + * JUnit 4 based integration test which verifies + * {@link BeforeTransaction @BeforeTransaction} and + * {@link AfterTransaction @AfterTransaction} behavior. + * + * @author Sam Brannen + * @since 2.5 + */ +@SuppressWarnings("deprecation") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@TestExecutionListeners({ TransactionalTestExecutionListener.class }) +public class BeforeAndAfterTransactionAnnotationTests extends AbstractTransactionalSpringRunnerTests { + + protected static SimpleJdbcTemplate simpleJdbcTemplate; + + protected static int numBeforeTransactionCalls = 0; + protected static int numAfterTransactionCalls = 0; + + protected boolean inTransaction = false; + + + @BeforeClass + public static void beforeClass() { + BeforeAndAfterTransactionAnnotationTests.numBeforeTransactionCalls = 0; + BeforeAndAfterTransactionAnnotationTests.numAfterTransactionCalls = 0; + } + + @AfterClass + public static void afterClass() { + assertEquals("Verifying the final number of rows in the person table after all tests.", 3, + countRowsInPersonTable(simpleJdbcTemplate)); + assertEquals("Verifying the total number of calls to beforeTransaction().", 2, + BeforeAndAfterTransactionAnnotationTests.numBeforeTransactionCalls); + assertEquals("Verifying the total number of calls to afterTransaction().", 2, + BeforeAndAfterTransactionAnnotationTests.numAfterTransactionCalls); + } + + @BeforeTransaction + public void beforeTransaction() { + assertInTransaction(false); + this.inTransaction = true; + BeforeAndAfterTransactionAnnotationTests.numBeforeTransactionCalls++; + clearPersonTable(simpleJdbcTemplate); + assertEquals("Adding yoda", 1, addPerson(simpleJdbcTemplate, YODA)); + } + + @AfterTransaction + public void afterTransaction() { + assertInTransaction(false); + this.inTransaction = false; + BeforeAndAfterTransactionAnnotationTests.numAfterTransactionCalls++; + assertEquals("Deleting yoda", 1, deletePerson(simpleJdbcTemplate, YODA)); + assertEquals("Verifying the number of rows in the person table after a transactional test method.", 0, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Before + public void before() { + assertEquals("Verifying the number of rows in the person table before a test method.", (this.inTransaction ? 1 + : 0), countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + @Transactional + public void transactionalMethod1() { + assertInTransaction(true); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Verifying the number of rows in the person table within transactionalMethod1().", 2, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + @Transactional + public void transactionalMethod2() { + assertInTransaction(true); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within transactionalMethod2().", 3, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + public void nonTransactionalMethod() { + assertInTransaction(false); + assertEquals("Adding luke", 1, addPerson(simpleJdbcTemplate, LUKE)); + assertEquals("Adding leia", 1, addPerson(simpleJdbcTemplate, LEIA)); + assertEquals("Adding yoda", 1, addPerson(simpleJdbcTemplate, YODA)); + assertEquals("Verifying the number of rows in the person table without a transaction.", 3, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + void setDataSource(DataSource dataSource) { + simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelDisabledSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelDisabledSpringRunnerTests.java new file mode 100644 index 00000000..7ae72d58 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelDisabledSpringRunnerTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; + +/** + * @author Juergen Hoeller + * @author Sam Brannen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners(ClassLevelDisabledSpringRunnerTests.CustomTestExecutionListener.class) +@IfProfileValue(name = "ClassLevelDisabledSpringRunnerTests.profile_value.name", value = "enigmaX") +public class ClassLevelDisabledSpringRunnerTests { + + @Test + public void testIfProfileValueDisabled() { + fail("The body of a disabled test should never be executed!"); + } + + + public static class CustomTestExecutionListener implements TestExecutionListener { + + @Override + public void beforeTestClass(TestContext testContext) throws Exception { + fail("A listener method for a disabled test should never be executed!"); + } + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + fail("A listener method for a disabled test should never be executed!"); + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + fail("A listener method for a disabled test should never be executed!"); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + fail("A listener method for a disabled test should never be executed!"); + } + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + fail("A listener method for a disabled test should never be executed!"); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelTransactionalSpringRunnerTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelTransactionalSpringRunnerTests-context.xml new file mode 100644 index 00000000..9df7964a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelTransactionalSpringRunnerTests-context.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.ClassLevelTransactionalSpringRunnerTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..02c0dcf0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassLevelTransactionalSpringRunnerTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.Transactional; + +/** + * <p> + * JUnit 4 based integration test which verifies support of Spring's + * {@link Transactional @Transactional}, {@link NotTransactional + * @NotTransactional}, {@link TestExecutionListeners + * @TestExecutionListeners}, and {@link ContextConfiguration + * @ContextConfiguration} annotations in conjunction with the + * {@link SpringJUnit4ClassRunner} and the following + * {@link TestExecutionListener TestExecutionListeners}: + * </p> + * <ul> + * <li>{@link DependencyInjectionTestExecutionListener}</li> + * <li>{@link DirtiesContextTestExecutionListener}</li> + * <li>{@link TransactionalTestExecutionListener}</li> + * </ul> + * <p> + * This class specifically tests usage of {@code @Transactional} + * defined at the <strong>class level</strong>. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see MethodLevelTransactionalSpringRunnerTests + */ +@SuppressWarnings("deprecation") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class ClassLevelTransactionalSpringRunnerTests extends AbstractTransactionalSpringRunnerTests { + + protected static SimpleJdbcTemplate simpleJdbcTemplate; + + + @AfterClass + public static void verifyFinalTestData() { + assertEquals("Verifying the final number of rows in the person table after all tests.", 4, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Before + public void verifyInitialTestData() { + clearPersonTable(simpleJdbcTemplate); + assertEquals("Adding bob", 1, addPerson(simpleJdbcTemplate, BOB)); + assertEquals("Verifying the initial number of rows in the person table.", 1, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertEquals("Deleting bob", 1, deletePerson(simpleJdbcTemplate, BOB)); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within a transaction.", 2, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + @NotTransactional + public void modifyTestDataWithoutTransaction() { + assertInTransaction(false); + assertEquals("Adding luke", 1, addPerson(simpleJdbcTemplate, LUKE)); + assertEquals("Adding leia", 1, addPerson(simpleJdbcTemplate, LEIA)); + assertEquals("Adding yoda", 1, addPerson(simpleJdbcTemplate, YODA)); + assertEquals("Verifying the number of rows in the person table without a transaction.", 4, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + public void setDataSource(DataSource dataSource) { + simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..4bb3fb5b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import org.junit.runner.RunWith; + +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.ResourceUtils; + +/** + * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * we can specify an explicit, <em>classpath</em> location for our application + * context. + * + * @author Sam Brannen + * @since 2.5 + * @see SpringJUnit4ClassRunnerAppCtxTests + * @see #CLASSPATH_CONTEXT_RESOURCE_PATH + * @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests + * @see RelativePathSpringJUnit4ClassRunnerAppCtxTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }) +public class ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { + + /** + * Classpath-based resource path for the application context configuration + * for {@link SpringJUnit4ClassRunnerAppCtxTests}: + * {@code "classpath:/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml"} + * + * @see SpringJUnit4ClassRunnerAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH + * @see ResourceUtils#CLASSPATH_URL_PREFIX + */ + public static final String CLASSPATH_CONTEXT_RESOURCE_PATH = ResourceUtils.CLASSPATH_URL_PREFIX + + SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH; + + /* all tests are in the parent class. */ +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ConcreteTransactionalJUnit4SpringContextTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/ConcreteTransactionalJUnit4SpringContextTests-context.xml new file mode 100644 index 00000000..a9117841 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ConcreteTransactionalJUnit4SpringContextTests-context.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <import resource="transactionalTests-context.xml" /> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.ConcreteTransactionalJUnit4SpringContextTests$DatabaseSetup" /> + + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Fido" /> + </bean> + + <bean id="foo" class="java.lang.String"> + <constructor-arg value="Foo" /> + </bean> + + <bean id="bar" class="java.lang.String"> + <constructor-arg value="Bar" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ConcreteTransactionalJUnit4SpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/ConcreteTransactionalJUnit4SpringContextTests.java new file mode 100644 index 00000000..bd7ce811 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ConcreteTransactionalJUnit4SpringContextTests.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.test.jdbc.SimpleJdbcTestUtils; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; + +import static org.junit.Assert.*; +import static org.springframework.test.transaction.TransactionTestUtils.*; + +/** + * Combined integration test for {@link AbstractJUnit4SpringContextTests} and + * {@link AbstractTransactionalJUnit4SpringContextTests}. + * + * @author Sam Brannen + * @since 2.5 + */ +@SuppressWarnings("deprecation") +@ContextConfiguration +public class ConcreteTransactionalJUnit4SpringContextTests extends AbstractTransactionalJUnit4SpringContextTests + implements BeanNameAware, InitializingBean { + + protected static final String BOB = "bob"; + protected static final String JANE = "jane"; + protected static final String SUE = "sue"; + protected static final String LUKE = "luke"; + protected static final String LEIA = "leia"; + protected static final String YODA = "yoda"; + + private boolean beanInitialized = false; + + private String beanName = "replace me with [" + getClass().getName() + "]"; + + private Employee employee; + + @Autowired + private Pet pet; + + @Autowired(required = false) + protected Long nonrequiredLong; + + @Resource + protected String foo; + + protected String bar; + + + protected static int clearPersonTable(final SimpleJdbcTemplate simpleJdbcTemplate) { + return SimpleJdbcTestUtils.deleteFromTables(simpleJdbcTemplate, "person"); + } + + protected static void createPersonTable(final SimpleJdbcTemplate simpleJdbcTemplate) { + try { + simpleJdbcTemplate.update("CREATE TABLE person (name VARCHAR(20) NOT NULL, PRIMARY KEY(name))"); + } + catch (DataAccessException dae) { + /* ignore */ + } + } + + protected static int countRowsInPersonTable(final SimpleJdbcTemplate simpleJdbcTemplate) { + return SimpleJdbcTestUtils.countRowsInTable(simpleJdbcTemplate, "person"); + } + + protected static int addPerson(final SimpleJdbcTemplate simpleJdbcTemplate, final String name) { + return simpleJdbcTemplate.update("INSERT INTO person VALUES(?)", name); + } + + protected static int deletePerson(final SimpleJdbcTemplate simpleJdbcTemplate, final String name) { + return simpleJdbcTemplate.update("DELETE FROM person WHERE name=?", name); + } + + @Override + @Resource + public void setDataSource(DataSource dataSource) { + super.setDataSource(dataSource); + } + + @Autowired + protected final void setEmployee(final Employee employee) { + this.employee = employee; + } + + @Resource + protected final void setBar(final String bar) { + this.bar = bar; + } + + @Override + public final void setBeanName(final String beanName) { + this.beanName = beanName; + } + + @Override + public final void afterPropertiesSet() throws Exception { + this.beanInitialized = true; + } + + @Test + @NotTransactional + public final void verifyApplicationContext() { + assertInTransaction(false); + assertNotNull("The application context should have been set due to ApplicationContextAware semantics.", + super.applicationContext); + } + + @Test + @NotTransactional + public final void verifyBeanInitialized() { + assertInTransaction(false); + assertTrue("This test bean should have been initialized due to InitializingBean semantics.", + this.beanInitialized); + } + + @Test + @NotTransactional + public final void verifyBeanNameSet() { + assertInTransaction(false); + assertEquals("The bean name of this test instance should have been set to the fully qualified class name " + + "due to BeanNameAware semantics.", getClass().getName(), this.beanName); + } + + @Test + @NotTransactional + public final void verifyAnnotationAutowiredFields() { + assertInTransaction(false); + assertNull("The nonrequiredLong property should NOT have been autowired.", this.nonrequiredLong); + assertNotNull("The pet field should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } + + @Test + @NotTransactional + public final void verifyAnnotationAutowiredMethods() { + assertInTransaction(false); + assertNotNull("The employee setter method should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } + + @Test + @NotTransactional + public final void verifyResourceAnnotationWiredFields() { + assertInTransaction(false); + assertEquals("The foo field should have been wired via @Resource.", "Foo", this.foo); + } + + @Test + @NotTransactional + public final void verifyResourceAnnotationWiredMethods() { + assertInTransaction(false); + assertEquals("The bar method should have been wired via @Resource.", "Bar", this.bar); + } + + @BeforeTransaction + public void beforeTransaction() { + assertEquals("Verifying the number of rows in the person table before a transactional test method.", 1, + countRowsInPersonTable(super.simpleJdbcTemplate)); + assertEquals("Adding yoda", 1, addPerson(super.simpleJdbcTemplate, YODA)); + } + + @Before + public void setUp() throws Exception { + assertEquals("Verifying the number of rows in the person table before a test method.", + (inTransaction() ? 2 : 1), countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + @Test + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertEquals("Adding jane", 1, addPerson(super.simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(super.simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table in modifyTestDataWithinTransaction().", 4, + countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + @After + public void tearDown() throws Exception { + assertEquals("Verifying the number of rows in the person table after a test method.", + (inTransaction() ? 4 : 1), countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals("Deleting yoda", 1, deletePerson(super.simpleJdbcTemplate, YODA)); + assertEquals("Verifying the number of rows in the person table after a transactional test method.", 1, + countRowsInPersonTable(super.simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + public void setDataSource(DataSource dataSource) { + SimpleJdbcTemplate simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + clearPersonTable(simpleJdbcTemplate); + addPerson(simpleJdbcTemplate, BOB); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/CustomDefaultContextLoaderClassSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/CustomDefaultContextLoaderClassSpringRunnerTests.java new file mode 100644 index 00000000..3ee77974 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/CustomDefaultContextLoaderClassSpringRunnerTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.model.InitializationError; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.support.GenericPropertiesContextLoader; + +/** + * Integration tests which verify that a subclass of {@link SpringJUnit4ClassRunner} + * can specify a custom <em>default ContextLoader class name</em> that overrides + * the standard default class name. + * + * @author Sam Brannen + * @since 3.0 + */ +@RunWith(CustomDefaultContextLoaderClassSpringRunnerTests.PropertiesBasedSpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = "PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests-context.properties") +public class CustomDefaultContextLoaderClassSpringRunnerTests { + + @Autowired + private Pet cat; + + @Autowired + private String testString; + + + @Test + public void verifyAnnotationAutowiredFields() { + assertNotNull("The cat field should have been autowired.", this.cat); + assertEquals("Garfield", this.cat.getName()); + + assertNotNull("The testString field should have been autowired.", this.testString); + assertEquals("Test String", this.testString); + } + + + public static final class PropertiesBasedSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner { + + public PropertiesBasedSpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError { + super(clazz); + } + + @Override + protected String getDefaultContextLoaderClassName(Class<?> clazz) { + return GenericPropertiesContextLoader.class.getName(); + } + + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackFalseTransactionalSpringRunnerTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackFalseTransactionalSpringRunnerTests-context.xml new file mode 100644 index 00000000..d9663d7c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackFalseTransactionalSpringRunnerTests-context.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password="" /> + + <bean id="txMgr" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.DefaultRollbackFalseTransactionalSpringRunnerTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackFalseTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackFalseTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..08f91b53 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackFalseTransactionalSpringRunnerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.transaction.annotation.Transactional; + +/** + * <p> + * JUnit 4 based integration test which verifies proper transactional behavior when the + * {@link TransactionConfiguration#defaultRollback() defaultRollback} attribute + * of the {@link TransactionConfiguration} annotation is set to <strong>{@code false}</strong>. + * Also tests configuration of the + * {@link TransactionConfiguration#transactionManager() transaction manager name}. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see TransactionConfiguration + */ +@SuppressWarnings("deprecation") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@TransactionConfiguration(transactionManager = "txMgr", defaultRollback = false) +@Transactional +public class DefaultRollbackFalseTransactionalSpringRunnerTests extends AbstractTransactionalSpringRunnerTests { + + protected static SimpleJdbcTemplate simpleJdbcTemplate; + + + @AfterClass + public static void verifyFinalTestData() { + assertEquals("Verifying the final number of rows in the person table after all tests.", 2, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Before + public void verifyInitialTestData() { + clearPersonTable(simpleJdbcTemplate); + assertEquals("Adding bob", 1, addPerson(simpleJdbcTemplate, BOB)); + assertEquals("Verifying the initial number of rows in the person table.", 1, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertEquals("Deleting bob", 1, deletePerson(simpleJdbcTemplate, BOB)); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within a transaction.", 2, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + public void setDataSource(DataSource dataSource) { + simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackTrueTransactionalSpringRunnerTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackTrueTransactionalSpringRunnerTests-context.xml new file mode 100644 index 00000000..1c44b0c0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackTrueTransactionalSpringRunnerTests-context.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.DefaultRollbackTrueTransactionalSpringRunnerTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackTrueTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackTrueTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..d7bfb8c2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/DefaultRollbackTrueTransactionalSpringRunnerTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.transaction.annotation.Transactional; + +/** + * JUnit 4 based integration test which verifies proper transactional behavior when the + * {@link TransactionConfiguration#defaultRollback() defaultRollback} attribute + * of the {@link TransactionConfiguration} annotation is set to <strong>{@code true}</strong>. + * + * @author Sam Brannen + * @since 2.5 + * @see TransactionConfiguration + */ +@SuppressWarnings("deprecation") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@TransactionConfiguration(defaultRollback = true) +public class DefaultRollbackTrueTransactionalSpringRunnerTests extends AbstractTransactionalSpringRunnerTests { + + protected static int originalNumRows; + + protected static SimpleJdbcTemplate simpleJdbcTemplate; + + + @AfterClass + public static void verifyFinalTestData() { + assertEquals("Verifying the final number of rows in the person table after all tests.", originalNumRows, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Before + public void verifyInitialTestData() { + originalNumRows = clearPersonTable(simpleJdbcTemplate); + assertEquals("Adding bob", 1, addPerson(simpleJdbcTemplate, BOB)); + assertEquals("Verifying the initial number of rows in the person table.", 1, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test(timeout = 1000) + @Transactional + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within a transaction.", 3, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + public void setDataSource(DataSource dataSource) { + simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/EnabledAndIgnoredSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/EnabledAndIgnoredSpringRunnerTests.java new file mode 100644 index 00000000..ca7b3126 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/EnabledAndIgnoredSpringRunnerTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.annotation.ProfileValueSource; +import org.springframework.test.annotation.ProfileValueSourceConfiguration; +import org.springframework.test.context.TestExecutionListeners; + +/** + * Verifies proper handling of JUnit's {@link Ignore @Ignore} and Spring's + * {@link IfProfileValue @IfProfileValue} and + * {@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration} + * (with the <em>implicit, default {@link ProfileValueSource}</em>) annotations in + * conjunction with the {@link SpringJUnit4ClassRunner}. + * <p> + * Note that {@link TestExecutionListeners @TestExecutionListeners} is + * explicitly configured with an empty list, thus disabling all default + * listeners. + * + * @author Sam Brannen + * @since 2.5 + * @see HardCodedProfileValueSourceSpringRunnerTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners( {}) +public class EnabledAndIgnoredSpringRunnerTests { + + protected static final String NAME = "EnabledAndIgnoredSpringRunnerTests.profile_value.name"; + + protected static final String VALUE = "enigma"; + + protected static int numTestsExecuted = 0; + + + @BeforeClass + public static void setProfileValue() { + numTestsExecuted = 0; + System.setProperty(NAME, VALUE); + } + + @AfterClass + public static void verifyNumTestsExecuted() { + assertEquals("Verifying the number of tests executed.", 3, numTestsExecuted); + } + + @Test + @IfProfileValue(name = NAME, value = VALUE + "X") + public void testIfProfileValueDisabled() { + numTestsExecuted++; + fail("The body of a disabled test should never be executed!"); + } + + @Test + @IfProfileValue(name = NAME, value = VALUE) + public void testIfProfileValueEnabledViaSingleValue() { + numTestsExecuted++; + } + + @Test + @IfProfileValue(name = NAME, values = { "foo", VALUE, "bar" }) + public void testIfProfileValueEnabledViaMultipleValues() { + numTestsExecuted++; + } + + @Test + public void testIfProfileValueNotConfigured() { + numTestsExecuted++; + } + + @Test + @Ignore + public void testJUnitIgnoreAnnotation() { + numTestsExecuted++; + fail("The body of an ignored test should never be executed!"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ExpectedExceptionSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/ExpectedExceptionSpringRunnerTests.java new file mode 100644 index 00000000..d9403519 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ExpectedExceptionSpringRunnerTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.*; + +import java.util.ArrayList; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.JUnit4; +import org.springframework.test.annotation.ExpectedException; +import org.springframework.test.context.TestExecutionListeners; + +/** + * Verifies proper handling of the following in conjunction with the + * {@link SpringJUnit4ClassRunner}: + * <ul> + * <li>JUnit's {@link Test#expected() @Test(expected=...)}</li> + * <li>Spring's {@link ExpectedException @ExpectedException}</li> + * </ul> + * + * @author Sam Brannen + * @since 3.0 + */ +@SuppressWarnings("deprecation") +@RunWith(JUnit4.class) +public class ExpectedExceptionSpringRunnerTests { + + @Test + public void expectedExceptions() throws Exception { + Class<ExpectedExceptionSpringRunnerTestCase> testClass = ExpectedExceptionSpringRunnerTestCase.class; + TrackingRunListener listener = new TrackingRunListener(); + RunNotifier notifier = new RunNotifier(); + notifier.addListener(listener); + + new SpringJUnit4ClassRunner(testClass).run(notifier); + assertEquals("Verifying number of failures for test class [" + testClass + "].", 1, + listener.getTestFailureCount()); + assertEquals("Verifying number of tests started for test class [" + testClass + "].", 3, + listener.getTestStartedCount()); + assertEquals("Verifying number of tests finished for test class [" + testClass + "].", 3, + listener.getTestFinishedCount()); + } + + + @Ignore("TestCase classes are run manually by the enclosing test class") + @RunWith(SpringJUnit4ClassRunner.class) + @TestExecutionListeners({}) + public static final class ExpectedExceptionSpringRunnerTestCase { + + // Should Pass. + @Test(expected = IndexOutOfBoundsException.class) + public void verifyJUnitExpectedException() { + new ArrayList<Object>().get(1); + } + + // Should Pass. + @Test + @ExpectedException(IndexOutOfBoundsException.class) + public void verifySpringExpectedException() { + new ArrayList<Object>().get(1); + } + + // Should Fail due to duplicate configuration. + @Test(expected = IllegalStateException.class) + @ExpectedException(IllegalStateException.class) + public void verifyJUnitAndSpringExpectedException() { + new ArrayList<Object>().get(1); + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/FailingBeforeAndAfterMethodsTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/FailingBeforeAndAfterMethodsTests-context.xml new file mode 100644 index 00000000..3c4fc976 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/FailingBeforeAndAfterMethodsTests-context.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password="" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/FailingBeforeAndAfterMethodsTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/FailingBeforeAndAfterMethodsTests.java new file mode 100644 index 00000000..0f4fb936 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/FailingBeforeAndAfterMethodsTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; + +/** + * <p> + * JUnit 4 based integration test for verifying that '<i>before</i>' and '<i>after</i>' + * methods of {@link TestExecutionListener TestExecutionListeners} as well as + * {@link BeforeTransaction @BeforeTransaction} and + * {@link AfterTransaction @AfterTransaction} methods can fail a test in a + * JUnit 4.4 environment, as requested in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3960" + * target="_blank">SPR-3960</a>. + * </p> + * <p> + * Indirectly, this class also verifies that all {@link TestExecutionListener} + * lifecycle callbacks are called. + * </p> + * <p> + * As of Spring 3.0, this class also tests support for the new + * {@link TestExecutionListener#beforeTestClass(TestContext) beforeTestClass()} + * and {@link TestExecutionListener#afterTestClass(TestContext) + * afterTestClass()} lifecycle callback methods. + * </p> + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(Parameterized.class) +public class FailingBeforeAndAfterMethodsTests { + + protected final Class<?> clazz; + + + public FailingBeforeAndAfterMethodsTests(final Class<?> clazz) { + this.clazz = clazz; + } + + @Parameters + public static Collection<Object[]> testData() { + return Arrays.asList(new Object[][] {// + // + { AlwaysFailingBeforeTestClassTestCase.class },// + { AlwaysFailingAfterTestClassTestCase.class },// + { AlwaysFailingPrepareTestInstanceTestCase.class },// + { AlwaysFailingBeforeTestMethodTestCase.class },// + { AlwaysFailingAfterTestMethodTestCase.class },// + { FailingBeforeTransactionTestCase.class },// + { FailingAfterTransactionTestCase.class } // + }); + } + + @Test + public void runTestAndAssertCounters() throws Exception { + final TrackingRunListener listener = new TrackingRunListener(); + final RunNotifier notifier = new RunNotifier(); + notifier.addListener(listener); + + new SpringJUnit4ClassRunner(this.clazz).run(notifier); + assertEquals("Verifying number of failures for test class [" + this.clazz + "].", 1, + listener.getTestFailureCount()); + } + + + // ------------------------------------------------------------------- + + static class AlwaysFailingBeforeTestClassTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void beforeTestClass(TestContext testContext) { + fail("always failing beforeTestClass()"); + } + } + + static class AlwaysFailingAfterTestClassTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void afterTestClass(TestContext testContext) { + fail("always failing afterTestClass()"); + } + } + + static class AlwaysFailingPrepareTestInstanceTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + fail("always failing prepareTestInstance()"); + } + } + + static class AlwaysFailingBeforeTestMethodTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void beforeTestMethod(TestContext testContext) { + fail("always failing beforeTestMethod()"); + } + } + + static class AlwaysFailingAfterTestMethodTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void afterTestMethod(TestContext testContext) { + fail("always failing afterTestMethod()"); + } + } + + @RunWith(SpringJUnit4ClassRunner.class) + @TestExecutionListeners({}) + public static abstract class BaseTestCase { + + @Test + public void testNothing() { + } + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @TestExecutionListeners(AlwaysFailingBeforeTestClassTestExecutionListener.class) + public static class AlwaysFailingBeforeTestClassTestCase extends BaseTestCase { + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @TestExecutionListeners(AlwaysFailingAfterTestClassTestExecutionListener.class) + public static class AlwaysFailingAfterTestClassTestCase extends BaseTestCase { + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @TestExecutionListeners(AlwaysFailingPrepareTestInstanceTestExecutionListener.class) + public static class AlwaysFailingPrepareTestInstanceTestCase extends BaseTestCase { + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @TestExecutionListeners(AlwaysFailingBeforeTestMethodTestExecutionListener.class) + public static class AlwaysFailingBeforeTestMethodTestCase extends BaseTestCase { + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @TestExecutionListeners(AlwaysFailingAfterTestMethodTestExecutionListener.class) + public static class AlwaysFailingAfterTestMethodTestCase extends BaseTestCase { + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @ContextConfiguration("FailingBeforeAndAfterMethodsTests-context.xml") + public static class FailingBeforeTransactionTestCase extends AbstractTransactionalJUnit4SpringContextTests { + + @Test + public void testNothing() { + } + + @BeforeTransaction + public void beforeTransaction() { + fail("always failing beforeTransaction()"); + } + } + + @Ignore("TestCase classes are run manually by the enclosing test class") + @ContextConfiguration("FailingBeforeAndAfterMethodsTests-context.xml") + public static class FailingAfterTransactionTestCase extends AbstractTransactionalJUnit4SpringContextTests { + + @Test + public void testNothing() { + } + + @AfterTransaction + public void afterTransaction() { + fail("always failing afterTransaction()"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/HardCodedProfileValueSourceSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/HardCodedProfileValueSourceSpringRunnerTests.java new file mode 100644 index 00000000..cea2a29c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/HardCodedProfileValueSourceSpringRunnerTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import org.junit.BeforeClass; +import org.springframework.test.annotation.ProfileValueSource; +import org.springframework.test.annotation.ProfileValueSourceConfiguration; + +/** + * <p> + * Verifies proper handling of JUnit's {@link org.junit.Ignore @Ignore} and + * Spring's {@link org.springframework.test.annotation.IfProfileValue + * @IfProfileValue} and {@link ProfileValueSourceConfiguration + * @ProfileValueSourceConfiguration} (with an + * <em>explicit, custom defined {@link ProfileValueSource}</em>) annotations in + * conjunction with the {@link SpringJUnit4ClassRunner}. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see EnabledAndIgnoredSpringRunnerTests + */ +@ProfileValueSourceConfiguration(HardCodedProfileValueSourceSpringRunnerTests.HardCodedProfileValueSource.class) +public class HardCodedProfileValueSourceSpringRunnerTests extends EnabledAndIgnoredSpringRunnerTests { + + @BeforeClass + public static void setProfileValue() { + numTestsExecuted = 0; + // Set the system property to something other than VALUE as a sanity + // check. + System.setProperty(NAME, "999999999999"); + } + + + public static class HardCodedProfileValueSource implements ProfileValueSource { + + @Override + public String get(final String key) { + return (key.equals(NAME) ? VALUE : null); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/InheritedConfigSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/InheritedConfigSpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..fec86ee6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/InheritedConfigSpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2007 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 + * + * http://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.junit4; + +import java.lang.annotation.Inherited; + +import org.springframework.test.context.ContextConfiguration; + +/** + * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests} which verifies that + * the configuration of an application context and dependency injection of a + * test instance function as expected within a class hierarchy, since + * {@link ContextConfiguration configuration} is {@link Inherited inherited}. + * + * @author Sam Brannen + * @since 2.5 + * @see SpringJUnit4ClassRunnerAppCtxTests + */ +public class InheritedConfigSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { + /* all tests are in the parent class. */ +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/MethodLevelTransactionalSpringRunnerTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/MethodLevelTransactionalSpringRunnerTests-context.xml new file mode 100644 index 00000000..c9e6fa05 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/MethodLevelTransactionalSpringRunnerTests-context.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.MethodLevelTransactionalSpringRunnerTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/MethodLevelTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/MethodLevelTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..6cc35c64 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/MethodLevelTransactionalSpringRunnerTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.Transactional; + +/** + * <p> + * JUnit 4 based integration test which verifies support of Spring's + * {@link Transactional @Transactional}, {@link TestExecutionListeners + * @TestExecutionListeners}, and {@link ContextConfiguration + * @ContextConfiguration} annotations in conjunction with the + * {@link SpringJUnit4ClassRunner} and the following + * {@link TestExecutionListener TestExecutionListeners}: + * </p> + * <ul> + * <li>{@link DependencyInjectionTestExecutionListener}</li> + * <li>{@link DirtiesContextTestExecutionListener}</li> + * <li>{@link TransactionalTestExecutionListener}</li> + * </ul> + * <p> + * This class specifically tests usage of {@code @Transactional} + * defined at the <strong>method level</strong>. In contrast to + * {@link ClassLevelTransactionalSpringRunnerTests}, this class omits usage of + * {@code @NotTransactional}. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see ClassLevelTransactionalSpringRunnerTests + */ +@SuppressWarnings("deprecation") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, + TransactionalTestExecutionListener.class }) +public class MethodLevelTransactionalSpringRunnerTests extends AbstractTransactionalSpringRunnerTests { + + protected static SimpleJdbcTemplate simpleJdbcTemplate; + + + @AfterClass + public static void verifyFinalTestData() { + assertEquals("Verifying the final number of rows in the person table after all tests.", 4, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Before + public void verifyInitialTestData() { + clearPersonTable(simpleJdbcTemplate); + assertEquals("Adding bob", 1, addPerson(simpleJdbcTemplate, BOB)); + assertEquals("Verifying the initial number of rows in the person table.", 1, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + @Transactional("transactionManager2") + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertEquals("Deleting bob", 1, deletePerson(simpleJdbcTemplate, BOB)); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within a transaction.", 2, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + public void modifyTestDataWithoutTransaction() { + assertInTransaction(false); + assertEquals("Adding luke", 1, addPerson(simpleJdbcTemplate, LUKE)); + assertEquals("Adding leia", 1, addPerson(simpleJdbcTemplate, LEIA)); + assertEquals("Adding yoda", 1, addPerson(simpleJdbcTemplate, YODA)); + assertEquals("Verifying the number of rows in the person table without a transaction.", 4, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + public void setDataSource2(DataSource dataSource) { + simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context1.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context1.xml new file mode 100644 index 00000000..b40fed69 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context1.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context2.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context2.xml new file mode 100644 index 00000000..f6a90a62 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context2.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Fido" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context3.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context3.xml new file mode 100644 index 00000000..d8c31a18 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context3.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="foo" class="java.lang.String"> + <constructor-arg value="Foo" /> + </bean> + + <bean id="bar" class="java.lang.String"> + <constructor-arg value="Bar" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..a77f7239 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.ResourceUtils; + +/** + * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * we can specify multiple resource locations for our application context, each + * configured differently. + * <p> + * As of Spring 3.0, + * {@code MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests} is also used + * to verify support for the new {@code value} attribute alias for + * {@code @ContextConfiguration}'s {@code locations} attribute. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see SpringJUnit4ClassRunnerAppCtxTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration( { MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.CLASSPATH_RESOURCE_PATH, + MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.LOCAL_RESOURCE_PATH, + MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.ABSOLUTE_RESOURCE_PATH }) +public class MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { + + public static final String CLASSPATH_RESOURCE_PATH = ResourceUtils.CLASSPATH_URL_PREFIX + + "/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context1.xml"; + public static final String LOCAL_RESOURCE_PATH = "MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context2.xml"; + public static final String ABSOLUTE_RESOURCE_PATH = "/org/springframework/test/context/junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context3.xml"; + + /* all tests are in the parent class. */ +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ParameterizedDependencyInjectionTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/ParameterizedDependencyInjectionTests-context.xml new file mode 100644 index 00000000..67590a6b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ParameterizedDependencyInjectionTests-context.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="employee1" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + + <bean id="employee2" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="Jane Smith" /> + <property name="age" value="38" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Fido" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ParameterizedDependencyInjectionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/ParameterizedDependencyInjectionTests.java new file mode 100644 index 00000000..bf079197 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/ParameterizedDependencyInjectionTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +/** + * Simple JUnit 4 based integration test which demonstrates how to use JUnit's + * {@link Parameterized} Runner in conjunction with + * {@link ContextConfiguration @ContextConfiguration}, the + * {@link DependencyInjectionTestExecutionListener}, and a + * {@link TestContextManager} to provide dependency injection to a + * <em>parameterized test instance</em>. + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(Parameterized.class) +@ContextConfiguration +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class }) +public class ParameterizedDependencyInjectionTests { + + private static final List<Employee> employees = new ArrayList<Employee>(); + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private Pet pet; + + private final String employeeBeanName; + private final String employeeName; + + private final TestContextManager testContextManager; + + + public ParameterizedDependencyInjectionTests(final String employeeBeanName, final String employeeName) + throws Exception { + this.testContextManager = new TestContextManager(getClass()); + this.employeeBeanName = employeeBeanName; + this.employeeName = employeeName; + } + + @Parameters + public static Collection<String[]> employeeData() { + return Arrays.asList(new String[][] { { "employee1", "John Smith" }, { "employee2", "Jane Smith" } }); + } + + @BeforeClass + public static void clearEmployees() { + employees.clear(); + } + + @Before + public void injectDependencies() throws Throwable { + this.testContextManager.prepareTestInstance(this); + } + + @Test + public final void verifyPetAndEmployee() { + + // Verifying dependency injection: + assertNotNull("The pet field should have been autowired.", this.pet); + + // Verifying 'parameterized' support: + final Employee employee = (Employee) this.applicationContext.getBean(this.employeeBeanName); + employees.add(employee); + assertEquals("Verifying the name of the employee configured as bean [" + this.employeeBeanName + "].", + this.employeeName, employee.getName()); + } + + @AfterClass + public static void verifyNumParameterizedRuns() { + assertEquals("Verifying the number of times the parameterized test method was executed.", + employeeData().size(), employees.size()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests-context.properties b/spring-test/src/test/java/org/springframework/test/context/junit4/PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests-context.properties new file mode 100644 index 00000000..6df81585 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests-context.properties @@ -0,0 +1,5 @@ +cat.(class)=org.springframework.tests.sample.beans.Pet +cat.$0=Garfield + +testString.(class)=java.lang.String +testString.$0=Test String diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..fd82c559 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.Properties; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.support.GenericPropertiesContextLoader; + +/** + * <p> + * JUnit 4 based test class, which verifies the expected functionality of + * {@link SpringJUnit4ClassRunner} in conjunction with support for application contexts + * loaded from Java {@link Properties} files. Specifically, the + * {@link ContextConfiguration#loader() loader} attribute of {@code ContextConfiguration} + * and the + * {@link org.springframework.test.context.support.GenericPropertiesContextLoader#getResourceSuffix() + * resourceSuffix} property of {@code GenericPropertiesContextLoader} are tested. + * </p> + * <p> + * Since no {@link ContextConfiguration#locations() locations} are explicitly defined, the + * {@code resourceSuffix} is set to "-context.properties", and since default + * resource locations will be detected by default, this test class's dependencies will be + * injected via {@link Autowired annotation-based autowiring} from beans defined in the + * {@link ApplicationContext} loaded from the default classpath resource: " + * {@code /org/springframework/test/junit4/PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests-context.properties} + * ". + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see GenericPropertiesContextLoader + * @see SpringJUnit4ClassRunnerAppCtxTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(loader = GenericPropertiesContextLoader.class) +public class PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests { + + @Autowired + private Pet cat; + + @Autowired + private String testString; + + + @Test + public void verifyAnnotationAutowiredFields() { + assertNotNull("The cat field should have been autowired.", this.cat); + assertEquals("Garfield", this.cat.getName()); + + assertNotNull("The testString field should have been autowired.", this.testString); + assertEquals("Test String", this.testString); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/RelativePathSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/RelativePathSpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..478874a7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/RelativePathSpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2007 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 + * + * http://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.junit4; + +import org.junit.runner.RunWith; + +import org.springframework.test.context.ContextConfiguration; + +/** + * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * we can specify an explicit, <em>relative path</em> location for our + * application context. + * + * @author Sam Brannen + * @since 2.5 + * @see SpringJUnit4ClassRunnerAppCtxTests + * @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "SpringJUnit4ClassRunnerAppCtxTests-context.xml" }) +public class RelativePathSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { + /* all tests are in the parent class. */ +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/RepeatedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/RepeatedSpringRunnerTests.java new file mode 100644 index 00000000..6b724126 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/RepeatedSpringRunnerTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.annotation.Timed; +import org.springframework.test.context.TestExecutionListeners; + +/** + * Verifies proper handling of the following in conjunction with the + * {@link SpringJUnit4ClassRunner}: + * <ul> + * <li>Spring's {@link Repeat @Repeat}</li> + * <li>Spring's {@link Timed @Timed}</li> + * </ul> + * + * @author Sam Brannen + * @since 3.0 + */ +@RunWith(Parameterized.class) +public class RepeatedSpringRunnerTests { + + private static final AtomicInteger invocationCount = new AtomicInteger(); + + private final Class<? extends AbstractRepeatedTestCase> testClass; + + private final int expectedFailureCount; + + private final int expectedTestStartedCount; + + private final int expectedTestFinishedCount; + + private final int expectedInvocationCount; + + + public RepeatedSpringRunnerTests(Class<? extends AbstractRepeatedTestCase> testClass, int expectedFailureCount, + int expectedTestStartedCount, int expectedTestFinishedCount, int expectedInvocationCount) { + this.testClass = testClass; + this.expectedFailureCount = expectedFailureCount; + this.expectedTestStartedCount = expectedTestStartedCount; + this.expectedTestFinishedCount = expectedTestFinishedCount; + this.expectedInvocationCount = expectedInvocationCount; + } + + @Parameters + public static Collection<Object[]> repetitionData() { + return Arrays.asList(new Object[][] {// + // + { NonAnnotatedRepeatedTestCase.class, 0, 1, 1, 1 },// + { DefaultRepeatValueRepeatedTestCase.class, 0, 1, 1, 1 },// + { NegativeRepeatValueRepeatedTestCase.class, 0, 1, 1, 1 },// + { RepeatedFiveTimesRepeatedTestCase.class, 0, 1, 1, 5 },// + { TimedRepeatedTestCase.class, 3, 4, 4, (5 + 1 + 4 + 10) } // + }); + } + + @Test + public void assertRepetitions() throws Exception { + TrackingRunListener listener = new TrackingRunListener(); + RunNotifier notifier = new RunNotifier(); + notifier.addListener(listener); + invocationCount.set(0); + + new SpringJUnit4ClassRunner(this.testClass).run(notifier); + assertEquals("Verifying number of failures for test class [" + this.testClass + "].", + this.expectedFailureCount, listener.getTestFailureCount()); + assertEquals("Verifying number of tests started for test class [" + this.testClass + "].", + this.expectedTestStartedCount, listener.getTestStartedCount()); + assertEquals("Verifying number of tests finished for test class [" + this.testClass + "].", + this.expectedTestFinishedCount, listener.getTestFinishedCount()); + assertEquals("Verifying number of invocations for test class [" + this.testClass + "].", + this.expectedInvocationCount, invocationCount.get()); + } + + + @RunWith(SpringJUnit4ClassRunner.class) + @TestExecutionListeners({}) + public abstract static class AbstractRepeatedTestCase { + + protected void incrementInvocationCount() throws IOException { + invocationCount.incrementAndGet(); + } + } + + public static final class NonAnnotatedRepeatedTestCase extends AbstractRepeatedTestCase { + + @Test + @Timed(millis = 10000) + public void nonAnnotated() throws Exception { + incrementInvocationCount(); + } + } + + public static final class DefaultRepeatValueRepeatedTestCase extends AbstractRepeatedTestCase { + + @Test + @Repeat + @Timed(millis = 10000) + public void defaultRepeatValue() throws Exception { + incrementInvocationCount(); + } + } + + public static final class NegativeRepeatValueRepeatedTestCase extends AbstractRepeatedTestCase { + + @Test + @Repeat(-5) + @Timed(millis = 10000) + public void negativeRepeatValue() throws Exception { + incrementInvocationCount(); + } + } + + public static final class RepeatedFiveTimesRepeatedTestCase extends AbstractRepeatedTestCase { + + @Test + @Repeat(5) + public void repeatedFiveTimes() throws Exception { + incrementInvocationCount(); + } + } + + /** + * Unit tests for claims raised in <a + * href="http://jira.springframework.org/browse/SPR-6011" + * target="_blank">SPR-6011</a>. + */ + @Ignore("TestCase classes are run manually by the enclosing test class") + public static final class TimedRepeatedTestCase extends AbstractRepeatedTestCase { + + @Test + @Timed(millis = 1000) + @Repeat(5) + public void repeatedFiveTimesButDoesNotExceedTimeout() throws Exception { + incrementInvocationCount(); + } + + @Test + @Timed(millis = 10) + @Repeat(1) + public void singleRepetitionExceedsTimeout() throws Exception { + incrementInvocationCount(); + Thread.sleep(15); + } + + @Test + @Timed(millis = 20) + @Repeat(4) + public void firstRepetitionOfManyExceedsTimeout() throws Exception { + incrementInvocationCount(); + Thread.sleep(25); + } + + @Test + @Timed(millis = 100) + @Repeat(10) + public void collectiveRepetitionsExceedTimeout() throws Exception { + incrementInvocationCount(); + Thread.sleep(11); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests-context.xml new file mode 100644 index 00000000..44a8ae54 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests-context.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password="" /> + + <bean id="txMgr" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..231ea884 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2008 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; + +/** + * Extension of {@link DefaultRollbackFalseTransactionalSpringRunnerTests} which + * tests method-level <em>rollback override</em> behavior via the + * {@link Rollback @Rollback} annotation. + * + * @author Sam Brannen + * @since 2.5 + * @see Rollback + */ +@SuppressWarnings("deprecation") +@ContextConfiguration +public class RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests extends + DefaultRollbackFalseTransactionalSpringRunnerTests { + + protected static int originalNumRows; + + protected static SimpleJdbcTemplate simpleJdbcTemplate; + + + @AfterClass + public static void verifyFinalTestData() { + assertEquals("Verifying the final number of rows in the person table after all tests.", originalNumRows, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Before + @Override + public void verifyInitialTestData() { + originalNumRows = clearPersonTable(simpleJdbcTemplate); + assertEquals("Adding bob", 1, addPerson(simpleJdbcTemplate, BOB)); + assertEquals("Verifying the initial number of rows in the person table.", 1, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Test + @Rollback(true) + @Override + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertEquals("Deleting bob", 1, deletePerson(simpleJdbcTemplate, BOB)); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within a transaction.", 2, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + public void setDataSource(DataSource dataSource) { + simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests-context.xml new file mode 100644 index 00000000..cda53c56 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests-context.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="databaseSetup" + class="org.springframework.test.context.junit4.RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests$DatabaseSetup" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..a662b7b8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; + +/** + * Extension of {@link DefaultRollbackTrueTransactionalSpringRunnerTests} which + * tests method-level <em>rollback override</em> behavior via the + * {@link Rollback @Rollback} annotation. + * + * @author Sam Brannen + * @since 2.5 + * @see Rollback + */ +@SuppressWarnings("deprecation") +@ContextConfiguration +public class RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests extends + DefaultRollbackTrueTransactionalSpringRunnerTests { + + protected static SimpleJdbcTemplate simpleJdbcTemplate; + + + @AfterClass + public static void verifyFinalTestData() { + assertEquals("Verifying the final number of rows in the person table after all tests.", 3, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Override + @Before + public void verifyInitialTestData() { + clearPersonTable(simpleJdbcTemplate); + assertEquals("Adding bob", 1, addPerson(simpleJdbcTemplate, BOB)); + assertEquals("Verifying the initial number of rows in the person table.", 1, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + @Override + @Test + @Transactional + @Rollback(false) + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertEquals("Adding jane", 1, addPerson(simpleJdbcTemplate, JANE)); + assertEquals("Adding sue", 1, addPerson(simpleJdbcTemplate, SUE)); + assertEquals("Verifying the number of rows in the person table within a transaction.", 3, + countRowsInPersonTable(simpleJdbcTemplate)); + } + + + public static class DatabaseSetup { + + @Resource + public void setDataSource(DataSource dataSource) { + simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + createPersonTable(simpleJdbcTemplate); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit47ClassRunnerRuleTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit47ClassRunnerRuleTests.java new file mode 100644 index 00000000..3229df93 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit47ClassRunnerRuleTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.springframework.test.context.TestExecutionListeners; + +/** + * Verifies support for JUnit 4.7 {@link Rule Rules} in conjunction with the + * {@link SpringJUnit4ClassRunner}. The body of this test class is taken from + * the JUnit 4.7 release notes. + * + * @author JUnit 4.7 Team + * @author Sam Brannen + * @since 3.0 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners( {}) +public class SpringJUnit47ClassRunnerRuleTests { + + @Rule + public TestName name = new TestName(); + + + @Test + public void testA() { + assertEquals("testA", name.getMethodName()); + } + + @Test + public void testB() { + assertEquals("testB", name.getMethodName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml new file mode 100644 index 00000000..704b3478 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Fido" /> + </bean> + + <bean id="foo" class="java.lang.String"> + <constructor-arg value="Foo" /> + </bean> + + <bean id="bar" class="java.lang.String"> + <constructor-arg value="Bar" /> + </bean> + + <bean id="quux" class="java.lang.String"> + <constructor-arg value="Quux" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..4973d7c6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.BeansException; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.GenericXmlContextLoader; + +/** + * <p> + * SpringJUnit4ClassRunnerAppCtxTests serves as a <em>proof of concept</em> + * JUnit 4 based test class, which verifies the expected functionality of + * {@link SpringJUnit4ClassRunner} in conjunction with the following: + * </p> + * <ul> + * <li>{@link ContextConfiguration @ContextConfiguration}</li> + * <li>{@link Autowired @Autowired}</li> + * <li>{@link Qualifier @Qualifier}</li> + * <li>{@link Resource @Resource}</li> + * <li>{@link Value @Value}</li> + * <li>{@link Inject @Inject}</li> + * <li>{@link Named @Named}</li> + * <li>{@link ApplicationContextAware}</li> + * <li>{@link BeanNameAware}</li> + * <li>{@link InitializingBean}</li> + * </ul> + * <p> + * Since no application context resource + * {@link ContextConfiguration#locations() locations} are explicitly declared + * and since the {@link ContextConfiguration#loader() ContextLoader} is left set + * to the default value of {@link GenericXmlContextLoader}, this test class's + * dependencies will be injected via {@link Autowired @Autowired}, + * {@link Inject @Inject}, and {@link Resource @Resource} from beans defined in + * the {@link ApplicationContext} loaded from the default classpath resource: + * + * {@code "/org/springframework/test/context/junit/SpringJUnit4ClassRunnerAppCtxTests-context.xml"} + * . + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests + * @see RelativePathSpringJUnit4ClassRunnerAppCtxTests + * @see InheritedConfigSpringJUnit4ClassRunnerAppCtxTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class }) +public class SpringJUnit4ClassRunnerAppCtxTests implements ApplicationContextAware, BeanNameAware, InitializingBean { + + /** + * Default resource path for the application context configuration for + * {@link SpringJUnit4ClassRunnerAppCtxTests}: + * + * {@code "/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml"} + */ + public static final String DEFAULT_CONTEXT_RESOURCE_PATH = "/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml"; + + private ApplicationContext applicationContext; + + private boolean beanInitialized = false; + + private String beanName = "replace me with [" + getClass().getName() + "]"; + + private Employee employee; + + @Autowired + private Pet autowiredPet; + + @Inject + private Pet injectedPet; + + @Autowired(required = false) + protected Long nonrequiredLong; + + @Resource + protected String foo; + + protected String bar; + + @Value("enigma") + private String literalFieldValue; + + @Value("#{2 == (1+1)}") + private Boolean spelFieldValue; + + private String literalParameterValue; + + private Boolean spelParameterValue; + + @Autowired + @Qualifier("quux") + protected String quux; + + @Inject + @Named("quux") + protected String namedQuux; + + + // ------------------------------------------------------------------------| + + @Override + public final void afterPropertiesSet() throws Exception { + this.beanInitialized = true; + } + + @Override + public final void setApplicationContext(final ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public final void setBeanName(final String beanName) { + this.beanName = beanName; + } + + @Autowired + protected final void setEmployee(final Employee employee) { + this.employee = employee; + } + + @Resource + protected final void setBar(final String bar) { + this.bar = bar; + } + + @Autowired + public void setLiteralParameterValue(@Value("enigma") String literalParameterValue) { + this.literalParameterValue = literalParameterValue; + } + + @Autowired + public void setSpelParameterValue(@Value("#{2 == (1+1)}") Boolean spelParameterValue) { + this.spelParameterValue = spelParameterValue; + } + + // ------------------------------------------------------------------------| + + @Test + public final void verifyApplicationContextSet() { + assertNotNull("The application context should have been set due to ApplicationContextAware semantics.", + this.applicationContext); + } + + @Test + public final void verifyBeanInitialized() { + assertTrue("This test bean should have been initialized due to InitializingBean semantics.", + this.beanInitialized); + } + + @Test + public final void verifyBeanNameSet() { + assertEquals("The bean name of this test instance should have been set due to BeanNameAware semantics.", + getClass().getName(), this.beanName); + } + + @Test + public final void verifyAnnotationAutowiredAndInjectedFields() { + assertNull("The nonrequiredLong field should NOT have been autowired.", this.nonrequiredLong); + assertEquals("The quux field should have been autowired via @Autowired and @Qualifier.", "Quux", this.quux); + assertEquals("The namedFoo field should have been injected via @Inject and @Named.", "Quux", this.namedQuux); + assertSame("@Autowired/@Qualifier and @Inject/@Named quux should be the same object.", this.quux, + this.namedQuux); + + assertNotNull("The pet field should have been autowired.", this.autowiredPet); + assertNotNull("The pet field should have been injected.", this.injectedPet); + assertEquals("Fido", this.autowiredPet.getName()); + assertEquals("Fido", this.injectedPet.getName()); + assertSame("@Autowired and @Inject pet should be the same object.", this.autowiredPet, this.injectedPet); + } + + @Test + public final void verifyAnnotationAutowiredMethods() { + assertNotNull("The employee setter method should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } + + @Test + public final void verifyAutowiredAtValueFields() { + assertNotNull("Literal @Value field should have been autowired", this.literalFieldValue); + assertNotNull("SpEL @Value field should have been autowired.", this.spelFieldValue); + assertEquals("enigma", this.literalFieldValue); + assertEquals(Boolean.TRUE, this.spelFieldValue); + } + + @Test + public final void verifyAutowiredAtValueMethods() { + assertNotNull("Literal @Value method parameter should have been autowired.", this.literalParameterValue); + assertNotNull("SpEL @Value method parameter should have been autowired.", this.spelParameterValue); + assertEquals("enigma", this.literalParameterValue); + assertEquals(Boolean.TRUE, this.spelParameterValue); + } + + @Test + public final void verifyResourceAnnotationInjectedFields() { + assertEquals("The foo field should have been injected via @Resource.", "Foo", this.foo); + } + + @Test + public final void verifyResourceAnnotationInjectedMethods() { + assertEquals("The bar method should have been wired via @Resource.", "Bar", this.bar); + } + +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerTests.java new file mode 100644 index 00000000..aedd5ad4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2007 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 + * + * http://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.junit4; + +import org.junit.Test; + +import org.springframework.test.context.TestContextManager; + +/** + * @author Rick Evans + * @author Sam Brannen + * @since 2.5 + */ +public class SpringJUnit4ClassRunnerTests { + + @Test(expected = Exception.class) + public void checkThatExceptionsAreNotSilentlySwallowed() throws Exception { + SpringJUnit4ClassRunner runner = new SpringJUnit4ClassRunner(getClass()) { + + @Override + protected TestContextManager createTestContextManager(Class<?> clazz) { + return new TestContextManager(clazz) { + + @Override + public void prepareTestInstance(Object testInstance) { + throw new RuntimeException("This RuntimeException should be caught and wrapped in an Exception."); + } + }; + } + }; + runner.createTest(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4TestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4TestSuite.java new file mode 100644 index 00000000..72ddd1b8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4TestSuite.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.test.context.ClassLevelDirtiesContextTests; +import org.springframework.test.context.SpringRunnerContextCacheTests; +import org.springframework.test.context.junit4.annotation.AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests; +import org.springframework.test.context.junit4.annotation.BeanOverridingDefaultConfigClassesInheritedTests; +import org.springframework.test.context.junit4.annotation.BeanOverridingExplicitConfigClassesInheritedTests; +import org.springframework.test.context.junit4.annotation.DefaultConfigClassesBaseTests; +import org.springframework.test.context.junit4.annotation.DefaultConfigClassesInheritedTests; +import org.springframework.test.context.junit4.annotation.DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests; +import org.springframework.test.context.junit4.annotation.DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests; +import org.springframework.test.context.junit4.annotation.DefaultLoaderDefaultConfigClassesBaseTests; +import org.springframework.test.context.junit4.annotation.DefaultLoaderDefaultConfigClassesInheritedTests; +import org.springframework.test.context.junit4.annotation.DefaultLoaderExplicitConfigClassesBaseTests; +import org.springframework.test.context.junit4.annotation.DefaultLoaderExplicitConfigClassesInheritedTests; +import org.springframework.test.context.junit4.annotation.ExplicitConfigClassesBaseTests; +import org.springframework.test.context.junit4.annotation.ExplicitConfigClassesInheritedTests; +import org.springframework.test.context.junit4.orm.HibernateSessionFlushingTests; +import org.springframework.test.context.junit4.profile.annotation.DefaultProfileAnnotationConfigTests; +import org.springframework.test.context.junit4.profile.annotation.DevProfileAnnotationConfigTests; +import org.springframework.test.context.junit4.profile.xml.DefaultProfileXmlConfigTests; +import org.springframework.test.context.junit4.profile.xml.DevProfileXmlConfigTests; + +/** + * JUnit test suite for tests involving {@link SpringJUnit4ClassRunner} and the + * <em>Spring TestContext Framework</em>; only intended to be run manually as a + * convenience. + * + * <p>This test suite serves a dual purpose of verifying that tests run with + * {@link SpringJUnit4ClassRunner} can be used in conjunction with JUnit's + * {@link Suite} runner. + * + * <p>Note that tests included in this suite will be executed at least twice if + * run from an automated build process, test runner, etc. that is not configured + * to exclude tests based on a "*TestSuite.class" pattern match. + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(Suite.class) +// Note: the following 'multi-line' layout is for enhanced code readability. +@SuiteClasses({// +StandardJUnit4FeaturesTests.class,// + StandardJUnit4FeaturesSpringRunnerTests.class,// + SpringJUnit47ClassRunnerRuleTests.class,// + AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.class,// + DefaultConfigClassesBaseTests.class,// + DefaultConfigClassesInheritedTests.class,// + BeanOverridingDefaultConfigClassesInheritedTests.class,// + ExplicitConfigClassesBaseTests.class,// + ExplicitConfigClassesInheritedTests.class,// + BeanOverridingExplicitConfigClassesInheritedTests.class,// + DefaultLoaderDefaultConfigClassesBaseTests.class,// + DefaultLoaderDefaultConfigClassesInheritedTests.class,// + DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.class,// + DefaultLoaderExplicitConfigClassesBaseTests.class,// + DefaultLoaderExplicitConfigClassesInheritedTests.class,// + DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.class,// + DefaultProfileAnnotationConfigTests.class,// + DevProfileAnnotationConfigTests.class,// + DefaultProfileXmlConfigTests.class,// + DevProfileXmlConfigTests.class,// + ExpectedExceptionSpringRunnerTests.class,// + TimedSpringRunnerTests.class,// + RepeatedSpringRunnerTests.class,// + EnabledAndIgnoredSpringRunnerTests.class,// + HardCodedProfileValueSourceSpringRunnerTests.class,// + SpringJUnit4ClassRunnerAppCtxTests.class,// + ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.class,// + AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.class,// + RelativePathSpringJUnit4ClassRunnerAppCtxTests.class,// + MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.class,// + InheritedConfigSpringJUnit4ClassRunnerAppCtxTests.class,// + PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests.class,// + CustomDefaultContextLoaderClassSpringRunnerTests.class,// + SpringRunnerContextCacheTests.class,// + ClassLevelDirtiesContextTests.class,// + ParameterizedDependencyInjectionTests.class,// + ClassLevelTransactionalSpringRunnerTests.class,// + MethodLevelTransactionalSpringRunnerTests.class,// + DefaultRollbackTrueTransactionalSpringRunnerTests.class,// + DefaultRollbackFalseTransactionalSpringRunnerTests.class,// + RollbackOverrideDefaultRollbackTrueTransactionalSpringRunnerTests.class,// + RollbackOverrideDefaultRollbackFalseTransactionalSpringRunnerTests.class,// + BeforeAndAfterTransactionAnnotationTests.class,// + TimedTransactionalSpringRunnerTests.class,// + HibernateSessionFlushingTests.class // +}) +public class SpringJUnit4TestSuite { + /* this test case consists entirely of tests loaded as a suite. */ +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/StandardJUnit4FeaturesSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/StandardJUnit4FeaturesSpringRunnerTests.java new file mode 100644 index 00000000..4dd270ca --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/StandardJUnit4FeaturesSpringRunnerTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2008 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 + * + * http://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.junit4; + +import org.junit.runner.RunWith; + +import org.springframework.test.context.TestExecutionListeners; + +/** + * <p> + * Simple unit test to verify that {@link SpringJUnit4ClassRunner} does not + * hinder correct functionality of standard JUnit 4.4+ testing features. + * </p> + * <p> + * Note that {@link TestExecutionListeners @TestExecutionListeners} is + * explicitly configured with an empty list, thus disabling all default + * listeners. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see StandardJUnit4FeaturesTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners({}) +public class StandardJUnit4FeaturesSpringRunnerTests extends StandardJUnit4FeaturesTests { + + /* All tests are in the parent class... */ + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/StandardJUnit4FeaturesTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/StandardJUnit4FeaturesTests.java new file mode 100644 index 00000000..3fd0aac6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/StandardJUnit4FeaturesTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2007 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 + * + * http://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.junit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import java.util.ArrayList; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Simple unit test to verify the expected functionality of standard JUnit 4.4+ + * testing features. + * <p> + * Currently testing: {@link Test @Test} (including expected exceptions and + * timeouts), {@link BeforeClass @BeforeClass}, {@link Before @Before}, and + * <em>assumptions</em>. + * </p> + * <p> + * Due to the fact that JUnit does not guarantee a particular ordering of test + * method execution, the following are currently not tested: + * {@link org.junit.AfterClass @AfterClass} and {@link org.junit.After @After}. + * </p> + * + * @author Sam Brannen + * @since 2.5 + * @see StandardJUnit4FeaturesSpringRunnerTests + */ +public class StandardJUnit4FeaturesTests { + + private static int staticBeforeCounter = 0; + + + @BeforeClass + public static void incrementStaticBeforeCounter() { + StandardJUnit4FeaturesTests.staticBeforeCounter++; + } + + + private int beforeCounter = 0; + + + @Test + @Ignore + public void alwaysFailsButShouldBeIgnored() { + fail("The body of an ignored test should never be executed!"); + } + + @Test + public void alwaysSucceeds() { + assertTrue(true); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void expectingAnIndexOutOfBoundsException() { + new ArrayList<Object>().get(1); + } + + @Test + public void failedAssumptionShouldPrecludeImminentFailure() { + assumeTrue(false); + fail("A failed assumption should preclude imminent failure!"); + } + + @Before + public void incrementBeforeCounter() { + this.beforeCounter++; + } + + @Test(timeout = 10000) + public void noOpShouldNotTimeOut() { + /* no-op */ + } + + @Test + public void verifyBeforeAnnotation() { + assertEquals(1, this.beforeCounter); + } + + @Test + public void verifyBeforeClassAnnotation() { + // Instead of testing for equality to 1, we just assert that the value + // was incremented at least once, since this test class may serve as a + // parent class to other tests in a suite, etc. + assertTrue(StandardJUnit4FeaturesTests.staticBeforeCounter > 0); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java new file mode 100644 index 00000000..3877058b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4; + +import static org.junit.Assert.*; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.JUnit4; +import org.springframework.test.annotation.Timed; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.tests.Assume; +import org.springframework.tests.TestGroup; + +/** + * Verifies proper handling of the following in conjunction with the + * {@link SpringJUnit4ClassRunner}: + * <ul> + * <li>JUnit's {@link Test#timeout() @Test(timeout=...)}</li> + * <li>Spring's {@link Timed @Timed}</li> + * </ul> + * + * @author Sam Brannen + * @since 3.0 + */ +@RunWith(JUnit4.class) +public class TimedSpringRunnerTests { + + @Test + public void timedTests() throws Exception { + Assume.group(TestGroup.PERFORMANCE); + Class<TimedSpringRunnerTestCase> testClass = TimedSpringRunnerTestCase.class; + TrackingRunListener listener = new TrackingRunListener(); + RunNotifier notifier = new RunNotifier(); + notifier.addListener(listener); + + new SpringJUnit4ClassRunner(testClass).run(notifier); + assertEquals("Verifying number of failures for test class [" + testClass + "].", 3, + listener.getTestFailureCount()); + assertEquals("Verifying number of tests started for test class [" + testClass + "].", 5, + listener.getTestStartedCount()); + assertEquals("Verifying number of tests finished for test class [" + testClass + "].", 5, + listener.getTestFinishedCount()); + } + + + @Ignore("TestCase classes are run manually by the enclosing test class") + @RunWith(SpringJUnit4ClassRunner.class) + @TestExecutionListeners({}) + public static final class TimedSpringRunnerTestCase { + + // Should Pass. + @Test(timeout = 2000) + public void jUnitTimeoutWithNoOp() { + /* no-op */ + } + + // Should Pass. + @Test + @Timed(millis = 2000) + public void springTimeoutWithNoOp() { + /* no-op */ + } + + // Should Fail due to timeout. + @Test(timeout = 10) + public void jUnitTimeoutWithSleep() throws Exception { + Thread.sleep(20); + } + + // Should Fail due to timeout. + @Test + @Timed(millis = 10) + public void springTimeoutWithSleep() throws Exception { + Thread.sleep(20); + } + + // Should Fail due to duplicate configuration. + @Test(timeout = 200) + @Timed(millis = 200) + public void springAndJUnitTimeouts() { + /* no-op */ + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedTransactionalSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedTransactionalSpringRunnerTests.java new file mode 100644 index 00000000..751ec07e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedTransactionalSpringRunnerTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.annotation.Timed; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; + +/** + * JUnit 4 based integration test which verifies support of Spring's + * {@link Transactional @Transactional} and {@link NotTransactional + * @NotTransactional} annotations in conjunction with {@link Timed + * @Timed} and JUnit 4's {@link Test#timeout() timeout} attribute. + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("transactionalTests-context.xml") +@Transactional +@SuppressWarnings("deprecation") +public class TimedTransactionalSpringRunnerTests { + + @Test + @Timed(millis = 10000) + @Repeat(5) + public void transactionalWithSpringTimeout() { + assertInTransaction(true); + } + + @Test(timeout = 10000) + @Repeat(5) + public void transactionalWithJUnitTimeout() { + assertInTransaction(true); + } + + @Test + @NotTransactional + @Timed(millis = 10000) + @Repeat(5) + public void notTransactionalWithSpringTimeout() { + assertInTransaction(false); + } + + @Test(timeout = 10000) + @NotTransactional + @Repeat(5) + public void notTransactionalWithJUnitTimeout() { + assertInTransaction(false); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TrackingRunListener.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TrackingRunListener.java new file mode 100644 index 00000000..4c257540 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TrackingRunListener.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.runner.Description; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +/** + * Simple {@link RunListener} which tracks how many times certain JUnit callback + * methods were called: only intended for the integration test suite. + * + * @author Sam Brannen + * @since 3.0 + */ +public class TrackingRunListener extends RunListener { + + private final AtomicInteger testFailureCount = new AtomicInteger(); + + private final AtomicInteger testStartedCount = new AtomicInteger(); + + private final AtomicInteger testFinishedCount = new AtomicInteger(); + + + public int getTestFailureCount() { + return this.testFailureCount.get(); + } + + public int getTestStartedCount() { + return this.testStartedCount.get(); + } + + public int getTestFinishedCount() { + return this.testFinishedCount.get(); + } + + @Override + public void testFailure(Failure failure) throws Exception { + this.testFailureCount.incrementAndGet(); + } + + @Override + public void testStarted(Description description) throws Exception { + this.testStartedCount.incrementAndGet(); + } + + @Override + public void testFinished(Description description) throws Exception { + this.testFinishedCount.incrementAndGet(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java new file mode 100644 index 00000000..208751f2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.test.context.junit4.aci.annotation.InitializerWithoutConfigFilesOrClassesTest; +import org.springframework.test.context.junit4.aci.annotation.MergedInitializersAnnotationConfigTests; +import org.springframework.test.context.junit4.aci.annotation.MultipleInitializersAnnotationConfigTests; +import org.springframework.test.context.junit4.aci.annotation.OrderedInitializersAnnotationConfigTests; +import org.springframework.test.context.junit4.aci.annotation.OverriddenInitializersAnnotationConfigTests; +import org.springframework.test.context.junit4.aci.annotation.SingleInitializerAnnotationConfigTests; +import org.springframework.test.context.junit4.aci.xml.MultipleInitializersXmlConfigTests; + +/** + * Convenience test suite for integration tests that verify support for + * {@link ApplicationContextInitializer ApplicationContextInitializers} (ACIs) + * in the TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(Suite.class) +// Note: the following 'multi-line' layout is for enhanced code readability. +@SuiteClasses({// + MultipleInitializersXmlConfigTests.class,// + SingleInitializerAnnotationConfigTests.class,// + MultipleInitializersAnnotationConfigTests.class,// + MergedInitializersAnnotationConfigTests.class,// + OverriddenInitializersAnnotationConfigTests.class,// + OrderedInitializersAnnotationConfigTests.class,// + InitializerWithoutConfigFilesOrClassesTest.class // +}) +public class AciTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/DevProfileInitializer.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/DevProfileInitializer.java new file mode 100644 index 00000000..f5424607 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/DevProfileInitializer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; + +/** + * @author Sam Brannen + * @since 3.2 + */ +public class DevProfileInitializer implements ApplicationContextInitializer<GenericApplicationContext> { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + applicationContext.getEnvironment().setActiveProfiles("dev"); + } +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/FooBarAliasInitializer.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/FooBarAliasInitializer.java new file mode 100644 index 00000000..1259b82d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/FooBarAliasInitializer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; + +/** + * @author Sam Brannen + * @since 3.2 + */ +public class FooBarAliasInitializer implements ApplicationContextInitializer<GenericApplicationContext> { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + applicationContext.registerAlias("foo", "bar"); + } +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/DevProfileConfig.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/DevProfileConfig.java new file mode 100644 index 00000000..9d9834da --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/DevProfileConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +/** + * @author Sam Brannen + * @since 3.2 + */ +@Configuration +@Profile("dev") +class DevProfileConfig { + + @Bean + public String baz() { + return "dev profile config"; + } +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/GlobalConfig.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/GlobalConfig.java new file mode 100644 index 00000000..41393776 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/GlobalConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Sam Brannen + * @since 3.2 + */ +@Configuration +class GlobalConfig { + + @Bean + public String foo() { + return "foo"; + } + + @Bean + public String baz() { + return "global config"; + } +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/InitializerWithoutConfigFilesOrClassesTest.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/InitializerWithoutConfigFilesOrClassesTest.java new file mode 100644 index 00000000..5d1459c4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/InitializerWithoutConfigFilesOrClassesTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.aci.annotation.InitializerWithoutConfigFilesOrClassesTest.EntireAppInitializer; + +/** + * Integration test that verifies support for {@link ApplicationContextInitializer + * ApplicationContextInitializers} in the TestContext framework when the test + * class declares neither XML configuration files nor annotated configuration classes. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(initializers = EntireAppInitializer.class) +public class InitializerWithoutConfigFilesOrClassesTest { + + @Autowired + private String foo; + + + @Test + public void foo() { + assertEquals("foo", foo); + } + + + static class EntireAppInitializer implements ApplicationContextInitializer<GenericApplicationContext> { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + new AnnotatedBeanDefinitionReader(applicationContext).register(GlobalConfig.class); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MergedInitializersAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MergedInitializersAnnotationConfigTests.java new file mode 100644 index 00000000..0bc08e1c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MergedInitializersAnnotationConfigTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.aci.DevProfileInitializer; + +/** + * Integration tests that verify support for {@link ApplicationContextInitializer + * ApplicationContextInitializers} in conjunction with annotation-driven + * configuration in the TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@ContextConfiguration(initializers = DevProfileInitializer.class) +public class MergedInitializersAnnotationConfigTests extends SingleInitializerAnnotationConfigTests { + + @Override + @Test + public void activeBeans() { + assertEquals("foo", foo); + assertEquals("foo", bar); + assertEquals("dev profile config", baz); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MultipleInitializersAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MultipleInitializersAnnotationConfigTests.java new file mode 100644 index 00000000..145a44bb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MultipleInitializersAnnotationConfigTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.aci.DevProfileInitializer; +import org.springframework.test.context.junit4.aci.FooBarAliasInitializer; + +/** + * Integration tests that verify support for {@link ApplicationContextInitializer + * ApplicationContextInitializers} in conjunction with annotation-driven + * configuration in the TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { GlobalConfig.class, DevProfileConfig.class }, initializers = { + FooBarAliasInitializer.class, DevProfileInitializer.class }) +public class MultipleInitializersAnnotationConfigTests { + + @Autowired + private String foo, bar, baz; + + + @Test + public void activeBeans() { + assertEquals("foo", foo); + assertEquals("foo", bar); + assertEquals("dev profile config", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OrderedInitializersAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OrderedInitializersAnnotationConfigTests.java new file mode 100644 index 00000000..2eac4332 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OrderedInitializersAnnotationConfigTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.aci.annotation.OrderedInitializersAnnotationConfigTests.ConfigTwo; +import org.springframework.test.context.junit4.aci.annotation.OrderedInitializersAnnotationConfigTests.ConfigOne; +import org.springframework.test.context.junit4.aci.annotation.OrderedInitializersAnnotationConfigTests.GlobalConfig; +import org.springframework.test.context.junit4.aci.annotation.OrderedInitializersAnnotationConfigTests.OrderedOneInitializer; +import org.springframework.test.context.junit4.aci.annotation.OrderedInitializersAnnotationConfigTests.OrderedTwoInitializer; + +/** + * Integration tests that verify that any {@link ApplicationContextInitializer + * ApplicationContextInitializers} implementing + * {@link org.springframework.core.Ordered Ordered} or marked with + * {@link org.springframework.core.annotation.Order @Order} will be sorted + * appropriately in conjunction with annotation-driven configuration in the + * TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +// Note: the ordering of the config classes is intentionally: global, two, one. +// Note: the ordering of the initializers is intentionally: two, one. +@ContextConfiguration(classes = { GlobalConfig.class, ConfigTwo.class, ConfigOne.class }, initializers = { + OrderedTwoInitializer.class, OrderedOneInitializer.class }) +public class OrderedInitializersAnnotationConfigTests { + + private static final String PROFILE_GLOBAL = "global"; + private static final String PROFILE_ONE = "one"; + private static final String PROFILE_TWO = "two"; + + @Autowired + private String foo, bar, baz; + + + @Test + public void activeBeans() { + assertEquals(PROFILE_GLOBAL, foo); + assertEquals(PROFILE_GLOBAL, bar); + assertEquals(PROFILE_TWO, baz); + } + + + // ------------------------------------------------------------------------- + + @Configuration + static class GlobalConfig { + + @Bean + public String foo() { + return PROFILE_GLOBAL; + } + + @Bean + public String bar() { + return PROFILE_GLOBAL; + } + + @Bean + public String baz() { + return PROFILE_GLOBAL; + } + } + + @Configuration + @Profile(PROFILE_ONE) + static class ConfigOne { + + @Bean + public String foo() { + return PROFILE_ONE; + } + + @Bean + public String bar() { + return PROFILE_ONE; + } + + @Bean + public String baz() { + return PROFILE_ONE; + } + } + + @Configuration + @Profile(PROFILE_TWO) + static class ConfigTwo { + + @Bean + public String baz() { + return PROFILE_TWO; + } + } + + // ------------------------------------------------------------------------- + + static class OrderedOneInitializer implements ApplicationContextInitializer<GenericApplicationContext>, Ordered { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + applicationContext.getEnvironment().setActiveProfiles(PROFILE_ONE); + } + + @Override + public int getOrder() { + return 1; + } + } + + @Order(2) + static class OrderedTwoInitializer implements ApplicationContextInitializer<GenericApplicationContext> { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + applicationContext.getEnvironment().setActiveProfiles(PROFILE_TWO); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OverriddenInitializersAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OverriddenInitializersAnnotationConfigTests.java new file mode 100644 index 00000000..f0399853 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OverriddenInitializersAnnotationConfigTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.aci.DevProfileInitializer; + +/** + * Integration tests that verify support for {@link ApplicationContextInitializer + * ApplicationContextInitializers} in conjunction with annotation-driven + * configuration in the TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@ContextConfiguration(initializers = DevProfileInitializer.class, inheritInitializers = false) +public class OverriddenInitializersAnnotationConfigTests extends SingleInitializerAnnotationConfigTests { + + @Test + @Override + public void activeBeans() { + assertEquals("foo", foo); + assertNull(bar); + assertEquals("dev profile config", baz); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/SingleInitializerAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/SingleInitializerAnnotationConfigTests.java new file mode 100644 index 00000000..32528450 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/SingleInitializerAnnotationConfigTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.annotation; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.aci.FooBarAliasInitializer; + +/** + * Integration tests that verify support for {@link ApplicationContextInitializer + * ApplicationContextInitializers} in conjunction with annotation-driven + * configuration in the TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { GlobalConfig.class, DevProfileConfig.class }, initializers = FooBarAliasInitializer.class) +public class SingleInitializerAnnotationConfigTests { + + @Autowired + protected String foo; + + @Autowired(required = false) + @Qualifier("bar") + protected String bar; + + @Autowired + protected String baz; + + + @Test + public void activeBeans() { + assertEquals("foo", foo); + assertEquals("foo", bar); + assertEquals("global config", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml new file mode 100644 index 00000000..d8e6c2f8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="foo" class="java.lang.String"> + <constructor-arg value="foo" /> + </bean> + + <bean id="baz" class="java.lang.String"> + <constructor-arg value="global config" /> + </bean> + + <beans profile="dev"> + <bean id="baz" class="java.lang.String"> + <constructor-arg value="dev profile config" /> + </bean> + </beans> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests.java new file mode 100644 index 00000000..4314538c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.aci.xml; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.aci.DevProfileInitializer; +import org.springframework.test.context.junit4.aci.FooBarAliasInitializer; + +/** + * Integration tests that verify support for {@link ApplicationContextInitializer + * ApplicationContextInitializers} in conjunction with XML configuration files + * in the TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(initializers = { FooBarAliasInitializer.class, DevProfileInitializer.class }) +public class MultipleInitializersXmlConfigTests { + + @Autowired + private String foo, bar, baz; + + + @Test + public void activeBeans() { + assertEquals("foo", foo); + assertEquals("foo", bar); + assertEquals("dev profile config", baz); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java new file mode 100644 index 00000000..219010f1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.annotation; + +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework. + * + * <p>Furthermore, by extending {@link SpringJUnit4ClassRunnerAppCtxTests}, + * this class also verifies support for several basic features of the + * Spring TestContext Framework. See JavaDoc in + * {@code SpringJUnit4ClassRunnerAppCtxTests} for details. + * + * <p>Configuration will be loaded from {@link PojoAndStringConfig}. + * + * @author Sam Brannen + * @since 3.1 + */ +@ContextConfiguration(classes = PojoAndStringConfig.class, inheritLocations = false) +public class AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { + /* all tests are in the parent class. */ +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigTestSuite.java new file mode 100644 index 00000000..f0dde3f7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigTestSuite.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.junit4.annotation; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * JUnit test suite for annotation-driven <em>configuration class</em> + * support in the Spring TestContext Framework. + * + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(Suite.class) +// Note: the following 'multi-line' layout is for enhanced code readability. +@SuiteClasses({// +AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.class,// + DefaultConfigClassesBaseTests.class,// + DefaultConfigClassesInheritedTests.class,// + BeanOverridingDefaultConfigClassesInheritedTests.class,// + ExplicitConfigClassesBaseTests.class,// + ExplicitConfigClassesInheritedTests.class,// + BeanOverridingExplicitConfigClassesInheritedTests.class,// + DefaultLoaderDefaultConfigClassesBaseTests.class,// + DefaultLoaderDefaultConfigClassesInheritedTests.class,// + DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.class,// + DefaultLoaderExplicitConfigClassesBaseTests.class,// + DefaultLoaderExplicitConfigClassesInheritedTests.class,// + DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.class // +}) +public class AnnotationConfigTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java new file mode 100644 index 00000000..a0c51ded --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework. + * + * <p>Configuration will be loaded from {@link DefaultConfigClassesBaseTests.ContextConfiguration} + * and {@link BeanOverridingDefaultConfigClassesInheritedTests.ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + */ +@ContextConfiguration +public class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { + + @Configuration + static class ContextConfiguration { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("Yoda"); + employee.setAge(900); + employee.setCompany("The Force"); + return employee; + } + } + + + @Test + @Override + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("The employee bean should have been overridden.", "Yoda", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java new file mode 100644 index 00000000..54a9dd26 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.test.context.ContextConfiguration; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework. + * + * <p>Configuration will be loaded from {@link DefaultConfigClassesBaseTests.ContextConfiguration} + * and {@link BeanOverridingDefaultConfigClassesInheritedTests.ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + */ +@ContextConfiguration(classes = BeanOverridingDefaultConfigClassesInheritedTests.ContextConfiguration.class) +public class BeanOverridingExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { + + @Test + @Override + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("The employee bean should have been overridden.", "Yoda", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesBaseTests.java new file mode 100644 index 00000000..e8a440a2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesBaseTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework. + * + * <p>Configuration will be loaded from {@link DefaultConfigClassesBaseTests.ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + * @see DefaultLoaderDefaultConfigClassesBaseTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class) +public class DefaultConfigClassesBaseTests { + + @Configuration + static class ContextConfiguration { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("John Smith"); + employee.setAge(42); + employee.setCompany("Acme Widgets, Inc."); + return employee; + } + } + + + @Autowired + protected Employee employee; + + + @Test + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee field should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesInheritedTests.java new file mode 100644 index 00000000..05c74a29 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesInheritedTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework. + * + * <p>Configuration will be loaded from {@link DefaultConfigClassesBaseTests.ContextConfiguration} + * and {@link DefaultConfigClassesInheritedTests.ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + */ +@ContextConfiguration +public class DefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { + + @Configuration + static class ContextConfiguration { + + @Bean + public Pet pet() { + return new Pet("Fido"); + } + } + + + @Autowired + private Pet pet; + + + @Test + public void verifyPetSetFromExtendedContextConfig() { + assertNotNull("The pet should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java new file mode 100644 index 00000000..7af6a541 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework in conjunction with the + * {@link DelegatingSmartContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + */ +@ContextConfiguration +public class DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests extends + DefaultLoaderDefaultConfigClassesBaseTests { + + @Configuration + static class Config { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("Yoda"); + employee.setAge(900); + employee.setCompany("The Force"); + return employee; + } + } + + + @Test + @Override + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("The employee bean should have been overridden.", "Yoda", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java new file mode 100644 index 00000000..26dd5f47 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework in conjunction with the + * {@link DelegatingSmartContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + */ +@ContextConfiguration(classes = DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.Config.class) +public class DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests extends + DefaultLoaderExplicitConfigClassesBaseTests { + + @Test + @Override + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("The employee bean should have been overridden.", "Yoda", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java new file mode 100644 index 00000000..00da19f1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework in conjunction with the + * {@link DelegatingSmartContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + * @see DefaultConfigClassesBaseTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class DefaultLoaderDefaultConfigClassesBaseTests { + + @Configuration + static class Config { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("John Smith"); + employee.setAge(42); + employee.setCompany("Acme Widgets, Inc."); + return employee; + } + } + + + @Autowired + protected Employee employee; + + + @Test + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee field should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java new file mode 100644 index 00000000..9b4793e4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework in conjunction with the + * {@link DelegatingSmartContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + */ +@ContextConfiguration +public class DefaultLoaderDefaultConfigClassesInheritedTests extends DefaultLoaderDefaultConfigClassesBaseTests { + + @Configuration + static class Config { + + @Bean + public Pet pet() { + return new Pet("Fido"); + } + } + + + @Autowired + private Pet pet; + + + @Test + public void verifyPetSetFromExtendedContextConfig() { + assertNotNull("The pet should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java new file mode 100644 index 00000000..6613a695 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework in conjunction with the + * {@link DelegatingSmartContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultLoaderDefaultConfigClassesBaseTests.Config.class) +public class DefaultLoaderExplicitConfigClassesBaseTests { + + @Autowired + protected Employee employee; + + + @Test + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java new file mode 100644 index 00000000..a0f7668a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework in conjunction with the + * {@link DelegatingSmartContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultLoaderDefaultConfigClassesInheritedTests.Config.class) +public class DefaultLoaderExplicitConfigClassesInheritedTests extends DefaultLoaderExplicitConfigClassesBaseTests { + + @Autowired + private Pet pet; + + + @Test + public void verifyPetSetFromExtendedContextConfig() { + assertNotNull("The pet should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesBaseTests.java new file mode 100644 index 00000000..7da5e68b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesBaseTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework. + * + * <p>Configuration will be loaded from {@link DefaultConfigClassesBaseTests.ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesBaseTests.ContextConfiguration.class) +public class ExplicitConfigClassesBaseTests { + + @Autowired + protected Employee employee; + + + @Test + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesInheritedTests.java new file mode 100644 index 00000000..cca20f48 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesInheritedTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +/** + * Integration tests that verify support for configuration classes in + * the Spring TestContext Framework. + * + * <p>Configuration will be loaded from {@link DefaultConfigClassesInheritedTests.ContextConfiguration} + * and {@link DefaultConfigClassesBaseTests.ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesInheritedTests.ContextConfiguration.class) +public class ExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { + + @Autowired + private Pet pet; + + + @Test + public void verifyPetSetFromExtendedContextConfig() { + assertNotNull("The pet should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/PojoAndStringConfig.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/PojoAndStringConfig.java new file mode 100644 index 00000000..19eefe38 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/PojoAndStringConfig.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.annotation; + +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * ApplicationContext configuration class for various integration tests. + * + * <p>The beans defined in this configuration class map directly to the + * beans defined in {@code SpringJUnit4ClassRunnerAppCtxTests-context.xml}. + * Consequently, the application contexts loaded from these two sources + * should be identical with regard to bean definitions. + * + * @author Sam Brannen + * @since 3.1 + */ +@Configuration +public class PojoAndStringConfig { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("John Smith"); + employee.setAge(42); + employee.setCompany("Acme Widgets, Inc."); + return employee; + } + + @Bean + public Pet pet() { + return new Pet("Fido"); + } + + @Bean + public String foo() { + return "Foo"; + } + + @Bean + public String bar() { + return "Bar"; + } + + @Bean + public String quux() { + return "Quux"; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests-context.xml new file mode 100644 index 00000000..d1f5ca78 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests-context.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" + xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd"> + + <context:component-scan base-package="org.springframework.test.context.junit4.orm" /> + + <tx:annotation-driven /> + + <jdbc:embedded-database id="dataSource" type="HSQL"> + <jdbc:script location="classpath:/org/springframework/test/context/junit4/orm/db-schema.sql" /> + <jdbc:script location="classpath:/org/springframework/test/context/junit4/orm/db-test-data.sql" /> + </jdbc:embedded-database> + + <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean" + p:dataSource-ref="dataSource"> + <property name="hibernateProperties"> + <props> + <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop> + <prop key="hibernate.show_sql">false</prop> + </props> + </property> + <property name="mappingResources"> + <list> + <value>org/springframework/test/context/junit4/orm/domain/Person.hbm.xml</value> + <value>org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml</value> + </list> + </property> + </bean> + + <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager" + p:sessionFactory-ref="sessionFactory" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java new file mode 100644 index 00000000..69aecb72 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.orm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import org.hibernate.SessionFactory; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.GenericJDBCException; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests; +import org.springframework.test.context.junit4.orm.domain.DriversLicense; +import org.springframework.test.context.junit4.orm.domain.Person; +import org.springframework.test.context.junit4.orm.service.PersonService; + +/** + * Transactional integration tests regarding <i>manual</i> session flushing with + * Hibernate. + * + * @author Sam Brannen + * @since 3.0 + */ +@ContextConfiguration +public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4SpringContextTests { + + private static final String SAM = "Sam"; + private static final String JUERGEN = "Juergen"; + + @Autowired + private PersonService personService; + + @Autowired + private SessionFactory sessionFactory; + + + protected int countRowsInPersonTable() { + return countRowsInTable("person"); + } + + protected void assertPersonCount(int expectedCount) { + assertEquals("Verifying number of rows in the 'person' table.", expectedCount, countRowsInPersonTable()); + } + + @Before + public void setUp() { + assertInTransaction(true); + assertNotNull("PersonService should have been autowired.", personService); + assertNotNull("SessionFactory should have been autowired.", sessionFactory); + } + + @Test + public void findSam() { + Person sam = personService.findByName(SAM); + assertNotNull("Should be able to find Sam", sam); + DriversLicense driversLicense = sam.getDriversLicense(); + assertNotNull("Sam's driver's license should not be null", driversLicense); + assertEquals("Verifying Sam's driver's license number", new Long(1234), driversLicense.getNumber()); + } + + @Test + public void saveJuergenWithDriversLicense() { + DriversLicense driversLicense = new DriversLicense(2L, 2222L); + Person juergen = new Person(JUERGEN, driversLicense); + int numRows = countRowsInPersonTable(); + personService.save(juergen); + assertPersonCount(numRows + 1); + assertNotNull("Should be able to save and retrieve Juergen", personService.findByName(JUERGEN)); + assertNotNull("Juergen's ID should have been set", juergen.getId()); + } + + @Test(expected = ConstraintViolationException.class) + public void saveJuergenWithNullDriversLicense() { + personService.save(new Person(JUERGEN)); + } + + private void updateSamWithNullDriversLicense() { + Person sam = personService.findByName(SAM); + assertNotNull("Should be able to find Sam", sam); + sam.setDriversLicense(null); + personService.save(sam); + } + + @Test + // no expected exception! + public void updateSamWithNullDriversLicenseWithoutSessionFlush() { + updateSamWithNullDriversLicense(); + // False positive, since an exception will be thrown once the session is + // finally flushed (i.e., in production code) + } + + @Test(expected = GenericJDBCException.class) + public void updateSamWithNullDriversLicenseWithSessionFlush() { + updateSamWithNullDriversLicense(); + // Manual flush is required to avoid false positive in test + sessionFactory.getCurrentSession().flush(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/db-schema.sql b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/db-schema.sql new file mode 100644 index 00000000..960b101e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/db-schema.sql @@ -0,0 +1,16 @@ +DROP TABLE drivers_license IF EXISTS; +DROP TABLE person IF EXISTS; + +CREATE TABLE person ( + id INTEGER NOT NULL IDENTITY PRIMARY KEY, + name VARCHAR(50) NOT NULL, + drivers_license_id INTEGER NOT NULL +); +CREATE UNIQUE INDEX person_name ON person(name); +CREATE UNIQUE INDEX person_drivers_license_id ON person(drivers_license_id); + +CREATE TABLE drivers_license ( + id INTEGER NOT NULL IDENTITY PRIMARY KEY, + license_number INTEGER NOT NULL +); +CREATE UNIQUE INDEX drivers_license_license_number ON drivers_license(license_number); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/db-test-data.sql b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/db-test-data.sql new file mode 100644 index 00000000..a174e3fb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/db-test-data.sql @@ -0,0 +1,3 @@ +INSERT INTO drivers_license(id, license_number) values(1, 1234); + +INSERT INTO person(id, name, drivers_license_id) values(1, 'Sam', 1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml new file mode 100644 index 00000000..02525630 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" + "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> + +<hibernate-mapping auto-import="true" default-lazy="false"> + + <class name="org.springframework.test.context.junit4.orm.domain.DriversLicense" table="drivers_license"> + <id name="id" column="id"> + <generator class="identity" /> + </id> + <property name="number" column="license_number" /> + </class> + +</hibernate-mapping> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.java new file mode 100644 index 00000000..dc06571f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.orm.domain; + +/** + * DriversLicense POJO. + * + * @author Sam Brannen + * @since 3.0 + */ +public class DriversLicense { + + private Long id; + + private Long number; + + + public DriversLicense() { + } + + public DriversLicense(Long number) { + this(null, number); + } + + public DriversLicense(Long id, Long number) { + this.id = id; + this.number = number; + } + + public Long getId() { + return this.id; + } + + protected void setId(Long id) { + this.id = id; + } + + public Long getNumber() { + return this.number; + } + + public void setNumber(Long number) { + this.number = number; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.hbm.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.hbm.xml new file mode 100644 index 00000000..aa6478b9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.hbm.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" + "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> + +<hibernate-mapping auto-import="true" default-lazy="false"> + + <class name="org.springframework.test.context.junit4.orm.domain.Person" table="person"> + <id name="id" column="id"> + <generator class="identity" /> + </id> + <property name="name" column="name" /> + <many-to-one name="driversLicense" class="org.springframework.test.context.junit4.orm.domain.DriversLicense" + column="drivers_license_id" unique="true" /> + </class> + +</hibernate-mapping> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.java new file mode 100644 index 00000000..c6f765ea --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.orm.domain; + +/** + * Person POJO. + * + * @author Sam Brannen + * @since 3.0 + */ +public class Person { + + private Long id; + private String name; + private DriversLicense driversLicense; + + + public Person() { + } + + public Person(Long id) { + this(id, null, null); + } + + public Person(String name) { + this(name, null); + } + + public Person(String name, DriversLicense driversLicense) { + this(null, name, driversLicense); + } + + public Person(Long id, String name, DriversLicense driversLicense) { + this.id = id; + this.name = name; + this.driversLicense = driversLicense; + } + + public Long getId() { + return this.id; + } + + protected void setId(Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public DriversLicense getDriversLicense() { + return this.driversLicense; + } + + public void setDriversLicense(DriversLicense driversLicense) { + this.driversLicense = driversLicense; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/PersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/PersonRepository.java new file mode 100644 index 00000000..409aade5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/PersonRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.orm.repository; + +import org.springframework.test.context.junit4.orm.domain.Person; + +/** + * Person Repository API. + * + * @author Sam Brannen + * @since 3.0 + */ +public interface PersonRepository { + + Person findByName(String name); + + Person save(Person person); + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/hibernate/HibernatePersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/hibernate/HibernatePersonRepository.java new file mode 100644 index 00000000..eac2522c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/hibernate/HibernatePersonRepository.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.orm.repository.hibernate; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.test.context.junit4.orm.domain.Person; +import org.springframework.test.context.junit4.orm.repository.PersonRepository; + +/** + * Hibernate implementation of the {@link PersonRepository} API. + * + * @author Sam Brannen + * @since 3.0 + */ +@Repository +public class HibernatePersonRepository implements PersonRepository { + + private final SessionFactory sessionFactory; + + + @Autowired + public HibernatePersonRepository(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + public Person save(Person person) { + this.sessionFactory.getCurrentSession().save(person); + return person; + } + + @Override + public Person findByName(String name) { + return (Person) this.sessionFactory.getCurrentSession().createQuery( + "from Person person where person.name = :name").setString("name", name).uniqueResult(); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/PersonService.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/PersonService.java new file mode 100644 index 00000000..76421e37 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/PersonService.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.orm.service; + +import org.springframework.test.context.junit4.orm.domain.Person; + +/** + * Person Service API. + * + * @author Sam Brannen + * @since 3.0 + */ +public interface PersonService { + + Person findByName(String name); + + Person save(Person person); + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/impl/StandardPersonService.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/impl/StandardPersonService.java new file mode 100644 index 00000000..92602e15 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/impl/StandardPersonService.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.orm.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit4.orm.domain.Person; +import org.springframework.test.context.junit4.orm.repository.PersonRepository; +import org.springframework.test.context.junit4.orm.service.PersonService; +import org.springframework.transaction.annotation.Transactional; + +/** + * Standard implementation of the {@link PersonService} API. + * + * @author Sam Brannen + * @since 3.0 + */ +@Service +@Transactional(readOnly = true) +public class StandardPersonService implements PersonService { + + private final PersonRepository personRepository; + + + @Autowired + public StandardPersonService(PersonRepository personRepository) { + this.personRepository = personRepository; + } + + @Override + public Person findByName(String name) { + return this.personRepository.findByName(name); + } + + @Override + @Transactional(readOnly = false) + public Person save(Person person) { + return this.personRepository.save(person); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileAnnotationConfigTests.java new file mode 100644 index 00000000..836dca41 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileAnnotationConfigTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.profile.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +/** + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { DefaultProfileConfig.class, DevProfileConfig.class }, loader = AnnotationConfigContextLoader.class) +public class DefaultProfileAnnotationConfigTests { + + @Autowired + protected Pet pet; + + @Autowired(required = false) + protected Employee employee; + + + @Test + public void pet() { + assertNotNull(pet); + assertEquals("Fido", pet.getName()); + } + + @Test + public void employee() { + assertNull("employee bean should not be created for the default profile", employee); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileConfig.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileConfig.java new file mode 100644 index 00000000..44b5675a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.profile.annotation; + +import org.springframework.tests.sample.beans.Pet; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Sam Brannen + * @since 3.1 + */ +@Configuration +public class DefaultProfileConfig { + + @Bean + public Pet pet() { + return new Pet("Fido"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileAnnotationConfigTests.java new file mode 100644 index 00000000..813cc828 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileAnnotationConfigTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011 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 + * + * http://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.junit4.profile.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; + +/** + * @author Sam Brannen + * @since 3.1 + */ +@ActiveProfiles("dev") +public class DevProfileAnnotationConfigTests extends DefaultProfileAnnotationConfigTests { + + @Test + @Override + public void employee() { + assertNotNull("employee bean should be loaded for the 'dev' profile", employee); + assertEquals("John Smith", employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileConfig.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileConfig.java new file mode 100644 index 00000000..cafc8b6e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.profile.annotation; + +import org.springframework.tests.sample.beans.Employee; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +/** + * @author Sam Brannen + * @since 3.1 + */ +@Profile("dev") +@Configuration +public class DevProfileConfig { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("John Smith"); + employee.setAge(42); + employee.setCompany("Acme Widgets, Inc."); + return employee; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/ProfileAnnotationConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/ProfileAnnotationConfigTestSuite.java new file mode 100644 index 00000000..b3b7068c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/ProfileAnnotationConfigTestSuite.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.profile.annotation; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * JUnit test suite for <em>bean definition profile</em> support in the + * Spring TestContext Framework with annotation-based configuration. + * + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(Suite.class) +// Note: the following 'multi-line' layout is for enhanced code readability. +@SuiteClasses({// +DefaultProfileAnnotationConfigTests.class,// + DevProfileAnnotationConfigTests.class // +}) +public class ProfileAnnotationConfigTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileAnnotationConfigTests.java new file mode 100644 index 00000000..07a8d2d5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileAnnotationConfigTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.profile.importresource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Juergen Hoeller + * @since 3.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultProfileConfig.class) +public class DefaultProfileAnnotationConfigTests { + + @Autowired + protected Pet pet; + + @Autowired(required = false) + protected Employee employee; + + + @Test + public void pet() { + assertNotNull(pet); + assertEquals("Fido", pet.getName()); + } + + @Test + public void employee() { + assertNull("employee bean should not be created for the default profile", employee); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileConfig.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileConfig.java new file mode 100644 index 00000000..818212dd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.profile.importresource; + +import org.springframework.tests.sample.beans.Pet; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; + +/** + * @author Juergen Hoeller + * @since 3.1 + */ +@Configuration +@ImportResource("org/springframework/test/context/junit4/profile/importresource/import.xml") +public class DefaultProfileConfig { + + @Bean + public Pet pet() { + return new Pet("Fido"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileAnnotationConfigTests.java new file mode 100644 index 00000000..9e6ff731 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileAnnotationConfigTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.profile.importresource; + +import org.junit.Test; + +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.Assert.*; + +/** + * @author Juergen Hoeller + * @since 3.1 + */ +@ActiveProfiles("dev") +public class DevProfileAnnotationConfigTests extends DefaultProfileAnnotationConfigTests { + + @Test + @Override + public void employee() { + assertNotNull("employee bean should be loaded for the 'dev' profile", employee); + assertEquals("John Smith", employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/import.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/import.xml new file mode 100644 index 00000000..3861d0ec --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/import.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <beans profile="dev"> + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + </beans> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml new file mode 100644 index 00000000..0e672de0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Fido" /> + </bean> + + <beans profile="dev"> + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + </beans> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests.java new file mode 100644 index 00000000..1ed4f546 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.profile.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class DefaultProfileXmlConfigTests { + + @Autowired + protected Pet pet; + + @Autowired(required = false) + protected Employee employee; + + + @Test + public void pet() { + assertNotNull(pet); + assertEquals("Fido", pet.getName()); + } + + @Test + public void employee() { + assertNull("employee bean should not be created for the default profile", employee); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileXmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileXmlConfigTests.java new file mode 100644 index 00000000..43d6a5a7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileXmlConfigTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011 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 + * + * http://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.junit4.profile.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; + +/** + * @author Sam Brannen + * @since 3.1 + */ +@ActiveProfiles("dev") +public class DevProfileXmlConfigTests extends DefaultProfileXmlConfigTests { + + @Test + @Override + public void employee() { + assertNotNull("employee bean should be loaded for the 'dev' profile", employee); + assertEquals("John Smith", employee.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/ProfileXmlConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/ProfileXmlConfigTestSuite.java new file mode 100644 index 00000000..38a5939c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/ProfileXmlConfigTestSuite.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.junit4.profile.xml; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * JUnit test suite for <em>bean definition profile</em> support in the + * Spring TestContext Framework with XML-based configuration. + * + * @author Sam Brannen + * @since 3.1 + */ +@RunWith(Suite.class) +// Note: the following 'multi-line' layout is for enhanced code readability. +@SuiteClasses({// +DefaultProfileXmlConfigTests.class,// + DevProfileXmlConfigTests.class // +}) +public class ProfileXmlConfigTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests-context.xml new file mode 100644 index 00000000..7238260b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests-context.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="Yoda" /> + <property name="age" value="900" /> + <property name="company" value="The Force" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests.java new file mode 100644 index 00000000..cf46ce66 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007 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 + * + * http://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.junit4.spr3896; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.test.context.ContextConfiguration; + +/** + * JUnit 4 based integration test for verifying support for the + * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of + * {@link ContextConfiguration @ContextConfiguration} indirectly proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@ContextConfiguration +public class BeanOverridingDefaultLocationsInheritedTests extends DefaultLocationsBaseTests { + + @Test + @Override + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("The employee bean should have been overridden.", "Yoda", this.employee.getName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingExplicitLocationsInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingExplicitLocationsInheritedTests.java new file mode 100644 index 00000000..5d4a3ef0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingExplicitLocationsInheritedTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007 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 + * + * http://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.junit4.spr3896; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.test.context.ContextConfiguration; + +/** + * JUnit 4 based integration test for verifying support for the + * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of + * {@link ContextConfiguration @ContextConfiguration} indirectly proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@ContextConfiguration("BeanOverridingDefaultLocationsInheritedTests-context.xml") +public class BeanOverridingExplicitLocationsInheritedTests extends ExplicitLocationsBaseTests { + + @Test + @Override + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("The employee bean should have been overridden.", "Yoda", this.employee.getName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests-context.xml new file mode 100644 index 00000000..b40fed69 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests-context.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="employee" class="org.springframework.tests.sample.beans.Employee"> + <property name="name" value="John Smith" /> + <property name="age" value="42" /> + <property name="company" value="Acme Widgets, Inc." /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests.java new file mode 100644 index 00000000..7131201d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr3896; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * JUnit 4 based integration test for verifying support for the + * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of + * {@link ContextConfiguration @ContextConfiguration} indirectly proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class DefaultLocationsBaseTests { + + @Autowired + protected Employee employee; + + + @Test + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests-context.xml new file mode 100644 index 00000000..f6a90a62 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests-context.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet"> + <constructor-arg value="Fido" /> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests.java new file mode 100644 index 00000000..1f488ec2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr3896; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +/** + * JUnit 4 based integration test for verifying support for the + * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of + * {@link ContextConfiguration @ContextConfiguration} indirectly proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@ContextConfiguration +public class DefaultLocationsInheritedTests extends DefaultLocationsBaseTests { + + @Autowired + private Pet pet; + + + @Test + public void verifyPetSetFromExtendedContextConfig() { + assertNotNull("The pet should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/ExplicitLocationsBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/ExplicitLocationsBaseTests.java new file mode 100644 index 00000000..cd81b6be --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/ExplicitLocationsBaseTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr3896; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * JUnit 4 based integration test for verifying support for the + * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of + * {@link ContextConfiguration @ContextConfiguration} indirectly proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("DefaultLocationsBaseTests-context.xml") +public class ExplicitLocationsBaseTests { + + @Autowired + protected Employee employee; + + + @Test + public void verifyEmployeeSetFromBaseContextConfig() { + assertNotNull("The employee should have been autowired.", this.employee); + assertEquals("John Smith", this.employee.getName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/ExplicitLocationsInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/ExplicitLocationsInheritedTests.java new file mode 100644 index 00000000..b0176425 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/ExplicitLocationsInheritedTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr3896; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +/** + * JUnit 4 based integration test for verifying support for the + * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of + * {@link ContextConfiguration @ContextConfiguration} indirectly proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@ContextConfiguration("DefaultLocationsInheritedTests-context.xml") +public class ExplicitLocationsInheritedTests extends ExplicitLocationsBaseTests { + + @Autowired + private Pet pet; + + + @Test + public void verifyPetSetFromExtendedContextConfig() { + assertNotNull("The pet should have been autowired.", this.pet); + assertEquals("Fido", this.pet.getName()); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/Spr3896SuiteTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/Spr3896SuiteTests.java new file mode 100644 index 00000000..5b399b58 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/Spr3896SuiteTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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 + * + * http://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.junit4.spr3896; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * JUnit 4 based test suite for functionality proposed in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3896" + * target="_blank">SPR-3896</a>. + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(Suite.class) +// Note: the following 'multi-line' layout is for enhanced code readability. +@SuiteClasses({ + +DefaultLocationsBaseTests.class, + +DefaultLocationsInheritedTests.class, + +ExplicitLocationsBaseTests.class, + +ExplicitLocationsInheritedTests.class, + +BeanOverridingDefaultLocationsInheritedTests.class, + +BeanOverridingExplicitLocationsInheritedTests.class + +}) +public class Spr3896SuiteTests { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/Jsr250LifecycleTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/Jsr250LifecycleTests.java new file mode 100644 index 00000000..44b7d1bd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/Jsr250LifecycleTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr4868; + +import static org.junit.Assert.assertNotNull; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +/** + * Integration tests that investigate the applicability of JSR-250 lifecycle + * annotations in test classes. + * + * <p>This class does not really contain actual <em>tests</em> per se. Rather it + * can be used to empirically verify the expected log output (see below). In + * order to see the log output, one would naturally need to ensure that the + * logger category for this class is enabled at {@code INFO} level. + * + * <h4>Expected Log Output</h4> + * <pre> + * INFO : org.springframework.test.context.junit4.spr4868.LifecycleBean - initializing + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - beforeAllTests() + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - setUp() + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - test1() + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - tearDown() + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - beforeAllTests() + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - setUp() + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - test2() + * INFO : org.springframework.test.context.junit4.spr4868.ExampleTest - tearDown() + * INFO : org.springframework.test.context.junit4.spr4868.LifecycleBean - destroying + * </pre> + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class }) +@ContextConfiguration +public class Jsr250LifecycleTests { + + private final Log logger = LogFactory.getLog(Jsr250LifecycleTests.class); + + + @Configuration + static class Config { + + @Bean + public LifecycleBean lifecycleBean() { + return new LifecycleBean(); + } + } + + + @Autowired + private LifecycleBean lifecycleBean; + + + @PostConstruct + public void beforeAllTests() { + logger.info("beforeAllTests()"); + } + + @PreDestroy + public void afterTestSuite() { + logger.info("afterTestSuite()"); + } + + @Before + public void setUp() throws Exception { + logger.info("setUp()"); + } + + @After + public void tearDown() throws Exception { + logger.info("tearDown()"); + } + + @Test + public void test1() { + logger.info("test1()"); + assertNotNull(lifecycleBean); + } + + @Test + public void test2() { + logger.info("test2()"); + assertNotNull(lifecycleBean); + } + +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/LifecycleBean.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/LifecycleBean.java new file mode 100644 index 00000000..1b8dda70 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/LifecycleBean.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr4868; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Sam Brannen + * @since 3.2 + */ +class LifecycleBean { + + private final Log logger = LogFactory.getLog(LifecycleBean.class); + + + @PostConstruct + public void init() { + logger.info("initializing"); + } + + @PreDestroy + public void destroy() { + logger.info("destroying"); + } + +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests-context.xml new file mode 100644 index 00000000..6b39409b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests-context.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="foo" class="java.lang.String"> + <constructor-arg value="normal" /> + </bean> + + <bean id="customFoo" class="java.lang.String"> + <constructor-arg value="custom" /> + </bean> + +</beans>
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java new file mode 100644 index 00000000..6bdeeee2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr6128; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests to verify claims made in <a + * href="http://jira.springframework.org/browse/SPR-6128" + * target="_blank">SPR-6128</a>. + * + * @author Sam Brannen + * @author Chris Beams + * @since 3.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class AutowiredQualifierTests { + + @Autowired + private String foo; + + @Autowired + @Qualifier("customFoo") + private String customFoo; + + + @Test + public void test() { + assertThat(foo, equalTo("normal")); + assertThat(customFoo, equalTo("custom")); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/Spr8849Tests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/Spr8849Tests.java new file mode 100644 index 00000000..dd589867 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/Spr8849Tests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr8849; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * Test suite to investigate claims raised in + * <a href="https://jira.springsource.org/browse/SPR-8849">SPR-8849</a>. + * + * <p>By using a SpEL expression to generate a random {@code id} for the + * embedded database (see {@code datasource-config.xml}), we ensure that each + * {@code ApplicationContext} that imports the common configuration will create + * an embedded database with a unique name (since the {@code id} is used as the + * database name within + * {@link org.springframework.jdbc.config.EmbeddedDatabaseBeanDefinitionParser#useIdAsDatabaseNameIfGiven()}). + * + * <p>To reproduce the problem mentioned in SPEX-8849, change the {@code id} of + * the embedded database in {@code datasource-config.xml} to "dataSource" (or + * anything else that is not random) and run this <em>suite</em>. + * + * @author Sam Brannen + * @since 3.2 + */ +@SuppressWarnings("javadoc") +@RunWith(Suite.class) +@SuiteClasses({ TestClass1.class, TestClass2.class }) +public class Spr8849Tests { + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1-context.xml new file mode 100644 index 00000000..63314700 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1-context.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <import resource="datasource-config.xml"/> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1.java new file mode 100644 index 00000000..b45b6d76 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr8849; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * This name of this class intentionally does not end with "Test" or "Tests" + * since it should only be run as part of the test suite: {@link Spr8849Tests}. + * + * @author Mickael Leduque + * @author Sam Brannen + * @since 3.2 + * @see Spr8849Tests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class TestClass1 { + + @Autowired + DataSource datasource; + + + @Test + public void dummyTest() { + // it's sufficient if the ApplicationContext loads without errors. + } + +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2-context.xml new file mode 100644 index 00000000..63314700 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2-context.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <import resource="datasource-config.xml"/> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2.java new file mode 100644 index 00000000..bc8a7800 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr8849; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * This name of this class intentionally does not end with "Test" or "Tests" + * since it should only be run as part of the test suite: {@link Spr8849Tests}. + * + * @author Mickael Leduque + * @author Sam Brannen + * @since 3.2 + * @see Spr8849Tests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class TestClass2 { + + @Autowired + DataSource dataSource; + + + @Test + public void dummyTest() { + // it's sufficient if the ApplicationContext loads without errors. + } + +}
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/datasource-config.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/datasource-config.xml new file mode 100644 index 00000000..ed82b6f3 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/datasource-config.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd"> + + <jdbc:embedded-database id="#{T(java.util.UUID).randomUUID().toString()}"> + <jdbc:script location="classpath:/org/springframework/test/context/junit4/spr8849/spr8849-schema.sql" /> + </jdbc:embedded-database> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/spr8849-schema.sql b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/spr8849-schema.sql new file mode 100644 index 00000000..a17d13a9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/spr8849-schema.sql @@ -0,0 +1,3 @@ +CREATE TABLE enigma ( + id INTEGER NOT NULL IDENTITY PRIMARY KEY +); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AbstractTransactionalAnnotatedConfigClassTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AbstractTransactionalAnnotatedConfigClassTests.java new file mode 100644 index 00000000..59cef2c1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AbstractTransactionalAnnotatedConfigClassTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9051; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; +import static org.springframework.test.transaction.TransactionTestUtils.inTransaction; + +import javax.sql.DataSource; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +/** + * This set of tests (i.e., all concrete subclasses) investigates the claims made in + * <a href="https://jira.springsource.org/browse/SPR-9051" target="_blank">SPR-9051</a> + * with regard to transactional tests. + * + * @author Sam Brannen + * @since 3.2 + * @see org.springframework.test.context.testng.AnnotationConfigTransactionalTestNGSpringContextTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) +public abstract class AbstractTransactionalAnnotatedConfigClassTests { + + protected static final String JANE = "jane"; + protected static final String SUE = "sue"; + protected static final String YODA = "yoda"; + + protected DataSource dataSourceFromTxManager; + protected DataSource dataSourceViaInjection; + + protected JdbcTemplate jdbcTemplate; + + @Autowired + private Employee employee; + + + @Autowired + public void setTransactionManager(DataSourceTransactionManager transactionManager) { + this.dataSourceFromTxManager = transactionManager.getDataSource(); + } + + @Autowired + public void setDataSource(DataSource dataSource) { + this.dataSourceViaInjection = dataSource; + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + private int countRowsInTable(String tableName) { + return jdbcTemplate.queryForObject("SELECT COUNT(0) FROM " + tableName, Integer.class); + } + + private int createPerson(String name) { + return jdbcTemplate.update("INSERT INTO person VALUES(?)", name); + } + + protected int deletePerson(String name) { + return jdbcTemplate.update("DELETE FROM person WHERE name=?", name); + } + + protected void assertNumRowsInPersonTable(int expectedNumRows, String testState) { + assertEquals("the number of rows in the person table (" + testState + ").", expectedNumRows, + countRowsInTable("person")); + } + + protected void assertAddPerson(final String name) { + assertEquals("Adding '" + name + "'", 1, createPerson(name)); + } + + @Test + public void autowiringFromConfigClass() { + assertNotNull("The employee should have been autowired.", employee); + assertEquals("John Smith", employee.getName()); + } + + @BeforeTransaction + public void beforeTransaction() { + assertNumRowsInPersonTable(0, "before a transactional test method"); + assertAddPerson(YODA); + } + + @Before + public void setUp() throws Exception { + assertNumRowsInPersonTable((inTransaction() ? 1 : 0), "before a test method"); + } + + @Test + @Transactional + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertAddPerson(JANE); + assertAddPerson(SUE); + assertNumRowsInPersonTable(3, "in modifyTestDataWithinTransaction()"); + } + + @After + public void tearDown() throws Exception { + assertNumRowsInPersonTable((inTransaction() ? 3 : 0), "after a test method"); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals("Deleting yoda", 1, deletePerson(YODA)); + assertNumRowsInPersonTable(0, "after a transactional test method"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AnnotatedConfigClassesWithoutAtConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AnnotatedConfigClassesWithoutAtConfigurationTests.java new file mode 100644 index 00000000..e5b9bc0c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AnnotatedConfigClassesWithoutAtConfigurationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr9051; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * This set of tests refutes the claims made in + * <a href="https://jira.springsource.org/browse/SPR-9051" target="_blank">SPR-9051</a>. + * + * <p><b>The Claims</b>: + * + * <blockquote> + * When a {@code @ContextConfiguration} test class references a config class + * missing an {@code @Configuration} annotation, {@code @Bean} dependencies are + * wired successfully but the bean lifecycle is not applied (no init methods are + * invoked, for example). Adding the missing {@code @Configuration} annotation + * solves the problem, however the problem and solution isn't obvious since + * wiring/injection appeared to work. + * </blockquote> + * + * @author Sam Brannen + * @author Phillip Webb + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AnnotatedConfigClassesWithoutAtConfigurationTests.AnnotatedFactoryBeans.class) +public class AnnotatedConfigClassesWithoutAtConfigurationTests { + + /** + * This is intentionally <b>not</b> annotated with {@code @Configuration}. + * Consequently, this class contains what we call <i>annotated factory bean + * methods</i> instead of standard bean definition methods. + */ + static class AnnotatedFactoryBeans { + + static final AtomicInteger enigmaCallCount = new AtomicInteger(); + + + @Bean + public String enigma() { + return "enigma #" + enigmaCallCount.incrementAndGet(); + } + + @Bean + public LifecycleBean lifecycleBean() { + // The following call to enigma() literally invokes the local + // enigma() method, not a CGLIB proxied version, since these methods + // are essentially factory bean methods. + LifecycleBean bean = new LifecycleBean(enigma()); + assertFalse(bean.isInitialized()); + return bean; + } + } + + + @Autowired + private String enigma; + + @Autowired + private LifecycleBean lifecycleBean; + + + @Test + public void testSPR_9051() throws Exception { + assertNotNull(enigma); + assertNotNull(lifecycleBean); + assertTrue(lifecycleBean.isInitialized()); + Set<String> names = new HashSet<String>(); + names.add(enigma.toString()); + names.add(lifecycleBean.getName()); + assertEquals(names, new HashSet<String>(Arrays.asList("enigma #1", "enigma #2"))); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AtBeanLiteModeScopeTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AtBeanLiteModeScopeTests.java new file mode 100644 index 00000000..035bc9f2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AtBeanLiteModeScopeTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr9051; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests that verify proper scoping of beans created in + * <em>{@code @Bean} Lite Mode</em>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AtBeanLiteModeScopeTests.LiteBeans.class) +public class AtBeanLiteModeScopeTests { + + /** + * This is intentionally <b>not</b> annotated with {@code @Configuration}. + */ + static class LiteBeans { + + @Bean + public LifecycleBean singleton() { + LifecycleBean bean = new LifecycleBean("singleton"); + assertFalse(bean.isInitialized()); + return bean; + } + + @Bean + @Scope("prototype") + public LifecycleBean prototype() { + LifecycleBean bean = new LifecycleBean("prototype"); + assertFalse(bean.isInitialized()); + return bean; + } + } + + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + @Qualifier("singleton") + private LifecycleBean injectedSingletonBean; + + @Autowired + @Qualifier("prototype") + private LifecycleBean injectedPrototypeBean; + + + @Test + public void singletonLiteBean() { + assertNotNull(injectedSingletonBean); + assertTrue(injectedSingletonBean.isInitialized()); + + LifecycleBean retrievedSingletonBean = applicationContext.getBean("singleton", LifecycleBean.class); + assertNotNull(retrievedSingletonBean); + assertTrue(retrievedSingletonBean.isInitialized()); + + assertSame(injectedSingletonBean, retrievedSingletonBean); + } + + @Test + public void prototypeLiteBean() { + assertNotNull(injectedPrototypeBean); + assertTrue(injectedPrototypeBean.isInitialized()); + + LifecycleBean retrievedPrototypeBean = applicationContext.getBean("prototype", LifecycleBean.class); + assertNotNull(retrievedPrototypeBean); + assertTrue(retrievedPrototypeBean.isInitialized()); + + assertNotSame(injectedPrototypeBean, retrievedPrototypeBean); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/LifecycleBean.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/LifecycleBean.java new file mode 100644 index 00000000..07c0550c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/LifecycleBean.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr9051; + +import javax.annotation.PostConstruct; + +/** + * Simple POJO that contains lifecycle callbacks. + * + * @author Sam Brannen + * @since 3.2 + */ +public class LifecycleBean { + + private final String name; + + private boolean initialized = false; + + + public LifecycleBean(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @PostConstruct + public void init() { + initialized = true; + } + + public boolean isInitialized() { + return this.initialized; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java new file mode 100644 index 00000000..6e0f84f7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9051; + +import static org.junit.Assert.assertSame; + +import javax.sql.DataSource; + +import org.junit.Before; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Concrete implementation of {@link AbstractTransactionalAnnotatedConfigClassTests} + * that uses a true {@link Configuration @Configuration class}. + * + * @author Sam Brannen + * @since 3.2 + * @see TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests + */ +@ContextConfiguration +public class TransactionalAnnotatedConfigClassWithAtConfigurationTests extends + AbstractTransactionalAnnotatedConfigClassTests { + + /** + * This is <b>intentionally</b> annotated with {@code @Configuration}. + * + * <p>Consequently, this class contains standard singleton bean methods + * instead of <i>annotated factory bean methods</i>. + */ + @Configuration + static class Config { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("John Smith"); + employee.setAge(42); + employee.setCompany("Acme Widgets, Inc."); + return employee; + } + + @Bean + public PlatformTransactionManager transactionManager() { + return new DataSourceTransactionManager(dataSource()); + } + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder()// + .addScript("classpath:/org/springframework/test/context/junit4/spr9051/schema.sql")// + // Ensure that this in-memory database is only used by this class: + .setName(getClass().getName())// + .build(); + } + + } + + + @Before + public void compareDataSources() throws Exception { + // NOTE: the two DataSource instances ARE the same! + assertSame(dataSourceFromTxManager, dataSourceViaInjection); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java new file mode 100644 index 00000000..622f0dfe --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9051; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +import javax.sql.DataSource; + +import org.junit.Before; +import org.springframework.tests.sample.beans.Employee; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Concrete implementation of {@link AbstractTransactionalAnnotatedConfigClassTests} + * that does <b>not</b> use a true {@link Configuration @Configuration class} but + * rather a <em>lite mode</em> configuration class (see the Javadoc for {@link Bean @Bean} + * for details). + * + * @author Sam Brannen + * @since 3.2 + * @see Bean + * @see TransactionalAnnotatedConfigClassWithAtConfigurationTests + */ +@ContextConfiguration(classes = TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.AnnotatedFactoryBeans.class) +public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests extends + AbstractTransactionalAnnotatedConfigClassTests { + + /** + * This is intentionally <b>not</b> annotated with {@code @Configuration}. + * + * <p>Consequently, this class contains <i>annotated factory bean methods</i> + * instead of standard singleton bean methods. + */ + // @Configuration + static class AnnotatedFactoryBeans { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("John Smith"); + employee.setAge(42); + employee.setCompany("Acme Widgets, Inc."); + return employee; + } + + @Bean + public PlatformTransactionManager transactionManager() { + return new DataSourceTransactionManager(dataSource()); + } + + /** + * Since this method does not reside in a true {@code @Configuration class}, + * it acts as a factory method when invoked directly (e.g., from + * {@link #transactionManager()}) and as a singleton bean when retrieved + * through the application context (e.g., when injected into the test + * instance). The result is that this method will be called twice: + * + * <ol> + * <li>once <em>indirectly</em> by the {@link TransactionalTestExecutionListener} + * when it retrieves the {@link PlatformTransactionManager} from the + * application context</li> + * <li>and again when the {@link DataSource} is injected into the test + * instance in {@link AbstractTransactionalAnnotatedConfigClassTests#setDataSource(DataSource)}.</li> + *</ol> + * + * Consequently, the {@link JdbcTemplate} used by this test instance and + * the {@link PlatformTransactionManager} used by the Spring TestContext + * Framework will operate on two different {@code DataSource} instances, + * which is almost certainly not the desired or intended behavior. + */ + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder()// + .addScript("classpath:/org/springframework/test/context/junit4/spr9051/schema.sql")// + // Ensure that this in-memory database is only used by this class: + .setName(getClass().getName())// + .build(); + } + + } + + + @Before + public void compareDataSources() throws Exception { + // NOTE: the two DataSource instances are NOT the same! + assertNotSame(dataSourceFromTxManager, dataSourceViaInjection); + } + + /** + * Overrides {@code afterTransaction()} in order to assert a different result. + * + * <p>See in-line comments for details. + * + * @see AbstractTransactionalAnnotatedConfigClassTests#afterTransaction() + * @see AbstractTransactionalAnnotatedConfigClassTests#modifyTestDataWithinTransaction() + */ + @AfterTransaction + @Override + public void afterTransaction() { + assertEquals("Deleting yoda", 1, deletePerson(YODA)); + + // NOTE: We would actually expect that there are now ZERO entries in the + // person table, since the transaction is rolled back by the framework; + // however, since our JdbcTemplate and the transaction manager used by + // the Spring TestContext Framework use two different DataSource + // instances, our insert statements were executed in transactions that + // are not controlled by the test framework. Consequently, there was no + // rollback for the two insert statements in + // modifyTestDataWithinTransaction(). + // + assertNumRowsInPersonTable(2, "after a transactional test method"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/schema.sql b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/schema.sql new file mode 100644 index 00000000..81d7e08d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/schema.sql @@ -0,0 +1,6 @@ +DROP TABLE person IF EXISTS; + +CREATE TABLE person ( + name VARCHAR(20) NOT NULL, + PRIMARY KEY(name) +);
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9604/LookUpTxMgrViaTransactionManagementConfigurerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9604/LookUpTxMgrViaTransactionManagementConfigurerTests.java new file mode 100644 index 00000000..5dbba76a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9604/LookUpTxMgrViaTransactionManagementConfigurerTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9604; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.TransactionManagementConfigurer; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests that verify the behavior requested in + * <a href="https://jira.springsource.org/browse/SPR-9604">SPR-9604</a>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class LookUpTxMgrViaTransactionManagementConfigurerTests { + + private static final CallCountingTransactionManager txManager1 = new CallCountingTransactionManager(); + private static final CallCountingTransactionManager txManager2 = new CallCountingTransactionManager(); + + + @Configuration + static class Config implements TransactionManagementConfigurer { + + @Override + public PlatformTransactionManager annotationDrivenTransactionManager() { + return txManager1(); + } + + @Bean + public PlatformTransactionManager txManager1() { + return txManager1; + } + + @Bean + public PlatformTransactionManager txManager2() { + return txManager2; + } + } + + + @BeforeTransaction + public void beforeTransaction() { + txManager1.clear(); + txManager2.clear(); + } + + @Test + public void transactionalTest() { + assertEquals(1, txManager1.begun); + assertEquals(1, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(0, txManager1.rollbacks); + + assertEquals(0, txManager2.begun); + assertEquals(0, txManager2.inflight); + assertEquals(0, txManager2.commits); + assertEquals(0, txManager2.rollbacks); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(1, txManager1.begun); + assertEquals(0, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(1, txManager1.rollbacks); + + assertEquals(0, txManager2.begun); + assertEquals(0, txManager2.inflight); + assertEquals(0, txManager2.commits); + assertEquals(0, txManager2.rollbacks); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpNonexistentTxMgrTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpNonexistentTxMgrTests.java new file mode 100644 index 00000000..78720a8f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpNonexistentTxMgrTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9645; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Integration tests that verify the behavior requested in + * <a href="https://jira.springsource.org/browse/SPR-9645">SPR-9645</a>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class LookUpNonexistentTxMgrTests { + + private static final CallCountingTransactionManager txManager = new CallCountingTransactionManager(); + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager transactionManager() { + return txManager; + } + } + + @Test + public void nonTransactionalTest() { + assertEquals(0, txManager.begun); + assertEquals(0, txManager.inflight); + assertEquals(0, txManager.commits); + assertEquals(0, txManager.rollbacks); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndDefaultNameTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndDefaultNameTests.java new file mode 100644 index 00000000..331342d4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndDefaultNameTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9645; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests that verify the behavior requested in + * <a href="https://jira.springsource.org/browse/SPR-9645">SPR-9645</a>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class LookUpTxMgrByTypeAndDefaultNameTests { + + private static final CallCountingTransactionManager txManager1 = new CallCountingTransactionManager(); + private static final CallCountingTransactionManager txManager2 = new CallCountingTransactionManager(); + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager transactionManager() { + return txManager1; + } + + @Bean + public PlatformTransactionManager txManager2() { + return txManager2; + } + } + + @BeforeTransaction + public void beforeTransaction() { + txManager1.clear(); + txManager2.clear(); + } + + @Test + public void transactionalTest() { + assertEquals(1, txManager1.begun); + assertEquals(1, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(0, txManager1.rollbacks); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(1, txManager1.begun); + assertEquals(0, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(1, txManager1.rollbacks); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndNameTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndNameTests.java new file mode 100644 index 00000000..76a82b82 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndNameTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9645; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests that verify the behavior requested in + * <a href="https://jira.springsource.org/browse/SPR-9645">SPR-9645</a>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +@TransactionConfiguration(transactionManager = "txManager1") +public class LookUpTxMgrByTypeAndNameTests { + + private static final CallCountingTransactionManager txManager1 = new CallCountingTransactionManager(); + private static final CallCountingTransactionManager txManager2 = new CallCountingTransactionManager(); + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager txManager1() { + return txManager1; + } + + @Bean + public PlatformTransactionManager txManager2() { + return txManager2; + } + } + + @BeforeTransaction + public void beforeTransaction() { + txManager1.clear(); + txManager2.clear(); + } + + @Test + public void transactionalTest() { + assertEquals(1, txManager1.begun); + assertEquals(1, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(0, txManager1.rollbacks); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(1, txManager1.begun); + assertEquals(0, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(1, txManager1.rollbacks); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndQualifierAtClassLevelTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndQualifierAtClassLevelTests.java new file mode 100644 index 00000000..8492b02d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndQualifierAtClassLevelTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9645; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests that verify the behavior requested in + * <a href="https://jira.springsource.org/browse/SPR-9645">SPR-9645</a>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional("txManager1") +public class LookUpTxMgrByTypeAndQualifierAtClassLevelTests { + + private static final CallCountingTransactionManager txManager1 = new CallCountingTransactionManager(); + private static final CallCountingTransactionManager txManager2 = new CallCountingTransactionManager(); + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager txManager1() { + return txManager1; + } + + @Bean + public PlatformTransactionManager txManager2() { + return txManager2; + } + } + + @BeforeTransaction + public void beforeTransaction() { + txManager1.clear(); + txManager2.clear(); + } + + @Test + public void transactionalTest() { + assertEquals(1, txManager1.begun); + assertEquals(1, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(0, txManager1.rollbacks); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(1, txManager1.begun); + assertEquals(0, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(1, txManager1.rollbacks); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndQualifierAtMethodLevelTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndQualifierAtMethodLevelTests.java new file mode 100644 index 00000000..363d47a9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeAndQualifierAtMethodLevelTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9645; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests that verify the behavior requested in + * <a href="https://jira.springsource.org/browse/SPR-9645">SPR-9645</a>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class LookUpTxMgrByTypeAndQualifierAtMethodLevelTests { + + private static final CallCountingTransactionManager txManager1 = new CallCountingTransactionManager(); + private static final CallCountingTransactionManager txManager2 = new CallCountingTransactionManager(); + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager txManager1() { + return txManager1; + } + + @Bean + public PlatformTransactionManager txManager2() { + return txManager2; + } + } + + @BeforeTransaction + public void beforeTransaction() { + txManager1.clear(); + txManager2.clear(); + } + + @Transactional("txManager1") + @Test + public void transactionalTest() { + assertEquals(1, txManager1.begun); + assertEquals(1, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(0, txManager1.rollbacks); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(1, txManager1.begun); + assertEquals(0, txManager1.inflight); + assertEquals(0, txManager1.commits); + assertEquals(1, txManager1.rollbacks); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeTests.java new file mode 100644 index 00000000..957533f9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9645/LookUpTxMgrByTypeTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.junit4.spr9645; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests that verify the behavior requested in + * <a href="https://jira.springsource.org/browse/SPR-9645">SPR-9645</a>. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class LookUpTxMgrByTypeTests { + + private static final CallCountingTransactionManager txManager = new CallCountingTransactionManager(); + + @Configuration + static class Config { + + @Bean + public PlatformTransactionManager txManager() { + return txManager; + } + } + + @BeforeTransaction + public void beforeTransaction() { + txManager.clear(); + } + + @Test + public void transactionalTest() { + assertEquals(1, txManager.begun); + assertEquals(1, txManager.inflight); + assertEquals(0, txManager.commits); + assertEquals(0, txManager.rollbacks); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(1, txManager.begun); + assertEquals(0, txManager.inflight); + assertEquals(0, txManager.commits); + assertEquals(1, txManager.rollbacks); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799AnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799AnnotationConfigTests.java new file mode 100644 index 00000000..647b0960 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799AnnotationConfigTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr9799; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * Integration tests used to assess claims raised in + * <a href="https://jira.springsource.org/browse/SPR-9799" target="_blank">SPR-9799</a>. + * + * @author Sam Brannen + * @since 3.2 + * @see Spr9799XmlConfigTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +// NOTE: if we omit the @WebAppConfiguration declaration, the ApplicationContext will fail +// to load since @EnableWebMvc requires that the context be a WebApplicationContext. +@WebAppConfiguration +public class Spr9799AnnotationConfigTests { + + @Configuration + @EnableWebMvc + static class Config { + /* intentionally no beans defined */ + } + + + @Test + public void applicationContextLoads() { + // no-op + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests.java new file mode 100644 index 00000000..4e93b67c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.junit4.spr9799; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests used to assess claims raised in + * <a href="https://jira.springsource.org/browse/SPR-9799" target="_blank">SPR-9799</a>. + * + * @author Sam Brannen + * @since 3.2 + * @see Spr9799AnnotationConfigTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class Spr9799XmlConfigTests { + + @Test + public void applicationContextLoads() { + // nothing to assert: we just want to make sure that the context loads without + // errors. + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/transactionalTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/junit4/transactionalTests-context.xml new file mode 100644 index 00000000..01a7cf9f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/transactionalTests-context.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password=""/> + + <bean id="dataSource2" class="org.springframework.jdbc.datasource.DriverManagerDataSource" + p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:transactional_tests" p:username="sa" p:password=""/> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:dataSource-ref="dataSource"/> + + <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:dataSource-ref="dataSource2"> + </bean> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/support/AnnotatedFooConfigInnerClassTestCase.java b/spring-test/src/test/java/org/springframework/test/context/support/AnnotatedFooConfigInnerClassTestCase.java new file mode 100644 index 00000000..bb2e80b9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/AnnotatedFooConfigInnerClassTestCase.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Not an actual <em>test case</em>. + * + * @author Sam Brannen + * @since 3.1 + * @see AnnotationConfigContextLoaderTests + */ +public class AnnotatedFooConfigInnerClassTestCase { + + @Configuration + static class FooConfig { + + @Bean + public String foo() { + return "foo"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/AnnotationConfigContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/support/AnnotationConfigContextLoaderTests.java new file mode 100644 index 00000000..9d071b16 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/AnnotationConfigContextLoaderTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +/** + * Unit tests for {@link AnnotationConfigContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + */ +public class AnnotationConfigContextLoaderTests { + + private final AnnotationConfigContextLoader contextLoader = new AnnotationConfigContextLoader(); + + + @Test + public void detectDefaultConfigurationClassesForAnnotatedInnerClass() { + Class<?>[] configClasses = contextLoader.detectDefaultConfigurationClasses(ContextConfigurationInnerClassTestCase.class); + assertNotNull(configClasses); + assertEquals("annotated static ContextConfiguration should be considered.", 1, configClasses.length); + + configClasses = contextLoader.detectDefaultConfigurationClasses(AnnotatedFooConfigInnerClassTestCase.class); + assertNotNull(configClasses); + assertEquals("annotated static FooConfig should be considered.", 1, configClasses.length); + } + + @Test + public void detectDefaultConfigurationClassesForMultipleAnnotatedInnerClasses() { + Class<?>[] configClasses = contextLoader.detectDefaultConfigurationClasses(MultipleStaticConfigurationClassesTestCase.class); + assertNotNull(configClasses); + assertEquals("multiple annotated static classes should be considered.", 2, configClasses.length); + } + + @Test + public void detectDefaultConfigurationClassesForNonAnnotatedInnerClass() { + Class<?>[] configClasses = contextLoader.detectDefaultConfigurationClasses(PlainVanillaFooConfigInnerClassTestCase.class); + assertNotNull(configClasses); + assertEquals("non-annotated static FooConfig should NOT be considered.", 0, configClasses.length); + } + + @Test + public void detectDefaultConfigurationClassesForFinalAnnotatedInnerClass() { + Class<?>[] configClasses = contextLoader.detectDefaultConfigurationClasses(FinalConfigInnerClassTestCase.class); + assertNotNull(configClasses); + assertEquals("final annotated static Config should NOT be considered.", 0, configClasses.length); + } + + @Test + public void detectDefaultConfigurationClassesForPrivateAnnotatedInnerClass() { + Class<?>[] configClasses = contextLoader.detectDefaultConfigurationClasses(PrivateConfigInnerClassTestCase.class); + assertNotNull(configClasses); + assertEquals("private annotated inner classes should NOT be considered.", 0, configClasses.length); + } + + @Test + public void detectDefaultConfigurationClassesForNonStaticAnnotatedInnerClass() { + Class<?>[] configClasses = contextLoader.detectDefaultConfigurationClasses(NonStaticConfigInnerClassesTestCase.class); + assertNotNull(configClasses); + assertEquals("non-static annotated inner classes should NOT be considered.", 0, configClasses.length); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/ContextConfigurationInnerClassTestCase.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextConfigurationInnerClassTestCase.java new file mode 100644 index 00000000..ea0c4e84 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextConfigurationInnerClassTestCase.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.context.annotation.Configuration; + +/** + * Not an actual <em>test case</em>. + * + * @author Sam Brannen + * @since 3.1 + * @see AnnotationConfigContextLoaderTests + */ +public class ContextConfigurationInnerClassTestCase { + + @Configuration + static class ContextConfiguration { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/CustomizedGenericXmlContextLoaderTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/support/CustomizedGenericXmlContextLoaderTests-context.xml new file mode 100644 index 00000000..a0570128 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/CustomizedGenericXmlContextLoaderTests-context.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <!-- intentionally empty: only needed so that the ContextLoader can load a file --> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/support/CustomizedGenericXmlContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/support/CustomizedGenericXmlContextLoaderTests.java new file mode 100644 index 00000000..1d930831 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/CustomizedGenericXmlContextLoaderTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import org.springframework.context.support.GenericApplicationContext; + +/** + * Unit test which verifies that extensions of + * {@link AbstractGenericContextLoader} are able to <em>customize</em> the + * newly created {@code ApplicationContext}. Specifically, this test + * addresses the issues raised in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-4008" + * target="_blank">SPR-4008</a>: <em>Supply an opportunity to customize context + * before calling refresh in ContextLoaders</em>. + * + * @author Sam Brannen + * @since 2.5 + */ +public class CustomizedGenericXmlContextLoaderTests { + + @Test + public void customizeContext() throws Exception { + + final StringBuilder builder = new StringBuilder(); + final String expectedContents = "customizeContext() was called"; + + new GenericXmlContextLoader() { + + @Override + protected void customizeContext(GenericApplicationContext context) { + assertFalse("The context should not yet have been refreshed.", context.isActive()); + builder.append(expectedContents); + } + }.loadContext("classpath:/org/springframework/test/context/support/CustomizedGenericXmlContextLoaderTests-context.xml"); + + assertEquals("customizeContext() should have been called.", expectedContents, builder.toString()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/DelegatingSmartContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/support/DelegatingSmartContextLoaderTests.java new file mode 100644 index 00000000..20355683 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/DelegatingSmartContextLoaderTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.ObjectUtils; + +/** + * Unit tests for {@link DelegatingSmartContextLoader}. + * + * @author Sam Brannen + * @since 3.1 + */ +public class DelegatingSmartContextLoaderTests { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0]; + + private final DelegatingSmartContextLoader loader = new DelegatingSmartContextLoader(); + + + private static void assertEmpty(Object[] array) { + assertTrue(ObjectUtils.isEmpty(array)); + } + + // --- SmartContextLoader - processContextConfiguration() ------------------ + + @Test(expected = IllegalStateException.class) + public void processContextConfigurationWithoutLocationsAndConfigurationClassesForBogusTestClass() { + ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(getClass(), + EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class); + loader.processContextConfiguration(configAttributes); + } + + @Test + public void processContextConfigurationWithDefaultXmlConfigGeneration() { + ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(XmlTestCase.class, + EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class); + loader.processContextConfiguration(configAttributes); + assertEquals(1, configAttributes.getLocations().length); + assertEmpty(configAttributes.getClasses()); + } + + @Test + public void processContextConfigurationWithDefaultConfigurationClassGeneration() { + ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(ConfigClassTestCase.class, + EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class); + loader.processContextConfiguration(configAttributes); + assertEquals(1, configAttributes.getClasses().length); + assertEmpty(configAttributes.getLocations()); + } + + @Test(expected = IllegalStateException.class) + public void processContextConfigurationWithDefaultXmlConfigAndConfigurationClassGeneration() { + ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes( + ImproperDuplicateDefaultXmlAndConfigClassTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, + true, ContextLoader.class); + loader.processContextConfiguration(configAttributes); + } + + @Test + public void processContextConfigurationWithLocation() { + String[] locations = new String[] { "classpath:/foo.xml" }; + ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(getClass(), locations, + EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class); + loader.processContextConfiguration(configAttributes); + assertArrayEquals(locations, configAttributes.getLocations()); + assertEmpty(configAttributes.getClasses()); + } + + @Test + public void processContextConfigurationWithConfigurationClass() { + Class<?>[] classes = new Class<?>[] { getClass() }; + ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(getClass(), + EMPTY_STRING_ARRAY, classes, true, null, true, ContextLoader.class); + loader.processContextConfiguration(configAttributes); + assertArrayEquals(classes, configAttributes.getClasses()); + assertEmpty(configAttributes.getLocations()); + } + + // --- SmartContextLoader - loadContext() ---------------------------------- + + @Test(expected = IllegalArgumentException.class) + public void loadContextWithNullConfig() throws Exception { + MergedContextConfiguration mergedConfig = null; + loader.loadContext(mergedConfig); + } + + @Test(expected = IllegalStateException.class) + public void loadContextWithoutLocationsAndConfigurationClasses() throws Exception { + MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + loader.loadContext(mergedConfig); + } + + private void assertApplicationContextLoadsAndContainsFooString(MergedContextConfiguration mergedConfig) + throws Exception { + ApplicationContext applicationContext = loader.loadContext(mergedConfig); + assertNotNull(applicationContext); + assertEquals("foo", applicationContext.getBean(String.class)); + assertTrue(applicationContext instanceof ConfigurableApplicationContext); + ((ConfigurableApplicationContext) applicationContext).close(); + } + + @Test + public void loadContextWithXmlConfig() throws Exception { + MergedContextConfiguration mergedConfig = new MergedContextConfiguration( + XmlTestCase.class, + new String[] { "classpath:/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$XmlTestCase-context.xml" }, + EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader); + assertApplicationContextLoadsAndContainsFooString(mergedConfig); + } + + @Test + public void loadContextWithConfigurationClass() throws Exception { + MergedContextConfiguration mergedConfig = new MergedContextConfiguration(ConfigClassTestCase.class, + EMPTY_STRING_ARRAY, new Class<?>[] { ConfigClassTestCase.Config.class }, EMPTY_STRING_ARRAY, loader); + assertApplicationContextLoadsAndContainsFooString(mergedConfig); + } + + // --- ContextLoader ------------------------------------------------------- + + @Test(expected = UnsupportedOperationException.class) + public void processLocations() { + loader.processLocations(getClass(), EMPTY_STRING_ARRAY); + } + + @Test(expected = UnsupportedOperationException.class) + public void loadContextFromLocations() throws Exception { + loader.loadContext(EMPTY_STRING_ARRAY); + } + + + // ------------------------------------------------------------------------- + + static class XmlTestCase { + } + + static class ConfigClassTestCase { + + @Configuration + static class Config { + + @Bean + public String foo() { + return new String("foo"); + } + } + + static class NotAConfigClass { + + } + } + + static class ImproperDuplicateDefaultXmlAndConfigClassTestCase { + + @Configuration + static class Config { + // intentionally empty: we just need the class to be present to fail + // the test + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/FinalConfigInnerClassTestCase.java b/spring-test/src/test/java/org/springframework/test/context/support/FinalConfigInnerClassTestCase.java new file mode 100644 index 00000000..1c5d85d6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/FinalConfigInnerClassTestCase.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.context.annotation.Configuration; + +/** + * Not an actual <em>test case</em>. + * + * @author Sam Brannen + * @since 3.1 + * @see AnnotationConfigContextLoaderTests + */ +public class FinalConfigInnerClassTestCase { + + // Intentionally FINAL. + @Configuration + static final class Config { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/GenericXmlContextLoaderResourceLocationsTests$1ClasspathExistentDefaultLocationsTestCase-context.xml b/spring-test/src/test/java/org/springframework/test/context/support/GenericXmlContextLoaderResourceLocationsTests$1ClasspathExistentDefaultLocationsTestCase-context.xml new file mode 100644 index 00000000..44d11048 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/GenericXmlContextLoaderResourceLocationsTests$1ClasspathExistentDefaultLocationsTestCase-context.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <!-- intentionally empty: only needed so that the ContextLoader can find this file --> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/support/GenericXmlContextLoaderResourceLocationsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/GenericXmlContextLoaderResourceLocationsTests.java new file mode 100644 index 00000000..bf53acc5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/GenericXmlContextLoaderResourceLocationsTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextLoader; +import org.springframework.util.ObjectUtils; + +/** + * JUnit 4 based unit test which verifies proper + * {@link ContextLoader#processLocations(Class, String...) processing} of + * {@code resource locations} by a {@link GenericXmlContextLoader} + * configured via {@link ContextConfiguration @ContextConfiguration}. + * Specifically, this test addresses the issues raised in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3949" + * target="_blank">SPR-3949</a>: + * <em>ContextConfiguration annotation should accept not only classpath resources</em>. + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(Parameterized.class) +public class GenericXmlContextLoaderResourceLocationsTests { + + private static final Log logger = LogFactory.getLog(GenericXmlContextLoaderResourceLocationsTests.class); + + protected final Class<?> testClass; + protected final String[] expectedLocations; + + + public GenericXmlContextLoaderResourceLocationsTests(final Class<?> testClass, final String[] expectedLocations) { + this.testClass = testClass; + this.expectedLocations = expectedLocations; + } + + @Parameters + public static Collection<Object[]> contextConfigurationLocationsData() { + @ContextConfiguration + class ClasspathNonExistentDefaultLocationsTestCase { + } + + @ContextConfiguration + class ClasspathExistentDefaultLocationsTestCase { + } + + @ContextConfiguration({ "context1.xml", "context2.xml" }) + class ImplicitClasspathLocationsTestCase { + } + + @ContextConfiguration("classpath:context.xml") + class ExplicitClasspathLocationsTestCase { + } + + @ContextConfiguration("file:/testing/directory/context.xml") + class ExplicitFileLocationsTestCase { + } + + @ContextConfiguration("http://example.com/context.xml") + class ExplicitUrlLocationsTestCase { + } + + @ContextConfiguration({ "context1.xml", "classpath:context2.xml", "/context3.xml", + "file:/testing/directory/context.xml", "http://example.com/context.xml" }) + class ExplicitMixedPathTypesLocationsTestCase { + } + + return Arrays.asList(new Object[][] { + + { ClasspathNonExistentDefaultLocationsTestCase.class, new String[] {} }, + + { + ClasspathExistentDefaultLocationsTestCase.class, + new String[] { "classpath:/org/springframework/test/context/support/GenericXmlContextLoaderResourceLocationsTests$1ClasspathExistentDefaultLocationsTestCase-context.xml" } }, + + { + ImplicitClasspathLocationsTestCase.class, + new String[] { "classpath:/org/springframework/test/context/support/context1.xml", + "classpath:/org/springframework/test/context/support/context2.xml" } }, + + { ExplicitClasspathLocationsTestCase.class, new String[] { "classpath:context.xml" } }, + + { ExplicitFileLocationsTestCase.class, new String[] { "file:/testing/directory/context.xml" } }, + + { ExplicitUrlLocationsTestCase.class, new String[] { "http://example.com/context.xml" } }, + + { + ExplicitMixedPathTypesLocationsTestCase.class, + new String[] { "classpath:/org/springframework/test/context/support/context1.xml", + "classpath:context2.xml", "classpath:/context3.xml", "file:/testing/directory/context.xml", + "http://example.com/context.xml" } } + + }); + } + + @Test + public void assertContextConfigurationLocations() throws Exception { + + final ContextConfiguration contextConfig = this.testClass.getAnnotation(ContextConfiguration.class); + final ContextLoader contextLoader = new GenericXmlContextLoader(); + final String[] configuredLocations = (String[]) AnnotationUtils.getValue(contextConfig); + final String[] processedLocations = contextLoader.processLocations(this.testClass, configuredLocations); + + if (logger.isDebugEnabled()) { + logger.debug("----------------------------------------------------------------------"); + logger.debug("Configured locations: " + ObjectUtils.nullSafeToString(configuredLocations)); + logger.debug("Expected locations: " + ObjectUtils.nullSafeToString(this.expectedLocations)); + logger.debug("Processed locations: " + ObjectUtils.nullSafeToString(processedLocations)); + } + + assertArrayEquals("Verifying locations for test [" + this.testClass + "].", this.expectedLocations, + processedLocations); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/MultipleStaticConfigurationClassesTestCase.java b/spring-test/src/test/java/org/springframework/test/context/support/MultipleStaticConfigurationClassesTestCase.java new file mode 100644 index 00000000..a9069a1d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/MultipleStaticConfigurationClassesTestCase.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.context.annotation.Configuration; + +/** + * Not an actual <em>test case</em>. + * + * @author Sam Brannen + * @since 3.1 + * @see AnnotationConfigContextLoaderTests + */ +public class MultipleStaticConfigurationClassesTestCase { + + @Configuration + static class ConfigA { + } + + @Configuration + static class ConfigB { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/NonStaticConfigInnerClassesTestCase.java b/spring-test/src/test/java/org/springframework/test/context/support/NonStaticConfigInnerClassesTestCase.java new file mode 100644 index 00000000..db9d9179 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/NonStaticConfigInnerClassesTestCase.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.context.annotation.Configuration; + +/** + * Not an actual <em>test case</em>. + * + * @author Sam Brannen + * @since 3.1 + * @see AnnotationConfigContextLoaderTests + */ +public class NonStaticConfigInnerClassesTestCase { + + // Intentionally not static + @Configuration + class FooConfig { + } + + // Intentionally not static + @Configuration + class BarConfig { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/PlainVanillaFooConfigInnerClassTestCase.java b/spring-test/src/test/java/org/springframework/test/context/support/PlainVanillaFooConfigInnerClassTestCase.java new file mode 100644 index 00000000..427191bc --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/PlainVanillaFooConfigInnerClassTestCase.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +/** + * Not an actual <em>test case</em>. + * + * @author Sam Brannen + * @since 3.1 + * @see AnnotationConfigContextLoaderTests + */ +public class PlainVanillaFooConfigInnerClassTestCase { + + // Intentionally NOT annotated with @Configuration + static class FooConfig { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/PrivateConfigInnerClassTestCase.java b/spring-test/src/test/java/org/springframework/test/context/support/PrivateConfigInnerClassTestCase.java new file mode 100644 index 00000000..5432f578 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/PrivateConfigInnerClassTestCase.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.support; + +import org.springframework.context.annotation.Configuration; + +/** + * Not an actual <em>test case</em>. + * + * @author Sam Brannen + * @since 3.1 + * @see AnnotationConfigContextLoaderTests + */ +public class PrivateConfigInnerClassTestCase { + + // Intentionally private + @Configuration + private static class PrivateConfig { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/AnnotationConfigTransactionalTestNGSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/AnnotationConfigTransactionalTestNGSpringContextTests.java new file mode 100644 index 00000000..66ffc202 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/AnnotationConfigTransactionalTestNGSpringContextTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.testng; + +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; +import static org.springframework.test.transaction.TransactionTestUtils.inTransaction; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import javax.sql.DataSource; + +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.PlatformTransactionManager; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Integration tests that verify support for + * {@link org.springframework.context.annotation.Configuration @Configuration} classes + * with TestNG-based tests. + * + * <p> + * Configuration will be loaded from + * {@link AnnotationConfigTransactionalTestNGSpringContextTests.ContextConfiguration}. + * + * @author Sam Brannen + * @since 3.1 + */ +@SuppressWarnings("deprecation") +@ContextConfiguration +public class AnnotationConfigTransactionalTestNGSpringContextTests extends + AbstractTransactionalTestNGSpringContextTests { + + private static final String JANE = "jane"; + private static final String SUE = "sue"; + private static final String YODA = "yoda"; + + private static final int NUM_TESTS = 2; + private static final int NUM_TX_TESTS = 1; + + private static int numSetUpCalls = 0; + private static int numSetUpCallsInTransaction = 0; + private static int numTearDownCalls = 0; + private static int numTearDownCallsInTransaction = 0; + + @Autowired + private Employee employee; + + @Autowired + private Pet pet; + + + private int createPerson(String name) { + return simpleJdbcTemplate.update("INSERT INTO person VALUES(?)", name); + } + + private int deletePerson(String name) { + return simpleJdbcTemplate.update("DELETE FROM person WHERE name=?", name); + } + + private void assertNumRowsInPersonTable(int expectedNumRows, String testState) { + assertEquals(countRowsInTable("person"), expectedNumRows, "the number of rows in the person table (" + + testState + ")."); + } + + private void assertAddPerson(final String name) { + assertEquals(createPerson(name), 1, "Adding '" + name + "'"); + } + + @BeforeClass + public void beforeClass() { + numSetUpCalls = 0; + numSetUpCallsInTransaction = 0; + numTearDownCalls = 0; + numTearDownCallsInTransaction = 0; + } + + @AfterClass + public void afterClass() { + assertEquals(numSetUpCalls, NUM_TESTS, "number of calls to setUp()."); + assertEquals(numSetUpCallsInTransaction, NUM_TX_TESTS, "number of calls to setUp() within a transaction."); + assertEquals(numTearDownCalls, NUM_TESTS, "number of calls to tearDown()."); + assertEquals(numTearDownCallsInTransaction, NUM_TX_TESTS, "number of calls to tearDown() within a transaction."); + } + + @Test + @NotTransactional + public void autowiringFromConfigClass() { + assertNotNull(employee, "The employee should have been autowired."); + assertEquals(employee.getName(), "John Smith"); + + assertNotNull(pet, "The pet should have been autowired."); + assertEquals(pet.getName(), "Fido"); + } + + @BeforeTransaction + public void beforeTransaction() { + assertNumRowsInPersonTable(1, "before a transactional test method"); + assertAddPerson(YODA); + } + + @BeforeMethod + public void setUp() throws Exception { + numSetUpCalls++; + if (inTransaction()) { + numSetUpCallsInTransaction++; + } + assertNumRowsInPersonTable((inTransaction() ? 2 : 1), "before a test method"); + } + + @Test + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertAddPerson(JANE); + assertAddPerson(SUE); + assertNumRowsInPersonTable(4, "in modifyTestDataWithinTransaction()"); + } + + @AfterMethod + public void tearDown() throws Exception { + numTearDownCalls++; + if (inTransaction()) { + numTearDownCallsInTransaction++; + } + assertNumRowsInPersonTable((inTransaction() ? 4 : 1), "after a test method"); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(deletePerson(YODA), 1, "Deleting yoda"); + assertNumRowsInPersonTable(1, "after a transactional test method"); + } + + + @Configuration + static class ContextConfiguration { + + @Bean + public Employee employee() { + Employee employee = new Employee(); + employee.setName("John Smith"); + employee.setAge(42); + employee.setCompany("Acme Widgets, Inc."); + return employee; + } + + @Bean + public Pet pet() { + return new Pet("Fido"); + } + + @Bean + public PlatformTransactionManager transactionManager() { + return new DataSourceTransactionManager(dataSource()); + } + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder()// + .addScript("classpath:/org/springframework/test/context/testng/schema.sql")// + .addScript("classpath:/org/springframework/test/context/testng/data.sql")// + .build(); + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/ConcreteTransactionalTestNGSpringContextTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/testng/ConcreteTransactionalTestNGSpringContextTests-context.xml new file mode 100644 index 00000000..a46a6da3 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/ConcreteTransactionalTestNGSpringContextTests-context.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" + xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> + + <bean id="employee" class="org.springframework.tests.sample.beans.Employee" p:name="John Smith" p:age="42" + p:company="Acme Widgets, Inc." /> + + <bean id="pet" class="org.springframework.tests.sample.beans.Pet" c:_="Fido" /> + + <bean id="foo" class="java.lang.String" c:_="Foo" /> + + <bean id="bar" class="java.lang.String" c:_="Bar" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + + <jdbc:embedded-database id="dataSource"> + <jdbc:script location="classpath:/org/springframework/test/context/testng/schema.sql" /> + <jdbc:script location="classpath:/org/springframework/test/context/testng/data.sql" /> + </jdbc:embedded-database> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/ConcreteTransactionalTestNGSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/ConcreteTransactionalTestNGSpringContextTests.java new file mode 100644 index 00000000..ee25cb32 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/ConcreteTransactionalTestNGSpringContextTests.java @@ -0,0 +1,233 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.testng; + +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; +import static org.springframework.test.transaction.TransactionTestUtils.inTransaction; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import javax.annotation.Resource; + +import org.springframework.tests.sample.beans.Employee; +import org.springframework.tests.sample.beans.Pet; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.NotTransactional; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Combined integration test for {@link AbstractTestNGSpringContextTests} and + * {@link AbstractTransactionalTestNGSpringContextTests}. + * + * @author Sam Brannen + * @since 2.5 + */ +@SuppressWarnings("deprecation") +@ContextConfiguration +public class ConcreteTransactionalTestNGSpringContextTests extends AbstractTransactionalTestNGSpringContextTests + implements BeanNameAware, InitializingBean { + + private static final String JANE = "jane"; + private static final String SUE = "sue"; + private static final String YODA = "yoda"; + + private static final int NUM_TESTS = 8; + private static final int NUM_TX_TESTS = 1; + + private static int numSetUpCalls = 0; + private static int numSetUpCallsInTransaction = 0; + private static int numTearDownCalls = 0; + private static int numTearDownCallsInTransaction = 0; + + private boolean beanInitialized = false; + + private String beanName = "replace me with [" + getClass().getName() + "]"; + + private Employee employee; + + @Autowired + private Pet pet; + + @Autowired(required = false) + protected Long nonrequiredLong; + + @Resource() + protected String foo; + + protected String bar; + + + private int createPerson(String name) { + return simpleJdbcTemplate.update("INSERT INTO person VALUES(?)", name); + } + + private int deletePerson(String name) { + return simpleJdbcTemplate.update("DELETE FROM person WHERE name=?", name); + } + + @Override + public void afterPropertiesSet() throws Exception { + this.beanInitialized = true; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + @Autowired + protected void setEmployee(Employee employee) { + this.employee = employee; + } + + @Resource + protected void setBar(String bar) { + this.bar = bar; + } + + private void assertNumRowsInPersonTable(int expectedNumRows, String testState) { + assertEquals(countRowsInTable("person"), expectedNumRows, "the number of rows in the person table (" + + testState + ")."); + } + + private void assertAddPerson(final String name) { + assertEquals(createPerson(name), 1, "Adding '" + name + "'"); + } + + @BeforeClass + public void beforeClass() { + numSetUpCalls = 0; + numSetUpCallsInTransaction = 0; + numTearDownCalls = 0; + numTearDownCallsInTransaction = 0; + } + + @AfterClass + public void afterClass() { + assertEquals(numSetUpCalls, NUM_TESTS, "number of calls to setUp()."); + assertEquals(numSetUpCallsInTransaction, NUM_TX_TESTS, "number of calls to setUp() within a transaction."); + assertEquals(numTearDownCalls, NUM_TESTS, "number of calls to tearDown()."); + assertEquals(numTearDownCallsInTransaction, NUM_TX_TESTS, "number of calls to tearDown() within a transaction."); + } + + @Test + @NotTransactional + public void verifyApplicationContextSet() { + assertInTransaction(false); + assertNotNull(super.applicationContext, + "The application context should have been set due to ApplicationContextAware semantics."); + Employee employeeBean = (Employee) super.applicationContext.getBean("employee"); + assertEquals(employeeBean.getName(), "John Smith", "employee's name."); + } + + @Test + @NotTransactional + public void verifyBeanInitialized() { + assertInTransaction(false); + assertTrue(beanInitialized, + "This test instance should have been initialized due to InitializingBean semantics."); + } + + @Test + @NotTransactional + public void verifyBeanNameSet() { + assertInTransaction(false); + assertEquals(beanName, getClass().getName(), + "The bean name of this test instance should have been set due to BeanNameAware semantics."); + } + + @Test + @NotTransactional + public void verifyAnnotationAutowiredFields() { + assertInTransaction(false); + assertNull(nonrequiredLong, "The nonrequiredLong field should NOT have been autowired."); + assertNotNull(pet, "The pet field should have been autowired."); + assertEquals(pet.getName(), "Fido", "pet's name."); + } + + @Test + @NotTransactional + public void verifyAnnotationAutowiredMethods() { + assertInTransaction(false); + assertNotNull(employee, "The setEmployee() method should have been autowired."); + assertEquals(employee.getName(), "John Smith", "employee's name."); + } + + @Test + @NotTransactional + public void verifyResourceAnnotationInjectedFields() { + assertInTransaction(false); + assertEquals(foo, "Foo", "The foo field should have been injected via @Resource."); + } + + @Test + @NotTransactional + public void verifyResourceAnnotationInjectedMethods() { + assertInTransaction(false); + assertEquals(bar, "Bar", "The setBar() method should have been injected via @Resource."); + } + + @BeforeTransaction + public void beforeTransaction() { + assertNumRowsInPersonTable(1, "before a transactional test method"); + assertAddPerson(YODA); + } + + @BeforeMethod + public void setUp() throws Exception { + numSetUpCalls++; + if (inTransaction()) { + numSetUpCallsInTransaction++; + } + assertNumRowsInPersonTable((inTransaction() ? 2 : 1), "before a test method"); + } + + @Test + public void modifyTestDataWithinTransaction() { + assertInTransaction(true); + assertAddPerson(JANE); + assertAddPerson(SUE); + assertNumRowsInPersonTable(4, "in modifyTestDataWithinTransaction()"); + } + + @AfterMethod + public void tearDown() throws Exception { + numTearDownCalls++; + if (inTransaction()) { + numTearDownCallsInTransaction++; + } + assertNumRowsInPersonTable((inTransaction() ? 4 : 1), "after a test method"); + } + + @AfterTransaction + public void afterTransaction() { + assertEquals(deletePerson(YODA), 1, "Deleting yoda"); + assertNumRowsInPersonTable(1, "after a transactional test method"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/DirtiesContextTransactionalTestNGSpringContextTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/testng/DirtiesContextTransactionalTestNGSpringContextTests-context.xml new file mode 100644 index 00000000..f1786428 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/DirtiesContextTransactionalTestNGSpringContextTests-context.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> + + <jdbc:embedded-database id="dataSource" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/DirtiesContextTransactionalTestNGSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/DirtiesContextTransactionalTestNGSpringContextTests.java new file mode 100644 index 00000000..82fb1928 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/DirtiesContextTransactionalTestNGSpringContextTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.testng; + +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertSame; + +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContextManager; +import org.testng.annotations.Test; + +/** + * <p> + * TestNG based integration test to assess the claim in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3880" + * target="_blank">SPR-3880</a> that a "context marked dirty using + * {@link DirtiesContext @DirtiesContext} in [a] TestNG based test is not + * reloaded in subsequent tests". + * </p> + * <p> + * After careful analysis, it turns out that the {@link ApplicationContext} was + * in fact reloaded; however, due to how the test instance was instrumented with + * the {@link TestContextManager} in {@link AbstractTestNGSpringContextTests}, + * dependency injection was not being performed on the test instance between + * individual tests. DirtiesContextTransactionalTestNGSpringContextTests + * therefore verifies the expected behavior and correct semantics. + * </p> + * + * @author Sam Brannen + * @since 2.5 + */ +@ContextConfiguration +public class DirtiesContextTransactionalTestNGSpringContextTests extends AbstractTransactionalTestNGSpringContextTests { + + private ApplicationContext dirtiedApplicationContext; + + + @SuppressWarnings("deprecation") + private void performCommonAssertions() { + assertInTransaction(true); + assertNotNull(super.applicationContext, + "The application context should have been set due to ApplicationContextAware semantics."); + assertNotNull(super.simpleJdbcTemplate, + "The SimpleJdbcTemplate should have been created in setDataSource() via DI for the DataSource."); + } + + @Test + @DirtiesContext + public void dirtyContext() { + performCommonAssertions(); + this.dirtiedApplicationContext = super.applicationContext; + } + + @Test(dependsOnMethods = { "dirtyContext" }) + public void verifyContextWasDirtied() { + performCommonAssertions(); + assertNotSame(super.applicationContext, this.dirtiedApplicationContext, + "The application context should have been 'dirtied'."); + this.dirtiedApplicationContext = super.applicationContext; + } + + @Test(dependsOnMethods = { "verifyContextWasDirtied" }) + public void verifyContextWasNotDirtied() { + assertSame(this.applicationContext, this.dirtiedApplicationContext, + "The application context should NOT have been 'dirtied'."); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/FailingBeforeAndAfterMethodsTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/testng/FailingBeforeAndAfterMethodsTests-context.xml new file mode 100644 index 00000000..f1786428 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/FailingBeforeAndAfterMethodsTests-context.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> + + <jdbc:embedded-database id="dataSource" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + +</beans> diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/FailingBeforeAndAfterMethodsTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/FailingBeforeAndAfterMethodsTests.java new file mode 100644 index 00000000..74e8f251 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/FailingBeforeAndAfterMethodsTests.java @@ -0,0 +1,257 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.testng; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.testng.ITestContext; +import org.testng.ITestListener; +import org.testng.ITestResult; +import org.testng.TestNG; + +/** + * <p> + * JUnit 4 based integration test for verifying that '<i>before</i>' and '<i>after</i>' + * methods of {@link TestExecutionListener TestExecutionListeners} as well as + * {@link BeforeTransaction @BeforeTransaction} and + * {@link AfterTransaction @AfterTransaction} methods can fail a test in a + * TestNG environment, as requested in <a + * href="http://opensource.atlassian.com/projects/spring/browse/SPR-3960" + * target="_blank">SPR-3960</a>. + * </p> + * <p> + * Indirectly, this class also verifies that all {@link TestExecutionListener} + * lifecycle callbacks are called. + * </p> + * <p> + * As of Spring 3.0, this class also tests support for the new + * {@link TestExecutionListener#beforeTestClass(TestContext) beforeTestClass()} + * and {@link TestExecutionListener#afterTestClass(TestContext) + * afterTestClass()} lifecycle callback methods. + * </p> + * + * @author Sam Brannen + * @since 2.5 + */ +@RunWith(Parameterized.class) +public class FailingBeforeAndAfterMethodsTests { + + protected final Class<?> clazz; + protected final int expectedTestStartCount; + protected final int expectedTestSuccessCount; + protected final int expectedFailureCount; + protected final int expectedFailedConfigurationsCount; + + + public FailingBeforeAndAfterMethodsTests(final Class<?> clazz, final int expectedTestStartCount, + final int expectedTestSuccessCount, final int expectedFailureCount, + final int expectedFailedConfigurationsCount) { + this.clazz = clazz; + this.expectedTestStartCount = expectedTestStartCount; + this.expectedTestSuccessCount = expectedTestSuccessCount; + this.expectedFailureCount = expectedFailureCount; + this.expectedFailedConfigurationsCount = expectedFailedConfigurationsCount; + } + + @Parameters + public static Collection<Object[]> testData() { + return Arrays.asList(new Object[][] {// + // + { AlwaysFailingBeforeTestClassTestCase.class, 1, 0, 0, 1 },// + { AlwaysFailingAfterTestClassTestCase.class, 1, 1, 0, 1 },// + { AlwaysFailingPrepareTestInstanceTestCase.class, 1, 0, 0, 1 },// + { AlwaysFailingBeforeTestMethodTestCase.class, 1, 0, 0, 1 },// + { AlwaysFailingAfterTestMethodTestCase.class, 1, 1, 0, 1 },// + { FailingBeforeTransactionTestCase.class, 1, 0, 0, 1 },// + { FailingAfterTransactionTestCase.class, 1, 1, 0, 1 } // + }); + } + + @Test + public void runTestAndAssertCounters() throws Exception { + final FailureTrackingTestListener listener = new FailureTrackingTestListener(); + final TestNG testNG = new TestNG(); + testNG.addListener(listener); + testNG.setTestClasses(new Class<?>[] { this.clazz }); + testNG.setVerbose(0); + testNG.run(); + + assertEquals("Verifying number of test starts for test class [" + this.clazz + "].", + this.expectedTestStartCount, listener.testStartCount); + assertEquals("Verifying number of successful tests for test class [" + this.clazz + "].", + this.expectedTestSuccessCount, listener.testSuccessCount); + assertEquals("Verifying number of failures for test class [" + this.clazz + "].", this.expectedFailureCount, + listener.testFailureCount); + assertEquals("Verifying number of failed configurations for test class [" + this.clazz + "].", + this.expectedFailedConfigurationsCount, listener.failedConfigurationsCount); + } + + + static class FailureTrackingTestListener implements ITestListener { + + int testStartCount = 0; + int testSuccessCount = 0; + int testFailureCount = 0; + int failedConfigurationsCount = 0; + + + @Override + public void onFinish(ITestContext testContext) { + this.failedConfigurationsCount += testContext.getFailedConfigurations().size(); + } + + @Override + public void onStart(ITestContext testContext) { + } + + @Override + public void onTestFailedButWithinSuccessPercentage(ITestResult testResult) { + } + + @Override + public void onTestFailure(ITestResult testResult) { + this.testFailureCount++; + } + + @Override + public void onTestSkipped(ITestResult testResult) { + } + + @Override + public void onTestStart(ITestResult testResult) { + this.testStartCount++; + } + + @Override + public void onTestSuccess(ITestResult testResult) { + this.testSuccessCount++; + } + } + + // ------------------------------------------------------------------- + + static class AlwaysFailingBeforeTestClassTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void beforeTestClass(TestContext testContext) { + org.testng.Assert.fail("always failing beforeTestClass()"); + } + } + + static class AlwaysFailingAfterTestClassTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void afterTestClass(TestContext testContext) { + org.testng.Assert.fail("always failing afterTestClass()"); + } + } + + static class AlwaysFailingPrepareTestInstanceTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + org.testng.Assert.fail("always failing prepareTestInstance()"); + } + } + + static class AlwaysFailingBeforeTestMethodTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void beforeTestMethod(TestContext testContext) { + org.testng.Assert.fail("always failing beforeTestMethod()"); + } + } + + static class AlwaysFailingAfterTestMethodTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void afterTestMethod(TestContext testContext) { + org.testng.Assert.fail("always failing afterTestMethod()"); + } + } + + // ------------------------------------------------------------------- + + @TestExecutionListeners(value = {}, inheritListeners = false) + public static abstract class BaseTestCase extends AbstractTestNGSpringContextTests { + + @org.testng.annotations.Test + public void testNothing() { + } + } + + @TestExecutionListeners(AlwaysFailingBeforeTestClassTestExecutionListener.class) + public static class AlwaysFailingBeforeTestClassTestCase extends BaseTestCase { + } + + @TestExecutionListeners(AlwaysFailingAfterTestClassTestExecutionListener.class) + public static class AlwaysFailingAfterTestClassTestCase extends BaseTestCase { + } + + @TestExecutionListeners(AlwaysFailingPrepareTestInstanceTestExecutionListener.class) + public static class AlwaysFailingPrepareTestInstanceTestCase extends BaseTestCase { + } + + @TestExecutionListeners(AlwaysFailingBeforeTestMethodTestExecutionListener.class) + public static class AlwaysFailingBeforeTestMethodTestCase extends BaseTestCase { + } + + @TestExecutionListeners(AlwaysFailingAfterTestMethodTestExecutionListener.class) + public static class AlwaysFailingAfterTestMethodTestCase extends BaseTestCase { + } + + @ContextConfiguration("FailingBeforeAndAfterMethodsTests-context.xml") + public static class FailingBeforeTransactionTestCase extends AbstractTransactionalTestNGSpringContextTests { + + @org.testng.annotations.Test + public void testNothing() { + } + + @BeforeTransaction + public void beforeTransaction() { + org.testng.Assert.fail("always failing beforeTransaction()"); + } + } + + @ContextConfiguration("FailingBeforeAndAfterMethodsTests-context.xml") + public static class FailingAfterTransactionTestCase extends AbstractTransactionalTestNGSpringContextTests { + + @org.testng.annotations.Test + public void testNothing() { + } + + @AfterTransaction + public void afterTransaction() { + org.testng.Assert.fail("always failing afterTransaction()"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/TimedTransactionalTestNGSpringContextTests-context.xml b/spring-test/src/test/java/org/springframework/test/context/testng/TimedTransactionalTestNGSpringContextTests-context.xml new file mode 100644 index 00000000..bbff4c9e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/TimedTransactionalTestNGSpringContextTests-context.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> + + <jdbc:embedded-database id="dataSource" /> + + <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" + p:data-source-ref="dataSource" /> + +</beans>
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/TimedTransactionalTestNGSpringContextTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/TimedTransactionalTestNGSpringContextTests.java new file mode 100644 index 00000000..bedd776b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/TimedTransactionalTestNGSpringContextTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.testng; + +import static org.springframework.test.transaction.TransactionTestUtils.assertInTransaction; + +import org.springframework.test.context.ContextConfiguration; +import org.testng.annotations.Test; + +/** + * Timed integration tests for + * {@link AbstractTransactionalTestNGSpringContextTests}; used to verify claim + * raised in <a href="http://jira.springframework.org/browse/SPR-6124" + * target="_blank">SPR-6124</a>. + * + * @author Sam Brannen + * @since 3.0 + */ +@ContextConfiguration +public class TimedTransactionalTestNGSpringContextTests extends AbstractTransactionalTestNGSpringContextTests { + + @Test + public void testWithoutTimeout() { + assertInTransaction(true); + } + + // TODO Enable TestNG test with timeout once we have a solution. + @Test(timeOut = 10000, enabled = false) + public void testWithTimeout() { + assertInTransaction(true); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/data.sql b/spring-test/src/test/java/org/springframework/test/context/testng/data.sql new file mode 100644 index 00000000..10d02a9c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/data.sql @@ -0,0 +1 @@ +INSERT INTO person VALUES('bob');
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/schema.sql b/spring-test/src/test/java/org/springframework/test/context/testng/schema.sql new file mode 100644 index 00000000..d5bacef4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/schema.sql @@ -0,0 +1,4 @@ +CREATE TABLE person ( + name VARCHAR(20) NOT NULL, + PRIMARY KEY(name) +);
\ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/web/ServletTestExecutionListenerTestNGIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/web/ServletTestExecutionListenerTestNGIntegrationTests.java new file mode 100644 index 00000000..55aa29e7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/web/ServletTestExecutionListenerTestNGIntegrationTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.testng.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.context.web.ServletTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.testng.annotations.Test; + +import static org.junit.Assert.*; + +/** + * TestNG-based integration tests for {@link ServletTestExecutionListener}. + * + * @author Sam Brannen + * @since 3.2.9 + * @see org.springframework.test.context.web.ServletTestExecutionListenerJUnitIntegrationTests + */ +@ContextConfiguration +@WebAppConfiguration +public class ServletTestExecutionListenerTestNGIntegrationTests extends AbstractTestNGSpringContextTests { + + @Configuration + static class Config { + /* no beans required for this test */ + } + + + @Autowired + private MockHttpServletRequest servletRequest; + + + /** + * Verifies bug fix for <a href="https://jira.spring.io/browse/SPR-11626">SPR-11626</a>. + * + * @see #ensureMocksAreReinjectedBetweenTests_2 + */ + @Test + public void ensureMocksAreReinjectedBetweenTests_1() { + assertInjectedServletRequestEqualsRequestInRequestContextHolder(); + } + + /** + * Verifies bug fix for <a href="https://jira.spring.io/browse/SPR-11626">SPR-11626</a>. + * + * @see #ensureMocksAreReinjectedBetweenTests_1 + */ + @Test + public void ensureMocksAreReinjectedBetweenTests_2() { + assertInjectedServletRequestEqualsRequestInRequestContextHolder(); + } + + private void assertInjectedServletRequestEqualsRequestInRequestContextHolder() { + assertEquals("Injected ServletRequest must be stored in the RequestContextHolder", servletRequest, + ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/web/TestNGSpringContextWebTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/web/TestNGSpringContextWebTests.java new file mode 100644 index 00000000..05f39889 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/testng/web/TestNGSpringContextWebTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.testng.web; + +import java.io.File; + +import javax.servlet.ServletContext; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.context.ServletContextAware; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.ServletWebRequest; +import org.testng.annotations.Test; + +import static org.junit.Assert.*; + +/** + * TestNG-based integration tests that verify support for loading a + * {@link WebApplicationContext} when extending {@link AbstractTestNGSpringContextTests}. + * + * @author Sam Brannen + * @since 3.2.7 + */ +@ContextConfiguration +@WebAppConfiguration +public class TestNGSpringContextWebTests extends AbstractTestNGSpringContextTests implements ServletContextAware { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "enigma"; + } + } + + + protected ServletContext servletContext; + + @Autowired + protected WebApplicationContext wac; + + @Autowired + protected MockServletContext mockServletContext; + + @Autowired + protected MockHttpServletRequest request; + + @Autowired + protected MockHttpServletResponse response; + + @Autowired + protected MockHttpSession session; + + @Autowired + protected ServletWebRequest webRequest; + + @Autowired + protected String foo; + + + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Test + public void basicWacFeatures() throws Exception { + assertNotNull("ServletContext should be set in the WAC.", wac.getServletContext()); + + assertNotNull("ServletContext should have been set via ServletContextAware.", servletContext); + + assertNotNull("ServletContext should have been autowired from the WAC.", mockServletContext); + assertNotNull("MockHttpServletRequest should have been autowired from the WAC.", request); + assertNotNull("MockHttpServletResponse should have been autowired from the WAC.", response); + assertNotNull("MockHttpSession should have been autowired from the WAC.", session); + assertNotNull("ServletWebRequest should have been autowired from the WAC.", webRequest); + + Object rootWac = mockServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + assertNotNull("Root WAC must be stored in the ServletContext as: " + + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, rootWac); + assertSame("test WAC and Root WAC in ServletContext must be the same object.", wac, rootWac); + assertSame("ServletContext instances must be the same object.", mockServletContext, wac.getServletContext()); + assertSame("ServletContext in the WAC and in the mock request", mockServletContext, request.getServletContext()); + + assertEquals("Getting real path for ServletContext resource.", + new File("src/main/webapp/index.jsp").getCanonicalPath(), mockServletContext.getRealPath("index.jsp")); + + } + + @Test + public void fooEnigmaAutowired() { + assertEquals("enigma", foo); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.java new file mode 100644 index 00000000..334983d8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import static org.junit.Assert.*; + +import java.io.File; + +import javax.servlet.ServletContext; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.context.ServletContextAware; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +public abstract class AbstractBasicWacTests implements ServletContextAware { + + protected ServletContext servletContext; + + @Autowired + protected WebApplicationContext wac; + + @Autowired + protected MockServletContext mockServletContext; + + @Autowired + protected MockHttpServletRequest request; + + @Autowired + protected MockHttpServletResponse response; + + @Autowired + protected MockHttpSession session; + + @Autowired + protected ServletWebRequest webRequest; + + @Autowired + protected String foo; + + + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Test + public void basicWacFeatures() throws Exception { + assertNotNull("ServletContext should be set in the WAC.", wac.getServletContext()); + + assertNotNull("ServletContext should have been set via ServletContextAware.", servletContext); + + assertNotNull("ServletContext should have been autowired from the WAC.", mockServletContext); + assertNotNull("MockHttpServletRequest should have been autowired from the WAC.", request); + assertNotNull("MockHttpServletResponse should have been autowired from the WAC.", response); + assertNotNull("MockHttpSession should have been autowired from the WAC.", session); + assertNotNull("ServletWebRequest should have been autowired from the WAC.", webRequest); + + Object rootWac = mockServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + assertNotNull("Root WAC must be stored in the ServletContext as: " + + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, rootWac); + assertSame("test WAC and Root WAC in ServletContext must be the same object.", wac, rootWac); + assertSame("ServletContext instances must be the same object.", mockServletContext, wac.getServletContext()); + assertSame("ServletContext in the WAC and in the mock request", mockServletContext, request.getServletContext()); + + assertEquals("Getting real path for ServletContext resource.", + new File("src/main/webapp/index.jsp").getCanonicalPath(), mockServletContext.getRealPath("index.jsp")); + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.java new file mode 100644 index 00000000..de4ef83c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Sam Brannen + * @since 3.2 + */ +@ContextConfiguration +public class BasicAnnotationConfigWacTests extends AbstractBasicWacTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "enigma"; + } + } + + + @Test + public void fooEnigmaAutowired() { + assertEquals("enigma", foo); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.java new file mode 100644 index 00000000..7489209a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Sam Brannen + * @since 3.2 + */ +@ContextConfiguration +public class BasicXmlWacTests extends AbstractBasicWacTests { + + @Test + public void fooBarAutowired() { + assertEquals("bar", foo); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/JUnit4SpringContextWebTests.java b/spring-test/src/test/java/org/springframework/test/context/web/JUnit4SpringContextWebTests.java new file mode 100644 index 00000000..c1133bdc --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/JUnit4SpringContextWebTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.web; + +import java.io.File; + +import javax.servlet.ServletContext; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.web.context.ServletContextAware; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.ServletWebRequest; + +import static org.junit.Assert.*; + +/** + * JUnit-based integration tests that verify support for loading a + * {@link WebApplicationContext} when extending {@link AbstractJUnit4SpringContextTests}. + * + * @author Sam Brannen + * @since 3.2.7 + */ +@ContextConfiguration +@WebAppConfiguration +public class JUnit4SpringContextWebTests extends AbstractJUnit4SpringContextTests implements ServletContextAware { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "enigma"; + } + } + + + protected ServletContext servletContext; + + @Autowired + protected WebApplicationContext wac; + + @Autowired + protected MockServletContext mockServletContext; + + @Autowired + protected MockHttpServletRequest request; + + @Autowired + protected MockHttpServletResponse response; + + @Autowired + protected MockHttpSession session; + + @Autowired + protected ServletWebRequest webRequest; + + @Autowired + protected String foo; + + + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Test + public void basicWacFeatures() throws Exception { + assertNotNull("ServletContext should be set in the WAC.", wac.getServletContext()); + + assertNotNull("ServletContext should have been set via ServletContextAware.", servletContext); + + assertNotNull("ServletContext should have been autowired from the WAC.", mockServletContext); + assertNotNull("MockHttpServletRequest should have been autowired from the WAC.", request); + assertNotNull("MockHttpServletResponse should have been autowired from the WAC.", response); + assertNotNull("MockHttpSession should have been autowired from the WAC.", session); + assertNotNull("ServletWebRequest should have been autowired from the WAC.", webRequest); + + Object rootWac = mockServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + assertNotNull("Root WAC must be stored in the ServletContext as: " + + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, rootWac); + assertSame("test WAC and Root WAC in ServletContext must be the same object.", wac, rootWac); + assertSame("ServletContext instances must be the same object.", mockServletContext, wac.getServletContext()); + assertSame("ServletContext in the WAC and in the mock request", mockServletContext, request.getServletContext()); + + assertEquals("Getting real path for ServletContext resource.", + new File("src/main/webapp/index.jsp").getCanonicalPath(), mockServletContext.getRealPath("index.jsp")); + + } + + @Test + public void fooEnigmaAutowired() { + assertEquals("enigma", foo); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/RequestAndSessionScopedBeansWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/RequestAndSessionScopedBeansWacTests.java new file mode 100644 index 00000000..829d70ff --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/RequestAndSessionScopedBeansWacTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.web; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.tests.sample.beans.TestBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.context.WebApplicationContext; + +/** + * Integration tests that verify support for request and session scoped beans + * in conjunction with the TestContext Framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class RequestAndSessionScopedBeansWacTests { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private MockHttpServletRequest request; + + @Autowired + private MockHttpSession session; + + + @Test + public void requestScope() throws Exception { + final String beanName = "requestScopedTestBean"; + final String contextPath = "/path"; + + assertNull(request.getAttribute(beanName)); + + request.setContextPath(contextPath); + TestBean testBean = wac.getBean(beanName, TestBean.class); + + assertEquals(contextPath, testBean.getName()); + assertSame(testBean, request.getAttribute(beanName)); + assertSame(testBean, wac.getBean(beanName, TestBean.class)); + } + + @Test + public void sessionScope() throws Exception { + final String beanName = "sessionScopedTestBean"; + + assertNull(session.getAttribute(beanName)); + + TestBean testBean = wac.getBean(beanName, TestBean.class); + + assertSame(testBean, session.getAttribute(beanName)); + assertSame(testBean, wac.getBean(beanName, TestBean.class)); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/ServletTestExecutionListenerJUnitIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/web/ServletTestExecutionListenerJUnitIntegrationTests.java new file mode 100644 index 00000000..f8dfa2c9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/ServletTestExecutionListenerJUnitIntegrationTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.web; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import static org.junit.Assert.*; + +/** + * JUnit-based integration tests for {@link ServletTestExecutionListener}. + * + * @author Sam Brannen + * @since 3.2.9 + * @see org.springframework.test.context.testng.web.ServletTestExecutionListenerTestNGIntegrationTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class ServletTestExecutionListenerJUnitIntegrationTests { + + @Configuration + static class Config { + /* no beans required for this test */ + } + + + @Autowired + private MockHttpServletRequest servletRequest; + + + /** + * Verifies bug fix for <a href="https://jira.spring.io/browse/SPR-11626">SPR-11626</a>. + * + * @see #ensureMocksAreReinjectedBetweenTests_2 + */ + @Test + public void ensureMocksAreReinjectedBetweenTests_1() { + assertInjectedServletRequestEqualsRequestInRequestContextHolder(); + } + + /** + * Verifies bug fix for <a href="https://jira.spring.io/browse/SPR-11626">SPR-11626</a>. + * + * @see #ensureMocksAreReinjectedBetweenTests_1 + */ + @Test + public void ensureMocksAreReinjectedBetweenTests_2() { + assertInjectedServletRequestEqualsRequestInRequestContextHolder(); + } + + private void assertInjectedServletRequestEqualsRequestInRequestContextHolder() { + assertEquals("Injected ServletRequest must be stored in the RequestContextHolder", servletRequest, + ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/ServletTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/web/ServletTestExecutionListenerTests.java new file mode 100644 index 00000000..54b23941 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/ServletTestExecutionListenerTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.web; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.context.ApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.TestContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletWebRequest; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.context.web.ServletTestExecutionListener.*; + +/** + * Unit tests for {@link ServletTestExecutionListener}. + * + * @author Sam Brannen + * @since 3.2.6 + */ +public class ServletTestExecutionListenerTests { + + private static final String SET_UP_OUTSIDE_OF_STEL = "SET_UP_OUTSIDE_OF_STEL"; + + private final WebApplicationContext wac = mock(WebApplicationContext.class); + private final MockServletContext mockServletContext = new MockServletContext(); + private final TestContext testContext = mock(TestContext.class); + private final ServletTestExecutionListener listener = new ServletTestExecutionListener(); + + + private void assertAttributesAvailable() { + assertNotNull("request attributes should be available", RequestContextHolder.getRequestAttributes()); + } + + private void assertAttributesNotAvailable() { + assertNull("request attributes should not be available", RequestContextHolder.getRequestAttributes()); + } + + private void assertAttributeExists() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + assertNotNull("request attributes should exist", requestAttributes); + Object setUpOutsideOfStel = requestAttributes.getAttribute(SET_UP_OUTSIDE_OF_STEL, + RequestAttributes.SCOPE_REQUEST); + assertNotNull(SET_UP_OUTSIDE_OF_STEL + " should exist as a request attribute", setUpOutsideOfStel); + } + + private void assertAttributeDoesNotExist() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + assertNotNull("request attributes should exist", requestAttributes); + Object setUpOutsideOfStel = requestAttributes.getAttribute(SET_UP_OUTSIDE_OF_STEL, + RequestAttributes.SCOPE_REQUEST); + assertNull(SET_UP_OUTSIDE_OF_STEL + " should NOT exist as a request attribute", setUpOutsideOfStel); + } + + @Before + public void setUp() { + when(wac.getServletContext()).thenReturn(mockServletContext); + when(testContext.getApplicationContext()).thenReturn(wac); + + MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext); + MockHttpServletResponse response = new MockHttpServletResponse(); + ServletWebRequest servletWebRequest = new ServletWebRequest(request, response); + + request.setAttribute(SET_UP_OUTSIDE_OF_STEL, "true"); + + RequestContextHolder.setRequestAttributes(servletWebRequest); + assertAttributeExists(); + } + + @Test + public void standardApplicationContext() throws Exception { + Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(getClass()); + when(testContext.getApplicationContext()).thenReturn(mock(ApplicationContext.class)); + + listener.beforeTestClass(testContext); + assertAttributeExists(); + + listener.prepareTestInstance(testContext); + assertAttributeExists(); + + listener.beforeTestMethod(testContext); + assertAttributeExists(); + + listener.afterTestMethod(testContext); + assertAttributeExists(); + } + + @Test + public void legacyWebTestCaseWithoutExistingRequestAttributes() throws Exception { + Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(LegacyWebTestCase.class); + + RequestContextHolder.resetRequestAttributes(); + assertAttributesNotAvailable(); + + listener.beforeTestClass(testContext); + + listener.prepareTestInstance(testContext); + assertAttributesNotAvailable(); + verify(testContext, times(0)).setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + when(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE)).thenReturn(null); + + listener.beforeTestMethod(testContext); + assertAttributesNotAvailable(); + verify(testContext, times(0)).setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + + listener.afterTestMethod(testContext); + verify(testContext, times(1)).removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE); + assertAttributesNotAvailable(); + } + + @Test + public void legacyWebTestCaseWithPresetRequestAttributes() throws Exception { + Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(LegacyWebTestCase.class); + + listener.beforeTestClass(testContext); + assertAttributeExists(); + + listener.prepareTestInstance(testContext); + assertAttributeExists(); + verify(testContext, times(0)).setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + when(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE)).thenReturn(null); + + listener.beforeTestMethod(testContext); + assertAttributeExists(); + verify(testContext, times(0)).setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + when(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE)).thenReturn(null); + + listener.afterTestMethod(testContext); + verify(testContext, times(1)).removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE); + assertAttributeExists(); + } + + @Test + public void atWebAppConfigTestCaseWithoutExistingRequestAttributes() throws Exception { + Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(AtWebAppConfigWebTestCase.class); + + RequestContextHolder.resetRequestAttributes(); + listener.beforeTestClass(testContext); + assertAttributesNotAvailable(); + + assertWebAppConfigTestCase(); + } + + @Test + public void atWebAppConfigTestCaseWithPresetRequestAttributes() throws Exception { + Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(AtWebAppConfigWebTestCase.class); + + listener.beforeTestClass(testContext); + assertAttributesAvailable(); + + assertWebAppConfigTestCase(); + } + + private void assertWebAppConfigTestCase() throws Exception { + listener.prepareTestInstance(testContext); + assertAttributeDoesNotExist(); + verify(testContext, times(1)).setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + verify(testContext, times(1)).setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + when(testContext.getAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE)).thenReturn(Boolean.TRUE); + when(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE)).thenReturn(Boolean.TRUE); + + listener.beforeTestMethod(testContext); + assertAttributeDoesNotExist(); + verify(testContext, times(1)).setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + verify(testContext, times(1)).setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE); + + listener.afterTestMethod(testContext); + verify(testContext).removeAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE); + verify(testContext).removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE); + assertAttributesNotAvailable(); + } + + + static class LegacyWebTestCase { + } + + @WebAppConfiguration + static class AtWebAppConfigWebTestCase { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/WebContextLoaderTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/web/WebContextLoaderTestSuite.java new file mode 100644 index 00000000..8ccd8b4f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/WebContextLoaderTestSuite.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.web; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.test.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; + +/** + * Convenience test suite for integration tests that verify support for + * {@link WebApplicationContext} {@linkplain ContextLoader context loaders} + * in the TestContext framework. + * + * @author Sam Brannen + * @since 3.2 + */ +@RunWith(Suite.class) +// Note: the following 'multi-line' layout is for enhanced code readability. +@SuiteClasses({// +BasicXmlWacTests.class,// + BasicAnnotationConfigWacTests.class,// + RequestAndSessionScopedBeansWacTests.class // +}) +public class WebContextLoaderTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/jdbc/JdbcTestUtilsTests.java b/spring-test/src/test/java/org/springframework/test/jdbc/JdbcTestUtilsTests.java new file mode 100644 index 00000000..6ebdf918 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/jdbc/JdbcTestUtilsTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.jdbc; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; + +import java.io.LineNumberReader; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Unit tests for {@link JdbcTestUtils}. + * + * @author Thomas Risberg + * @author Sam Brannen + * @author Phillip Webb + * @since 2.5.4 + */ +@RunWith(MockitoJUnitRunner.class) +public class JdbcTestUtilsTests { + + @Mock + private JdbcTemplate jdbcTemplate; + + @Test + public void containsDelimiters() { + assertTrue("test with ';' is wrong", !JdbcTestUtils.containsSqlScriptDelimiters("select 1\n select ';'", ';')); + assertTrue("test with delimiter ; is wrong", + JdbcTestUtils.containsSqlScriptDelimiters("select 1; select 2", ';')); + assertTrue("test with '\\n' is wrong", + !JdbcTestUtils.containsSqlScriptDelimiters("select 1; select '\\n\n';", '\n')); + assertTrue("test with delimiter \\n is wrong", + JdbcTestUtils.containsSqlScriptDelimiters("select 1\n select 2", '\n')); + } + + @Test + public void splitSqlScriptDelimitedWithSemicolon() { + String rawStatement1 = "insert into customer (id, name)\nvalues (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')"; + String cleanedStatement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')"; + String rawStatement2 = "insert into orders(id, order_date, customer_id)\nvalues (1, '2008-01-02', 2)"; + String cleanedStatement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String rawStatement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String cleanedStatement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + char delim = ';'; + String script = rawStatement1 + delim + rawStatement2 + delim + rawStatement3 + delim; + List<String> statements = new ArrayList<String>(); + JdbcTestUtils.splitSqlScript(script, delim, statements); + assertEquals("wrong number of statements", 3, statements.size()); + assertEquals("statement 1 not split correctly", cleanedStatement1, statements.get(0)); + assertEquals("statement 2 not split correctly", cleanedStatement2, statements.get(1)); + assertEquals("statement 3 not split correctly", cleanedStatement3, statements.get(2)); + } + + @Test + public void splitSqlScriptDelimitedWithNewLine() { + String statement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')"; + String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + char delim = '\n'; + String script = statement1 + delim + statement2 + delim + statement3 + delim; + List<String> statements = new ArrayList<String>(); + JdbcTestUtils.splitSqlScript(script, delim, statements); + assertEquals("wrong number of statements", 3, statements.size()); + assertEquals("statement 1 not split correctly", statement1, statements.get(0)); + assertEquals("statement 2 not split correctly", statement2, statements.get(1)); + assertEquals("statement 3 not split correctly", statement3, statements.get(2)); + } + + @Test + public void readAndSplitScriptContainingComments() throws Exception { + + EncodedResource resource = new EncodedResource(new ClassPathResource("test-data-with-comments.sql", getClass())); + LineNumberReader lineNumberReader = new LineNumberReader(resource.getReader()); + + String script = JdbcTestUtils.readScript(lineNumberReader); + + char delim = ';'; + List<String> statements = new ArrayList<String>(); + JdbcTestUtils.splitSqlScript(script, delim, statements); + + String statement1 = "insert into customer (id, name) values (1, 'Rod; Johnson'), (2, 'Adrian Collier')"; + String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + // Statement 4 addresses the error described in SPR-9982. + String statement4 = "INSERT INTO persons( person_id , name) VALUES( 1 , 'Name' )"; + + assertEquals("wrong number of statements", 4, statements.size()); + assertEquals("statement 1 not split correctly", statement1, statements.get(0)); + assertEquals("statement 2 not split correctly", statement2, statements.get(1)); + assertEquals("statement 3 not split correctly", statement3, statements.get(2)); + assertEquals("statement 4 not split correctly", statement4, statements.get(3)); + } + + @Test + public void testDeleteNoWhere() throws Exception { + given(jdbcTemplate.update("DELETE FROM person")).willReturn(10); + int deleted = JdbcTestUtils.deleteFromTableWhere(jdbcTemplate, "person", null); + assertThat(deleted, equalTo(10)); + } + + @Test + public void testDeleteWhere() throws Exception { + given(jdbcTemplate.update("DELETE FROM person WHERE name = 'Bob' and age > 25")).willReturn(10); + int deleted = JdbcTestUtils.deleteFromTableWhere(jdbcTemplate, "person", "name = 'Bob' and age > 25"); + assertThat(deleted, equalTo(10)); + } + + @Test + public void deleteWhereAndArguments() throws Exception { + given(jdbcTemplate.update("DELETE FROM person WHERE name = ? and age > ?", "Bob", 25)).willReturn(10); + int deleted = JdbcTestUtils.deleteFromTableWhere(jdbcTemplate, "person", "name = ? and age > ?", "Bob", 25); + assertThat(deleted, equalTo(10)); + } + + +} diff --git a/spring-test/src/test/java/org/springframework/test/transaction/TransactionTestUtils.java b/spring-test/src/test/java/org/springframework/test/transaction/TransactionTestUtils.java new file mode 100644 index 00000000..d5598c26 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/transaction/TransactionTestUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.transaction; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Collection of JDK 1.4+ utilities for tests involving transactions. Intended + * for internal use within the Spring testing suite. + * + * <p>All {@code assert*()} methods throw {@link AssertionError}s. + * + * @author Sam Brannen + * @since 2.5 + */ +public abstract class TransactionTestUtils { + + /** + * Convenience method for determining if a transaction is active for the + * current {@link Thread}. + * @return {@code true} if a transaction is currently active + */ + public static boolean inTransaction() { + return TransactionSynchronizationManager.isActualTransactionActive(); + } + + /** + * Asserts whether or not a transaction is active for the current + * {@link Thread}. + * @param transactionExpected whether or not a transaction is expected + * @throws AssertionError if the supplied assertion fails + * @see #inTransaction() + */ + public static void assertInTransaction(boolean transactionExpected) { + if (transactionExpected) { + assertCondition(inTransaction(), "The current thread should be associated with a transaction."); + } + else { + assertCondition(!inTransaction(), "The current thread should not be associated with a transaction"); + } + } + + /** + * Fails by throwing an {@code AssertionError} with the supplied + * {@code message}. + * @param message the exception message to use + * @see #assertCondition(boolean, String) + */ + private static void fail(String message) throws AssertionError { + throw new AssertionError(message); + } + + /** + * Assert the provided boolean {@code condition}, throwing + * {@code AssertionError} with the supplied {@code message} if + * the test result is {@code false}. + * @param condition a boolean expression + * @param message the exception message to use if the assertion fails + * @throws AssertionError if condition is {@code false} + * @see #fail(String) + */ + private static void assertCondition(boolean condition, String message) throws AssertionError { + if (!condition) { + fail(message); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java new file mode 100644 index 00000000..7e10a054 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java @@ -0,0 +1,280 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.util; + +import static org.junit.Assert.*; +import static org.springframework.test.util.ReflectionTestUtils.*; + +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.test.AssertThrows; +import org.springframework.test.util.subpackage.Component; +import org.springframework.test.util.subpackage.LegacyEntity; +import org.springframework.test.util.subpackage.Person; + +/** + * Unit tests for {@link ReflectionTestUtils}. + * + * @author Sam Brannen + * @author Juergen Hoeller + */ +@SuppressWarnings("deprecation") +public class ReflectionTestUtilsTests { + + private static final Float PI = new Float((float) 22 / 7); + + private final Person person = new Person(); + private final Component component = new Component(); + + + @Test + public void setAndGetFields() throws Exception { + + // --------------------------------------------------------------------- + // Standard + + setField(person, "id", new Long(99), long.class); + setField(person, "name", "Tom"); + setField(person, "age", new Integer(42)); + setField(person, "eyeColor", "blue", String.class); + setField(person, "likesPets", Boolean.TRUE); + setField(person, "favoriteNumber", PI, Number.class); + + assertEquals("ID (private field in a superclass)", 99, person.getId()); + assertEquals("name (protected field)", "Tom", person.getName()); + assertEquals("age (private field)", 42, person.getAge()); + assertEquals("eye color (package private field)", "blue", person.getEyeColor()); + assertEquals("'likes pets' flag (package private boolean field)", true, person.likesPets()); + assertEquals("'favorite number' (package field)", PI, person.getFavoriteNumber()); + + assertEquals(new Long(99), getField(person, "id")); + assertEquals("Tom", getField(person, "name")); + assertEquals(new Integer(42), getField(person, "age")); + assertEquals("blue", getField(person, "eyeColor")); + assertEquals(Boolean.TRUE, getField(person, "likesPets")); + assertEquals(PI, getField(person, "favoriteNumber")); + + // --------------------------------------------------------------------- + // Null - non-primitives + + setField(person, "name", null, String.class); + setField(person, "eyeColor", null, String.class); + setField(person, "favoriteNumber", null, Number.class); + + assertNull("name (protected field)", person.getName()); + assertNull("eye color (package private field)", person.getEyeColor()); + assertNull("'favorite number' (package field)", person.getFavoriteNumber()); + + // --------------------------------------------------------------------- + // Null - primitives + + new AssertThrows(IllegalArgumentException.class, + "Calling setField() with NULL for a primitive type should throw an IllegalArgumentException.") { + + @Override + public void test() throws Exception { + setField(person, "id", null, long.class); + } + }.runTest(); + + new AssertThrows(IllegalArgumentException.class, + "Calling setField() with NULL for a primitive type should throw an IllegalArgumentException.") { + + @Override + public void test() throws Exception { + setField(person, "age", null, int.class); + } + }.runTest(); + + new AssertThrows(IllegalArgumentException.class, + "Calling setField() with NULL for a primitive type should throw an IllegalArgumentException.") { + + @Override + public void test() throws Exception { + setField(person, "likesPets", null, boolean.class); + } + }.runTest(); + } + + /** + * Verifies behavior requested in <a href="https://jira.springsource.org/browse/SPR-9571">SPR-9571</a>. + */ + @Test + public void setFieldOnLegacyEntityWithSideEffectsInToString() { + String testCollaborator = "test collaborator"; + LegacyEntity entity = new LegacyEntity(); + setField(entity, "collaborator", testCollaborator, Object.class); + assertTrue(entity.toString().contains(testCollaborator)); + } + + @Test + public void invokeSetterAndMethods() throws Exception { + + // --------------------------------------------------------------------- + // Standard - properties + + invokeSetterMethod(person, "id", new Long(99), long.class); + invokeSetterMethod(person, "name", "Tom"); + invokeSetterMethod(person, "age", new Integer(42)); + invokeSetterMethod(person, "eyeColor", "blue", String.class); + invokeSetterMethod(person, "likesPets", Boolean.TRUE); + invokeSetterMethod(person, "favoriteNumber", PI, Number.class); + + assertEquals("ID (protected method in a superclass)", 99, person.getId()); + assertEquals("name (private method)", "Tom", person.getName()); + assertEquals("age (protected method)", 42, person.getAge()); + assertEquals("eye color (package private method)", "blue", person.getEyeColor()); + assertEquals("'likes pets' flag (protected method for a boolean)", true, person.likesPets()); + assertEquals("'favorite number' (protected method for a Number)", PI, person.getFavoriteNumber()); + + assertEquals(new Long(99), invokeGetterMethod(person, "id")); + assertEquals("Tom", invokeGetterMethod(person, "name")); + assertEquals(new Integer(42), invokeGetterMethod(person, "age")); + assertEquals("blue", invokeGetterMethod(person, "eyeColor")); + assertEquals(Boolean.TRUE, invokeGetterMethod(person, "likesPets")); + assertEquals(PI, invokeGetterMethod(person, "favoriteNumber")); + + // --------------------------------------------------------------------- + // Standard - setter methods + + invokeSetterMethod(person, "setId", new Long(1), long.class); + invokeSetterMethod(person, "setName", "Jerry", String.class); + invokeSetterMethod(person, "setAge", new Integer(33), int.class); + invokeSetterMethod(person, "setEyeColor", "green", String.class); + invokeSetterMethod(person, "setLikesPets", Boolean.FALSE, boolean.class); + invokeSetterMethod(person, "setFavoriteNumber", new Integer(42), Number.class); + + assertEquals("ID (protected method in a superclass)", 1, person.getId()); + assertEquals("name (private method)", "Jerry", person.getName()); + assertEquals("age (protected method)", 33, person.getAge()); + assertEquals("eye color (package private method)", "green", person.getEyeColor()); + assertEquals("'likes pets' flag (protected method for a boolean)", false, person.likesPets()); + assertEquals("'favorite number' (protected method for a Number)", new Integer(42), person.getFavoriteNumber()); + + assertEquals(new Long(1), invokeGetterMethod(person, "getId")); + assertEquals("Jerry", invokeGetterMethod(person, "getName")); + assertEquals(new Integer(33), invokeGetterMethod(person, "getAge")); + assertEquals("green", invokeGetterMethod(person, "getEyeColor")); + assertEquals(Boolean.FALSE, invokeGetterMethod(person, "likesPets")); + assertEquals(new Integer(42), invokeGetterMethod(person, "getFavoriteNumber")); + + // --------------------------------------------------------------------- + // Null - non-primitives + + invokeSetterMethod(person, "name", null, String.class); + invokeSetterMethod(person, "eyeColor", null, String.class); + invokeSetterMethod(person, "favoriteNumber", null, Number.class); + + assertNull("name (private method)", person.getName()); + assertNull("eye color (package private method)", person.getEyeColor()); + assertNull("'favorite number' (protected method for a Number)", person.getFavoriteNumber()); + + // --------------------------------------------------------------------- + // Null - primitives + + new AssertThrows(IllegalArgumentException.class, + "Calling invokeSetterMethod() with NULL for a primitive type should throw an IllegalArgumentException.") { + + @Override + public void test() throws Exception { + invokeSetterMethod(person, "id", null, long.class); + } + }.runTest(); + + new AssertThrows(IllegalArgumentException.class, + "Calling invokeSetterMethod() with NULL for a primitive type should throw an IllegalArgumentException.") { + + @Override + public void test() throws Exception { + invokeSetterMethod(person, "age", null, int.class); + } + }.runTest(); + + new AssertThrows(IllegalArgumentException.class, + "Calling invokeSetterMethod() with NULL for a primitive type should throw an IllegalArgumentException.") { + + @Override + public void test() throws Exception { + invokeSetterMethod(person, "likesPets", null, boolean.class); + } + }.runTest(); + } + + @Test + public void invokeMethodWithAutoboxingAndUnboxing() { + // IntelliJ IDEA 11 won't accept int assignment here + Integer difference = invokeMethod(component, "subtract", 5, 2); + assertEquals("subtract(5, 2)", 3, difference.intValue()); + } + + @Ignore("[SPR-8644] findMethod() does not currently support var-args") + @Test + public void invokeMethodWithPrimitiveVarArgs() { + // IntelliJ IDEA 11 won't accept int assignment here + Integer sum = invokeMethod(component, "add", 1, 2, 3, 4); + assertEquals("add(1,2,3,4)", 10, sum.intValue()); + } + + @Test + public void invokeMethodWithPrimitiveVarArgsAsSingleArgument() { + // IntelliJ IDEA 11 won't accept int assignment here + Integer sum = invokeMethod(component, "add", new int[] { 1, 2, 3, 4 }); + assertEquals("add(1,2,3,4)", 10, sum.intValue()); + } + + @Test + public void invokeMethodsSimulatingLifecycleEvents() { + assertNull("number", component.getNumber()); + assertNull("text", component.getText()); + + // Simulate autowiring a configuration method + invokeMethod(component, "configure", new Integer(42), "enigma"); + assertEquals("number should have been configured", new Integer(42), component.getNumber()); + assertEquals("text should have been configured", "enigma", component.getText()); + + // Simulate @PostConstruct life-cycle event + invokeMethod(component, "init"); + // assertions in init() should succeed + + // Simulate @PreDestroy life-cycle event + invokeMethod(component, "destroy"); + assertNull("number", component.getNumber()); + assertNull("text", component.getText()); + } + + @Test(expected = IllegalStateException.class) + public void invokeMethodWithIncompatibleArgumentTypes() { + invokeMethod(component, "subtract", "foo", 2.0); + } + + @Test(expected = IllegalStateException.class) + public void invokeInitMethodBeforeAutowiring() { + invokeMethod(component, "init"); + } + + @Test(expected = IllegalStateException.class) + public void invokeMethodWithTooFewArguments() { + invokeMethod(component, "configure", new Integer(42)); + } + + @Test(expected = IllegalStateException.class) + public void invokeMethodWithTooManyArguments() { + invokeMethod(component, "configure", new Integer(42), "enigma", "baz", "quux"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/Component.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/Component.java new file mode 100644 index 00000000..edb2e719 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/Component.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.util.subpackage; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Simple POJO representing a <em>component</em>; intended for use in + * unit tests. + * + * @author Sam Brannen + * @since 3.1 + */ +public class Component { + + private Integer number; + private String text; + + + public Integer getNumber() { + return this.number; + } + + public String getText() { + return this.text; + } + + @Autowired + protected void configure(Integer number, String text) { + this.number = number; + this.text = text; + } + + @PostConstruct + protected void init() { + Assert.state(number != null, "number must not be null"); + Assert.state(StringUtils.hasText(text), "text must not be empty"); + } + + @PreDestroy + protected void destroy() { + this.number = null; + this.text = null; + } + + int subtract(int a, int b) { + return a - b; + } + + int add(int... args) { + int sum = 0; + for (int i = 0; i < args.length; i++) { + sum += args[i]; + } + return sum; + } + + int multiply(Integer... args) { + int product = 1; + for (int i = 0; i < args.length; i++) { + product *= args[i]; + } + return product; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/LegacyEntity.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/LegacyEntity.java new file mode 100644 index 00000000..19507c93 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/LegacyEntity.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.util.subpackage; + +import org.springframework.core.style.ToStringCreator; + +/** + * A <em>legacy entity</em> whose {@link #toString()} method has side effects; + * intended for use in unit tests. + * + * @author Sam Brannen + * @since 3.2 + */ +public class LegacyEntity { + + private Object collaborator = new Object() { + + public String toString() { + throw new RuntimeException( + "Invoking toString() on the default collaborator causes an undesirable side effect"); + }; + }; + + + public String toString() { + return new ToStringCreator(this)// + .append("collaborator", this.collaborator)// + .toString(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/PersistentEntity.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/PersistentEntity.java new file mode 100644 index 00000000..3ff3d91d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/PersistentEntity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2007-2011 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 + * + * http://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.util.subpackage; + +/** + * Abstract base class for <em>persistent entities</em>; intended for use in + * unit tests. + * + * @author Sam Brannen + * @since 2.5 + */ +public abstract class PersistentEntity { + + private long id; + + + public final long getId() { + return this.id; + } + + protected final void setId(long id) { + this.id = id; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/Person.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/Person.java new file mode 100644 index 00000000..66304779 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/Person.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.util.subpackage; + +import org.springframework.core.style.ToStringCreator; + +/** + * Concrete subclass of {@link PersistentEntity} representing a <em>person</em> + * entity; intended for use in unit tests. + * + * @author Sam Brannen + * @since 2.5 + */ +public class Person extends PersistentEntity { + + protected String name; + + private int age; + + String eyeColor; + + boolean likesPets = false; + + private Number favoriteNumber; + + + public final String getName() { + return this.name; + } + + @SuppressWarnings("unused") + private final void setName(final String name) { + this.name = name; + } + + public final int getAge() { + return this.age; + } + + protected final void setAge(final int age) { + this.age = age; + } + + public final String getEyeColor() { + return this.eyeColor; + } + + final void setEyeColor(final String eyeColor) { + this.eyeColor = eyeColor; + } + + public final boolean likesPets() { + return this.likesPets; + } + + protected final void setLikesPets(final boolean likesPets) { + this.likesPets = likesPets; + } + + public final Number getFavoriteNumber() { + return this.favoriteNumber; + } + + protected final void setFavoriteNumber(Number favoriteNumber) { + this.favoriteNumber = favoriteNumber; + } + + public String toString() { + return new ToStringCreator(this) + + .append("id", this.getId()) + + .append("name", this.name) + + .append("age", this.age) + + .append("eyeColor", this.eyeColor) + + .append("likesPets", this.likesPets) + + .append("favoriteNumber", this.favoriteNumber) + + .toString(); + } +} diff --git a/spring-test/src/test/java/org/springframework/web/multipart/MockMultipartHttpServletRequestTests.java b/spring-test/src/test/java/org/springframework/web/multipart/MockMultipartHttpServletRequestTests.java new file mode 100644 index 00000000..029f584c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/web/multipart/MockMultipartHttpServletRequestTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2006 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 + * + * http://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.web.multipart; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ObjectUtils; + +/** + * @author Juergen Hoeller + */ +public class MockMultipartHttpServletRequestTests { + + @Test + public void mockMultipartHttpServletRequestWithByteArray() throws IOException { + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + assertFalse(request.getFileNames().hasNext()); + assertNull(request.getFile("file1")); + assertNull(request.getFile("file2")); + assertTrue(request.getFileMap().isEmpty()); + + request.addFile(new MockMultipartFile("file1", "myContent1".getBytes())); + request.addFile(new MockMultipartFile("file2", "myOrigFilename", "text/plain", "myContent2".getBytes())); + doTestMultipartHttpServletRequest(request); + } + + @Test + public void mockMultipartHttpServletRequestWithInputStream() throws IOException { + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + request.addFile(new MockMultipartFile("file1", new ByteArrayInputStream("myContent1".getBytes()))); + request.addFile(new MockMultipartFile("file2", "myOrigFilename", "text/plain", new ByteArrayInputStream( + "myContent2".getBytes()))); + doTestMultipartHttpServletRequest(request); + } + + private void doTestMultipartHttpServletRequest(MultipartHttpServletRequest request) throws IOException { + Set<String> fileNames = new HashSet<String>(); + Iterator<String> fileIter = request.getFileNames(); + while (fileIter.hasNext()) { + fileNames.add(fileIter.next()); + } + assertEquals(2, fileNames.size()); + assertTrue(fileNames.contains("file1")); + assertTrue(fileNames.contains("file2")); + MultipartFile file1 = request.getFile("file1"); + MultipartFile file2 = request.getFile("file2"); + Map<String, MultipartFile> fileMap = request.getFileMap(); + List<String> fileMapKeys = new LinkedList<String>(fileMap.keySet()); + assertEquals(2, fileMapKeys.size()); + assertEquals(file1, fileMap.get("file1")); + assertEquals(file2, fileMap.get("file2")); + + assertEquals("file1", file1.getName()); + assertEquals("", file1.getOriginalFilename()); + assertNull(file1.getContentType()); + assertTrue(ObjectUtils.nullSafeEquals("myContent1".getBytes(), file1.getBytes())); + assertTrue(ObjectUtils.nullSafeEquals("myContent1".getBytes(), + FileCopyUtils.copyToByteArray(file1.getInputStream()))); + assertEquals("file2", file2.getName()); + assertEquals("myOrigFilename", file2.getOriginalFilename()); + assertEquals("text/plain", file2.getContentType()); + assertTrue(ObjectUtils.nullSafeEquals("myContent2".getBytes(), file2.getBytes())); + assertTrue(ObjectUtils.nullSafeEquals("myContent2".getBytes(), + FileCopyUtils.copyToByteArray(file2.getInputStream()))); + } + +} diff --git a/spring-test/src/test/resources/log4j.xml b/spring-test/src/test/resources/log4j.xml new file mode 100644 index 00000000..4375dcce --- /dev/null +++ b/spring-test/src/test/resources/log4j.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> + +<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> + + <!-- Appenders --> + <appender name="console" class="org.apache.log4j.ConsoleAppender"> + <param name="Target" value="System.out" /> + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%-5p: %c - %m%n" /> + </layout> + </appender> + + <appender name="file" class="org.apache.log4j.FileAppender"> + <param name="File" value="build/spring-test.log" /> + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%-5p: %c - %m%n" /> + </layout> + </appender> + + <logger name="org.springframework.beans"> + <level value="warn" /> + </logger> + + <logger name="org.springframework.test.context.TestContext"> + <level value="warn" /> + </logger> + <logger name="org.springframework.test.context.ContextLoaderUtils"> + <level value="warn" /> + </logger> + +<!-- + <logger name="org.springframework.test.context.support.DelegatingSmartContextLoader"> + <level value="info" /> + </logger> + <logger name="org.springframework.test.context.support.AbstractGenericContextLoader"> + <level value="info" /> + </logger> + <logger name="org.springframework.test.context.support.AnnotationConfigContextLoader"> + <level value="info" /> + </logger> +--> + + <logger name="org.springframework.test.context.transaction.TransactionalTestExecutionListener"> + <level value="warn" /> + </logger> + + <logger name="org.springframework.test.context.web"> + <level value="warn" /> + </logger> + + <logger name="org.springframework.test.context"> + <level value="warn" /> + </logger> + + <!-- Root Logger --> + <root> + <priority value="error" /> + <appender-ref ref="console" /> + <appender-ref ref="file" /> + </root> + +</log4j:configuration>
\ No newline at end of file diff --git a/spring-test/src/test/resources/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests-ChildConfig.xml b/spring-test/src/test/resources/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests-ChildConfig.xml new file mode 100644 index 00000000..2189b42f --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/hierarchies/standard/SingleTestClassWithTwoLevelContextHierarchyAndMixedConfigTypesTests-ChildConfig.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="bar" class="java.lang.String" c:_="bar" /> + + <bean id="baz" class="java.lang.String" c:_="baz-child" /> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyAndMixedConfigTypesTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyAndMixedConfigTypesTests-context.xml new file mode 100644 index 00000000..3abc44bd --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/hierarchies/standard/TestHierarchyLevelTwoWithSingleLevelContextHierarchyAndMixedConfigTypesTests-context.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="foo" class="java.lang.String" c:_="foo-level-2" /> + + <bean id="baz" class="java.lang.String" c:_="baz" /> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/context/hierarchies/web/DispatcherWacRootWacEarTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/hierarchies/web/DispatcherWacRootWacEarTests-context.xml new file mode 100644 index 00000000..496fc0b0 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/hierarchies/web/DispatcherWacRootWacEarTests-context.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="dispatcher" class="java.lang.String" c:_="dispatcher" /> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests-context.xml new file mode 100644 index 00000000..d0c28615 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests-context.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:mvc="http://www.springframework.org/schema/mvc" + xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <mvc:annotation-driven /> + <mvc:default-servlet-handler /> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$ImproperDuplicateDefaultXmlAndConfigClassTestCase-context.xml b/spring-test/src/test/resources/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$ImproperDuplicateDefaultXmlAndConfigClassTestCase-context.xml new file mode 100644 index 00000000..6f4ac807 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$ImproperDuplicateDefaultXmlAndConfigClassTestCase-context.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:c="http://www.springframework.org/schema/c" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <!-- intentionally empty: we just need the file to be present to fail the test --> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$XmlTestCase-context.xml b/spring-test/src/test/resources/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$XmlTestCase-context.xml new file mode 100644 index 00000000..965ab428 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$XmlTestCase-context.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:c="http://www.springframework.org/schema/c" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + + <bean id="foo" class="java.lang.String" c:_="foo" /> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/context/web/BasicXmlWacTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/web/BasicXmlWacTests-context.xml new file mode 100644 index 00000000..90653e21 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/web/BasicXmlWacTests-context.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="foo" class="java.lang.String" c:_="bar" /> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/context/web/RequestAndSessionScopedBeansWacTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/web/RequestAndSessionScopedBeansWacTests-context.xml new file mode 100644 index 00000000..7f2a6c5b --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/web/RequestAndSessionScopedBeansWacTests-context.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="requestScopedTestBean" class="org.springframework.tests.sample.beans.TestBean" scope="request"> + <property name="name" value="#{request.contextPath}" /> + </bean> + + <bean id="sessionScopedTestBean" class="org.springframework.tests.sample.beans.TestBean" scope="session" /> + +</beans> diff --git a/spring-test/src/test/resources/org/springframework/test/jdbc/test-data-with-comments.sql b/spring-test/src/test/resources/org/springframework/test/jdbc/test-data-with-comments.sql new file mode 100644 index 00000000..82483ca4 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/jdbc/test-data-with-comments.sql @@ -0,0 +1,16 @@ +-- The next comment line has no text after the '--' prefix. +-- +-- The next comment line starts with a space. + -- x, y, z... + +insert into customer (id, name) +values (1, 'Rod; Johnson'), (2, 'Adrian Collier'); +-- This is also a comment. +insert into orders(id, order_date, customer_id) +values (1, '2008-01-02', 2); +insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2); +INSERT INTO persons( person_id-- + , name) +VALUES( 1 -- person_id + , 'Name' --name +);--
\ No newline at end of file |