Skip to content

Commit

Permalink
Implement test resources property provider factory (#231)
Browse files Browse the repository at this point in the history
* Implement test resources property provider factory

This commit introduces a new annotation, `@TestResourceProperties`, which
can be applied on a test in order to tell that the test needs to resolve
one or more test resource properties _before_ the application context is
available.

For example, with:

```java
@MicronautTest
@TestResourcesProperties(
    value = "redis.url"
)
class SomeTest { ... }
```

Then the value of the `redis.url` property will automatically be
fetched from the test resources service and made available as if
it had been done in a `TestPropertyProvider`. The difference is
that this without this annotation, it was required to call the
test resources client directly in the provider, which was error
prone. In particular, the `properties` argument of the `resolve`
call are not easy to figure out.

This annotation is therefore a convention to avoid having to
call the client directly. However, in some cases it might be
necessary to read a property in order to compute a different
one which needs to be available in the test.

This can be done by adding a `providers` argument to the
annotation:

```java
@TestResourcesProperties(
    value = "rabbitmq.uri",
    providers = RabbitTest.RabbitMQProvider.class
)
class RabbitTest {
  // ...
      @ReflectiveAccess
      public static class RabbitMQProvider implements TestResourcesPropertyProvider {
          @OverRide
          public Map<String, String> provide(Map<String, Object> testProperties) {
              String uri = (String) testProperties.get("rabbitmq.uri");
              return Map.of(
                  "rabbitmq.servers.product-cluster.port", String.valueOf(URI.create(uri).getPort())
              );
          }
      }
}
```

The `TestResourcesPropertyProvider` type has access to all properties
which are already resolved at the moment it is called, which includes
the properties asked in `@TestResourcesProperties`.

This should make the implementation of micronaut-projects/micronaut-nats#321
easier.

* Remove accidentally added files

* Add javadocs

* Add missing test

* Make spotless happy

* Add nullability annotations

* Fix checkstyle and japicmp
  • Loading branch information
melix authored May 3, 2023
1 parent b785dce commit fd82992
Show file tree
Hide file tree
Showing 19 changed files with 674 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id 'io.micronaut.build.internal.test-resources-simple-module'
id 'io.micronaut.build.internal.test-fixtures'
}

components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }

micronautBuild {
def (String major, String minor, String patch) = (version - '-SNAPSHOT').split("[.]")
binaryCompatibility.enabled = major.toInteger() >=2 && minor.toInteger() >= 0 && patch.toInteger() > 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Use this plugin for modules which are not Micronaut modules
* but follow the same conventions (e.g need publishing, binary
* compatibilty, etc...)
*/
plugins {
id 'io.micronaut.build.internal.test-resources-base'
id 'io.micronaut.build.internal.base-module'
}
6 changes: 5 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ micronaut-redis = "6.0.0-M2"
micronaut-security = "4.0.0-M2"
micronaut-serde = "2.0.0-M3"
micronaut-sql = "5.0.0-M3"
micronaut-test = "4.0.0-M2"
micronaut-test = "4.0.0-M3"
micronaut-gradle-plugin = "4.0.0-M1"
groovy = "4.0.11"
spock = "2.3-groovy-4.0"
Expand All @@ -51,6 +51,10 @@ managed-testcontainers = "1.17.6"
[libraries]
boms-testcontainers = { module = "org.testcontainers:testcontainers-bom", version.ref = "managed-testcontainers" }

junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher"}

micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "micronaut-aws" }
micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version.ref = "micronaut-data" }
micronaut-discovery = { module = "io.micronaut.discovery:micronaut-discovery-client", version.ref = "micronaut-discovery" }
Expand Down
16 changes: 16 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ plugins {
}
rootProject.name = 'testresources-parent'

enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

// Extensions as in "Micronaut Test extensions"
// This name was chosen to reduce the naming overhead
// e.g micronaut-test-resources-micronaut-test-extensions-core
// is just micronaut-test-resources-extensions-core
def extensionModules = [
'core'
]

