javaredisquarkus

How to use @BeforeAll in my tests of io.quarkus.redis.datasource.RedisDataSource


I have a set of test classes in Quarkus that tests my app classes which use io.quarkus.redis.datasource.RedisDataSource

I want to run @BeforeAll on each test class to reseed the redis db with known data. The BeforeAll method must be static:

  @BeforeAll
  static void resetTestData() {
    // use a static version of io.quarkus.redis.datasource.RedisDataSource; here 
    // so I can do something like:
    // GraphCommands<String> gc = ds.graph();
    // gc.graphQuery(redisdbkey, "CREATE (:XXX {key:'abcd', name:'fred'})");
  }

I can't work out how to make a static instance of io.quarkus.redis.datasource.RedisDataSource;

RedisDataSource ds = new RedisDataSource();

Throws a compiler error "Cannot instantiate the type RedisDataSource"

I tried making a Singleton instance like this:

@Singleton
public class RedisDataSourceSingleton {
  private static RedisDataSource instance;

  @Inject
  public RedisDataSourceSingleton(RedisDataSource ds) {
      instance = ds;
  }

  public static RedisDataSource getInstance() {
      if (instance == null) {
          throw new IllegalStateException("RedisDataSource is not initialized yet.");
      }
      return instance;
  }
}

but using the singleton like this:

import io.quarkus.redis.datasource.RedisDataSource;

// ...

  @BeforeAll
  static void resetTestData() {
    RedisDataSource ds = RedisDataSourceSingleton.getInstance();
    // use ds here
  }

throws: "The method getInstance() from the type RedisDataSourceSingleton refers to the missing type RedisDataSource"

Is there a way to get a static instance of io.quarkus.redis.datasource.RedisDataSource so I can use its methods in my @BeforeAll method? I am probably overthinking it!

Thanks, Murray


