Test

Solidblocks infra-test is an infrastructure testing library for writing unit tests in Java/Kotlin to test actual the state of your servers.

Usage

To use solidblocks-test just add the dependency to your Gradle or Maven build

build.gradle.kts

plugins {
    id("org.jetbrains.kotlin.jvm") version "2.2.10"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.junit.jupiter:junit-jupiter-api:5.11.0")
    implementation("org.junit.jupiter:junit-jupiter-engine:5.11.0")
    implementation("de.solidblocks:infra-test:v0.4.10-rc1")
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}

Depending on your use case you can either inject the test context using the SolidblocksTest extension

extension usage

package solidblocks.test.gradle

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
class ExtensionUsage {
    @Test
    fun extensionUsage(testContext: SolidblocksTestContext) {
        val localTestContext = testContext.local()
        // localTestContext.command(...)
    }
}

or use the factory methods to create the test context directly

factory method usage

package solidblocks.test.gradle

import localTestContext
import org.junit.jupiter.api.Test

class FactoryMethodUsage {
    @Test
    fun factoryMethodUsage() {
        val localTestContext = localTestContext()
        // localTestContext.command(...)
    }
}

The extension automatically manages the lifecycle of created resources and cleans up everything when the test is finished.

Logging

All produced log output is marked with its origin, to make it easy to distinguish it from other logs during a test run. The following snippets contains an example log with some log statements form the library itself [INFO], logs from forked commands [STDOUT] and logs from a cloud-init run [CLOUDINIT]

[     INFO] Terraform checksums already downloaded at '/home/pelle/git/solidblocks/solidblocks-test/.cache/terraform_1.14.2_SHA256SUMS'
[     INFO] Terraform already downloaded at '/home/pelle/git/solidblocks/solidblocks-test/.cache/terraform_1.14.2_linux_amd64.zip'
[     INFO] Extracting Terraform binary to '/home/pelle/git/solidblocks/solidblocks-test/.bin/1.14.2/terraform'
[     INFO] Terraform installed at '/home/pelle/git/solidblocks/solidblocks-test/.bin/1.14.2/terraform'
[     INFO] running '/home/pelle/git/solidblocks/solidblocks-test/.bin/1.14.2/terraform init -upgrade' in '/home/pelle/git/solidblocks/solidblocks-test/build/resources/test/terraformCloudInitTestBed1'
[   STDOUT] Initializing the backend...
[   STDOUT] Initializing provider plugins...
[   STDOUT] - Finding hetznercloud/hcloud versions matching ">= 1.48.0"...
[   STDOUT] - Finding hashicorp/random versions matching "3.7.2"...
[   STDOUT] - Finding hashicorp/tls versions matching "4.1.0"...
[   STDOUT] - Using previously-installed hashicorp/tls v4.1.0
[   STDOUT] - Using previously-installed hetznercloud/hcloud v1.57.0
[   STDOUT] - Using previously-installed hashicorp/random v3.7.2

...

[   STDOUT] private_key_openssh_ecdsa = <sensitive>
[   STDOUT] private_key_openssh_ed25519 = <sensitive>
[   STDOUT] private_key_openssh_rsa = <sensitive>
[   STDOUT] private_key_pem_ecdsa = <sensitive>
[   STDOUT] private_key_pem_ed25519 = <sensitive>
[   STDOUT] private_key_pem_rsa = <sensitive>
[CLOUDINIT] Cloud-init v. 22.4.2 running 'init-local' at Wed, 24 Dec 2025 13:27:44 +0000. Up 8.65 seconds.
[CLOUDINIT] Cloud-init v. 22.4.2 running 'init' at Wed, 24 Dec 2025 13:27:48 +0000. Up 11.98 seconds.

...

[CLOUDINIT] ci-info: +-------+-------------------------+---------+-----------+-------+
[CLOUDINIT] ci-info: | Route |       Destination       | Gateway | Interface | Flags |
[CLOUDINIT] ci-info: +-------+-------------------------+---------+-----------+-------+
[CLOUDINIT] ci-info: |   0   | 2a01:4f9:c010:957b::/64 |    ::   |    eth0   |   U   |
[CLOUDINIT] ci-info: |   1   |        fe80::/64        |    ::   |    eth0   |   U   |
[CLOUDINIT] ci-info: |   2   |           ::/0          | fe80::1 |    eth0   |   UG  |
[CLOUDINIT] ci-info: |   4   |          local          |    ::   |    eth0   |   U   |
[CLOUDINIT] ci-info: |   5   |          local          |    ::   |    eth0   |   U   |
[CLOUDINIT] ci-info: |   6   |        multicast        |    ::   |    eth0   |   U   |
[CLOUDINIT] ci-info: +-------+-------------------------+---------+-----------+-------+

Test Contexts

Test contexts can be used to test and assert different infrastructure related properties and behaviors.

Local and docker command context

The local and docker command contexts allow to run commands and scrips on the machine executing the Junit tests and asserts the outcomes.

Run command or script

package solidblocks.test.gradle.command

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import de.solidblocks.infra.test.assertions.shouldHaveExitCode
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
class LocalCommandContext {
    @Test
    fun localCommandContext(testContext: SolidblocksTestContext) {
        val result = testContext.local().command("whoami").runResult()

        result shouldHaveExitCode 0
    }
}

Run command or script inside Docker

