Blog

Adding integration tests to a Solr plugin project

Thanks to Torsten Bøgh Köster for this guest post!

When developing Solr plugins for the open source community (or your in-house Solr team) you’re faced with testing the plugin’s compatiblity against a range of Solr versions. The Solr Test Framework provides a great tool set for writing JUnit and integration tests against a single Solr version.

In this post I’ll guide you through adding Integration Tests to a Solr plugin project. We want to ensure the plugin’s functionality through a configurable range of Solr versions. I will add links to working examples in the Querqy Query Parser as I recently added those kind of tests there.

1. Integration testing

When it comes to integration testing, we strive to test our build artifact in possible runtime environments. In our case these are different versions of Solr.

But: Don’t Repeat Yourself: Don’t test functionality that you already tested in your unit tests again! Use a simple test setup to verify integration capabilities.

In our integration tests, we’ll spin up a Solr server and load a predefined product catalog into a collection. Then we’ll check if the plugin handles the configured query rewrites correctly. Tools used are:

1.1 Marking integration tests

When building and packaging our plugin, we wan’t to execute the integration tests after running the unit tests and after packaging the plugin. To achieve that, we need to distinguish Unit from integration tests. We’ll use JUnit categories for that.

We create a empty marker interface cool.solr.plugin.IntegrationTest and mark the integration tests as @Category(IntegrationTest.class)

1.2 Running integration tests in Maven

In Maven we configure the maven-surefire_plugin (Unit Tests) and maven-failsafe-plugin (Integration Tests) in the pom.xml to

  • exclude our marked test classes during unit testing and
  • include them during integration testing:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M5</version>
    <configuration>
        <!-- exclude integration tests -->
        <excludedGroups>cool.solr.plugin.IntegrationTest</excludedGroups>
    </configuration>
</plugin>
<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
        <includes>
            <include>**/*</include>
        </includes>
        <!-- run integration tests only -->
        <groups>cool.solr.plugin.IntegrationTest</groups>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

2. Testcontainers

The Testcontainers project is the Swiss army knife for launching Docker containers from JUnit tests. You need to add a dependency to Testcontainers in your Maven POM. It makes sense to add the Testcontainers Solr module as well.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.15.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>solr</artifactId>
    <version>1.15.1</version>
    <scope>test</scope>
</dependency>

Testcointainers provides a convenient GenericContainer wrapper for loading, launching and destroying Docker containers from within JUnit. To launch a Docker container from your JUnit test, define add a GenericContainer instance variable annotated as @Rule or @ClassRule to your test. JUnit rules are instantiated before your JUnit test @Before methods are called. Most importantly they are shut down properly after the test is finished.

@Category(IntegrationTest.class)
public class SolrQuerqyIntegrationTest {

    @ClassRule
    public SolrContainer solr = new SolrContainer(DockerImageName.parse("solr:8.8"));

    [...]
}

The code snippet above launches a Solr 8.8 Docker container before our tests starts, executes all test method in the test class and shuts it down afterwards.

2.1 Extending Testcontainers

Now we’re going to test our freshly built plugin in an off-the-shelf Solr container. We could build a custom Solr container (tedious and time consuming) or bind mount artifacts into the Docker container during startup.

Testcontainer’s GenericContainer has hooks to do so and the SolrContainer already makes use of them. We created a QuerqySolrContainer that inherits from SolrContainer (and transitive from GenericContainer). During construction, we need to mount two items into the starting Docker container:

  • the recently built and packaged plugin jar artifact
  • the prepared test configset
public class QuerqySolrContainer extends SolrContainer {

    private static final String PROP_PROJECT_BUILD_FINALNAME = 
        "project.build.finalName";
    private static final String PROP_PROJECT_BUILD_DIRECTORY = 
        "project.build.directory";

    public QuerqySolrContainer() {
        super(DockerImageName.parse("solr:8.8"));

        String querqyBinaryPath = String.format("%s/%s-jar-with-dependencies.jar",
                System.getProperty(PROP_PROJECT_BUILD_DIRECTORY), 
                System.getProperty(PROP_PROJECT_BUILD_FINALNAME));
        String querqyConfigurationPath = String.format("%s/test-classes/integration-test/%s",
                System.getProperty(PROP_PROJECT_BUILD_DIRECTORY), 
                "chorus");

        // link querqy binary into container
        addFileSystemBind(querqyBinaryPath, 
            "/opt/solr/server/solr-webapp/webapp/WEB-INF/lib/querqy.jar",
            BindMode.READ_ONLY);

        // link conf directory into container
        addFileSystemBind(querqyConfigurationPath,
            String.format("/opt/solr/server/solr/configsets/%s", QUERQY_IT_CONFIGSET), 
            BindMode.READ_ONLY);
    }
}

See QuerqySolrContainer in the Querqy project for a full example.

As plugin versions will change over time, we inject the Maven build properties into the JUnit test to locate the currently built JAR. Unfortunately these properties are not accessible by default, so we need to explicitly transfer them as systemPropertyVariables in our Maven POM:

<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
        <includes>
            <include>**/*</include>
        </includes>
        <groups>cool.solr.plugin.IntegrationTest</groups>
        <systemPropertyVariables>
            <project.build.directory>${project.build.directory}</project.build.directory>
            <project.build.finalName>${project.build.finalName}</project.build.finalName>
            <project.version>${project.version}</project.version>
       </systemPropertyVariables>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Building the project now launches a Solr 8.8 Docker container with our project artifact and test configset mounted. We can now overwrite the containerIsStarted hook in GenericContainer to e.g. upload the mounted configset. If needed, this is the place to create a collection or import test product data.

