diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 938c733d..32899a97 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -35,3 +35,8 @@ updates: package-ecosystem: "bundler" schedule: interval: "monthly" + + - directory: "/testing/testcontainers/java" + package-ecosystem: "gradle" + schedule: + interval: "monthly" diff --git a/.github/workflows/testcontainers-java.yml b/.github/workflows/testcontainers-java.yml new file mode 100644 index 00000000..014898aa --- /dev/null +++ b/.github/workflows/testcontainers-java.yml @@ -0,0 +1,47 @@ +name: Testcontainers for Java +on: + + pull_request: + branches: [ main ] + paths: + - '.github/workflows/testcontainers-java.yml' + - 'testing/testcontainers/java/**' + push: + branches: [ main ] + paths: + - '.github/workflows/testcontainers-java.yml' + - 'testing/testcontainers/java/**' + + # Allow job to be triggered manually. + workflow_dispatch: + +# Cancel in-progress jobs when pushing to the same branch. +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + test: + name: "CrateDB: ${{ matrix.cratedb-version }} + on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ "ubuntu-latest" ] + + steps: + + - name: Acquire sources + uses: actions/checkout@v3 + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" + cache: "gradle" + + - name: Run software tests + run: | + cd testing/testcontainers/java + ./gradlew check diff --git a/README.rst b/README.rst index b7127337..bf73cc9a 100644 --- a/README.rst +++ b/README.rst @@ -26,5 +26,8 @@ Layout CrateDB is part of a larger software stack. Those resources may also be used within "reference architecture" types of documentation. +- The ``testing`` folder contains reference implementations about how to use + different kinds of test layers for testing your applications with CrateDB. + .. _CrateDB: https://github.com/crate/crate diff --git a/testing/testcontainers/java/.gitignore b/testing/testcontainers/java/.gitignore new file mode 100644 index 00000000..27427779 --- /dev/null +++ b/testing/testcontainers/java/.gitignore @@ -0,0 +1,8 @@ +.gradle/ +.idea +*.iws +*.iml +*.ipr +build/ +out/ +target/ diff --git a/testing/testcontainers/java/.java-version b/testing/testcontainers/java/.java-version new file mode 100644 index 00000000..98d9bcb7 --- /dev/null +++ b/testing/testcontainers/java/.java-version @@ -0,0 +1 @@ +17 diff --git a/testing/testcontainers/java/README.rst b/testing/testcontainers/java/README.rst new file mode 100644 index 00000000..208ba9f8 --- /dev/null +++ b/testing/testcontainers/java/README.rst @@ -0,0 +1,89 @@ +####################### +Testcontainers for Java +####################### + +*How to run integration tests of Java applications with CrateDB.* + + +***** +About +***** + +Introduction +============ + +`Testcontainers for Java`_ is a Java library that supports JUnit tests, +providing lightweight, throwaway instances of common databases suitable +for integration testing scenarios. + +The `Testcontainers CrateDB Module`_ will provide your application test +framework with a single-node `CrateDB`_ instance. You will be able to choose +the `CrateDB OCI image`_ variant by version, or by selecting the ``nightly`` +release. + +What's inside +============= + +This directory includes different canonical examples how to use those +components within test harnesses of custom applications using `CrateDB`_. +Currently, all test cases are based on JUnit 4. This is an enumeration +of examples you can explore here: + +- ``TestClassScope``: Class-scoped testcontainer instance with JUnit 4 @Rule/@ClassRule integration. +- ``TestFunctionScope``: Function-scoped testcontainer instance with JUnit 4 @Rule/@ClassRule integration. +- ``TestJdbcUrlScheme``: Database containers launched via Testcontainers "TC" JDBC URL scheme. +- ``TestManual``: Function-scoped testcontainer instance with manual setup/teardown. +- ``TestManualWithLegacyCrateJdbcDriver``: + Function-scoped testcontainer instance with manual setup/teardown, using a custom + ``CrateDBContainer``, which uses the `legacy CrateDB JDBC driver`_. +- ``TestSharedSingleton`` [1]: + Testcontainer instance shared across multiple test classes, implemented using the Singleton pattern. +- ``TestSharedSingletonEnvironmentVersion``: + Testcontainer instance honoring the ``CRATEDB_VERSION`` environment variable, suitable + for running a test matrix on different versions of CrateDB, shared across multiple test + classes. + +[1]: Sometimes, it might be useful to define a container that is only started once for +several test classes. There is no special support for this use case provided by +the Testcontainers extension. Instead, this can be implemented using the Singleton +pattern. See also `Testcontainers » Manual container lifecycle control » Singleton +containers`_. + + +***** +Usage +***** + +1. Make sure Java 17 is installed. +2. Invoke software tests, using `Testcontainers for Java`_. It should work out + of the box:: + + # Run all tests. + ./gradlew test + + # Run individual tests. + ./gradlew test --tests TestFunctionScope + + # Run test case showing how to select CrateDB version per environment variable. + export CRATEDB_VERSION=5.2.3 + export CRATEDB_VERSION=nightly + ./gradlew test --tests TestSharedSingletonMatrix + +3. Invoke example application:: + + # Start CrateDB. + docker run -it --rm --publish=4200:4200 --publish=5432:5432 \ + crate:latest -Cdiscovery.type=single-node + + # Run example program, using both the CrateDB legacy + # JDBC driver, and the vanilla PostgreSQL JDBC driver. + ./gradlew run --args="jdbc:crate://localhost:5432/" + ./gradlew run --args="jdbc:postgresql://localhost:5432/" + + +.. _CrateDB: https://github.com/crate/crate +.. _CrateDB OCI image: https://hub.docker.com/_/crate +.. _legacy CrateDB JDBC driver: https://crate.io/docs/jdbc/ +.. _Testcontainers for Java: https://github.com/testcontainers/testcontainers-java +.. _Testcontainers CrateDB Module: https://www.testcontainers.org/modules/databases/cratedb/ +.. _Testcontainers » Manual container lifecycle control » Singleton containers: https://www.testcontainers.org/test_framework_integration/manual_lifecycle_control/#singleton-containers diff --git a/testing/testcontainers/java/build.gradle b/testing/testcontainers/java/build.gradle new file mode 100644 index 00000000..a4cae4a5 --- /dev/null +++ b/testing/testcontainers/java/build.gradle @@ -0,0 +1,67 @@ +/** + * An example application for demonstrating "Testcontainers for Java" with CrateDB and the PostgreSQL JDBC driver. + */ + +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'application' + id 'idea' + id 'java' +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + implementation 'org.postgresql:postgresql:42.6.0' + implementation 'io.crate:crate-jdbc:2.6.0' + implementation 'org.slf4j:slf4j-api:2.0.7' + implementation 'org.slf4j:slf4j-simple:2.0.7' + testImplementation 'junit:junit:4.13.2' + testImplementation "org.assertj:assertj-core:3.23.1" + testImplementation 'org.testcontainers:testcontainers:1.18.0' + testImplementation 'org.testcontainers:cratedb:1.18.0' + testImplementation 'org.testcontainers:postgresql:1.18.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +jar { + archiveBaseName = 'cratedb-example-testcontainers-java' + archiveVersion = '0.0.1-SNAPSHOT' +} + +sourceSets { + main { + java.srcDirs += [ + "src/generated/java", + "src/main/java", + ] + } +} + +test { + dependsOn 'cleanTest' +} + +application { + mainClass = 'io.crate.example.testing.Application' +} + +ext.javaMainClass = "io.crate.example.testing.Application" + + +idea.module.inheritOutputDirs = true +processResources.destinationDir = compileJava.destinationDir +compileJava.dependsOn processResources diff --git a/testing/testcontainers/java/gradle/wrapper/gradle-wrapper.jar b/testing/testcontainers/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/testing/testcontainers/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/testing/testcontainers/java/gradle/wrapper/gradle-wrapper.properties b/testing/testcontainers/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..070cb702 --- /dev/null +++ b/testing/testcontainers/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/testing/testcontainers/java/gradlew b/testing/testcontainers/java/gradlew new file mode 100755 index 00000000..a69d9cb6 --- /dev/null +++ b/testing/testcontainers/java/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/testing/testcontainers/java/gradlew.bat b/testing/testcontainers/java/gradlew.bat new file mode 100644 index 00000000..9109989e --- /dev/null +++ b/testing/testcontainers/java/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/testing/testcontainers/java/src/main/java/io/crate/example/testing/Application.java b/testing/testcontainers/java/src/main/java/io/crate/example/testing/Application.java new file mode 100644 index 00000000..89a877de --- /dev/null +++ b/testing/testcontainers/java/src/main/java/io/crate/example/testing/Application.java @@ -0,0 +1,107 @@ +/** + * An example application for demonstrating "Testcontainers for Java" with CrateDB and the PostgreSQL JDBC driver. + * + */ + +package io.crate.example.testing; + +import java.io.IOException; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +public class Application { + + public record Results(ResultSetMetaData metaData, List rows) {} + + private final String dsn; + private final String user; + + public Application(String connectionUrl) { + dsn = connectionUrl; + user = "crate"; + } + + public Application(String connectionUrl, String userName) { + dsn = connectionUrl; + user = userName; + } + + public static void main(String[] args) throws IOException, SQLException { + if (args.length != 1) { + throw new IllegalArgumentException( + """ + ERROR: Need a single argument, the database connection URL. + + Examples: + ./gradlew run --args="jdbc:crate://localhost:5432/" + ./gradlew run --args="jdbc:postgresql://localhost:5432/" + """); + } + String connectionUrl = args[0]; + Application app = new Application(connectionUrl); + printResults(app.querySummitsTable()); + System.out.println("Done."); + } + + public static void printResults(Results results) throws SQLException { + for (int i = 0; i < results.rows.size(); i++) { + Object[] row = results.rows.get(i); + System.out.printf(Locale.ENGLISH, "> row %d%n", i + 1); + for (int j = 1; j < results.metaData().getColumnCount(); j++) { + System.out.printf( + Locale.ENGLISH, + ">> col %d: %s: %s%n", + j, + results.metaData.getColumnName(j), + row[j]); + } + System.out.println(); + } + } + + /** + * Example database conversation: Query the built-in `sys.summits` table of CrateDB. + */ + public Results querySummitsTable() throws IOException, SQLException { + return this.query("SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3"); + } + + public Results query(String sql) throws IOException, SQLException { + Properties connectionProps = new Properties(); + connectionProps.put("user", user); + + try (Connection sqlConnection = DriverManager.getConnection(dsn, connectionProps)) { + sqlConnection.setAutoCommit(true); + if (sqlConnection.isClosed()) { + throw new IOException("ERROR: Unable to open connection to database"); + } + try (Statement stmt = sqlConnection.createStatement()) { + boolean checkResults = stmt.execute(sql); + if (checkResults) { + List rows = new ArrayList<>(); + ResultSet rs = stmt.getResultSet(); + while (rs.next()) { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + Object[] row = new Object[columnCount]; + for (int i = 1; i < columnCount; i++) { + row[i] = rs.getObject(i); + } + rows.add(row); + } + return new Results(rs.getMetaData(), rows); + } else { + throw new IOException("ERROR: Result is empty"); + } + } + } + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestClassScope.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestClassScope.java new file mode 100644 index 00000000..5ad15f5d --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestClassScope.java @@ -0,0 +1,43 @@ +package io.crate.example.testing; + +import io.crate.example.testing.utils.TestingHelpers; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.cratedb.CrateDBContainer; + +import java.io.IOException; +import java.sql.SQLException; + +import static io.crate.example.testing.utils.TestingHelpers.assertResults; + + +/** + * Class-scoped testcontainer instance with JUnit 4 @Rule/@ClassRule integration. + *

