Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I start an embedded Cassandra server *before* loading the Spring Context in a Spock test?

I seem to be running into a tricky situation with ordering in one of my integration tests. I have a REST server based on Spring Boot/Spring MVC and a Cassandra DB. I'm using the spring-data-cassandra jar file to enable POJOs to be inserted into the DB through the CassandraTemplate via a CrudRepository implementation. The application works fine, I can make REST calls and the framework(s) correctly convert my form data to POJOs and inserts the data in the DB. So far, so good.

What I'm trying to do is write an integration test that executes the entire stack. Obviously I don't want to depend on an external DB being available, so I'm using the EmbeddedCassandra stuff from cassandra-unit-spring and drive it through a Spock test. The problem I run into seems to have to do with some sort of ordering conflict between the SpringApplicationContextLoader defined in the @ContextConfiguration annotation, and the CassandraUnitTestExecutionListener class defined in the @TestExecutionListener annotation. Since this is a Spock test, it needs the SpringApplicationContextLoader class to automatically start the Spring Boot server. However, it does so before the Cassandra server has started up, which causes the Cluster/Session beans to fail to load since they apparently attempt to contact the server immediately upon creation. I have tried so many combinations of things that my head is spinning. I had to resort to what I consider a pretty ugly solution. I have a setup() method that uses a boolean to make sure the context and Spring Boot app is only run once. I tried putting this in a setupSpec() method that is only run once per test class, but that didn't work (some catch 22 problem). If I try to @Autowire the context, then it gets injected before the CassandraServer has started. As I mentioned, I've tried a gazillion different things.

This solution works, but it bugs me that I couldn't get it to work in a more elegant fashion with just annotations and DI. Also, the @Value annotated fields don't get initialized, which is why I had to resort to the kludge of obtaining the server port through the environment after starting Tomcat. Any suggestions on how to do this "the right way" are very welcome. Thanks.

@ActiveProfiles(profiles = ["test"])
@ContextConfiguration(loader = SpringApplicationContextLoader.class, classes = Application.class)
// @WebIntegrationTest("server.port:0") // Pick a random port for Tomcat
@EnableConfigurationProperties
@EmbeddedCassandra
@CassandraDataSet(value = ["cql/createUserMgmtKeySpace.cql", "cql/createUserMgmtTables.cql"], keyspace = "user_mgmt")
@TestExecutionListeners(listeners = [ CassandraUnitTestExecutionListener.class ]) //, DependencyInjectionTestExecutionListener.class ]) //, mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
@Title("Integration test for the lineup ingestion REST server and the Cassandra DAOs.")
public class CassandraSpec extends Specification {
  @Shared
  ApplicationContext context
  @Shared
  CQLDataLoader cql
  @Shared boolean initialized = false

  // This is the server port that Spring picked. We use it in the REST URLs
  //  @Value('${local.server.port}')
  @Shared
  int serverPort

  def setup() {
    if (initialized) {
      return
    }

    System.setProperty("spring.profiles.active", "test")
    SpringApplication app = new SpringApplication(CassandraConfig.class)
    app.headless = true
    context = app.run(Application.class)
    assert context
    cql = new CQLDataLoader(context.getBean("session"))
    serverPort = Integer.parseInt(context.environment.getProperty('server.port'))
    println("Tomcat port: ${serverPort}")
    initialized = true
  }

... test methods here ...
}
like image 483
user2337270 Avatar asked Mar 15 '23 02:03

user2337270


2 Answers

Face palm!!! After posting this, I found out that I needed to add CassandraUnitDependencyInjectionTestExecutionListener. The magic line is:

@TestExecutionListeners(listeners = [ CassandraUnitDependencyInjectionTestExecutionListener, CassandraUnitTestExecutionListener, DependencyInjectionTestExecutionListener.class ]) 

After that I was able to run the test without any of the other kludges, and I can inject an ephemeral port and use it in my RestTemplate URLs. Problem solved :-)

like image 190
user2337270 Avatar answered Mar 16 '23 16:03

user2337270


The answer above provided by @user2337270 is the correct one. Though I am not using the Spock testing framework, I spent a fair bit of time trying to get cassandra unit and spring to work. Adding CassandraUnitDependencyInjectionTestExecutionListener did the trick. I thought it may be helpful to other users to provide a sample of a full test class that works. Hope it helps.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { CassandraConfig.class, InboundDocumentCassandraDaoTest.CassandraPropertiesTest.class })
@EmbeddedCassandra
@CassandraDataSet(value = {"idoc.cql"}, keyspace = "idoc")
@TestExecutionListeners(listeners = {     CassandraUnitDependencyInjectionTestExecutionListener.class,  DependencyInjectionTestExecutionListener.class  })
public class InboundDocumentCassandraDaoTest {

    @Autowired
    CassandraOperations cassOp;

    @Autowired
    InboundDocumentDao dao;

    @Test
    public void insert_into_idocs() throws Exception {

        UUID id = UUIDs.timeBased();
        long addedDate = System.currentTimeMillis();

        Insert insert = QueryBuilder.insertInto("idocs");
        insert.setConsistencyLevel(ConsistencyLevel.ONE);
        insert.value("idoc_id", id);
        insert.value("idoc_added_date", addedDate);
        insert.value("idoc_type", "invoice");
        insert.value("json_doc", "{ \"foo\":\"bar\" }");

        cassOp.execute(insert);

        String jsonObj = dao.getInboundDocumentById(id.toString());
        assertNotNull(jsonObj);
        assertEquals("{ \"foo\":\"bar\" }", jsonObj);
    }

    @Configuration
    public static class CassandraPropertiesTest {
        @Bean
        public CassandraProperties cassadraProps() {
            CassandraProperties props = new CassandraProperties();
            props.setCassandraContactpoints("127.0.0.1");
            props.setCassandraPort("9142");
            props.setCassandraKeyspace("idoc");
            props.setReadConsistencyLevel("LOCAL_ONE");
            return props;
        }
    }
}

As an FYI, the idoc.cql file was placed in the src/main/resources folder.

like image 27
Mike Barlotta Avatar answered Mar 16 '23 15:03

Mike Barlotta