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