├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project └── plugins.sbt └── src ├── main ├── java │ └── org │ │ └── specs2 │ │ └── spring │ │ ├── BlankJndiBuilder.java │ │ ├── Environment.java │ │ ├── EnvironmentCreationException.java │ │ ├── EnvironmentExtractor.java │ │ ├── JndiBuilder.java │ │ ├── JndiEnvironmentSetter.java │ │ ├── TestContext.java │ │ ├── TestTransactionDefinitionExtractor.java │ │ └── annotation │ │ ├── Bean.java │ │ ├── DataSource.java │ │ ├── Jms.java │ │ ├── JmsQueue.java │ │ ├── JmsTopic.java │ │ ├── Jndi.java │ │ ├── MailSession.java │ │ ├── Property.java │ │ ├── SystemEnvironment.java │ │ ├── SystemProperties.java │ │ ├── TransactionManager.java │ │ ├── UseProfile.java │ │ └── WorkManager.java └── scala │ └── org │ └── specs2 │ └── spring │ ├── BeanTables.scala │ ├── DataAccess.scala │ ├── SettableEnvironment.scala │ └── SpecificationLike.scala └── test ├── resources └── META-INF │ ├── MANIFEST.MF │ └── spring │ └── module-context.xml └── scala └── org └── specs2 └── spring ├── EmptySuiteSpec.scala ├── HibernateTemplateDataAccessSpec.scala ├── NoSuchMethodArgsSpec.scala ├── SpecificationSpec.scala ├── SpringComponent.scala └── domain └── User.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._.DS_Store* 3 | .metadata 4 | .project 5 | .classpath 6 | .settings 7 | .history 8 | gen 9 | **/*.swp 10 | **/*~.nib 11 | **/build/ 12 | **/*.pbxuser 13 | **/*.perspective 14 | **/*.perspectivev3 15 | **/*.xcodeproj/xcuserdata/* 16 | **/*.xcodeproj/project.xcworkspace/xcuserdata/* 17 | **/target 18 | target 19 | *.iml 20 | project/*.ipr 21 | project/*.iml 22 | project/*.iws 23 | project/out 24 | project/*/target 25 | project/target 26 | project/*/bin 27 | project/*/build 28 | project/*.iml 29 | project/*/*.iml 30 | project/.idea 31 | project/.idea/* 32 | .idea 33 | .idea/* 34 | .idea/**/* 35 | .DS_Store 36 | project/.DS_Store 37 | project/*/.DS_Store 38 | tm.out 39 | tmlog*.log 40 | *.tm*.epoch -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # See http://about.travis-ci.org/docs/user/build-configuration/ 2 | language: scala 3 | 4 | scala: 5 | - 2.10.4 6 | 7 | # Testing with OpenJDK7 as well just to be adventurous! 8 | jdk: 9 | - oraclejdk7 10 | - openjdk7 11 | 12 | # Custom notification settings 13 | notifications: 14 | email: 15 | recipients: 16 | - anirvanchakraborty@gmail.com 17 | - jan.machacek@gmail.com 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012 Jan Machacek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 6 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 11 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 12 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 13 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Specs2 Spring [![Build Status](https://travis-ci.org/eigengo/specs2-spring.png?branch=master)](https://travis-ci.org/eigengo/specs2-spring) 2 | Specs2 Extension to simplify integration testing with Spring. 3 | 4 | > More details at blogs at http://www.cakesolutions.net/teamblogs and http://www.cakesolutions.org 5 | 6 | Most Spring enterprise applications use some DataSources, TransactionManagers and other JEE beasts. In 7 | addition to having the beans injected into our specs, we would like to use Specs2 to perform the necessary 8 | integration testing, but we don't really want to create separate application context files for the tests. 9 | 10 | Instead, we would like to set up the JNDI environment for the test code and use the same application context files 11 | for both testing and production. This is where this project helps: the annotations on our test classes specify 12 | the JNDI environment we wish to build for the test. 13 | 14 | Verba docent, exempla trahunt, so I'll start you off with a simple sample. Let there be: 15 | ```scala 16 | @Component 17 | class SomeComponent @Autowired()(private val hibernateTemplate: HibernateTemplate) { 18 | 19 | @Transactional 20 | def generate(count: Int) { 21 | for (c <- 0 until count) { 22 | val rider = new Rider() 23 | rider.setName("Rider #" + c) 24 | this.hibernateTemplate.saveOrUpdate(rider) 25 | } 26 | } 27 | 28 | } 29 | ``` 30 | To get this running, we give the ``META-INF/spring/module-context.xml`` configuration file: 31 | ```xml 32 | 33 | 35 | 36 | 37 | 38 | ** 39 | ** 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.specs2.springexample 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ``` 57 | This context file is the same for both tests and for production. The "variable" items (``DataSource`` and ``TransactionManager``) beans are looked up from JNDI. 58 | 59 | To the test, then. We have simply 60 | ```scala 61 | @TransactionManager(name = "java:comp/TransactionManager") 62 | @DataSource(name = "java:comp/env/jdbc/test", driverClass = classOf[JDBCDriver], url = "jdbc:hsqldb:mem:test") 63 | @Transactional 64 | @TransactionConfiguration(defaultRollback = true) 65 | @ContextConfiguration(Array("classpath*:/META-INF/spring/module-context.xml")) 66 | class SomeComponentSpec extends org.specs2.spring.Specification { 67 | @Autowired var someComponent: SomeComponent = _ 68 | @Autowired var hibernateTemplate: HibernateTemplate = _ 69 | 70 | "The rider generation mechanism" in { 71 | "generate 100 riders " ! generate(100) 72 | } 73 | 74 | def generate(count: Int) = { 75 | this.someComponent.generate(count) 76 | this.hibernateTemplate.loadAll(classOf[Rider]) must have size (count) 77 | } 78 | 79 | } 80 | ``` 81 | If I wanted to have another integration test (perhaps testing another class), I would write: 82 | ```scala 83 | @TransactionManager(name = "java:comp/TransactionManager") 84 | @DataSource(name = "java:comp/env/jdbc/test", driverClass = classOf[JDBCDriver], url = "jdbc:hsqldb:mem:test") 85 | @Transactional 86 | @TransactionConfiguration(defaultRollback = true) 87 | @ContextConfiguration(Array("classpath*:/META-INF/spring/module-context.xml")) 88 | class SomeOtherComponentSpec extends org.specs2.spring.Specification { 89 | @Autowired var someOtherComponent: SomeOtherComponent = _ 90 | 91 | "Another component" in { 92 | "do something clever " ! doSomethingClever 93 | } 94 | 95 | 96 | def doSomethingClever = { 97 | success 98 | } 99 | 100 | } 101 | ``` 102 | At this point, you may notice the duplication in the annotations. The Specs2 spring extension allows you to "merge" these annotations into one annotation that you can use throughout your tests. You can therefore have: 103 | ```scala 104 | @TransactionManager(name = "java:comp/TransactionManager") 105 | @DataSource(name = "java:comp/env/jdbc/test", driverClass = classOf[JDBCDriver], url = "jdbc:hsqldb:mem:test") 106 | @Transactional 107 | @TransactionConfiguration(defaultRollback = true) 108 | @ContextConfiguration(Array("classpath*:/META-INF/spring/module-context.xml")) 109 | public @interface IntegrationTest { 110 | } 111 | ``` 112 | and modify the specs to just 113 | ```scala 114 | @IntegrationTest 115 | class SomeComponentSpec extends Specification { ... } 116 | 117 | @IntegrationTest 118 | class SomeComponentSpec extends Specification { ... } 119 | ``` 120 | To set up multiple ``DataSource``s, ``MailSession``s, ... as well as JMS queues and topics, you can use the Jndi annotation like this: 121 | ```java 122 | @Jndi( 123 | dataSources = { 124 | @DataSource(name = "java:comp/env/jdbc/test", 125 | driverClass = JDBCDriver.class, url = "jdbc:hsqldb:mem:test"), 126 | @DataSource(name = "java:comp/env/jdbc/external", 127 | driverClass = JDBCDriver.class, url = "jdbc:hsqldb:mem:external") 128 | }, 129 | mailSessions = @MailSession(name = "java:comp/env/mail/foo"), 130 | transactionManager = @TransactionManager(name = "java:comp/TransactionManager"), 131 | jms = @Jms( 132 | connectionFactoryName = "java:comp/env/jms/connectionFactory", 133 | queues = {@JmsQueue(name = "java:comp/env/jms/requests"), 134 | @JmsQueue(name = "java:comp/env/jms/responses")}, 135 | topics = {@JmsTopic(name = "java:comp/env/jms/cacheFlush"), 136 | @JmsTopic(name = "java:comp/env/jms/ruleUpdate")} 137 | ), 138 | workManagers = @WorkManager(name = "java:comp/env/work/WorkManager", kind = WorkManager.Kind.CommonJ) 139 | ) 140 | @Transactional 141 | @TransactionConfiguration(defaultRollback = true) 142 | @ContextConfiguration("classpath*:/META-INF/spring/module-context.xml") 143 | @Retention(RetentionPolicy.RUNTIME) 144 | public @interface IntegrationTest { 145 | } 146 | ``` 147 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbtrelease._ 2 | 3 | /** Project */ 4 | name := "spring" 5 | 6 | version := "2.3.10" 7 | 8 | organization := "org.specs2" 9 | 10 | useGpg := true 11 | 12 | scalaVersion := "2.10.4" 13 | 14 | /** Shell */ 15 | shellPrompt := { state => System.getProperty("user.name") + "> " } 16 | 17 | shellPrompt in ThisBuild := { state => Project.extract(state).currentRef.project + "> " } 18 | 19 | /** Dependencies */ 20 | resolvers ++= Seq("snapshots-repo" at "http://scala-tools.org/repo-snapshots") 21 | 22 | libraryDependencies ++= Seq( 23 | "org.specs2" %% "specs2-core" % "2.3.10", 24 | "org.specs2" %% "specs2-mock" % "2.3.10" % "optional", 25 | "org.specs2" %% "specs2-junit" % "2.3.10" % "optional", 26 | "org.mockito" % "mockito-core" % "1.9.5" % "optional", 27 | "org.springframework" % "spring-core" % "3.2.9.RELEASE" % "provided", 28 | "org.springframework" % "spring-beans" % "3.2.9.RELEASE" % "provided", 29 | "org.springframework" % "spring-jdbc" % "3.2.9.RELEASE" % "provided", 30 | "org.springframework" % "spring-tx" % "3.2.9.RELEASE" % "provided", 31 | "org.springframework" % "spring-test" % "3.2.9.RELEASE" % "provided", 32 | "org.springframework" % "spring-test" % "3.2.9.RELEASE" % "provided", 33 | "org.springframework" % "spring-orm" % "3.2.9.RELEASE" % "provided", 34 | "org.springframework" % "spring-web" % "3.2.9.RELEASE" % "provided", 35 | "org.springframework" % "spring-webmvc" % "3.2.9.RELEASE" % "provided", 36 | "org.springframework" % "spring-aspects" % "3.2.9.RELEASE" % "provided", 37 | "junit" % "junit" % "4.7" % "optional", 38 | "org.hsqldb" % "hsqldb" % "2.2.4" % "provided", 39 | "org.hibernate" % "hibernate-core" % "4.0.1.Final" % "provided", 40 | "javax.persistence" % "persistence-api" % "1.0" % "provided", 41 | "javax.mail" % "mail" % "1.4.1" % "provided", 42 | "javax.transaction" % "jta" % "1.1" % "provided", 43 | "com.atomikos" % "transactions-jta" % "3.7.0" % "provided", 44 | "com.atomikos" % "transactions-jdbc" % "3.7.0" % "provided", 45 | "org.apache.activemq" % "activemq-core" % "5.4.1" % "provided", 46 | "org.springframework" % "spring-instrument" % "3.2.9.RELEASE" % "test->runtime", 47 | "org.aspectj" % "aspectjweaver" % "1.6.12" % "test->runtime" 48 | ) 49 | 50 | /** Compilation */ 51 | javacOptions ++= Seq("-source", "1.7", "-target", "1.7") 52 | 53 | javaOptions += "-Xmx2G -XX:MaxPermSize=1024m" 54 | 55 | scalacOptions ++= Seq("-deprecation", "-unchecked") 56 | 57 | maxErrors := 20 58 | 59 | pollInterval := 1000 60 | 61 | logBuffered := false 62 | 63 | cancelable := true 64 | 65 | credentials += Credentials(Path.userHome / ".sonatype") 66 | 67 | testOptions := Seq(Tests.Filter(s => 68 | Seq("Spec", "Suite", "Unit", "all").exists(s.endsWith(_)) && 69 | !s.endsWith("FeaturesSpec") || 70 | s.contains("UserGuide") || 71 | s.contains("index") || 72 | s.matches("org.specs2.guide.*"))) 73 | 74 | /** Console */ 75 | initialCommands in console := "import org.specs2.spring._" 76 | 77 | publishTo <<= version { v: String => 78 | val nexus = "https://oss.sonatype.org/" 79 | if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") 80 | else Some("releases" at nexus + "service/local/staging/deploy/maven2") 81 | } 82 | 83 | publishMavenStyle := true 84 | 85 | publishArtifact in Test := false 86 | 87 | pomIncludeRepository := { x => false } 88 | 89 | pomExtra := ( 90 | http://www.eigengo.com/ 91 | 92 | 93 | BSD-style 94 | http://www.opensource.org/licenses/bsd-license.php 95 | repo 96 | 97 | 98 | 99 | git@github.com:eigengo/specs2-spring.git 100 | scm:git:git@github.com:eigengo/specs2-spring.git 101 | 102 | 103 | 104 | janmachacek 105 | Jan Machacek 106 | http://www.eigengo.com 107 | 108 | 109 | anirvanchakraborty 110 | Anirvan Chakraborty 111 | http://www.eigengo.com 112 | 113 | 114 | ) 115 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "0.8.3") 2 | 3 | addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3") 4 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/BlankJndiBuilder.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * No-op JNDI builder; does not contribute anything to the environment. 7 | * 8 | * @author janm 9 | */ 10 | public class BlankJndiBuilder implements JndiBuilder { 11 | 12 | public BlankJndiBuilder() { 13 | 14 | } 15 | 16 | public void build(Map environment) throws Exception { 17 | // do nothing 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/Environment.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import org.specs2.spring.annotation.*; 4 | import org.springframework.util.Assert; 5 | 6 | import java.sql.Driver; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * Contains the information that will be used as the source for the objects that will be added to the 12 | * JNDI environment. 13 | * 14 | * @author janmachacek 15 | */ 16 | class Environment { 17 | private final List dataSources = new ArrayList(); 18 | private final List transactionManagers = new ArrayList(); 19 | private final List mailSessions = new ArrayList(); 20 | private final List jmsDefinitions = new ArrayList(); 21 | private final List beans = new ArrayList(); 22 | private Class builder = BlankJndiBuilder.class; 23 | 24 | /** 25 | * Adds a DataSource 26 | * 27 | * @param dataSource the data source, or {@code null}. 28 | */ 29 | void addDataSource(DataSource dataSource) { 30 | if (dataSource == null) return; 31 | this.dataSources.add(new DataSourceDefinition(dataSource.name(), dataSource.driverClass(), dataSource.url(), dataSource.username(), dataSource.password())); 32 | } 33 | 34 | /** 35 | * Adds all DataSources 36 | * 37 | * @param dataSources all data sources, never {@code null}. 38 | */ 39 | void addDataSources(DataSource... dataSources) { 40 | for (DataSource dataSource : dataSources) addDataSource(dataSource); 41 | } 42 | 43 | /** 44 | * Adds a TransactionManager 45 | * 46 | * @param transactionManager the transaction manager or {@code null}. 47 | */ 48 | void addTransactionManager(TransactionManager transactionManager) { 49 | if (transactionManager == null) return; 50 | this.transactionManagers.add(new TransactionManagerDefinition(transactionManager.name())); 51 | } 52 | 53 | /** 54 | * Adds all TransactionManagers 55 | * 56 | * @param transactionManagers all transaction managers, never {@code null}. 57 | */ 58 | void addTransactionManagers(TransactionManager... transactionManagers) { 59 | for (TransactionManager transactionManager : transactionManagers) addTransactionManager(transactionManager); 60 | } 61 | 62 | void addMailSession(MailSession mailSession) { 63 | if (mailSession == null) return; 64 | this.mailSessions.add(new MailSessionDefinition(mailSession.name(), mailSession.properties())); 65 | } 66 | 67 | 68 | /** 69 | * Adds all mail sessions 70 | * 71 | * @param mailSessions all mail sessions, never {@code null}. 72 | */ 73 | void addMailSessions(MailSession... mailSessions) { 74 | for (MailSession mailSession : mailSessions) addMailSession(mailSession); 75 | } 76 | 77 | /** 78 | * Add all JMS broker defintions 79 | * 80 | * @param jmses the jms definitions, never {@code null}. 81 | */ 82 | public void addJmsBrokers(Jms[] jmses) { 83 | for (Jms jms : jmses) { 84 | final JmsDefinition jmsDefinition = new JmsDefinition(jms.connectionFactoryName()); 85 | jmsDefinition.addQueues(jms.queues()); 86 | jmsDefinition.addTopics(jms.topics()); 87 | this.jmsDefinitions.add(jmsDefinition); 88 | } 89 | } 90 | 91 | /** 92 | * Adds a Bean definition 93 | * 94 | * @param bean the bean, or {@code null}. 95 | */ 96 | void addBean(Bean bean) { 97 | if (bean == null) return; 98 | this.beans.add(new BeanDefinition(bean.name(), bean.clazz())); 99 | } 100 | 101 | /** 102 | * Adds all Bean definitions 103 | * 104 | * @param beans all definitions, never {@code null}. 105 | */ 106 | void addBeans(Bean[] beans) { 107 | for (Bean bean : beans) addBean(bean); 108 | } 109 | 110 | /** 111 | * Gets the JNDI builder class 112 | * 113 | * @return the class of the JNDI builder, never {@code null}. 114 | */ 115 | Class getBuilder() { 116 | return builder; 117 | } 118 | 119 | /** 120 | * Sets the JNDI builder class, never {@code null}. 121 | * 122 | * @param builder the class of JNDI builder. 123 | */ 124 | void setBuilder(Class builder) { 125 | Assert.notNull(builder, "The 'builder' argument cannot be null."); 126 | 127 | this.builder = builder; 128 | } 129 | 130 | // -- Getters 131 | 132 | List getDataSources() { 133 | return dataSources; 134 | } 135 | 136 | List getTransactionManagers() { 137 | return transactionManagers; 138 | } 139 | 140 | List getMailSessions() { 141 | return mailSessions; 142 | } 143 | 144 | List getJmsDefinitions() { 145 | return jmsDefinitions; 146 | } 147 | 148 | List getBeans() { 149 | return beans; 150 | } 151 | 152 | 153 | /** 154 | * TransactionManager definition. The {@link #name} sets the JNDI name of the queue. 155 | */ 156 | static class TransactionManagerDefinition { 157 | private final String name; 158 | 159 | TransactionManagerDefinition(String name) { 160 | this.name = name; 161 | } 162 | 163 | String getName() { 164 | return name; 165 | } 166 | } 167 | 168 | /** 169 | * Bean definition. The {@link #name} sets the JNDI name of the queue; the {@link #type} sets the type of the bean 170 | * to be created. The bean must have nullary constructor. 171 | */ 172 | static class BeanDefinition { 173 | private final String name; 174 | private final Class type; 175 | 176 | BeanDefinition(String name, Class type) { 177 | this.name = name; 178 | this.type = type; 179 | } 180 | 181 | String getName() { 182 | return name; 183 | } 184 | 185 | Class getType() { 186 | return type; 187 | } 188 | } 189 | 190 | /** 191 | * JMS definition. The {@link #connectionFactoryName} sets the JNDI name of the JMS {@code ConnectionFactory}; the 192 | * {@link #jmsQueues} and {@link #jmsTopics} defines the JMS queues and topics, respectively. 193 | */ 194 | static class JmsDefinition { 195 | private final String connectionFactoryName; 196 | private final List jmsQueues = new ArrayList(); 197 | private final List jmsTopics = new ArrayList(); 198 | 199 | JmsDefinition(String connectionFactoryName) { 200 | this.connectionFactoryName = connectionFactoryName; 201 | } 202 | 203 | void addTopics(JmsTopic... jmsTopics) { 204 | for (JmsTopic topic : jmsTopics) { 205 | this.jmsTopics.add(new JmsTopicDefinition(topic.name())); 206 | } 207 | } 208 | 209 | void addQueues(JmsQueue... queues) { 210 | for (JmsQueue queue : queues) { 211 | this.jmsQueues.add(new JmsQueueDefinition(queue.name())); 212 | } 213 | } 214 | 215 | String getConnectionFactoryName() { 216 | return connectionFactoryName; 217 | } 218 | 219 | List getJmsQueues() { 220 | return jmsQueues; 221 | } 222 | 223 | List getJmsTopics() { 224 | return jmsTopics; 225 | } 226 | } 227 | 228 | /** 229 | * JMS queue definition. The {@link #name} sets the JNDI name of the queue. 230 | */ 231 | static class JmsQueueDefinition { 232 | private final String name; 233 | 234 | JmsQueueDefinition(String name) { 235 | this.name = name; 236 | } 237 | 238 | String getName() { 239 | return name; 240 | } 241 | } 242 | 243 | /** 244 | * JMS topic definition. The {@link #name} sets the JNDI name of the queue. 245 | */ 246 | static class JmsTopicDefinition { 247 | private final String name; 248 | 249 | JmsTopicDefinition(String name) { 250 | this.name = name; 251 | } 252 | 253 | String getName() { 254 | return name; 255 | } 256 | } 257 | 258 | /** 259 | * Mail Session definition. The {@link #name} sets the JNDI name of the queue; the {@link #properties} sets 260 | * the {@code javax.mail.Session} properties. 261 | */ 262 | static class MailSessionDefinition { 263 | private final String name; 264 | private final String[] properties; 265 | 266 | MailSessionDefinition(String name, String[] properties) { 267 | this.name = name; 268 | this.properties = properties; 269 | } 270 | 271 | String getName() { 272 | return name; 273 | } 274 | 275 | String[] getProperties() { 276 | return properties; 277 | } 278 | } 279 | 280 | /** 281 | * The DataSource definition. The {@link #name} sets the JNDI name of the queue; the {@link #driverClass} specifies 282 | * the JDBC driver; the {@link #url}, {@link #username}, {@link #password} sets the JDBC connection details. 283 | */ 284 | static class DataSourceDefinition { 285 | private final String name; 286 | private final Class driverClass; 287 | private final String url; 288 | private final String username; 289 | private final String password; 290 | 291 | DataSourceDefinition(String name, Class driverClass, String url, String username, String password) { 292 | this.name = name; 293 | this.driverClass = driverClass; 294 | this.url = url; 295 | this.username = username; 296 | this.password = password; 297 | } 298 | 299 | String getName() { 300 | return name; 301 | } 302 | 303 | Class getDriverClass() { 304 | return driverClass; 305 | } 306 | 307 | String getUrl() { 308 | return url; 309 | } 310 | 311 | String getUsername() { 312 | return username; 313 | } 314 | 315 | String getPassword() { 316 | return password; 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/EnvironmentCreationException.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import org.springframework.core.NestedRuntimeException; 4 | 5 | /** 6 | * Exception that gets thrown when the JNDI environment for the test cannot be created. This is usually fatal, 7 | * the test cannot proceed. 8 | * 9 | * @author janmachacek 10 | */ 11 | public class EnvironmentCreationException extends NestedRuntimeException { 12 | private static final long serialVersionUID = 5152661314945735372L; 13 | 14 | public EnvironmentCreationException(String msg) { 15 | super(msg); 16 | } 17 | 18 | public EnvironmentCreationException(Throwable cause) { 19 | super(cause.getMessage(), cause); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/EnvironmentExtractor.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import org.specs2.spring.annotation.*; 4 | import org.springframework.core.annotation.AnnotationUtils; 5 | import org.springframework.util.Assert; 6 | 7 | /** 8 | * Extracts the {@link Environment} from the test specification. It first tries to locate the {@link Jndi} annotation 9 | * (on the test class itself or on any annotation of its annotations). 10 | * 11 | * @author janmachacek 12 | */ 13 | public class EnvironmentExtractor { 14 | 15 | /** 16 | * Extracts the environment from the test object. 17 | * 18 | * @param specification the test object; never {@code null}. 19 | * @return the extracted Environment object. 20 | */ 21 | public Environment extract(Object specification) { 22 | Assert.notNull(specification, "The 'specification' argument cannot be null."); 23 | 24 | final Environment environment = new Environment(); 25 | 26 | final Class clazz = specification.getClass(); 27 | final Jndi annotation = AnnotationUtils.findAnnotation(clazz, Jndi.class); 28 | if (annotation != null) { 29 | environment.addDataSources(annotation.dataSources()); 30 | environment.addTransactionManagers(annotation.transactionManager()); 31 | environment.addMailSessions(annotation.mailSessions()); 32 | environment.addJmsBrokers(annotation.jms()); 33 | environment.addBeans(annotation.beans()); 34 | environment.setBuilder(annotation.builder()); 35 | } 36 | 37 | environment.addDataSource(AnnotationUtils.findAnnotation(clazz, DataSource.class)); 38 | environment.addTransactionManager(AnnotationUtils.findAnnotation(clazz, TransactionManager.class)); 39 | environment.addMailSession(AnnotationUtils.findAnnotation(clazz, MailSession.class)); 40 | environment.addBean(AnnotationUtils.findAnnotation(clazz, Bean.class)); 41 | 42 | return environment; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/JndiBuilder.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Performs custom processing to contribute items to the JNDI environment. 7 | * 8 | * @author janmachacek 9 | */ 10 | public interface JndiBuilder { 11 | 12 | /** 13 | * Adds the items into the environment; the key in the map is the JNDI name; the value is the 14 | * object associated with that name. 15 | * 16 | * @param environment the environment to be added to; never {@code null}. 17 | * @throws Exception if the environment could not be built. 18 | */ 19 | void build(Map environment) throws Exception; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/JndiEnvironmentSetter.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import com.atomikos.icatch.jta.UserTransactionManager; 4 | import com.atomikos.jdbc.AbstractDataSourceBean; 5 | import com.atomikos.jdbc.AtomikosDataSourceBean; 6 | import com.atomikos.jdbc.nonxa.AtomikosNonXADataSourceBean; 7 | import org.apache.activemq.ActiveMQConnectionFactory; 8 | import org.apache.activemq.command.ActiveMQQueue; 9 | import org.apache.activemq.command.ActiveMQTopic; 10 | import org.specs2.spring.annotation.Jndi; 11 | import org.springframework.mock.jndi.SimpleNamingContext; 12 | import org.springframework.util.Assert; 13 | import org.springframework.util.ClassUtils; 14 | import org.springframework.util.ReflectionUtils; 15 | 16 | import javax.mail.Session; 17 | import javax.naming.Context; 18 | import javax.naming.NamingException; 19 | import javax.naming.spi.InitialContextFactory; 20 | import javax.naming.spi.InitialContextFactoryBuilder; 21 | import javax.naming.spi.NamingManager; 22 | import javax.sql.XADataSource; 23 | import java.lang.reflect.Constructor; 24 | import java.lang.reflect.InvocationTargetException; 25 | import java.util.*; 26 | 27 | /** 28 | * Component that processes the {@link Jndi} annotation and sets up the JNDI environment according to the 29 | * values in the annotation. This will allow you to write Specs2 code that looks like this: 30 | *
 31 |  * 	@DataSource(...)
 32 |  * 	@ContextConfiguration(Array("classpath*:/META-INF/spring/module-context.xml"))
 33 |  * 	class FooServiceTest extends org.specs2.spring.Specification {
 34 |  * 	  @Autowired var service: FooService = _
 35 |  * 	  @Autowired var ht: HibernateTemplate = _
 36 |  * 

37 | * "Some such" in { 38 | * "makes many foos" ! makeFoos 39 | * } 40 | *

41 | * def makeFoos() = { 42 | * this.service.makeFoos() 43 | * this.ht.loadAll(classOf[Foo]) must have size (100) 44 | * } 45 | * } 46 | *

47 | * 48 | * @author janmachacek 49 | */ 50 | public class JndiEnvironmentSetter { 51 | private final Map entries = new HashMap(); 52 | 53 | public synchronized void add(Map entries) { 54 | this.entries.putAll(entries); 55 | } 56 | 57 | public synchronized void add(String name, Object value) { 58 | this.entries.put(name, value); 59 | } 60 | 61 | public synchronized void prepareEnvironment(Environment environment) { 62 | Assert.notNull(environment, "The 'environment' argument cannot be null."); 63 | 64 | try { 65 | NamingContextBuilder builder = NamingContextBuilder.activatedContextBuilder(); 66 | 67 | buildDataSources(builder, environment.getDataSources()); 68 | buildMailSessions(builder, environment.getMailSessions()); 69 | buildTransactionManagers(builder, environment.getTransactionManagers()); 70 | buildBeans(builder, environment.getBeans()); 71 | buildJms(builder, environment.getJmsDefinitions()); 72 | buildCustom(builder, environment.getBuilder()); 73 | 74 | for (Map.Entry entry : entries.entrySet()) { 75 | builder.bind(entry.getKey(), entry.getValue()); 76 | } 77 | 78 | } catch (NamingException e) { 79 | throw new RuntimeException(e); 80 | } 81 | } 82 | 83 | private void buildJms(NamingContextBuilder builder, List jmses) { 84 | for (Environment.JmsDefinition jms : jmses) { 85 | ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("vm://localhost"); 86 | builder.bind(jms.getConnectionFactoryName(), factory); 87 | for (Environment.JmsQueueDefinition queue : jms.getJmsQueues()) { 88 | ActiveMQQueue q = new ActiveMQQueue(); 89 | q.setPhysicalName("queue" + queue.hashCode()); 90 | builder.bind(queue.getName(), q); 91 | } 92 | for (Environment.JmsTopicDefinition topic : jms.getJmsTopics()) { 93 | ActiveMQTopic t = new ActiveMQTopic(); 94 | t.setPhysicalName("topic" + topic.hashCode()); 95 | builder.bind(topic.getName(), t); 96 | } 97 | } 98 | } 99 | 100 | private void buildTransactionManagers(NamingContextBuilder builder, List transactionManagers) { 101 | if (transactionManagers.isEmpty()) return; 102 | if (transactionManagers.size() > 1) 103 | throw new EnvironmentCreationException("Cannot have more than one TransactionManager"); 104 | Environment.TransactionManagerDefinition transactionManager = transactionManagers.get(0); 105 | builder.bind(transactionManager.getName(), new UserTransactionManager()); 106 | } 107 | 108 | private void buildCustom(NamingContextBuilder builder, Class builderClass) { 109 | final JndiBuilder jndiBuilder = instantiate(builderClass); 110 | Map environment = new HashMap(); 111 | try { 112 | jndiBuilder.build(environment); 113 | } catch (Exception e) { 114 | throw new EnvironmentCreationException(e); 115 | } 116 | for (Map.Entry entry : environment.entrySet()) { 117 | builder.bind(entry.getKey(), entry.getValue()); 118 | } 119 | } 120 | 121 | private void buildBeans(NamingContextBuilder builder, List beans) { 122 | for (Environment.BeanDefinition bean : beans) { 123 | Object o = instantiate(bean.getType()); 124 | 125 | builder.bind(bean.getName(), o); 126 | } 127 | } 128 | 129 | private T instantiate(Class type) { 130 | try { 131 | final Constructor constructor = type.getConstructor(); 132 | ReflectionUtils.makeAccessible(constructor); 133 | return constructor.newInstance(); 134 | } catch (NoSuchMethodException e) { 135 | throw new EnvironmentCreationException(e); 136 | } catch (InvocationTargetException e) { 137 | throw new EnvironmentCreationException(e); 138 | } catch (InstantiationException e) { 139 | throw new EnvironmentCreationException(e); 140 | } catch (IllegalAccessException e) { 141 | throw new EnvironmentCreationException(e); 142 | } 143 | } 144 | 145 | private void buildMailSessions(NamingContextBuilder builder, List mailSessions) { 146 | for (Environment.MailSessionDefinition mailSession : mailSessions) { 147 | Properties props = new Properties(); 148 | for (String property : mailSession.getProperties()) { 149 | int i = property.indexOf("="); 150 | if (i == -1) continue; 151 | props.setProperty(property.substring(0, i), property.substring(i + 1)); 152 | } 153 | Session session = Session.getInstance(props); 154 | 155 | builder.bind(mailSession.getName(), session); 156 | } 157 | 158 | } 159 | 160 | private void buildDataSources(NamingContextBuilder builder, List dataSources) { 161 | for (Environment.DataSourceDefinition dataSource : dataSources) { 162 | boolean xa = false; 163 | for (Class intf : ClassUtils.getAllInterfacesForClass(dataSource.getDriverClass())) { 164 | if (intf == XADataSource.class) { 165 | xa = true; 166 | break; 167 | } 168 | } 169 | 170 | javax.sql.DataSource ds; 171 | if (xa) { 172 | AtomikosDataSourceBean realDs = new AtomikosDataSourceBean(); 173 | realDs.setXaDataSourceClassName(dataSource.getDriverClass().getName()); 174 | Properties p = new Properties(); 175 | p.setProperty("user", dataSource.getUsername()); 176 | p.setProperty("password", dataSource.getPassword()); 177 | p.setProperty("URL", dataSource.getUrl()); 178 | realDs.setXaProperties(p); 179 | realDs.setUniqueResourceName(dataSource.getDriverClass().getName() + System.currentTimeMillis()); 180 | realDs.setPoolSize(5); 181 | ds = realDs; 182 | } else { 183 | AtomikosNonXADataSourceBean realDs = new AtomikosNonXADataSourceBean(); 184 | realDs.setDriverClassName(dataSource.getDriverClass().getName()); 185 | realDs.setUrl(dataSource.getUrl()); 186 | realDs.setUser(dataSource.getUsername()); 187 | realDs.setPassword(dataSource.getPassword()); 188 | realDs.setUniqueResourceName(dataSource.getDriverClass().getName() + System.currentTimeMillis()); 189 | realDs.setPoolSize(5); 190 | ds = realDs; 191 | } 192 | 193 | builder.bind(dataSource.getName(), ds); 194 | } 195 | } 196 | 197 | private static class NamingContextBuilder implements InitialContextFactoryBuilder { 198 | 199 | /** 200 | * An instance of this class bound to JNDI 201 | */ 202 | private static NamingContextBuilder activated; 203 | 204 | /** 205 | * If no SimpleNamingContextBuilder is already configuring JNDI, createOrUpdate and activate one. Otherwise take the existing activate 206 | * SimpleNamingContextBuilder, clear it and return it. 207 | *

208 | * This is mainly intended for test suites that want to reinitialize JNDI bindings from scratch repeatedly. 209 | * 210 | * @return an empty SimpleNamingContextBuilder that can be used to control JNDI bindings 211 | * @throws javax.naming.NamingException . 212 | */ 213 | public synchronized static NamingContextBuilder activatedContextBuilder() 214 | throws NamingException { 215 | if (activated == null) { 216 | // Create and activate new context builder. 217 | NamingContextBuilder builder = new NamingContextBuilder(); 218 | // The activate() call will cause an assigment to the activated 219 | // variable. 220 | builder.activate(); 221 | } 222 | 223 | return activated; 224 | } 225 | 226 | private final Hashtable boundObjects = new Hashtable(); 227 | 228 | /** 229 | * Register the context builder by registering it with the JNDI NamingManager. Note that once this has been done, 230 | * new InitialContext() will always return a context from this factory. Use the emptyActivatedContextBuilder() 231 | * static method to get an empty context (for example, in test methods). 232 | * 233 | * @throws IllegalStateException if there's already a naming context builder registeredwith the JNDI NamingManager 234 | * @throws javax.naming.NamingException . 235 | */ 236 | public void activate() throws IllegalStateException, NamingException { 237 | if (!NamingManager.hasInitialContextFactoryBuilder()) 238 | NamingManager.setInitialContextFactoryBuilder(this); 239 | activated = this; 240 | } 241 | 242 | /** 243 | * Clear all bindings in this context builder. 244 | */ 245 | public void clear() { 246 | for (Map.Entry e : this.boundObjects.entrySet()) { 247 | if (e.getValue() instanceof AbstractDataSourceBean) { 248 | ((AbstractDataSourceBean) e.getValue()).close(); 249 | } 250 | if (e.getValue() instanceof UserTransactionManager) { 251 | ((UserTransactionManager) e.getValue()).close(); 252 | } 253 | } 254 | this.boundObjects.clear(); 255 | } 256 | 257 | /** 258 | * Bind the given object under the given name, for all naming contexts that this context builder will generate. 259 | * 260 | * @param name the JNDI name of the object (e.g. "java:comp/env/jdbc/myds") 261 | * @param obj the object to bind (e.g. a DataSource implementation) 262 | */ 263 | public void bind(String name, Object obj) { 264 | // if (this.boundObjects.contains(name)) return; 265 | this.boundObjects.put(name, obj); 266 | } 267 | 268 | /** 269 | * Indicates whether we have at least one XA data source 270 | * 271 | * @return {@code true} if we have XA objects bound 272 | */ 273 | boolean isXa() { 274 | for (Map.Entry e : this.boundObjects.entrySet()) { 275 | if (e.getValue() instanceof XADataSource) return true; 276 | } 277 | 278 | return false; 279 | } 280 | 281 | T getSingleObject(Class clazz) { 282 | int count = 0; 283 | T object = null; 284 | for (Map.Entry e : this.boundObjects.entrySet()) { 285 | if (clazz.isAssignableFrom(e.getValue().getClass())) { 286 | if (count > 0) throw new RuntimeException("More than one object of type " + clazz + " found."); 287 | object = (T) e.getValue(); 288 | count++; 289 | } 290 | } 291 | if (object == null) throw new RuntimeException("No object of type " + clazz + " found."); 292 | 293 | return object; 294 | } 295 | 296 | /** 297 | * Simple InitialContextFactoryBuilder implementation, creating a new SimpleNamingContext instance. 298 | * 299 | * @see SimpleNamingContext 300 | */ 301 | public InitialContextFactory createInitialContextFactory(Hashtable environment) { 302 | return new InitialContextFactory() { 303 | @SuppressWarnings("unchecked") 304 | public Context getInitialContext(Hashtable environment) { 305 | return new SimpleNamingContext("", NamingContextBuilder.this.boundObjects, environment); 306 | } 307 | }; 308 | } 309 | 310 | @Override 311 | public String toString() { 312 | final StringBuilder sb = new StringBuilder(); 313 | sb.append("NamingContextBuilder"); 314 | sb.append("{boundObjects=").append(boundObjects); 315 | sb.append('}'); 316 | return sb.toString(); 317 | } 318 | 319 | boolean contains(String name) { 320 | return this.boundObjects.contains(name); 321 | } 322 | } 323 | 324 | } 325 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/TestContext.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import org.specs2.spring.annotation.Property; 4 | import org.specs2.spring.annotation.SystemEnvironment; 5 | import org.specs2.spring.annotation.SystemProperties; 6 | import org.specs2.spring.annotation.UseProfile; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.context.support.GenericXmlApplicationContext; 9 | import org.springframework.core.annotation.AnnotationUtils; 10 | import org.springframework.core.env.ConfigurableEnvironment; 11 | import org.springframework.core.env.StandardEnvironment; 12 | import org.springframework.test.context.ContextConfiguration; 13 | 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * Creates Spring ApplicationContext for the test 19 | * 20 | * @author janmachacek 21 | */ 22 | class TestContext { 23 | 24 | private GenericXmlApplicationContext context; 25 | 26 | /** 27 | * Creates the {@link ApplicationContext} and autowire the fields / setters test object. 28 | * 29 | * @param specification the specification object to set up. 30 | */ 31 | void createAndAutowire(Object specification) { 32 | final ContextConfiguration contextConfiguration = AnnotationUtils.findAnnotation(specification.getClass(), ContextConfiguration.class); 33 | if (contextConfiguration == null) return; 34 | 35 | this.context = new GenericXmlApplicationContext(); 36 | ContextLoaderFactory.getContextLoader(this.context).load(specification, contextConfiguration.value()); 37 | this.context.getAutowireCapableBeanFactory().autowireBean(specification); 38 | } 39 | 40 | boolean loaded() { 41 | return this.context != null; 42 | } 43 | 44 | T getBean(Class beanType) { 45 | return this.context.getBean(beanType); 46 | } 47 | 48 | T getBean(String beanName, Class beanType) { 49 | return this.context.getBean(beanName, beanType); 50 | } 51 | 52 | } 53 | 54 | class ContextLoaderFactory { 55 | static ContextLoader getContextLoader(GenericXmlApplicationContext context) { 56 | try { 57 | Class.forName("org.springframework.core.env.ConfigurableEnvironment"); 58 | return new Spring31ContextLoader(context); 59 | } catch (ClassNotFoundException e) { 60 | // Before 3.1 61 | return new Spring25ContextLoader(context); 62 | } 63 | } 64 | } 65 | 66 | interface ContextLoader { 67 | 68 | void load(Object specification, String[] configLocations); 69 | } 70 | 71 | class Spring25ContextLoader implements ContextLoader { 72 | private GenericXmlApplicationContext context; 73 | 74 | Spring25ContextLoader(GenericXmlApplicationContext context) { 75 | this.context = context; 76 | } 77 | 78 | @Override 79 | public void load(Object specification, String[] configLocations) { 80 | this.context.load(configLocations); 81 | this.context.refresh(); 82 | } 83 | } 84 | 85 | class Spring31ContextLoader implements ContextLoader { 86 | private GenericXmlApplicationContext context; 87 | 88 | Spring31ContextLoader(GenericXmlApplicationContext context) { 89 | this.context = context; 90 | } 91 | 92 | @Override 93 | public void load(Object specification, String[] configLocations) { 94 | this.context.setEnvironment(setupTestEnvironment(specification)); 95 | this.context.load(configLocations); 96 | this.context.refresh(); 97 | } 98 | 99 | private ConfigurableEnvironment setupTestEnvironment(Object specification) { 100 | TestEnvironment environment = new TestEnvironment(); 101 | final UseProfile usePro = AnnotationUtils.findAnnotation(specification.getClass(), UseProfile.class); 102 | if (usePro != null && usePro.value().length > 0) environment.setActiveProfiles(usePro.value()); 103 | 104 | final SystemEnvironment systemEnvironment = AnnotationUtils.findAnnotation(specification.getClass(), SystemEnvironment.class); 105 | if (systemEnvironment != null) { 106 | environment.setSystemEnvironment(systemEnvironment.clear(), systemEnvironment.overwrite(), 107 | systemEnvironment.nullValue(), systemEnvironment.value(), systemEnvironment.properties()); 108 | } 109 | final SystemProperties systemProperties = AnnotationUtils.findAnnotation(specification.getClass(), SystemProperties.class); 110 | if (systemProperties != null) { 111 | environment.setSystemProperties(systemProperties.clear(), systemProperties.overwrite(), 112 | systemProperties.nullValue(), systemProperties.value(), systemProperties.properties()); 113 | } 114 | 115 | return environment; 116 | } 117 | /** 118 | * Holds the environment for the test 119 | */ 120 | static final class TestEnvironment extends StandardEnvironment { 121 | private Properties systemProperties = null; 122 | private Properties systemEnvironment = null; 123 | 124 | void setSystemProperties(boolean clear, boolean overwrite, String nullValue, String[] value, Property[] properties) { 125 | final Map props = new HashMap(); 126 | if (!clear) props.putAll(super.getSystemProperties()); 127 | 128 | this.systemProperties = new Properties(overwrite, props, nullValue); 129 | this.systemProperties.addProperties(properties); 130 | this.systemProperties.addProperties(value); 131 | } 132 | 133 | void setSystemEnvironment(boolean clear, boolean overwrite, String nullValue, String[] value, Property[] properties) { 134 | final Map props = new HashMap(); 135 | if (!clear) props.putAll(super.getSystemEnvironment()); 136 | 137 | this.systemEnvironment = new Properties(overwrite, props, nullValue); 138 | this.systemEnvironment.addProperties(properties); 139 | this.systemEnvironment.addProperties(value); 140 | } 141 | 142 | @Override 143 | public Map getSystemProperties() { 144 | if (this.systemProperties == null) return super.getSystemProperties(); 145 | return this.systemProperties.getProperties(); 146 | } 147 | 148 | @Override 149 | public Map getSystemEnvironment() { 150 | if (this.systemEnvironment == null) return super.getSystemEnvironment(); 151 | return this.systemEnvironment.getProperties(); 152 | } 153 | } 154 | 155 | /** 156 | * Holds the properties that can be added or overwritten 157 | */ 158 | static final class Properties { 159 | private final boolean overwrite; 160 | private final Map environment; 161 | private final String nullValue; 162 | 163 | Properties(boolean overwrite, Map environment, String nullValue) { 164 | this.overwrite = overwrite; 165 | this.environment = environment; 166 | this.nullValue = nullValue; 167 | } 168 | 169 | void addProperties(String[] properties) { 170 | for (String property : properties) { 171 | int i = property.indexOf("="); 172 | if (i != -1) { 173 | String name = property.substring(0, i); 174 | String value = property.substring(i); 175 | 176 | addProperty(name, value); 177 | } 178 | } 179 | } 180 | 181 | void addProperties(Property[] properties) { 182 | for (Property property : properties) { 183 | addProperty(property.name(), property.value()); 184 | } 185 | } 186 | 187 | private void addProperty(String name, String value) { 188 | if (!this.overwrite) { 189 | if (this.environment.containsKey(name)) return; 190 | } 191 | Object realValue; 192 | if (this.nullValue.equals(value)) { 193 | realValue = null; 194 | } else { 195 | realValue = value; 196 | } 197 | this.environment.put(name, realValue); 198 | } 199 | 200 | Map getProperties() { 201 | return this.environment; 202 | } 203 | } 204 | 205 | } -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/TestTransactionDefinitionExtractor.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring; 2 | 3 | import org.springframework.core.annotation.AnnotationUtils; 4 | import org.springframework.test.context.transaction.TransactionConfiguration; 5 | import org.springframework.transaction.TransactionDefinition; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import org.springframework.transaction.support.DefaultTransactionDefinition; 8 | import org.springframework.util.Assert; 9 | 10 | /** 11 | * Determines transactional configuration for a given specification by looking at its annotations. 12 | * 13 | * @author janmachacek 14 | */ 15 | public class TestTransactionDefinitionExtractor { 16 | 17 | /** 18 | * Examines the {@code specification}'s annotations to prepare the {@code TestTransactionDefinition} object 19 | * that describes whether and how the specification's examples should be run. 20 | * 21 | * @param specification the specification to examine, never {@code null}. 22 | * @return the {@code TestTransactionDefinition} for the specification, never {@code null}. 23 | */ 24 | public TestTransactionDefinition extract(Object specification) { 25 | Assert.notNull(specification, "The 'specification' argument cannot be null."); 26 | 27 | final Transactional transactional = AnnotationUtils.findAnnotation(specification.getClass(), Transactional.class); 28 | final TransactionConfiguration transactionConfiguration = AnnotationUtils.findAnnotation(specification.getClass(), TransactionConfiguration.class); 29 | if (transactional == null) return TestTransactionDefinition.NOT_TRANSACTIONAL; 30 | DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED); 31 | transactionDefinition.setName("Test transaction"); 32 | boolean defaultRollback = true; 33 | String transactionManagerName = "transactionManager"; 34 | if (transactionConfiguration != null) { 35 | defaultRollback = transactionConfiguration.defaultRollback(); 36 | transactionManagerName = transactionConfiguration.transactionManager(); 37 | } 38 | 39 | return new TestTransactionDefinition(transactionDefinition, transactionManagerName, defaultRollback); 40 | } 41 | 42 | /** 43 | * Contains details of the example's expected transactional behaviour. The {@link #transactionDefinition} is the 44 | * definition that the {@code PlatformTransactionManager} will use to obtain the transaction; the {@link #defaultRollback} 45 | * indicates whether the transaction should be rolled back at the end of the example execution. 46 | */ 47 | public static class TestTransactionDefinition { 48 | private final TransactionDefinition transactionDefinition; 49 | private final boolean defaultRollback; 50 | private final String transactionManagerName; 51 | public final static TestTransactionDefinition NOT_TRANSACTIONAL = new TestTransactionDefinition(null, null, false); 52 | 53 | TestTransactionDefinition(TransactionDefinition transactionDefinition, String transactionManagerName, boolean defaultRollback) { 54 | this.transactionDefinition = transactionDefinition; 55 | this.transactionManagerName = transactionManagerName; 56 | this.defaultRollback = defaultRollback; 57 | } 58 | 59 | public TransactionDefinition getTransactionDefinition() { 60 | return transactionDefinition; 61 | } 62 | 63 | public boolean isDefaultRollback() { 64 | return defaultRollback; 65 | } 66 | 67 | public String getTransactionManagerName() { 68 | return transactionManagerName; 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/Bean.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.Inherited; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | /** 8 | * Specifies an arbitrary object to be added to the JNDI environment. 9 | * 10 | * @author janmachacek 11 | */ 12 | @Inherited 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface Bean { 15 | 16 | /** 17 | * The name in the JNDI environment; typically something like java:comp/env/bean/xyz 18 | * 19 | * @return the JNDI name 20 | */ 21 | String name(); 22 | 23 | /** 24 | * The type of the object; the type must have an accessible nullary constructor. 25 | * 26 | * @return the object name 27 | */ 28 | Class clazz(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/DataSource.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.Inherited; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.sql.Driver; 7 | 8 | /** 9 | * Specifies the {@link javax.activation.DataSource} to be added to the JNDI environment 10 | * 11 | * @author janmmachacek 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Inherited 15 | public @interface DataSource { 16 | 17 | /** 18 | * The name in the JNDI environment; typically something like java:comp/env/bean/xyz 19 | * 20 | * @return the JNDI name 21 | */ 22 | String name(); 23 | 24 | /** 25 | * Specifies the type of the JDBC driver 26 | * 27 | * @return the driver class 28 | */ 29 | Class driverClass(); 30 | 31 | /** 32 | * The JDBC URL 33 | * 34 | * @return the URL 35 | */ 36 | String url(); 37 | 38 | /** 39 | * The username to establish the DB connection 40 | * 41 | * @return the username 42 | */ 43 | String username() default "sa"; 44 | 45 | /** 46 | * The password to establish the DB connection 47 | * 48 | * @return the password 49 | */ 50 | String password() default ""; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/Jms.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | /** 4 | * Specifies JMS configuration 5 | * 6 | * @author janmmachacek 7 | */ 8 | public @interface Jms { 9 | 10 | /** 11 | * The name in the JNDI environment; typically something like java:comp/env/jms/connectionFactory 12 | * 13 | * @return the JNDI name 14 | */ 15 | String connectionFactoryName(); 16 | 17 | /** 18 | * Specifies any number of JMS queues managed by this connection factory 19 | * 20 | * @return the queues 21 | */ 22 | JmsQueue[] queues() default {}; 23 | 24 | /** 25 | * Specifies any number of JMS topics managed by this connection factory 26 | * 27 | * @return the topics 28 | */ 29 | JmsTopic[] topics() default {}; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/JmsQueue.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | /** 4 | * Specifies a JMS queue at the given JNDI name 5 | * 6 | * @author janmmachacek 7 | */ 8 | public @interface JmsQueue { 9 | /** 10 | * The name in the JNDI environment; typically something like java:comp/env/jms/xyz 11 | * 12 | * @return the JNDI name 13 | */ 14 | String name(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/JmsTopic.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | /** 4 | * Specifies a JMS topic at the given JNDI name 5 | * 6 | * @author janmmachacek 7 | */ 8 | public @interface JmsTopic { 9 | /** 10 | * The name in the JNDI environment; typically something like java:comp/env/jms/xyz 11 | * 12 | * @return the JNDI name 13 | */ 14 | String name(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/Jndi.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import org.specs2.spring.BlankJndiBuilder; 4 | import org.specs2.spring.JndiBuilder; 5 | 6 | import java.lang.annotation.Inherited; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | 10 | /** 11 | * Use this annotation on your tests to inject values into the JNDI environment for the 12 | * tests. 13 | *

You can specify any number of {@link #dataSources()}, {@link #mailSessions()}, 14 | * {@link #beans()}

15 | *

In addition to these fairly standard items, you can specify {@link #builder()}, 16 | * which needs to be an implementation of the {@link org.specs2.spring.JndiBuilder} interface with 17 | * nullary constructor. The runtime will instantiate the given class and call its 18 | * {@link org.specs2.spring.JndiBuilder#build(java.util.Map)} method.

19 | * 20 | * 21 | * @author janmmachacek 22 | */ 23 | @Retention(RetentionPolicy.RUNTIME) 24 | @Inherited 25 | public @interface Jndi { 26 | 27 | /** 28 | * Specify any number of {@link javax.sql.DataSource} JNDI entries. 29 | *

Typically, you'd write something like 30 | * @DataSource(name = "java:comp/env/jdbc/x", url = "jdbc:hsqldb:mem:x", username = "sa", password = ""): 31 | * this would register a {@link javax.sql.DataSource} entry in the JNDI environment under name java:comp/env/jdbc/x, 32 | * with the given {@code url}, {@code username} and {@code password}. 33 | *

34 | * 35 | * @return the data sources 36 | */ 37 | DataSource[] dataSources() default {}; 38 | 39 | /** 40 | * Specify any number of {@link javax.mail.Session} JNDI entries. 41 | * 42 | * @return the mail sessions 43 | */ 44 | MailSession[] mailSessions() default {}; 45 | 46 | /** 47 | * Specify any number of any objects as JNDI entries. The objects must have nullary constructors. 48 | * 49 | * @return the beans 50 | */ 51 | Bean[] beans() default {}; 52 | 53 | WorkManager[] workManagers() default {}; 54 | 55 | /** 56 | * Specify any number of {@link javax.transaction.TransactionManager} as JNDI entries. 57 | * 58 | * @return the transaction managers. 59 | */ 60 | TransactionManager[] transactionManager() default {}; 61 | 62 | /** 63 | * Configure the JMS environment; configure the queues, topics and connection factory. 64 | * 65 | * @return the JMS environment 66 | */ 67 | Jms[] jms() default {}; 68 | 69 | /** 70 | * If you require some complex environment setup, you can set this value. The type you specify here 71 | * must be an implementation of the {@link org.specs2.spring.JndiBuilder} with nullary constructor. 72 | * 73 | * @return the custom JndiBuilder 74 | */ 75 | Class builder() default BlankJndiBuilder.class; 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/MailSession.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | /** 7 | * Specifies the JNDI-bound {@link javax.mail.Session}. 8 | * 9 | * @author janmmachacek 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface MailSession { 13 | 14 | /** 15 | * The name in the JNDI environment; typically something like java:comp/env/bean/xyz 16 | * 17 | * @return the JNDI name 18 | */ 19 | String name(); 20 | 21 | /** 22 | * The JavaMail properties for the session; for example mail.smtp.host=localhost 23 | * 24 | * @return the JavaMail properties 25 | */ 26 | String[] properties() default {}; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/Property.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author janmachacek 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.TYPE) 13 | public @interface Property { 14 | /** 15 | * The name of the property 16 | * @return the name; never {@code null} 17 | */ 18 | String name(); 19 | 20 | /** 21 | * The value of the property; if you want {@code null}, use {@link org.specs2.spring.annotation.SystemEnvironment#nullValue()} 22 | * @return the value; never {@code null} 23 | */ 24 | String value(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/SystemEnvironment.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author janmachacek 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.TYPE) 13 | public @interface SystemEnvironment { 14 | /** 15 | * Indicates whether the properties specified entries should overwrite the existing values. When 16 | * {@code false} the existing values will be kept. 17 | * @return {@code true} if the existing values should be overwritten. 18 | */ 19 | boolean overwrite() default true; 20 | 21 | /** 22 | * Indicates whether the existing environment should be cleared. 23 | * @return {@code true} if the environment should be cleared. 24 | */ 25 | boolean clear() default false; 26 | 27 | /** 28 | * Value that, when used in {@link Property#value()} or in the {@code value} portion of the {@link #value()} will be interpreted 29 | * as {@code null} rather than a {@code String} with value {@code "null"}. 30 | * @return the {@code null} name. 31 | */ 32 | String nullValue() default "null"; 33 | 34 | /** 35 | * The properties 36 | * @return the properties 37 | */ 38 | Property[] properties() default {}; 39 | 40 | /** 41 | * The properties in {name1=value1, name2=value2, ..., namen=valuen} 42 | * syntax. 43 | * @return the properties. 44 | */ 45 | String[] value() default {}; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/SystemProperties.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author janmachacek 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.TYPE) 13 | public @interface SystemProperties { 14 | /** 15 | * Indicates whether the properties specified entries should overwrite the existing values. When 16 | * {@code false} the existing values will be kept. 17 | * @return {@code true} if the existing values should be overwritten. 18 | */ 19 | boolean overwrite() default true; 20 | 21 | /** 22 | * Indicates whether the existing environment should be cleared. 23 | * @return {@code true} if the environment should be cleared. 24 | */ 25 | boolean clear() default false; 26 | 27 | /** 28 | * Value that, when used in {@link Property#value()} or in the {@code value} portion of the {@link #value()} will be interpreted 29 | * as {@code null} rather than a {@code String} with value {@code "null"}. 30 | * @return the {@code null} name. 31 | */ 32 | String nullValue() default "null"; 33 | 34 | /** 35 | * The properties 36 | * @return the properties 37 | */ 38 | Property[] properties() default {}; 39 | 40 | /** 41 | * The properties in {name1=value1, name2=value2, ..., namen=valuen} 42 | * syntax. 43 | * @return the properties. 44 | */ 45 | String[] value() default {}; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/TransactionManager.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.Inherited; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | /** 8 | * Specifies the {@link javax.transaction.TransactionManager} to be added to the JNDI environment 9 | * 10 | * @author janmmachacek 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Inherited 14 | public @interface TransactionManager { 15 | /** 16 | * The name in the JNDI environment; typically something like java:comp/env/TransactionManager 17 | * 18 | * @return the JNDI name 19 | */ 20 | String name(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/UseProfile.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author janmachacek 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.TYPE) 13 | public @interface UseProfile { 14 | 15 | /** 16 | * Define the profiles that the test should load/use 17 | * @return the profile names 18 | */ 19 | String[] value(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/specs2/spring/annotation/WorkManager.java: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.annotation; 2 | 3 | /** 4 | * Specifies the JNDI-bound WorkManager, either the 5 | * commonj.work.WorkManager (when type is {@link org.specs2.spring.annotation.WorkManager.Kind#CommonJ}) or 6 | * javax.spi.resource.work.WorkManager (when type is {@link org.specs2.spring.annotation.WorkManager.Kind#Javax}).
7 | * The specified WorkManager will be bound at the given {@link #name()} and its thread pool will be configured to have 8 | * at least {@link #minimumThreads()} and at most {@link #maximumThreads()}.
9 | * The {@link #minimumThreads()} should be smaller than {@link #maximumThreads()}. 10 | * 11 | * @author janmachacek 12 | */ 13 | public @interface WorkManager { 14 | 15 | /** 16 | * The type of WorkManager to create 17 | */ 18 | public static enum Kind { 19 | /** 20 | * Create the commonj.work.WorkManager 21 | */ 22 | CommonJ, 23 | /** 24 | * Create the javax.spi.resource.work.WorkManager 25 | */ 26 | Javax 27 | } 28 | 29 | /** 30 | * The JNDI name for the WorkManager 31 | * 32 | * @return the name, typically "java:comp/env/work/WorkManager" 33 | */ 34 | String name(); 35 | 36 | /** 37 | * The kind of the WorkManager to create 38 | * 39 | * @return the type 40 | */ 41 | Kind kind(); 42 | 43 | /** 44 | * The minimum number of threads for the WorkManager 45 | * 46 | * @return the number of threads; always > 0. 47 | */ 48 | int minimumThreads() default 2; 49 | 50 | /** 51 | * The maximum number of threads for the WorkManager. 52 | * 53 | * @return the number of threads; always > 1 and <= {@link #minimumThreads()} 54 | */ 55 | int maximumThreads() default 4; 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/org/specs2/spring/BeanTables.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | import org.specs2.execute._ 4 | import org.specs2.text.Trim._ 5 | import org.springframework.beans.{BeanWrapperImpl, BeanWrapper} 6 | import java.util.{HashSet, ArrayList} 7 | import scala.reflect.ClassTag 8 | 9 | /** 10 | * @author janmachacek 11 | */ 12 | trait BeanTables { 13 | 14 | implicit def toTableHeader(a: String) = new TableHeader(List(a)) 15 | 16 | implicit def toDataRow(a: Any) = BeanRow(List(a)) 17 | 18 | case class TableHeader(titles: List[String]) { 19 | def ||(title: String) = copy(titles = this.titles :+ title) 20 | 21 | def |(title: String) = copy(titles = this.titles :+ title) 22 | 23 | def |(row: BeanRow) = new AnyRefTable(titles, List(row)) 24 | 25 | def |>[T1](row: BeanRow) = new AnyRefTable(titles, List(row), execute = true) 26 | } 27 | 28 | abstract class Table(val titles: List[String], val execute: Boolean = false) { 29 | /** 30 | * @return the header as a | separated string 31 | */ 32 | def showTitles = titles.mkString("|", "|", "|") 33 | 34 | /** 35 | * Collect the results of each row 36 | * @param results list of (row description, row execution result) 37 | * @return an aggregated Result from a list of results 38 | */ 39 | protected def collect[R <% Result](results: List[(String, R)]): DecoratedResult[BeanTable] = { 40 | val result = allSuccess(results) 41 | val header = result match { 42 | case Success(_, _) => showTitles 43 | case other => " " + showTitles 44 | } 45 | DecoratedResult(BeanTable(titles, results), result.updateMessage { 46 | header + "\n" + 47 | results.map((cur: (String, R)) => resultLine(cur._1, cur._2)).mkString("\n") 48 | }) 49 | } 50 | 51 | /**@return the logical and combination of all the results */ 52 | private def allSuccess[R <% Result](results: List[(String, R)]): Result = { 53 | def and(l: Result, r: (String, R)): Result = { 54 | if (l.isSuccess && r._2.isSuccess) l 55 | else if (!l.isSuccess) l 56 | else r._2 57 | } 58 | results.foldLeft(Success(""): Result)(and) 59 | } 60 | 61 | /**@return the status of the row + the values + the failure message if any */ 62 | private def resultLine(desc: String, result: Result): String = { 63 | result.status + " " + desc + { 64 | result match { 65 | case Success(_, _) => "" 66 | case _ => " " + result.message 67 | } 68 | } 69 | } 70 | 71 | case class PropertyDescriptor(name: String, value: Any) 72 | 73 | } 74 | 75 | case class AnyRefTable(override val titles: List[String], rows: List[BeanRow], override val execute: Boolean = false) extends Table(titles, execute) { 76 | outer => 77 | def |(row: BeanRow) = AnyRefTable(titles, outer.rows :+ row, execute) 78 | 79 | def |[B](f: (B) => Result)(implicit m: ClassTag[B]) = executeRow(m, f, execute) 80 | 81 | def |>[B](f: (B) => Result)(implicit m: ClassTag[B]) = executeRow(m, f, true) 82 | 83 | def |<[B](implicit m: ClassTag[B]): List[B] = { 84 | rows map {d: BeanRow => d.makeBean[B](titles, m)} 85 | } 86 | 87 | def |<[B](m: Class[B]): List[B] = 88 | rows map {d: BeanRow => d.makeBean[B](titles, m)} 89 | 90 | def |<[B](f: (B) => Unit)(implicit m: ClassTag[B]): List[B] = { 91 | rows map {d: BeanRow => 92 | val b = d.makeBean[B](titles, m) 93 | f(b) 94 | b 95 | } 96 | } 97 | 98 | def executeRow[B, R <% Result](m: ClassTag[B], f: (B) => R, exec: Boolean): DecoratedResult[BeanTable] = { 99 | if (exec) 100 | collect(rows map { 101 | (d: BeanRow) => (d.show, implicitly[R => Result].apply(f(d.makeBean(titles, m)))) 102 | }) 103 | else DecoratedResult(BeanTable(titles, Seq[BeanTableRow]()), Success("ok")) 104 | } 105 | } 106 | 107 | case class BeanRow(propertyValues: List[Any]) { 108 | def show = productIterator.mkString("|", "|", "|") 109 | 110 | def !(value: Any) = BeanRow(propertyValues :+ value) 111 | def !!(value: Any) = BeanRow(propertyValues :+ value) 112 | 113 | private [spring] def makeBean[T](propertyNames: List[String], m: Class[_]) = { 114 | val bean = m.newInstance().asInstanceOf[T] 115 | val wrapper = new BeanWrapperImpl(bean) 116 | 117 | for (i <- 0 until propertyNames.size) { 118 | val propertyName = propertyNames(i) 119 | val propertyValue = propertyValues(i) 120 | 121 | wrapper.setPropertyValue(propertyName, propertyValue) 122 | } 123 | 124 | bean 125 | } 126 | 127 | private [spring] def makeBean[T](propertyNames: List[String], m: ClassTag[T]): T = makeBean(propertyNames, m.runtimeClass) 128 | 129 | } 130 | 131 | } 132 | 133 | case class BeanTable(titles: Seq[String], rows: Seq[BeanTableRow]) { 134 | def isSuccess = rows.forall(_.isSuccess) 135 | } 136 | object BeanTable { 137 | def apply[R <% Result](titles: Seq[String], results: Seq[(String, R)]): BeanTable = BeanTable(titles, results.collect { case (v, r) => BeanTableRow(v, r) }) 138 | } 139 | case class BeanTableRow(cells: Seq[String], result: Result) { 140 | def isSuccess = result.isSuccess 141 | } 142 | object BeanTableRow { 143 | def apply[R](values: String, result: R)(implicit convert: R => Result): BeanTableRow = BeanTableRow(values.trimEnclosing("|").splitTrim("\\|"), convert(result)) 144 | } 145 | -------------------------------------------------------------------------------- /src/main/scala/org/specs2/spring/DataAccess.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | import org.springframework.orm.hibernate3.{HibernateCallback, HibernateTemplate} 4 | import org.hibernate.{HibernateException, SessionFactory, Session} 5 | import org.specs2.execute.{Success, Result} 6 | import scala.reflect.ClassTag 7 | 8 | /** 9 | * @author janmachacek 10 | */ 11 | trait SqlDataAccess { 12 | 13 | 14 | } 15 | 16 | /** 17 | * Convenience mixin for using Hibernate in your Spring integration tests; includes the ``insert`` method overloads 18 | * that work well with ``org.specs2.spring.BeanTables``. 19 | */ 20 | trait HibernateDataAccess { 21 | 22 | private def inSession[A](sessionFactory: SessionFactory)(f: (Session) => A) = { 23 | def openSession = { 24 | try { 25 | (sessionFactory.getCurrentSession, true) 26 | } catch { 27 | case e: HibernateException => 28 | (sessionFactory.openSession(), false) 29 | } 30 | } 31 | 32 | val (session, joinedExisting) = openSession 33 | 34 | val ret = f(session) 35 | 36 | if (!joinedExisting) { 37 | session.flush() 38 | session.close() 39 | } 40 | 41 | ret 42 | } 43 | 44 | /** 45 | * Removes all entities of the given type 46 | * 47 | * @param entity implicitly supplied class manifest of the entity type to be deleted 48 | * @param sessionFactory the session factory that will have the entities removed 49 | */ 50 | def deleteAll[T](implicit entity: ClassTag[T], sessionFactory: SessionFactory) { 51 | inSession(sessionFactory) { s => 52 | s.createQuery("delete from " + entity.runtimeClass.getName).executeUpdate() 53 | } 54 | } 55 | 56 | import org.specs2.execute._ 57 | 58 | /** 59 | * Returns a function that inserts the object and returns Success; the function can be supplied to the BeanTables 60 | * ``|>`` function. 61 | * Typical usage is 62 | *
 63 |    *  implicit var sessionFactory = make-SessionFactory-instance()
 64 |    *
 65 |    *  "Some service operation" in {
 66 |    *    "age" | "name" | "teamName" |
 67 |    *     32   ! "Jan"  ! "Wheelers" |
 68 |    *     30   ! "Ani"  ! "Team GB"  |> insert[Rider]
 69 |    *
 70 |    *    // tests that rely on the inserted Rider objects
 71 |    *    success
 72 |    *  }
 73 |    *  
74 | * 75 | * @return function that inserts the object and returns Success when the insert succeeds. 76 | */ 77 | def insert[T](implicit sessionFactory: SessionFactory): (T => Result) = { 78 | t => inSession(sessionFactory) { s => s.saveOrUpdate(t); Success("ok") } 79 | } 80 | 81 | /** 82 | * Returns a function that runs the supplied function f on the object; then inserts the object and returns Success; 83 | * the function can be supplied to the BeanTables ``|>`` function.
84 | * Typical usage is 85 | *
 86 |    *  implicit var sessionFactory = make-SessionFactory-instance()
 87 |    *
 88 |    *  "Some service operation" in {
 89 |    *    "age" | "name" | "teamName" |
 90 |    *     32   ! "Jan"  ! "Wheelers" |
 91 |    *     30   ! "Ani"  ! "Team GB"  |> insert[Rider] { r: Rider => r.addEntry(...) }
 92 |    *
 93 |    *    // tests that rely on the inserted Rider objects; each with one Entry inserted in the function given
 94 |    *    // to the insert[Rider] method
 95 |    *    success
 96 |    *  }
 97 |    *  
98 | * 99 | * @param f function that operates on the instance ``T``; this function will run before the Hibernate save. 100 | * @return function that inserts the object and returns Success when the insert succeeds. 101 | */ 102 | def insert[T, R](f: T => R)(implicit sessionFactory: SessionFactory): (T => Result) = { 103 | t => 104 | f(t) 105 | inSession(sessionFactory) { _.saveOrUpdate(t) } 106 | Success("ok") 107 | } 108 | 109 | } 110 | 111 | trait HibernateTemplateDataAccess { 112 | 113 | /** 114 | * Returns a function that runs the supplied function f on the object; then inserts the object and returns Success; 115 | * the function can be supplied to the BeanTables ``|>`` function.
116 | * Typical usage is 117 | *
118 |    *  implicit var hibernateTemplate = make-HibernateTemplate-instance()
119 |    *
120 |    *  "Some service operation" in {
121 |    *    "age" | "name" | "teamName" |
122 |    *     32   ! "Jan"  ! "Wheelers" |
123 |    *     30   ! "Ani"  ! "Team GB"  |> insert[Rider] { r: Rider => r.addEntry(...) }
124 |    *
125 |    *    // tests that rely on the inserted Rider objects; each with one Entry inserted in the function given
126 |    *    // to the insert[Rider] method
127 |    *    success
128 |    *  }
129 |    *  
130 | * 131 | * @param f function that operates on the instance ``T``; this function will run before the Hibernate save. 132 | * @return function that inserts the object and returns Success when the insert succeeds. 133 | */ 134 | def insert[T, R](f: T => R)(implicit hibernateTemplate: HibernateTemplate): (T => Result) = { 135 | t => 136 | f(t) 137 | hibernateTemplate.saveOrUpdate(t) 138 | Success("ok") 139 | } 140 | 141 | /** 142 | * Returns a function that inserts the object and returns Success; the function can be supplied to the BeanTables 143 | * ``|>`` function. 144 | * Typical usage is 145 | *
146 |    *  implicit var hibernateTemplate = make-HibernateTemplate-instance()
147 |    *
148 |    *  "Some service operation" in {
149 |    *    "age" | "name" | "teamName" |
150 |    *     32   ! "Jan"  ! "Wheelers" |
151 |    *     30   ! "Ani"  ! "Team GB"  |> insert[Rider]
152 |    *
153 |    *    // tests that rely on the inserted Rider objects
154 |    *    success
155 |    *  }
156 |    *  
157 | * 158 | * @return function that inserts the object and returns Success when the insert succeeds. 159 | */ 160 | def insert[T](implicit hibernateTemplate: HibernateTemplate): (T => Result) = { 161 | t => hibernateTemplate.saveOrUpdate(t); Success("ok") 162 | } 163 | 164 | /** 165 | * Removes all entities of the given type 166 | * 167 | * @param entity implicitly supplied class manifest of the entity type to be deleted 168 | * @param hibernateTemplate HibernateTemplate instance that will have the entities removed 169 | */ 170 | def deleteAll[T](implicit entity: ClassTag[T], hibernateTemplate: HibernateTemplate) { 171 | hibernateTemplate.execute(new HibernateCallback[Int] { 172 | def doInHibernate(session: Session) = 173 | session.createQuery("delete from " + entity.runtimeClass.getName).executeUpdate() 174 | }) 175 | } 176 | 177 | } -------------------------------------------------------------------------------- /src/main/scala/org/specs2/spring/SettableEnvironment.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | 4 | /** 5 | * @author janmachacek 6 | */ 7 | trait SettableEnvironment { 8 | this: SpecificationEnvironment => 9 | 10 | def addJndiEntry(name: String, value: AnyRef) { 11 | environmentSetter.add(name, value) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/org/specs2/spring/SpecificationLike.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | import org.springframework.jdbc.core.JdbcTemplate 4 | import org.springframework.orm.hibernate3.HibernateTemplate 5 | import org.springframework.transaction.PlatformTransactionManager 6 | import org.specs2.spring.TestTransactionDefinitionExtractor.TestTransactionDefinition 7 | import org.specs2.specification.{Step, SpecStart, Example} 8 | 9 | /** 10 | * Gives access to the Sprnig context for the specification 11 | */ 12 | trait SpecificationContext { 13 | 14 | private[spring] def testContext: TestContext 15 | 16 | } 17 | 18 | /** 19 | * Gives access to the JNDI environment for the specification 20 | */ 21 | trait SpecificationEnvironment { 22 | 23 | private[spring] def environmentSetter: JndiEnvironmentSetter 24 | 25 | } 26 | 27 | /** 28 | * Mutable Specification that sets up the JNDI environment and autowires the fields / setters of its subclasses. 29 | * 30 | * @author janmachacek 31 | */ 32 | trait SpecificationLike extends org.specs2.mutable.SpecificationLike 33 | with SpecificationContext 34 | with SpecificationEnvironment { 35 | 36 | private[spring] val testContext = new TestContext 37 | private[spring] val environmentSetter = new JndiEnvironmentSetter 38 | 39 | private def setup() { 40 | environmentSetter.prepareEnvironment(new EnvironmentExtractor().extract(this)) 41 | testContext.createAndAutowire(this) 42 | } 43 | 44 | override def is: org.specs2.specification.Fragments = { 45 | // setup the specification's transactional behaviour 46 | val ttd = new TestTransactionDefinitionExtractor().extract(this) 47 | val transformedFragments = 48 | if (ttd == TestTransactionDefinition.NOT_TRANSACTIONAL) 49 | // no transactions required 50 | fragments 51 | else { 52 | // transactions required, run each example body in a [separate] transaction 53 | fragments.map { 54 | case e: Example => 55 | Example(e.desc, { 56 | if (!testContext.loaded()) failure("No ApplicationContext prepared for the test. Did you forget the @ContextConfiguration annotation?") 57 | else { 58 | val transactionManager = testContext.getBean(ttd.getTransactionManagerName, classOf[PlatformTransactionManager]) 59 | if (transactionManager == null) failure("Test marked @Transactional, but no PlatformTransactionManager bean found.") 60 | else { 61 | val transactionStatus = transactionManager.getTransaction(ttd.getTransactionDefinition) 62 | try { 63 | val result = e.execute 64 | if (!ttd.isDefaultRollback) transactionManager.commit(transactionStatus) 65 | result 66 | } finally { 67 | if (ttd.isDefaultRollback) transactionManager.rollback(transactionStatus) 68 | } 69 | } 70 | } 71 | }) 72 | case f => f 73 | } 74 | } 75 | 76 | args(sequential = true) ^ Step(setup) ^ transformedFragments 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/test/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Bundle-ManifestVersion: 2 3 | Premain-Class: org.springframework.instrument.InstrumentationSavingAgent 4 | 5 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/spring/module-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | org.specs2.spring.domain 20 | 21 | 22 | 23 | 24 | hibernate.hbm2ddl.auto=create-drop 25 | hibernate.show_sql=true 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/test/scala/org/specs2/spring/EmptySuiteSpec.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | class EmptySuiteSpec extends SpecificationLike { 4 | 5 | "This test doesn't want to run" should { 6 | "Show you other thing that '0 example, 0 failure, 0 error' in SBT console or 'Empty test suite' in IntelliJ" in { 7 | success 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/test/scala/org/specs2/spring/HibernateTemplateDataAccessSpec.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | import domain.User 4 | import org.specs2.mock.Mockito 5 | import org.springframework.orm.hibernate3.HibernateTemplate 6 | 7 | /** 8 | * @author anirvanchakraborty 9 | */ 10 | class HibernateTemplateDataAccessSpec extends SpecificationLike with HibernateTemplateDataAccess with BeanTables with Mockito { 11 | implicit val template = mock[HibernateTemplate] 12 | 13 | "hibernate3 based " in { 14 | "insert works as expected" in { 15 | "username" | "firstName" | 16 | "doo" !! "bar" | 17 | "doo" !! "bar" |> insert[User] 18 | 19 | there were two (template).saveOrUpdate(new User {username="doo"; firstName="bar"}) 20 | } 21 | 22 | "deleteAll works as expected" in { 23 | val users = 24 | "username" | "firstName" | 25 | "doo" !! "bar" | 26 | "doo" !! "bar" |< classOf[User] 27 | deleteAll[User] 28 | // there were two (template).execute <-- Need to find what's best to test here 29 | success 30 | } 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/scala/org/specs2/spring/NoSuchMethodArgsSpec.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | // N.B. Adding @Transactional should cause the test to fail, because 4 | // it does not load ApplicationContext using the @ContextConfiguration 5 | // annotation 6 | //@Transactional 7 | class NoSuchMethodArgsSpec extends SpecificationLike { 8 | 9 | "this spec" should { 10 | "do nothing" in { 11 | success 12 | } 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/scala/org/specs2/spring/SpecificationSpec.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | import domain.User 4 | import org.springframework.test.context.ContextConfiguration 5 | import org.specs2.mock.Mockito 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.hibernate.SessionFactory 8 | 9 | /** 10 | * @author janmachacek 11 | */ 12 | @ContextConfiguration(Array("classpath*:/META-INF/spring/module-context.xml")) 13 | class SpecificationSpec extends SpecificationLike 14 | with BeanTables with HibernateDataAccess with SettableEnvironment with Mockito { 15 | 16 | @Autowired implicit var sessionFactory: SessionFactory = _ 17 | @Autowired var springComponent: SpringComponent = _ 18 | 19 | "springComponent must:" in { 20 | val user = User() 21 | user.username = "foo" 22 | user.firstName = "Jan" 23 | user.lastName = "Machacek" 24 | 25 | "find all users" in { 26 | "username" | "firstName" | 27 | "janm" !! "Jan" | 28 | "marco" !! "Marc" |> insert[User] 29 | 30 | springComponent.findAll[User].size() must_== (2) 31 | } 32 | 33 | "be able to save a user" in { 34 | springComponent.save(user) 35 | springComponent.get[User](user.id) must_==(user) 36 | 37 | springComponent.findAll[User].size() must_== (3) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/scala/org/specs2/spring/SpringComponent.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring 2 | 3 | import org.springframework.stereotype.Component 4 | import org.hibernate.SessionFactory 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.transaction.annotation.Transactional 7 | import scala.reflect.ClassTag 8 | 9 | /** 10 | * @author janmachacek 11 | */ 12 | 13 | trait SpringComponent { 14 | def get[T](id: Long)(implicit evidence: ClassTag[T]): T 15 | def save(entity: AnyRef) 16 | def findAll[T](implicit evidence: ClassTag[T]): java.util.List[T] 17 | } 18 | 19 | @Component 20 | class SpringComponentImpl @Autowired() (private val sessionFactory: SessionFactory) extends SpringComponent { 21 | 22 | @Transactional(readOnly = true) 23 | def get[T](id: Long)(implicit evidence: ClassTag[T]): T = 24 | sessionFactory.getCurrentSession.get(evidence.runtimeClass, id).asInstanceOf[T] 25 | 26 | @Transactional 27 | def save(entity: AnyRef) { 28 | sessionFactory.getCurrentSession.saveOrUpdate(entity) 29 | } 30 | 31 | @Transactional(readOnly = true) 32 | def findAll[T](implicit evidence: ClassTag[T]) = 33 | sessionFactory.getCurrentSession.createCriteria(evidence.runtimeClass).list().asInstanceOf[java.util.List[T]] 34 | 35 | } -------------------------------------------------------------------------------- /src/test/scala/org/specs2/spring/domain/User.scala: -------------------------------------------------------------------------------- 1 | package org.specs2.spring.domain 2 | 3 | import reflect.BeanProperty 4 | import javax.persistence.{Version, Entity, Id, GeneratedValue} 5 | 6 | /** 7 | * @author janmachacek 8 | */ 9 | 10 | @Entity 11 | case class User() { 12 | @Id 13 | @GeneratedValue 14 | @BeanProperty 15 | var id: Long = _ 16 | @Version 17 | @BeanProperty 18 | var version: Int = _ 19 | @BeanProperty 20 | var username: String = _ 21 | @BeanProperty 22 | var firstName: String = _ 23 | @BeanProperty 24 | var lastName: String = _ 25 | 26 | override def equals(that: Any) : Boolean = { 27 | that.isInstanceOf[User] && (this.hashCode() == that.asInstanceOf [User].hashCode()); 28 | } 29 | 30 | override def hashCode = username.hashCode 31 | } 32 | --------------------------------------------------------------------------------