protected void containerIsStarted(InspectContainerResponse containerInfo) {
    try {
        // upload configset that we linked into the container
        ExecResult result = execInContainer("solr", "zk", "upconfig",
            "-z", "localhost:9983", 
            "-n", "chorus", 
            "-d", String.format("/opt/solr/server/solr/configsets/%s", "chorus"));
            if (result.getExitCode() != 0) {
                throw new IllegalStateException(
                    String.format("Could not upload %s configset to Zookeeper: %s", "chorus", result));
            }
    }
}

2.2 Testing multiple Solr versions

We now have a fully running integration test for our Solr plugin. In this step we want to run the same test case for multiple Solr versions. We use the JUnit Parameterized extension for the job:

  • The Solr versions to test against are injected via the system property solr.test.versions as a comma separated list
  • Use Parameterized JUnit runner to run same test for different Solr versions

Find the details how to use the Parameterized JUnit runner in the commented code snippet below:

// Enable the Parameterized runner
@RunWith(Parameterized.class)
@Category(IntegrationTest.class)
public class SolrQuerqyIntegrationTest {

    // Supply a stream of parsed DockerImageName that
    // we extract from a system property
    @Parameters(name = "{0}")
    public static Iterable<? extends Object> solrVersionsToTest() {
        return Arrays.asList(
            System.getProperty("solr.test.versions")
                .split(","))
                .stream()
                .map(image -> DockerImageName.parse(image))
                .collect(Collectors.toList());
    }

    // this gets constructed per parameterized test run
    @Rule
    public QuerqySolrContainer solr;

    // for each run of the test, the parsed DockerImageName
    // is supplied as constructor argument to the test case
    public SolrQuerqyIntegrationTest(DockerImageName solrImage) {

        // we delegate the given DockerImageName to our custom
        // Solr container implementation
        this.solr = new QuerqySolrContainer(solrImage);
    }

    [...]
}

In our Maven POM we can now add a systemPropertyVariables entry named solr.test.versions with a comma separated list of Solr versions to test with:

<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
        <systemPropertyVariables>
            [...]
            <solr.test.versions>solr:8.8,solr:8.7,solr:8.6,solr:8.5,solr:8.4,solr:8.3,solr:8.2,solr:8.1</solr.test.versions> 
        </systemPropertyVariables>
    </configuration>
    [...]
</plugin>

Summary

To add integration tests for our Solr plugin we have to combine and stir a whole lot of applications, libraries and technologies, but it’s worth it. In terms of Querqy, releases are now more mature and confident as we can detect Solr version compatiblity problems at an early stage.

The integrations tests are run using Github Actions and verify every commit and pull request.

Join Torsten and over 1300 other search enthusiasts in Relevance Slack to swap tips, tricks and recipes like this one! Let us know if we can help with your Solr project.

Image from Testing Vectors by Vecteezy