Running and Testing Solr with Gradle

A while ago I blogged on testing Solr with Maven on the synyx blog. In this post I will show you how to setup a similar project with Gradle that can start the Solr webapp and execute tests against your configuration.

Running Solr

Solr is running as a webapp in any JEE servlet container like Tomcat or Jetty. The index and search configuration resides in a directory commonly referred to as Solr home that can be outside of the webapp directory. This is also the place where the Lucene index files are created. The location for Solr home can be set using an environment variable.

The Solr war file is available in Maven Central. This post describes how to run a war file that is deployed in a Maven repository using Gradle. Let's see how the Gradle build file looks like for running Solr:

import org.gradle.api.plugins.jetty.JettyRunWar

apply plugin: 'java'
apply plugin: 'jetty'

repositories {
mavenCentral()
}

// custom configuration for running the webapp
configurations {
solrWebApp
}

dependencies {
solrWebApp "org.apache.solr:solr:3.6.0@war"
}

// custom task that configures the jetty plugin
task runSolr(type: JettyRunWar) {
webApp = configurations.solrWebApp.singleFile

// jetty configuration
httpPort = 8082
contextPath = 'solr'
}

// executed before jetty starts
runSolr.doFirst {
System.setProperty("solr.solr.home", "./solrhome")
}

We are creating a custom configuration that contains the Solr war file. In the task runSolr we configure the Jetty plugin. To add the Solr home environment variable we can use the way described by Sebastian Himberger. We add a code block that is executed before Jetty starts and sets the environment variable using standard Java mechanisms. You can now start Solr using gradle runSolr. You will see some errors regarding multiple versions of slf4j that are very like caused by this bug.

Testing the Solr configuration

Solr provides some classes that start an embedded instance using your configuration. You can use these classes in any setup as they do not depend on the gradle jetty plugin. Starting with Solr 3.2 the test framework is not included in solr-core anymore. This is what the relevant part of the dependency section looks like now:

testCompile "junit:junit:4.10"
testCompile "org.apache.solr:solr-test-framework:3.6.0"

Now you can place a test in src/test/java that either uses the convenience methods provided by SolrTestCaseJ4 or you can instantiate an EmbeddedSolrServer and execute any SolrJ actions. Both of these ways will use your custom config. This way you can easily validate that configuration changes don't break existing functionality. An example of using the convenience methods:

import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrServerException;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;

public class BasicConfigurationTest extends SolrTestCaseJ4 {

@BeforeClass
public static void initCore() throws Exception {
SolrTestCaseJ4.initCore("solrhome/conf/solrconfig.xml", "solrhome/conf/schema.xml", "solrhome/");
}

@Test
public void noResultInEmptyIndex() throws SolrServerException {
assertQ("test query on empty index",
req("text that is not found")
, "//result[@numFound='0']"
);
}

@Test
public void pathIsMandatory() throws SolrServerException, IOException {
assertFailedU(adoc("title", "the title"));
}

@Test
public void simpleDocumentIsIndexedAndFound() throws SolrServerException, IOException {
assertU(adoc("path", "/tmp/foo", "content", "Some important content."));
assertU(commit());

assertQ("added document found",
req("important")
, "//result[@numFound='1']"
);
}

}

We extend the class SolrTestCaseJ4 that is responsible for creating the core and instanciating the runtime using the paths we provide with the method initCore(). Using the available assert methods you can execute queries and validate the result using XPath expressions.

An example that instanciates a SolrServer might look like this:

import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.SolrParams;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;

public class ServerBasedTalkTest extends SolrTestCaseJ4 {

private EmbeddedSolrServer server;

@BeforeClass
public static void initCore() throws Exception {
SolrTestCaseJ4.initCore("solr/conf/solrconfig.xml", "solr/conf/schema.xml");
}

@Before
public void initServer() {
server = new EmbeddedSolrServer(h.getCoreContainer(), h.getCore().getName());
}

@Test
public void queryOnEmptyIndexNoResults() throws SolrServerException {
QueryResponse response = server.query(new SolrQuery("text that is not found"));
assertTrue(response.getResults().isEmpty());
}

@Test
public void singleDocumentIsFound() throws IOException, SolrServerException {
SolrInputDocument document = new SolrInputDocument();
document.addField("path", "/tmp/foo");
document.addField("content", "Mein Hut der hat 4 Ecken");

server.add(document);
server.commit();

SolrParams params = new SolrQuery("ecke");
QueryResponse response = server.query(params);
assertEquals(1L, response.getResults().getNumFound());
assertEquals("/tmp/foo", response.getResults().get(0).get("path"));
}

@After
public void clearIndex() {
super.clearIndex();
}
}

The tests can now be executed using gradle test.

Testing your Solr configuration is important as changes in one place might easily lead to side effects with another search functionality. I recommend to add tests even for basic functionality and evolve the tests with your project.