package solidblocks.test.gradle.command

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import de.solidblocks.infra.test.assertions.shouldHaveExitCode
import de.solidblocks.infra.test.docker.DockerTestImage
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
class DockerCommandContext {
    @Test
    fun dockerCommandContext(testContext: SolidblocksTestContext) {
        val result = testContext.docker(DockerTestImage.UBUNTU_22).command("whoami").runResult()

        result shouldHaveExitCode 0
    }
}

Options

Both the local and the docker command execution can be configured before the command is executed

package solidblocks.test.gradle.command

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import java.nio.file.Path
import kotlin.time.Duration.Companion.seconds

@ExtendWith(SolidblocksTest::class)
class LocalCommandOptions {
    @Test
    fun localCommandContext(testContext: SolidblocksTestContext) {
        val command = testContext.local().command("whoami")

        // timeout for running the command
        command.timeout(10.seconds)

        // inherit environment variables from the shell that spawned the units tests
        command.inheritEnv(true)

        // set working directory for command execution
        command.workingDir(Path.of("/tmp"))

        // set environment variable for command
        command.env("ENV_VAR1" to "foo-bar")

        // command.runResult()
    }
}

Assertions

The following assertions are available for the results of local().command(...).runResult() as well as docker(DockerTestImage.UBUNTU_22).command(...).runResult()

package solidblocks.test.gradle.command

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import de.solidblocks.infra.test.assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

@ExtendWith(SolidblocksTest::class)
class CommandAssertions {
    @Test
    fun commandAssertions(testContext: SolidblocksTestContext) {
        val result = testContext.local().command("whoami").runResult()

        result shouldHaveExitCode 0

        result outputShouldBe "something"
        result stderrShouldBe "something"

        result stderrShouldMatch ".*something.*"
        result stdoutShouldMatch ".*something.*"

        result.stdoutShouldBeEmpty()
        result.stderrShouldBeEmpty()

        result runtimeShouldBeGreaterThan 10.milliseconds
        result runtimeShouldBeLessThan 5.seconds
    }
}

Terraform

The terraform context allows to apply Terraform configurations from within the test code. It exposes the full init(), apply() and destroy() lifecycle that can be used either to test Terraform modules, or to prepare an environment for other tests. When used the test context as well as the factory methods will always download the correct version for the architecture executing the tests, supporting Linux, MacOS and Windows on x86 and arm.

Test a Terraform config

Given a folder containing Terraform files, like for example

/module1/main.tf
resource "random_string" "string1" {
  length = 12
}

output "string1" {
  value = random_string.string1.id
}

The resources described in this file can be created using the Terraform test context

Test a terraform setup
package solidblocks.test.gradle.terraform

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
public class TerraformContext {

    @Test
    fun terraformExtension(context: SolidblocksTestContext) {
        val modulePath = TerraformContext::class.java.getResource("/module").path

        val terraform = context.terraform(modulePath)
        terraform.init()
        terraform.apply()

        // destroy will be called automatically when all tests from the class are finished
    }
}
Options
package solidblocks.test.gradle.terraform

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
public class TerraformOptions {

    @Test
    fun terraformExtension(context: SolidblocksTestContext) {
        val modulePath = TerraformOptions::class.java.getResource("/module").path

        // use a specific Terraform version
        val terraform = context.terraform(modulePath, "1.14.1")

        // set terraform variable (implicitly sets the
        // environment variable 'TF_VAR_variable1'
        terraform.addVariable("variable1", "foo-bar")

        // do not clean up any resources when the test run fails,
        // e.g. destroy will not be executed after test run is finished
        context.cleanupAfterTestFailure(false)
    }
}

Host

The host context allows to verify properties of remote hosts.

package solidblocks.test.gradle.host

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
class HostContext {
    @Test
    fun commandAssertions(testContext: SolidblocksTestContext) {
        val host = testContext.host("pelle.io")

    }
}

Assertions

package solidblocks.test.gradle.host

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import de.solidblocks.infra.test.assertions.portShouldBeClosed
import de.solidblocks.infra.test.assertions.portShouldBeOpen
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
class HostAssertions {
    @Test
    fun commandAssertions(testContext: SolidblocksTestContext) {
        val host = testContext.host("pelle.io")

        host portShouldBeOpen 22
        host portShouldBeClosed 80
    }
}

SSH

The host ssh context allows assertions to run on remote hosts. Prerequisites are a running SSH server o the host provided by <ssh_host> along with a matching <private_key>. The private key can be an pem or openssh encoded rsa, ed25519 or ecdsa key.

package solidblocks.test.gradle.ssh

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
public class SSHContext {

    @Test
    fun terraformExtension(context: SolidblocksTestContext) {
        val ssh = context.ssh("<ssh_host>", "<private_key>")

        val result = ssh.command("...")
    }
}

Assertions

SSHContext.command(...) supports the same assertions as local().command(...).runResult() and docker(...).command(...).runResult().

Cloud-Init

The cloud-init context allows assertions based on the artifacts generated after a cloud-init run. Like in the SSH context, the connection to the machine is created via SSH, so the same prerequisites as for the SSH context apply.

package solidblocks.test.gradle.cloudinit

import de.solidblocks.infra.test.SolidblocksTest
import de.solidblocks.infra.test.SolidblocksTestContext
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(SolidblocksTest::class)
public class CloudInitContext {

    @Test
    fun terraformExtension(context: SolidblocksTestContext) {
        val cloudInit = context.cloudInit("<ssh_host>", "<private_key>")

        // print the output of '/var/log/cloud-init-output.log' in case a
        // test fails. This is disabled by default to avoid accidental leakage
        // of secrets that may be processed during cloud-init runs
        cloudInit.printOutputLogOnTestFailure()
    }
}