Part 3.3: Testing your plugin

This tutorial covers how to write unit tests for your plugin.

Unit tests

Previous tutorials have covered the steps to install and setup the development environment and to write and document a basic plugin.

As plugins are being written to be reusable it is important to write some code (called a unit test) to test that the plugin is operating as expected.

A unit test is used to ensure that the plugin produces expected output when given specific input. For example, you can write a test that defines a title that your plugin receives as input and then write a test that defines what the expected output is, comparing this to the real output from the plugin processing the input title you supplied. You should write unit tests for each different function that the plugin implements to make sure it operates as expected.

Having these unit tests also allows automated testing to be set up to ensure that the plugin works correctly with newer versions of Funnelback.

Having well written tests is really important as it enables us to automatically upgrade the DXP without breaking search implementations.

Writing your first test

The plugin implemented in the implementing plugin functionality tutorial implemented two main behaviours. The first searches for a pattern in the title of each search result. The second behaviour replaces the pattern with the replacement supplied via configuration, or with a default value if this is not defined. As a result there are two behaviours to test.

Importing the project into IntelliJ created a set of files as defined by the Maven archetype. In the previous tutorial the plugin functionality was implemented by editing one of these files (TitleprefixSearchLifeCyclePlugin). The import also created another set of files for testing each of interfaces. TitleprefixSearchLifeCyclePluginTest is used to test the functionality implemented in TitleprefixSearchLifeCyclePlugin.

Open the TitleprefixSearchLifeCyclePluginTest file and replace its contents with the following:

package com.example.plugin.titleprefix;

import com.funnelback.plugin.search.SearchLifeCycleContext;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.funnelback.publicui.search.model.padre.Result;
import com.funnelback.publicui.search.model.transaction.testutils.TestableSearchTransaction;
import com.funnelback.plugin.search.mock.MockSearchLifeCycleContext;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class TitleprefixSearchLifeCyclePluginTest {

    TitleprefixSearchLifeCyclePlugin pluginUnderTest = new TitleprefixSearchLifeCyclePlugin(); (1)
    TestableSearchTransaction testInput; (2)
    SearchLifeCycleContext testInputContext;

    @Before (3)
    public void setup() {
        // Create some test data.
        testInputContext = new MockSearchLifeCycleContext();
        testInput = new TestableSearchTransaction() (4)
                .withResult(Result.builder().title("ExampleCorp - Fake search data to take home").build()) (5)
                .withResult(Result.builder().title("ExampleCorp - Another result with the same prefix").build())
                .withResult(Result.builder().title("This result does not match the pattern").build());
    }

    @Test (6)
    public void testPostProcess_with_replacement_pattern() { (7)
        testInput
                .withProfileSetting("plugin.title-prefix.config.pattern", "ExampleCorp -") (8)
                .withProfileSetting("plugin.title-prefix.config.replaceWith", "EC |");
        pluginUnderTest.postProcess(testInputContext, testInput); (9)
        Set<String> expectedResultTitles = Set.of(
                "EC | Fake search data to take home",
                "EC | Another result with the same prefix",
                "This result does not match the pattern"); (10)
        List<Result> mockResults = testInput.getResponse().getResultPacket().getResults(); (11)
        Set<String> actualResultTitles = mockResults.stream().map(finalResult -> finalResult.getTitle()).collect(Collectors.toSet()); (12)
        Assert.assertEquals("the pattern should have been replaced with the new one", expectedResultTitles,actualResultTitles); (13)
    }

    @Test
    public void testPostProcess_without_replacement_pattern(){ (14)
        testInput.withProfileSetting("plugin.title-prefix.config.pattern", "ExampleCorp - "); (15)
        pluginUnderTest.postProcess(testInputContext, testInput);
        Set<String> expectedResultTitles = Set.of(
                "Fake search data to take home",
                "Another result with the same prefix",
                "This result does not match the pattern");
        List<Result> mockResults = testInput.getResponse().getResultPacket().getResults();
        Set<String> actualResultTitles = mockResults.stream().map(finalResult -> finalResult.getTitle()).collect(Collectors.toSet());
        Assert.assertEquals("the pattern should have been removed", expectedResultTitles, actualResultTitles); (16)
    }
}
1 Creates a runnable version of our plugin’s implementation class.
2 Makes a reference to a test input object that will be re-used later. Items declared at this level can be accessed by any of the test methods.
3 The @Before annotation is from a library called junit, it means that everything in the following method will be run before each of the tests. This enables the next method to setup some test data for each of our tests.
4 The TestableSearchTransaction class is a special SearchTransaction that will be used in the tests to create an object similar to what the Funnelback system provides when a query is run. It is assigned to the reference at 2 so that it can be used by the tests.
5 By default the TestableSearchTransaction contains no data. Result.builder() is used to create a fake search result. A title is added to each search result as this is required data for the plugin to run. Additional SearchTransaction fields could be set here but are not required in order to test that the plugin is working.
6 The @Test annotation indicates that the next method will be considered a test and run when all tests are run. Before each test is executed, the @Before method references at 3 above will also be run.
7 There are no requirements on how a test method is named. When naming a test method choose a name that accurately describes what is being test. Having meaningful test names makes the test reporting a lot easier to understand. testPostProcess_with_replacement_pattern is the method implementing the first of the two tests.
8 Modifies the test input object that we created in step 4. Setting some of the values that would normally be read from configuration (either from the data source or results page configuration). Two configuration settings are set which is equivalent to a plugin containing the following in the results page configuration:
plugin.title-prefix.pattern=ExampleCorp -
plugin.title-prefix.replaceWith=EC |
9 Runs the plugin on the test input object (the transaction object created in step 2).
10 Defines the corresponding modified titles the plugin is expected to return given the test input data.
11 Gets a list containing the results as modified by running the plugin code in step 9.
12 Extracts the titles from each result (as that is all the plugin changes) into a set that can be compared with the expected titles.
13 Compares the titles as returned by running the plugin with what was expected. The testing framework has a few different ways of checking our expectations. If you type Assert. into IntelliJ, it should list several options, but they all have a similar format of inputs: Assert.<something>(message, expectation). Every test you write should have at least one assertion; this is what determines if a test passes or fails. When it fails, the message will be displayed and can provide additional information about what you as the test writer, expected to happen.
14 Declares the second test. Compare the name to the one chosen for the previous test and note how the title describes what is being tested.
15 Sets up the test in a similar manner to the previous one, but this time only one configuration value is set.
16 Defines the conditions for a successful test. The message is different to the first test because the expected behaviour is different.

Running the tests.

The tests can be run directly from within IntelliJ by pressing Ctrl+R or by selecting Run  Run 'TitlePrefixPluginSearchLifeCyclePluginTest' from the dropdown menu (1) in the image below. The tests run in the console (3) and indicate if the tests passed or failed (2). The tests defined above should pass without any problems.

To demonstrate how the unit tests help temporarily changing one of the expected result titles and rerun the tests. This time the tests will fail and an error message should be displayed.

running_tests
  1. Running the tests.

  2. The results of the test.

  3. Information about why it failed.

Revert the title that you changed so that the test once again passes.

Next steps

The next tutorial covers how to build and package a plugin for use with Funnelback.