def jdbcModules = [
'core',
'mysql',
Expand Down Expand Up @@ -52,6 +62,7 @@ include 'test-resources-client'
include 'test-resources-elasticsearch'
include 'test-resources-embedded'
include 'test-resources-hivemq'
include 'test-resources-junit5'
include 'test-resources-kafka'
include 'test-resources-mongodb'
include 'test-resources-neo4j'
Expand All @@ -61,6 +72,11 @@ include 'test-resources-server'
include 'test-resources-testcontainers'
include 'test-resources-hashicorp-vault'

extensionModules.each {
String projectName = "test-resources-extensions-$it"
include projectName
project(":test-resources-extensions-$it").projectDir = file("test-resources-extensions/$projectName")
}
jdbcModules.each {
String projectName = "test-resources-jdbc-$it"
include projectName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id 'io.micronaut.build.internal.test-resources-mntest-extension'
}

description = """
Provides Micronaut Test extensions which make it easier to integrate
with Test Resources.
"""

dependencies {
// Sync'es the version of JUnit 5 used by Micronaut Test
api(platform(mnTest.micronaut.test.bom))
api(mnTest.micronaut.test.core)
implementation(projects.micronautTestResourcesClient)

testAnnotationProcessor(mn.micronaut.inject.java)
testImplementation(mnTest.micronaut.test.junit5)
testFixturesAnnotationProcessor(mn.micronaut.inject.java)
testFixturesApi(libs.junit.platform.launcher)
testFixturesApi(platform(mnTest.micronaut.test.bom))
testFixturesImplementation(projects.micronautTestResourcesClient)
testRuntimeOnly(libs.junit.jupiter.engine)
testRuntimeOnly(mn.micronaut.context)
testRuntimeOnly(mn.snakeyaml)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2017-2021 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.
*/
package io.micronaut.test.extensions.testresources;

import io.micronaut.core.annotation.Internal;
import io.micronaut.testresources.client.TestResourcesClient;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

/**
* An internal class which can be used to inject a fake
* test resources client, for testing purposes.
*/
@Internal
public final class TestResourcesClientHolder {
private static TestResourcesClient CLIENT;

private TestResourcesClientHolder() {

}

public static void set(TestResourcesClient client) {
CLIENT = client;
}

public static TestResourcesClient get() {
return CLIENT;
}

public static TestResourcesClient lazy() {
return new LazyTestResourcesClient();
}

private static class LazyTestResourcesClient implements TestResourcesClient {

private static <T> T nullSafe(Supplier<T> value) {
if (CLIENT == null) {
return null;
}
return value.get();
}

@Override
public List<String> getResolvableProperties(Map<String, Collection<String>> propertyEntries, Map<String, Object> testResourcesConfig) {
return nullSafe(CLIENT::getResolvableProperties);
}

@Override
public Optional<String> resolve(String name, Map<String, Object> properties, Map<String, Object> testResourcesConfiguration) {
return nullSafe(() -> CLIENT.resolve(name, properties, testResourcesConfiguration));
}

@Override
public List<String> getRequiredProperties(String expression) {
return nullSafe(() -> CLIENT.getRequiredProperties(expression));
}

@Override
public List<String> getRequiredPropertyEntries() {
return nullSafe(CLIENT::getRequiredPropertyEntries);
}

@Override
public boolean closeAll() {
return nullSafe(CLIENT::closeAll);
}

@Override
public boolean closeScope(String id) {
return nullSafe(() -> CLIENT.closeScope(id));
}

@Override
public List<String> getResolvableProperties() {
return nullSafe(CLIENT::getResolvableProperties);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2017-2021 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.
*/
package io.micronaut.test.extensions.testresources;

import io.micronaut.test.extensions.testresources.annotation.TestResourcesProperties;
import io.micronaut.test.support.TestPropertyProvider;
import io.micronaut.test.support.TestPropertyProviderFactory;
import io.micronaut.testresources.client.TestResourcesClientFactory;

import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TestResourcesPropertiesFactory implements TestPropertyProviderFactory {
@Override
public TestPropertyProvider create(Map<String, Object> properties, Class<?> testClass) {
return new TestResourcesTestPropertyProvider(testClass, properties);

}

private static TestResourcesPropertyProvider instantitateProvider(Class<? extends TestResourcesPropertyProvider> provider) {
try {
return provider.getDeclaredConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException("Test resources property provider must have a public constructor without arguments", e);
}
}

private static class TestResourcesTestPropertyProvider implements TestPropertyProvider {
public static final String TEST_RESOURCES_PROPERTY_PREFIX = "test-resources.";
private final Class<?> testClass;
private final Map<String, Object> properties;

public TestResourcesTestPropertyProvider(Class<?> testClass, Map<String, Object> properties) {
this.testClass = testClass;
this.properties = properties;
}

@Override
public Map<String, String> getProperties() {
TestResourcesProperties annotation = testClass.getAnnotation(TestResourcesProperties.class);
if (annotation != null) {
String[] requestedProperties = annotation.value();
var client = TestResourcesClientFactory.fromSystemProperties()
.orElse(TestResourcesClientHolder.lazy());
var testResourcesConfig = properties.entrySet()
.stream()
.filter(e -> e.getKey().startsWith(TEST_RESOURCES_PROPERTY_PREFIX))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Map<String, String> resolvedProperties = Stream.of(requestedProperties)
.map(v -> new Object() {
private final String key = v;
private final String value = client.resolve(v, Map.of(), testResourcesConfig).orElse(null);
})
.filter(o -> o.value != null)
.collect(Collectors.toMap(e -> e.key, e -> e.value));

// Result represents what properties we're going to expose to tests
Map<String, String> result = new HashMap<>(resolvedProperties);
// Context represents what is available to resolvers for them to
// compute results
Map<String, Object> context = new HashMap<>(properties);
context.putAll(resolvedProperties);
result.putAll(resolvedProperties);
Class<? extends TestResourcesPropertyProvider>[] providers = annotation.providers();
for (Class<? extends TestResourcesPropertyProvider> provider : providers) {
var testResourcesPropertyProvider = instantitateProvider(provider);
Map<String, String> map = testResourcesPropertyProvider.provide(Collections.unmodifiableMap(context));
context.putAll(map);
result.putAll(map);
}
return result;
}
return Map.of();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2017-2021 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.
*/
package io.micronaut.test.extensions.testresources;

import io.micronaut.core.annotation.NonNull;

import java.util.Map;

/**
* A test resources property provider is a type which
* must be explicitly declared in via the {@link io.micronaut.test.extensions.testresources.annotation.TestResourcesProperties}
* annotation.
* <p/>
* It is responsible for supplying additional test properties,
* given the set of properties which are available before the
* application context is started.
* <p/>
* It can be used, in particular, to derive new properties
* from other properties resolved by the test resources client.
* <p/>
* This works in a very similar way as {@link io.micronaut.test.support.TestPropertyProvider},
* but has access to other properties in order to perform
* computation based on the value of these properties.
*/
@FunctionalInterface
public interface TestResourcesPropertyProvider {
/**
* Returns a map of properties which need to be exposed
* to the application context, given the map of properties
* which are already available during setup.
*
* These properties typically include the properties
* visible in the configuration files which do not require
* access to test resources.
*
* @param testProperties the set of properties available
* @return a map of properties to be added
*/
@NonNull
Map<String, String> provide(@NonNull Map<String, Object> testProperties);
}
Loading

0 comments on commit fd82992

Please sign in to comment.