Skip to content

Commit

Permalink
馃惓 #44 Provide helper utilities for integration tests
Browse files Browse the repository at this point in the history
- Add helper method for registering a test connector using Debezium
Container
- Add lifecycle hook for injecting a command context into subclasses
containing the desired command instance to reduce boilerplate
- Add Quarkus Test Profile
  • Loading branch information
iabudiab authored and gunnarmorling committed Jan 22, 2022
1 parent a9bf989 commit 360f287
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 36 deletions.
24 changes: 22 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,39 @@
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-core</artifactId>
<version>1.6.1.Final</version>
<version>1.8.0.Final</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-testing-testcontainers</artifactId>
<version>1.6.1.Final</version>
<version>1.8.0.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp-urlconnection</artifactId>
<version>4.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>2.8.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -159,6 +178,7 @@
<groups>java.,javax.,org.,com.</groups>
<removeUnused>true</removeUnused>
<staticAfter>true</staticAfter>
<compliance>17</compliance>
<!-- <staticGroups>java.,javax.,org.w3c.,org.xml.,junit.</staticGroups> -->
</configuration>
</plugin>
Expand Down
121 changes: 107 additions & 14 deletions src/test/java/org/kcctl/IntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,32 @@
*/
package org.kcctl;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.util.stream.Stream;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.platform.commons.util.ReflectionUtils;
import org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode;
import org.kcctl.support.InjectCommandContext;
import org.kcctl.support.KcctlCommandContext;
import org.kcctl.util.ConfigurationContext;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.utility.DockerImageName;

import io.debezium.testing.testcontainers.ConnectorConfiguration;
import io.debezium.testing.testcontainers.DebeziumContainer;
import picocli.CommandLine;

