Skip to content

tarantool/testcontainers-java-tarantool

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TestContainers Tarantool module

tests

Testcontainers module for the Tarantool database and application server and the Tarantool Cartridge framework.

See testcontainers.org for more information about TestContainers.

Installation

Add the Maven dependency:

<dependency>
  <groupId>io.tarantool</groupId>
  <artifactId>testcontainers-java-tarantool</artifactId>
  <version>1.3.1</version>
</dependency>

Usage Example

Standalone Tarantool server

For default setup, you need to have a file server.lua in your src/test/resources folder with contents similar to the following:

box.cfg {
    listen = 3301,
    memtx_memory = 128 * 1024 * 1024, -- 128 Mb
    -- log = 'file:/tmp/tarantool.log',
    log_level = 6,
}
-- API user will be able to login with this password
box.schema.user.create('api_user', { password = 'secret' })
-- API user will be able to create spaces, add or remove data, execute functions
box.schema.user.grant('api_user', 'read,write,execute', 'universe')

The most necessary part is exposing the port 3301 for external connections -- the container will not start without that setting in the startup script.

Instantiate a generic TarantoolContainer and use it in your tests:

public class SomeTest {

    @ClassRule
    static TarantoolContainer container = new TarantoolContainer();

    @BeforeAll
    public void setUp() {
        // Run some setup commands
        container.executeCommand("return 1, 2");
        // Or execute a script
        container.executeScript("org/testcontainers/containers/test.lua");
    }