+ * In case you can't use the URL support, or need to fine-tune the container, you can instantiate it yourself. + * Note that if you use @Rule, you will be given an isolated container for each test method. + * If you use @ClassRule, you will get on isolated container for all the methods in the test class. + *

+ *

+ * + * + *

+ */ +public class TestClassScope { + + @ClassRule + public static CrateDBContainer cratedb = new CrateDBContainer(TestingHelpers.nameFromLabel("5.2")); + + @Test + public void testReadSummits() throws SQLException, IOException { + + // Get JDBC URL to CrateDB instance. + String connectionUrl = cratedb.getJdbcUrl(); + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example test. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestFunctionScope.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestFunctionScope.java new file mode 100644 index 00000000..59894738 --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestFunctionScope.java @@ -0,0 +1,43 @@ +package io.crate.example.testing; + +import io.crate.example.testing.utils.TestingHelpers; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.cratedb.CrateDBContainer; + +import java.io.IOException; +import java.sql.SQLException; + +import static io.crate.example.testing.utils.TestingHelpers.assertResults; + + +/** + * Function-scoped testcontainer instance with JUnit 4 @Rule/@ClassRule integration. + *

+ * In case you can't use the URL support, or need to fine-tune the container, you can instantiate it yourself. + * Note that if you use @Rule, you will be given an isolated container for each test method. + * If you use @ClassRule, you will get on isolated container for all the methods in the test class. + *

+ *

+ * + * + *

+ */ +public class TestFunctionScope { + + @Rule + public CrateDBContainer cratedb = new CrateDBContainer(TestingHelpers.nameFromLabel("5.2")); + + @Test + public void testReadSummits() throws SQLException, IOException { + + // Get JDBC URL to CrateDB instance. + String connectionUrl = cratedb.getJdbcUrl(); + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example test. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestJdbcUrlScheme.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestJdbcUrlScheme.java new file mode 100644 index 00000000..0b77e7da --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestJdbcUrlScheme.java @@ -0,0 +1,79 @@ +package io.crate.example.testing; + +import org.junit.Test; + +import java.io.IOException; +import java.sql.SQLException; + +import static io.crate.example.testing.Application.printResults; +import static io.crate.example.testing.utils.TestingHelpers.assertResults; + + +/** + * Database containers launched via Testcontainers "TC" JDBC URL scheme. + *

+ * + * + *

+ */ +public class TestJdbcUrlScheme { + + /** + * Launch container with PostgreSQL 15. + */ + @Test + public void testReadSummitsPostgreSQL() throws SQLException, IOException { + String connectionUrl = "jdbc:tc:postgresql:15:///"; + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example query. + Application app = new Application(connectionUrl, "postgres"); + printResults(app.query("SELECT * FROM information_schema.sql_features LIMIT 3;")); + } + + /** + * Launch container with CrateDB 5.2. + */ + @Test + public void testReadSummitsCrateDB() throws SQLException, IOException { + // NOTE: Please note `jdbc:tc:crate` will not work, only `jdbc:tc:cratedb`. + // => `java.lang.UnsupportedOperationException: Database name crate not supported` + String connectionUrl = "jdbc:tc:cratedb:5.2://localhost/doc?user=crate"; + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example query. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + } + + /** + * Launch container with CrateDB 5.2, using daemon mode. + * + */ + @Test + public void testReadSummitsCrateDBWithDaemon() throws SQLException, IOException { + String connectionUrl = "jdbc:tc:cratedb:5.2://localhost/doc?user=crate&TC_DAEMON=true"; + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example query. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + } + + /** + * Launch container with CrateDB 5.2, using "Reusable Containers (Experimental)". + * + */ + @Test + public void testReadSummitsCrateDBWithReuse() throws SQLException, IOException { + String connectionUrl = "jdbc:tc:cratedb:5.2://localhost/doc?user=crate&TC_REUSABLE=true"; + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example query. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestManual.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestManual.java new file mode 100644 index 00000000..10632c39 --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestManual.java @@ -0,0 +1,40 @@ +package io.crate.example.testing; + +import io.crate.example.testing.utils.TestingHelpers; +import org.junit.Test; +import org.testcontainers.cratedb.CrateDBContainer; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.sql.SQLException; + +import static io.crate.example.testing.utils.TestingHelpers.assertResults; + + +/** + * Function-scoped testcontainer instance with manual setup/teardown. + * + */ +public class TestManual { + + @Test + public void testReadSummits() throws SQLException, IOException { + // Run CrateDB nightly. + DockerImageName image = TestingHelpers.nameFromLabel("nightly"); + try (CrateDBContainer cratedb = new CrateDBContainer(image)) { + cratedb.start(); + + // Get JDBC URL to CrateDB instance. + String connectionUrl = cratedb.getJdbcUrl(); + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example test. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + + // Tear down CrateDB. + cratedb.stop(); + } + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestManualWithLegacyCrateJdbcDriver.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestManualWithLegacyCrateJdbcDriver.java new file mode 100644 index 00000000..923bcc90 --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestManualWithLegacyCrateJdbcDriver.java @@ -0,0 +1,125 @@ +package io.crate.example.testing; + +import io.crate.example.testing.utils.TestingHelpers; +import org.junit.Test; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.sql.SQLException; + +import static io.crate.example.testing.utils.TestingHelpers.assertResults; + + +/** + * Function-scoped testcontainer instance with manual setup/teardown, using a custom CrateDBContainer which uses + * the legacy CrateDB JDBC driver. + * + */ +public class TestManualWithLegacyCrateJdbcDriver { + + @Test + public void testReadSummits() throws SQLException, IOException { + // Run CrateDB nightly. + DockerImageName image = TestingHelpers.nameFromLabel("nightly"); + try (CrateDBContainerLegacyJdbcDriver cratedb = new CrateDBContainerLegacyJdbcDriver(image)) { + cratedb.start(); + + // Get JDBC URL to CrateDB instance. + String connectionUrl = cratedb.getJdbcUrl(); + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example test. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + + // Tear down CrateDB. + cratedb.stop(); + } + } + + static class CrateDBContainerLegacyJdbcDriver extends JdbcDatabaseContainer { + + public static final String IMAGE = "crate"; + + protected static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE); + + private String databaseName = "crate"; + + private String username = "crate"; + + private String password = "crate"; + + public CrateDBContainerLegacyJdbcDriver(final DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + this.waitStrategy = Wait.forHttp("/").forPort(4200).forStatusCode(200); + + addExposedPort(5432); + addExposedPort(4200); + } + + @Override + public String getDriverClassName() { + return "io.crate.client.jdbc.CrateDriver"; + } + + @Override + public String getJdbcUrl() { + String additionalUrlParams = constructUrlParameters("?", "&"); + return ("jdbc:crate://" + + getHost() + + ":" + + getMappedPort(5432) + + "/" + + databaseName + + additionalUrlParams); + } + + @Override + public String getDatabaseName() { + return databaseName; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getTestQueryString() { + return "SELECT 1"; + } + + @Override + public CrateDBContainerLegacyJdbcDriver withDatabaseName(final String databaseName) { + this.databaseName = databaseName; + return self(); + } + + @Override + public CrateDBContainerLegacyJdbcDriver withUsername(final String username) { + this.username = username; + return self(); + } + + @Override + public CrateDBContainerLegacyJdbcDriver withPassword(final String password) { + this.password = password; + return self(); + } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestSharedSingleton.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestSharedSingleton.java new file mode 100644 index 00000000..2b36001f --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestSharedSingleton.java @@ -0,0 +1,50 @@ +package io.crate.example.testing; + +import io.crate.example.testing.utils.TestingHelpers; +import org.junit.Before; +import org.junit.Test; +import org.testcontainers.cratedb.CrateDBContainer; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.sql.SQLException; + +import static io.crate.example.testing.utils.TestingHelpers.assertResults; + + +/** + * Testcontainer instance shared across multiple test classes, implemented using the Singleton pattern. + * This test case uses the CrateDB nightly release. + *

+ * Sometimes it might be useful to define a container that is only started + * once for several test classes. There is no special support for this use + * case provided by the Testcontainers extension. Instead, this can be + * implemented using the Singleton pattern. + *

+ *
+ */ +public class TestSharedSingleton { + + private CrateDBContainer cratedb; + + @Before + public void startContainer() { + // Run CrateDB latest. + DockerImageName image = TestingHelpers.nameFromLabel("latest"); + cratedb = new CrateDBContainer(image); + cratedb.start(); + } + + @Test + public void testReadSummits() throws SQLException, IOException { + + // Get JDBC URL to CrateDB instance. + String connectionUrl = cratedb.getJdbcUrl(); + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example test. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestSharedSingletonEnvironmentVersion.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestSharedSingletonEnvironmentVersion.java new file mode 100644 index 00000000..648d4e76 --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/TestSharedSingletonEnvironmentVersion.java @@ -0,0 +1,50 @@ +package io.crate.example.testing; + +import io.crate.example.testing.utils.TestingHelpers; +import org.junit.Before; +import org.junit.Test; +import org.testcontainers.cratedb.CrateDBContainer; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.sql.SQLException; + +import static io.crate.example.testing.utils.TestingHelpers.assertResults; + + +/** + * Testcontainer instance honoring the `CRATEDB_VERSION` environment variable, + * suitable for running a test matrix on different versions of CrateDB. + *

+ * Possible values are: + *

+ *

+ */ +public class TestSharedSingletonEnvironmentVersion { + + private CrateDBContainer cratedb; + + @Before + public void startContainer() { + // Run designated CrateDB version. + DockerImageName image = TestingHelpers.nameFromEnvironment(); + cratedb = new CrateDBContainer(image); + cratedb.start(); + } + + @Test + public void testReadSummits() throws SQLException, IOException { + // Get JDBC URL to CrateDB instance. + String connectionUrl = cratedb.getJdbcUrl(); + System.out.printf("Connecting to %s%n", connectionUrl); + + // Invoke example test. + Application app = new Application(connectionUrl); + var results = app.querySummitsTable(); + assertResults(results); + } +} diff --git a/testing/testcontainers/java/src/test/java/io/crate/example/testing/utils/TestingHelpers.java b/testing/testcontainers/java/src/test/java/io/crate/example/testing/utils/TestingHelpers.java new file mode 100644 index 00000000..2afdb154 --- /dev/null +++ b/testing/testcontainers/java/src/test/java/io/crate/example/testing/utils/TestingHelpers.java @@ -0,0 +1,39 @@ +package io.crate.example.testing.utils; + +import io.crate.example.testing.Application; +import org.testcontainers.utility.DockerImageName; + +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class TestingHelpers { + + public static DockerImageName nameFromLabel(String label) { + String fullImageName; + if (label == null) { + fullImageName = "crate:latest"; + } else { + if (label.equals("nightly")) { + fullImageName = "crate/crate:nightly"; + } else { + fullImageName = String.format("crate:%s", label); + } + } + return DockerImageName.parse(fullImageName).asCompatibleSubstituteFor("crate"); + } + + public static DockerImageName nameFromEnvironment() { + String label = System.getenv("CRATEDB_VERSION"); + return nameFromLabel(label); + } + + public static void assertResults(Application.Results results) throws SQLException { + assertThat(results.metaData().getColumnCount()).isEqualTo(9); + assertThat(results.rows()).hasSize(3); + assertThat(results.rows().stream().map(r -> r[6]).toList()).containsExactly( + "Mont Blanc", + "Monte Rosa", + "Dom"); + } +}