@Tag("integration-test")
public abstract class IntegrationTest {

protected static final Network network = Network.newNetwork();
Expand All @@ -45,22 +53,107 @@ public abstract class IntegrationTest {
.withKafka(kafka)
.dependsOn(kafka);

protected ConfigurationContext configurationContext;

@BeforeAll
static void startContainers() {
public static void prepare() {
Startables.deepStart(Stream.of(kafka, kafkaConnect)).join();
}

@BeforeEach
void setupContext(@TempDir File tempDir) throws IOException {
var configFile = tempDir.toPath().resolve(".kcctl");
void injectCommandContext() {
ReflectionUtils.findFields(getClass(), it -> it.isAnnotationPresent(InjectCommandContext.class), HierarchyTraversalMode.TOP_DOWN)
.forEach(field -> {
try {
KcctlCommandContext<?> commandContext = prepareContext(field);
ReflectionUtils.makeAccessible(field);
field.set(this, commandContext);
}
catch (IllegalAccessException e) {
throw new IntegrationTestException("Couldn't inject KcctlCommandContext", e);
}
});
}

@AfterEach
public void cleanup() {
kafkaConnect.deleteAllConnectors();
}

protected void registerTestConnector(String name) {
ConnectorConfiguration config = ConnectorConfiguration.create()
.with("connector.class", "FileStreamSource")
.with("tasks.max", 1)
.with("topic", "local-file-source-test")
.with("file", "/tmp/test");

kafkaConnect.registerConnector(name, config);
}

private KcctlCommandContext<?> prepareContext(Field field) {
Type type = field.getGenericType();
Type genericType = ((ParameterizedType) type).getActualTypeArguments()[0];
Class<?> targetCommand = (Class<?>) genericType;

ensureCaseyCommand(targetCommand);

var context = initializeConfigurationContext();
var command = instantiateCommand(targetCommand, context);
var commandLine = new CommandLine(command);
var output = new StringWriter();
commandLine.setOut(new PrintWriter(output));

return new KcctlCommandContext<>(command, commandLine, output);
}

private void ensureCaseyCommand(Class<?> targetCommand) {
if (!targetCommand.isAnnotationPresent(CommandLine.Command.class)) {
throw new IntegrationTestException("KcctlCommandContext should target a type annotated with @CommandLine.Command");
}
}

private ConfigurationContext initializeConfigurationContext() {
try {
var tempDir = Files.createTempDirectory("kcctl-test");
var configFile = tempDir.resolve(".kcctl");

Files.writeString(configFile, String.format("""
{
"currentContext": "local",
"local": {
"cluster": "%s",
"username": "testuser",
"password": "testpassword"
}
}
""", kafkaConnect.getTarget()));

return new ConfigurationContext(tempDir.toFile());
}
catch (IOException e) {
throw new IntegrationTestException("Couldn't initialize configuration context", e);
}
}

private Object instantiateCommand(Class<?> targetCommand, ConfigurationContext configurationContext) {
try {
Constructor<?> constructor = targetCommand.getDeclaredConstructor(ConfigurationContext.class);
return constructor.newInstance(configurationContext);
}
catch (NoSuchMethodException e) {
throw new IntegrationTestException("Unsupported @CommandLine.Command type. Required a single argument constructor accepting a ConfigurationContext");
}
catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new IntegrationTestException("Couldn't instantiate command of type " + targetCommand, e);
}
}

public static class IntegrationTestException extends RuntimeException {

Files.writeString(configFile, String.format(
"{ \"currentContext\": \"local\", \"local\": { \"cluster\": \"%s\", \"username\": \"testuser\", \"password\": \"testpassword\" }}",
kafkaConnect.getTarget())
);
public IntegrationTestException(String msg) {
super(msg);
}

configurationContext = new ConfigurationContext(tempDir);
public IntegrationTestException(String msg, Throwable cause) {
super(msg, cause);
}
}
}
28 changes: 28 additions & 0 deletions src/test/java/org/kcctl/IntegrationTestProfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 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
*
* http://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.
*/
package org.kcctl;

import java.util.Set;

import io.quarkus.test.junit.QuarkusTestProfile;

public class IntegrationTestProfile implements QuarkusTestProfile {

@Override
public Set<String> tags() {
return Set.of("integration-test");
}
}
34 changes: 14 additions & 20 deletions src/test/java/org/kcctl/command/InfoCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,33 @@
*/
package org.kcctl.command;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.PrintWriter;
import java.io.StringWriter;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.Test;
import org.kcctl.IntegrationTest;
import org.kcctl.support.*;

import io.quarkus.test.junit.QuarkusTest;
import picocli.CommandLine;

import static org.assertj.core.api.Assertions.assertThat;

@QuarkusTest
@DisplayNameGeneration(ReplaceUnderscores.class)
class InfoCommandTest extends IntegrationTest {

InfoCommand infoCommand;

@BeforeEach
public void setup() {
infoCommand = new InfoCommand(configurationContext);
}
@InjectCommandContext
KcctlCommandContext<InfoCommand> context;

@Test
public void should_print_info() {
CommandLine commandLine = new CommandLine(infoCommand);

StringWriter output = new StringWriter();
commandLine.setOut(new PrintWriter(output));

int exitCode = commandLine.execute();
int exitCode = context.commandLine().execute();
assertThat(exitCode).isEqualTo(CommandLine.ExitCode.OK);
assertThat(output.toString()).contains("Version: 2.8.1");
assertThat(context.output().toString().trim().lines())
.map(String::trim)
.contains(
"URL: " + kafkaConnect.getTarget(),
"Version: 2.8.1",
"Commit: 839b886f9b732b15");
}
}
26 changes: 26 additions & 0 deletions src/test/java/org/kcctl/support/InjectCommandContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 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
*
* http://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.
*/
package org.kcctl.support;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectCommandContext {
}
24 changes: 24 additions & 0 deletions src/test/java/org/kcctl/support/KcctlCommandContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 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
*
* http://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.
*/
package org.kcctl.support;

import java.io.StringWriter;

import picocli.CommandLine;

public record KcctlCommandContext<T>(T command, CommandLine commandLine, StringWriter output) {

}

0 comments on commit 360f287

Please sign in to comment.