    @Test
    public void testSomething() throws Exception {

        // Use properties provided by the container
        TarantoolCredentials credentials =
            new SimpleTarantoolCredentials(container.getUsername(), container.getPassword());
        TarantoolServerAddress serverAddress =
            new TarantoolServerAddress(container.getHost(), container.getPort());

        // Create TarantoolClient instance and use it in tests
        try (ClusterTarantoolTupleClient client = new ClusterTarantoolTupleClient(credentials, serverAddress)) {
            Optional<TarantoolSpaceMetadata> spaceMetadata = client.metadata().getSpaceByName("test");
            ...

            // Execute some commands in Tarantool instance for verification
            List<Object> result = container.executeCommand("return 1, 2");
            ...
        }
    ...

Tarantool Cartridge cluster

For testing against Tarantool Cartridge you need to place a directory with the application code into the classpath (for example, into src/test/resources directory). Suppose we have the following directory structure in there:

src/test/resources/
├── cartridge
│   ├── Dockerfile.build.cartridge
│   ├── Dockerfile.cartridge
│   ├── app
│   │   └── roles
│   │       ├── api_router.lua
│   │       ├── api_storage.lua
│   │       └── custom.lua
│   ├── cartridge.post-build
│   ├── cartridge.pre-build
│   ├── deps.sh
│   ├── init.lua
│   ├── instances.yml
│   ├── stateboard.init.lua
│   ├── test
│   │   ├── helper
│   │   │   ├── integration.lua
│   │   │   └── unit.lua
│   │   ├── helper.lua
│   │   ├── integration
│   │   │   └── api_test.lua
│   │   └── unit
│   │       └── sample_test.lua
│   ├── testapp-scm-1.rockspec
│   ├── tmp
│   └── topology.lua

The file instances.yml contains the Cartridge nodes configuration, which looks like this:

testapp.router:
  advertise_uri: localhost:3301
  http_port: 8081

testapp.s1-master:
  advertise_uri: localhost:3302
  http_port: 8082

testapp.s1-replica:
  advertise_uri: localhost:3303
  http_port: 8083

testapp.s2-master:
  advertise_uri: localhost:3304
  http_port: 8084

testapp.s2-replica:
  advertise_uri: localhost:3305
  http_port: 8085

and the file topology.lua contains a custom script which sets up the cluster topology using the Cartridge API:

cartridge = require('cartridge')
replicasets = {{
    alias = 'app-router',
    roles = {'vshard-router', 'app.roles.custom', 'app.roles.api_router'},
    join_servers = {{uri = 'localhost:3301'}}
}, {
    alias = 's1-storage',
    roles = {'vshard-storage', 'app.roles.api_storage'},
    join_servers = {{uri = 'localhost:3302'}, {uri = 'localhost:3303'}}
}, {
    alias = 's2-storage',
    roles = {'vshard-storage', 'app.roles.api_storage'},
    join_servers = {{uri = 'localhost:3304'}, {uri = 'localhost:3305'}}
}}
return cartridge.admin_edit_topology({replicasets = replicasets})

Now we can set up a Cartridge container for tests:

@Testcontainers
public class SomeOtherTest {

    @Container
    private static final TarantoolCartridgeContainer container =
        // Pass the classpath-relative paths of the instances configuration and topology script files
        new TarantoolCartridgeContainer("cartridge/instances.yml", "cartridge/topology.lua")
            // Tarantool URI, optional. Default is "localhost"
            .withRouterHost("localhost")
            // Binary port, optional. Default is 3301
            .withRouterPort(3301)
            // Cartridge HTTP API port, optional, 8081 is default
            .withAPIPort(8801)
            // Specify the actual username, default is "admin"
            .withRouterUsername("admin")
            // Specify the actual password, see the "cluster_cookie" parameter
            // in the cartridge.cfg({...}) call in your application.
            // Usually it can be found in the init.lua module
            .withRouterPassword("secret-cluster-cookie")
            // allows to reuse the container build once for faster testing
            .withReuse(true); 

    // Use the created container in tests
    public void testFoo() {
        // Execute Lua commands in the router instance
        List<Object> result = container.executeCommand("return profile_get(1)");

        // Instantiate a client connected to the router node
        TarantoolCredentials credentials = new SimpleTarantoolCredentials(getRouterUsername(), getRouterPassword());
        TarantoolServerAddress address = new TarantoolServerAddress(getRouterHost(), getRouterPort());
        TarantoolClientConfig config = TarantoolClientConfig.builder().withCredentials(credentials).build();
        try (ClusterTarantoolTupleClient client = new ClusterTarantoolTupleClient(config, address)) {
            // Do something with the client...
        }
    }
Environment variables of cartridge container and build arguments:
Build arguments:

This section describes the Docker image build arguments and environment variables inside the container. It is worth nothing that almost all build arguments listed here are passed into environment variables of the same name. At the moment, the following arguments are available to build the image:

  • CARTRIDGE_SRC_DIR - directory on the host machine that contains all the .lua scripts needed to initialize and run cartridge. Defaults is cartridge. Only as a build argument.
  • TARANTOOL_WORKDIR - a directory where all data will be stored: snapshots, wal logs and cartridge config files. Defaults is /app. Converts to an environment variable. It is not recommended to override via the withEnv(...) method.
  • TARANTOOL_RUNDIR - a directory where PID and socket files are stored. Defaults is /tmp/run. Converts to an environment variable. It is not recommended to override via the withEnv(...) method.
  • TARANTOOL_DATADIR - a directory containing the instances working directories. Defaults is /tmp/data. Converts to an environment variable. It is not recommended to override via the withEnv(...) method.
  • TARANTOOL_LOGDIR - the directory where log files are stored. Defaults is /tmp/log. Converts to an environment
  • variable. It is not recommended to override via the withEnv(...) method.
  • TARANTOOL_INSTANCES_FILE - path to the configuration file. Defaults is ./instances.yml. Converts to an environment variable. It is not recommended to override via the withEnv(...) method.
  • START_DELAY - the time after which cartridge will actually run after the container has started. Converts to an environment variable. It is not recommended to override via the withEnv(...) method.

You can set the Docker image build arguments using a map, which is passed as an input argument to the constructor when creating a container in Java code. See example:

final Map<String, String> buildArgs = new HashMap<String, String>(){{
put("CARTRIDGE_SRC_DIR", "cartridge");
put("TARANTOOL_WORKDIR", "/app");
put("TARANTOOL_RUNDIR", "/tmp/new_run");
put("TARANTOOL_DATADIR", "/tmp/new_data");
put("TARANTOOL_LOGDIR", "/tmp/log");
put("TARANTOOL_INSTANCES_FILE", "./instances.yml");
put("START_DELAY", "1s");
}};

Environment variables:

To set an environment variable, use the withEnv(...) method of testcontainers API. Full list of variables the environments used in cartridge can be found here link.

Note: As shown in the previous section, some build arguments are converted to environment variables and used to cartridge build at the image build stage.

An example of how to set the TARANTOOL_CLUSTER_COOKIE parameter:

@Test
public void testTarantoolClusterCookieWithEnv() throws Exception {
try(TarantoolCartridgeContainer newContainer = new TarantoolCartridgeContainer(
"Dockerfile",
"cartridge",
"cartridge/instances.yml",
"cartridge/replicasets.yml")
.withEnv(TarantoolCartridgeContainer.ENV_TARANTOOL_CLUSTER_COOKIE, "secret")
.withRouterUsername("admin")
.withRouterPassword("secret")
.withStartupTimeout(Duration.ofMinutes(5))
.withLogConsumer(new Slf4jLogConsumer(
LoggerFactory.getLogger(TarantoolCartridgeBootstrapFromYamlTest.class))))
{
newContainer.start();
ExecResult res = newContainer.execInContainer("env");
assertTrue(CartridgeContainerTestUtils.isEnvInStdout(res.getStdout(),
new HashMap<String, String>(){{
put("TARANTOOL_CLUSTER_COOKIE", "secret");
}}));
List<Object> result = newContainer.executeCommandDecoded("return true");
assertEquals(1, result.size());
assertTrue((boolean) result.get(0));
}
}

Mapping ports

Often there is a need to connect to a container through a specific port. To achieve this goal it is necessary to know the mapped port specified in the Java code. To get the mapped port, use the getMappedPort(...)` method of testcontainers API. See examples:

public class TarantoolCartridgePortMappingTest {
@Container
private final static TarantoolCartridgeContainer container = new TarantoolCartridgeContainer(
"Dockerfile",
"mapping-ports-container",
"cartridge/instances.yml",
"cartridge/replicasets.yml")
.withEnv(TarantoolCartridgeContainer.ENV_TARANTOOL_CLUSTER_COOKIE, "secret")
.withRouterUsername("admin")
.withRouterPassword("secret")
.withStartupTimeout(Duration.ofMinutes(5))
.withLogConsumer(new Slf4jLogConsumer(
LoggerFactory.getLogger(TarantoolCartridgeBootstrapFromYamlTest.class)));
@Test
void portMappingTest() throws IOException {
final int httpPortToFirstRouter = 8081;
final int httpPortToSecondRouter = 8082;
final int portToFirstRouter = 3301;
final int portToSecondRouter = 3302;
final String url = "localhost";
container.addExposedPorts(httpPortToFirstRouter, httpPortToSecondRouter);
container.start();
final StringBuilder curlCommandToConnectToRouters = new StringBuilder("http://localhost:")
.append(container.getMappedPort(httpPortToFirstRouter));
// send get request to first router via http
HttpResponse response = sendCurlToRouterHttpAPI(curlCommandToConnectToRouters.toString());
assertEquals(200, response.getStatusLine().getStatusCode());
curlCommandToConnectToRouters.delete(0, curlCommandToConnectToRouters.length())
.append("http://localhost:")
.append(container.getMappedPort(httpPortToSecondRouter));
// send get request to second router via http
response = sendCurlToRouterHttpAPI(curlCommandToConnectToRouters.toString());
assertEquals(200, response.getStatusLine().getStatusCode());
// connect to first router via socket
String result = connectToRouterViaSocket(url, container.getMappedPort(portToFirstRouter));
assertFalse(result.isEmpty());
assertTrue(result.contains("Tarantool"));
// connect to second router via socket
result = connectToRouterViaSocket(url, container.getMappedPort(portToSecondRouter));
assertFalse(result.isEmpty());
assertTrue(result.contains("Tarantool"));
// Connect to random port
result = connectToRouterViaSocket(url, ThreadLocalRandom.current().nextInt(49152, 65535));
assertTrue(result.isEmpty());
}
private HttpResponse sendCurlToRouterHttpAPI(String url) throws IOException {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
HttpGet httpGetRequest = new HttpGet(url);
return httpClient.execute(httpGetRequest);
}
}
private String connectToRouterViaSocket(String url, int port) {
final String returnedString;
try (Socket socket = new Socket(url, port);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
returnedString = in.readLine();
} catch (IOException e) {
return "";
}
return returnedString;
}
}

License

See LICENSE.

Copyright

Copyright (c) 2020 Alexey Kuzin and other authors.

See AUTHORS for contributors.