Solution

  • Ok. Following Clement's comment above, I have found a way to do what I need using Vert.x

    To restate the problem: I have a set of test classes, each with multiple test methods that run in a sequence (think variations on CRUD). I need to reset the FalkorDB graph for each class before the tests in that class run.

    The @BeforeAll annotation runs before all tests in a class. However, it must be a static method and the graph reset must complete before any tests in that class start.

    A "gotcha" is that the Quarkus (JUnit?) test runner creates a new instance of the class for each test function in that class. That means using the class constructor as a way to initialise the test data is not an option in my case because the result of one test feeds into the next test in that class. The tests use the @TestMethodOrder(OrderAnnotation.class) and @Test @Order(1) ... annotations to ensure the sequence.

    My initial plan for BeforeAll was to delete the graph key then run the sequence of Cypher queries to recreate the data. However, doing that in a static way became problematic with the http queue and it is inefficient to be re-running a known set of Cypher queries. More importantly, I realised I could leverage the FalkorDB Graph Copy command to copy a previously created graph in the initial state I needed.

    I created a FalkorGraphSetup class:

    package com.flowt.falkordb.vertx;
    
    import io.quarkus.logging.Log;
    import io.vertx.core.Future;
    import io.vertx.core.Vertx;
    import io.vertx.redis.client.Redis;
    import io.vertx.redis.client.RedisOptions;
    import io.vertx.redis.client.Request;
    import io.vertx.redis.client.Command;
    import io.vertx.redis.client.Response;
    import io.vertx.redis.client.impl.CommandImpl;
    import io.vertx.redis.client.impl.KeyLocator;
    import io.vertx.redis.client.impl.keys.BeginSearchIndex;
    import io.vertx.redis.client.impl.keys.FindKeysRange;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.CountDownLatch;
    
    import org.eclipse.microprofile.config.ConfigProvider;
    
    /**
     * Setup the FalkorDB graph.
     *
     * This class uses static requests that return void
     * and are used solely to manipulate the FalkorDB graph.
     *
     * Its main (only?) purpose is to provide a way to reset the test data
     * because the JUnit @BeforeAll annotation must be a STATIC method.
     *
     * We use a CountDownLatch so the methods will return only after the graph request returns.
     * This effectively makes the methods synchronous, which we need
     * so we know the graph data has been initialised before the tests run.
     */
    public class FalkorGraphSetup {
      private static Redis redisClient;
    
      // FYI: Define the custom commands that are in FalkorDB but not in the vertx client
      // ToDO: Move these to a separate class
      static Command GRAPH_COPY = new CommandImpl("graph.COPY", -1, false, false, false, new KeyLocator(true, new BeginSearchIndex(1), new FindKeysRange(0, 1, 0)), new KeyLocator(false, new BeginSearchIndex(2), new FindKeysRange(0, 1, 0)));
    
      // Use a static block so this section only runs once with the result
      // that we have a static instance of the redisClient to use with the methods below.
      static {
        Vertx vertx = Vertx.vertx();
        String connectionString = ConfigProvider.getConfig().getValue("quarkus.falkordb.hosts", String.class);
        RedisOptions redisOptions = new RedisOptions().setConnectionString(connectionString);
        redisClient = Redis.createClient(vertx, redisOptions);
      }
    
      /**
       * Delete the graph by key.
       * @param key
       */
      public static void graphDelete(String key) {
        Log.info("FalkorGraphSetup.graphDelete key: " + key);
    
        // Make sure key is not null or empty
        if (key == null || key.isEmpty()) {
          Log.error("FalkorGraphSetup.graphDelete key is null or empty");
          return;
        }
    
        // Create a CountDownLatch with a count of 1
        CountDownLatch latch = new CountDownLatch(1);
    
        // Execute the delete command
        redisClient
            .send(Request.cmd(Command.DEL).arg(key))
            .onComplete(ar -> {
              if (ar.succeeded()) {
                  Response response = ar.result();
                  Log.info("graphDelete operation on key '" + key + "' succeeded with response: " + response);
              } else {
                  Throwable cause = ar.cause();
                  Log.error("graphDelete operation failed with cause: " + cause);
              }
    
              // Decrement the count of the latch, allowing the waiting thread to proceed
              latch.countDown();
            });
    
        try {
            // Wait for the asynchronous operation to complete
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.error("graphCopy was interrupted while waiting for completion");
        }
      }
    
    
      /**
       * FalkorDB only.
       * Copy a graph from src to dest using the custom GRAPH_COPY command defined above.
       * @param src
       * @param dest
       */
      public static void graphCopy(String src, String dest) {
        Log.info("FalkorGraphSetup.graphCopy src: " + src + " dest: " + dest);
    
        // Create a CountDownLatch with a count of 1
        CountDownLatch latch = new CountDownLatch(1);
    
        redisClient
            .send(Request.cmd(GRAPH_COPY).arg(src).arg(dest))
            .onComplete(ar -> {
              if (ar.succeeded()) {
                  Response response = ar.result();
                  Log.info("graphCopy operation from src: " + src + " to dest: " + dest + " succeeded with response: " + response);
              } else {
                  Throwable cause = ar.cause();
                  Log.error("graphCopy operation failed with cause: " + cause);
              }
    
              // Decrement the count of the latch, allowing the waiting thread to proceed
              latch.countDown();
            });
    
        try {
            // Wait for the asynchronous operation to complete
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.error("graphCopy was interrupted while waiting for completion");
        }
      }
    
      /**
       * Execute a graph query on the specified key.
       * @param key
       * @param cmdStr
       */
      public static void graphQuery(String key, String cmdStr) {
    
        Log.info("FalkorGraphSetup.graphQuery key: " + key + " query: " + cmdStr);
    
        // Make sure key is not null or empty
        if (key == null || key.isEmpty()) {
          Log.error("FalkorGraphSetup.graphQuery key is null or empty");
          return;
        }
    
        // Make sure cmdStr is not null or empty
        if (cmdStr == null || cmdStr.isEmpty()) {
          Log.error("FalkorGraphSetup.graphQuery cmdStr is null or empty");
          return;
        }
    
        // Create a CountDownLatch with a count of 1
        CountDownLatch latch = new CountDownLatch(1);
    
        redisClient
            .send(Request.cmd(Command.GRAPH_QUERY).arg(key).arg(cmdStr))
            .onComplete(ar -> {
              if (ar.succeeded()) {
                  Response response = ar.result();
                  Log.info("Graph operation succeeded with response: " + response);
              } else {
                  Throwable cause = ar.cause();
                  Log.error("Graph operation failed with cause: " + cause);
              }
    
              // Decrement the count of the latch, allowing the waiting thread to proceed
              latch.countDown();
            });
    
        try {
            // Wait for the asynchronous operation to complete
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.error("graphQuery was interrupted while waiting for completion");
        }
      }
    }
    
    

    So, when the Quarkus app starts I have a class that uses the normal Quarkus Redis Client to create a set of graphs in the FalkorDB, each with the suffix "_is" (initial state). eg MyGraph1_is. Nothing special there.

    Then, in my test class BeforeAll I delete the MyGraph1 graph and recreate it by copying the MyGraph1_is graph to MyGraph1. eg:

      @BeforeAll
      static void beforeAll() {
        Log.info("\n\n================ BEFORE ALL =================");
    
        // Delete the graph by key
        FalkorGraphSetup.graphDelete("MyGraph1);
    
        // Make a fresh copy
        FalkorGraphSetup.graphCopy("MyGraph1_is","MyGraph1);
    
        Log.info("\n================ BEFORE ALL END =================\n");
      }
    

    The maximum test graph has about 50 nodes and edges. I have about 20 test classes. At this scale it all works really well. I haven't tried it with giant graphs yet.

    Any suggestions for improvement would be very welcome.

    Ultimately, I am heading towards building a responsive Quarkus FalkorDB client using Vert.x modelled on the existing Quarkus Redis client but incorporating just the FalkorDB commands.

    Cheers, Murray