IGNITE-15486 JUnit configuration extension implemented (#329)

diff --git a/modules/configuration/pom.xml b/modules/configuration/pom.xml
index 2381420..eab28ee 100644
--- a/modules/configuration/pom.xml
+++ b/modules/configuration/pom.xml
@@ -67,6 +67,12 @@
             <artifactId>hamcrest-library</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
index ff48983..32cdd14 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
@@ -56,7 +56,7 @@
 /**
  * Class that handles configuration changes, by validating them, passing to storage and listening to storage updates.
  */
-public abstract class ConfigurationChanger {
+public abstract class ConfigurationChanger implements DynamicConfigurationChanger {
     /** Thread pool. */
     private final ForkJoinPool pool = new ForkJoinPool(2);
 
@@ -237,13 +237,8 @@
         }
     }
 
-    /**
-     * Changes the configuration.
-     *
-     * @param source Configuration source to create patch from.
-     * @return Future that is completed on change completion.
-     */
-    public CompletableFuture<Void> change(ConfigurationSource source) {
+    /** {@inheritDoc} */
+    @Override public CompletableFuture<Void> change(ConfigurationSource source) {
         return changeInternally(source);
     }
 
@@ -257,13 +252,8 @@
             roots.changeFuture.completeExceptionally(new NodeStoppingException());
     }
 
-    /**
-     * Get root node by root key. Subject to revisiting.
-     *
-     * @param rootKey Root key.
-     * @return Root node.
-     */
-    public InnerNode getRootNode(RootKey<?, ?> rootKey) {
+    /** {@inheritDoc} */
+    @Override public InnerNode getRootNode(RootKey<?, ?> rootKey) {
         return storageRoots.roots.getRoot(rootKey);
     }
 
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationNode.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationNode.java
index 8025153..1f0b48c 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationNode.java
@@ -46,7 +46,7 @@
     protected final RootKey<?, ?> rootKey;
 
     /** Configuration changer instance to get latest value of the root. */
-    protected final ConfigurationChanger changer;
+    protected final DynamicConfigurationChanger changer;
 
     /**
      * Cached value of current trees root. Useful to determine whether you have the latest configuration value or not.
@@ -70,7 +70,7 @@
      * @param rootKey Root key.
      * @param changer Configuration changer.
      */
-    protected ConfigurationNode(List<String> prefix, String key, RootKey<?, ?> rootKey, ConfigurationChanger changer) {
+    protected ConfigurationNode(List<String> prefix, String key, RootKey<?, ?> rootKey, DynamicConfigurationChanger changer) {
         this.keys = ConfigurationUtil.appendKey(prefix, key);
         this.key = key;
         this.rootKey = rootKey;
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java
index ebe8538..13f73a1 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java
@@ -52,7 +52,7 @@
         List<String> prefix,
         String key,
         RootKey<?, ?> rootKey,
-        ConfigurationChanger changer
+        DynamicConfigurationChanger changer
     ) {
         super(prefix, key, rootKey, changer);
     }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfigurationChanger.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfigurationChanger.java
new file mode 100644
index 0000000..ed3465f
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfigurationChanger.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.internal.configuration;
+
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.InnerNode;
+
+/**
+ * Interface to provide configuration access to up-to-date configuration trees in {@link DynamicConfiguration},
+ * {@link NamedListConfiguration} and {@link DynamicProperty}.
+ */
+public interface DynamicConfigurationChanger {
+    /**
+     * Changes the configuration.
+     *
+     * @param source Configuration source to create patch from.
+     * @return Future that is completed on change completion.
+     */
+    CompletableFuture<Void> change(ConfigurationSource source);
+
+    /**
+     * Get root node by root key.
+     *
+     * @param rootKey Root key.
+     * @return Root node.
+     */
+    InnerNode getRootNode(RootKey<?, ?> rootKey);
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java
index 3203c56..21e76ad 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java
@@ -42,7 +42,7 @@
         List<String> prefix,
         String key,
         RootKey<?, ?> rootKey,
-        ConfigurationChanger changer
+        DynamicConfigurationChanger changer
     ) {
         super(prefix, key, rootKey, changer);
     }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/NamedListConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/NamedListConfiguration.java
index bf7c3b0..46156de 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/NamedListConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/NamedListConfiguration.java
@@ -58,7 +58,7 @@
         List<String> prefix,
         String key,
         RootKey<?, ?> rootKey,
-        ConfigurationChanger changer,
+        DynamicConfigurationChanger changer,
         BiFunction<List<String>, String, T> creator
     ) {
         super(prefix, key, rootKey, changer);
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java
index 2479744..a78026e 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java
@@ -61,8 +61,8 @@
 import org.apache.ignite.configuration.annotation.InternalConfiguration;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.internal.configuration.ConfigurationChanger;
 import org.apache.ignite.internal.configuration.DynamicConfiguration;
+import org.apache.ignite.internal.configuration.DynamicConfigurationChanger;
 import org.apache.ignite.internal.configuration.DynamicProperty;
 import org.apache.ignite.internal.configuration.NamedListConfiguration;
 import org.apache.ignite.internal.configuration.TypeUtils;
@@ -141,7 +141,7 @@
     /** {@link ConstructableTreeNode#copy()} */
     private static final Method COPY;
 
-    /** {@link DynamicConfiguration#DynamicConfiguration(List, String, RootKey, ConfigurationChanger)} */
+    /** {@link DynamicConfiguration#DynamicConfiguration(List, String, RootKey, DynamicConfigurationChanger)} */
     private static final Constructor DYNAMIC_CONFIGURATION_CTOR;
 
     /** {@link DynamicConfiguration#add(ConfigurationProperty)} */
@@ -180,7 +180,7 @@
                 List.class,
                 String.class,
                 RootKey.class,
-                ConfigurationChanger.class
+                DynamicConfigurationChanger.class
             );
 
             DYNAMIC_CONFIGURATION_ADD = DynamicConfiguration.class.getDeclaredMethod(
@@ -231,7 +231,7 @@
      */
     public synchronized DynamicConfiguration<?, ?> instantiateCfg(
         RootKey<?, ?> rootKey,
-        ConfigurationChanger changer
+        DynamicConfigurationChanger changer
     ) {
         SchemaClassesInfo info = schemasInfo.get(rootKey.schemaClass());
 
@@ -242,7 +242,7 @@
                 List.class,
                 String.class,
                 RootKey.class,
-                ConfigurationChanger.class
+                DynamicConfigurationChanger.class
             );
 
             assert constructor.canAccess(null);
@@ -994,7 +994,7 @@
             arg("prefix", List.class),
             arg("key", String.class),
             arg("rootKey", RootKey.class),
-            arg("changer", ConfigurationChanger.class)
+            arg("changer", DynamicConfigurationChanger.class)
         );
 
         BytecodeBlock ctorBody = ctor.getBody()
@@ -1044,7 +1044,7 @@
                         "$new$" + newIdx++,
                         typeFromJavaClassName(fieldInfo.cfgClassName),
                         arg("rootKey", RootKey.class),
-                        arg("changer", ConfigurationChanger.class),
+                        arg("changer", DynamicConfigurationChanger.class),
                         arg("prefix", List.class),
                         arg("key", String.class)
                     );
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
index e093f12..e435eff 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
@@ -54,7 +54,7 @@
 /** */
 public class ConfigurationUtil {
     /** Configuration source that copies values without modifying tham. */
-    private static final ConfigurationSource EMPTY_CFG_SRC = new ConfigurationSource() {};
+    static final ConfigurationSource EMPTY_CFG_SRC = new ConfigurationSource() {};
 
     /**
      * Replaces all {@code .} and {@code \} characters with {@code \.} and {@code \\} respectively.
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/ConfigurationExtension.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/ConfigurationExtension.java
new file mode 100644
index 0000000..6fe1d3c
--- /dev/null
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/ConfigurationExtension.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.internal.configuration.testframework;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Parameter;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigObject;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.internal.configuration.DynamicConfigurationChanger;
+import org.apache.ignite.internal.configuration.RootInnerNode;
+import org.apache.ignite.internal.configuration.SuperRoot;
+import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
+import org.apache.ignite.internal.configuration.hocon.HoconConverter;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.InnerNode;
+import org.apache.ignite.internal.configuration.util.ConfigurationUtil;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * JUnit extension to inject configuration instances into test classes.
+ *
+ * @see InjectConfiguration
+ */
+public class ConfigurationExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
+    /** JUnit namespace for the extension. */
+    private static final Namespace namespace = Namespace.create(ConfigurationExtension.class);
+
+    /** Key to store {@link ConfigurationAsmGenerator} in {@link ExtensionContext.Store}. */
+    private static final Object CGEN_KEY = new Object();
+
+    /** {@inheritDoc} */
+    @Override public void beforeEach(ExtensionContext context) throws Exception {
+        ConfigurationAsmGenerator cgen = new ConfigurationAsmGenerator();
+
+        context.getStore(namespace).put(CGEN_KEY, cgen);
+
+        Object testInstance = context.getRequiredTestInstance();
+
+        for (Field field : getMatchingFields(testInstance.getClass())) {
+            field.setAccessible(true);
+
+            field.set(testInstance, cfgValue(field.getType(), field.getAnnotation(InjectConfiguration.class), cgen));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void afterEach(ExtensionContext context) throws Exception {
+        context.getStore(namespace).remove(CGEN_KEY);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsParameter(
+        ParameterContext parameterContext, ExtensionContext extensionContext
+    ) throws ParameterResolutionException {
+        return parameterContext.isAnnotated(InjectConfiguration.class)
+            && supportType(parameterContext.getParameter().getType());
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object resolveParameter(
+        ParameterContext parameterContext, ExtensionContext extensionContext
+    ) throws ParameterResolutionException {
+        Parameter parameter = parameterContext.getParameter();
+
+        ConfigurationAsmGenerator cgen =
+            extensionContext.getStore(namespace).get(CGEN_KEY, ConfigurationAsmGenerator.class);
+
+        try {
+            return cfgValue(parameter.getType(), parameter.getAnnotation(InjectConfiguration.class), cgen);
+        }
+        catch (ClassNotFoundException classNotFoundException) {
+            throw new ParameterResolutionException(
+                "Cannot find a configuration schema class that matches " + parameter.getType().getCanonicalName(),
+                classNotFoundException
+            );
+        }
+    }
+
+    /**
+     * Instantiates a configuration instance for injection.
+     *
+     * @param type Type of the field or parameter. Class name must end with {@code Configuration}.
+     * @param annotation Annotation present on the field or parameter.
+     * @param cgen Runtime code generator associated with the extension instance.
+     * @return Mock configuration instance.
+     * @throws ClassNotFoundException If corresponding configuration schema class is not found.
+     * @see #supportType(Class)
+     */
+    private static Object cfgValue(
+        Class<?> type,
+        InjectConfiguration annotation,
+        ConfigurationAsmGenerator cgen
+    ) throws ClassNotFoundException {
+        // Trying to find a schema class using configuration naming convention. This code won't work for inner Java
+        // classes, extension is designed to mock actual configurations from public API to configure Ignite components.
+        Class<?> schemaClass = Class.forName(type.getCanonicalName() + "Schema");
+
+        // Internal configuration extensions are not yet supported. This will probably be changed in the future.
+        cgen.compileRootSchema(schemaClass, Map.of());
+
+        // RootKey must be mocked, there's no way to instantiate it using a public constructor.
+        RootKey rootKey = mock(RootKey.class);
+
+        when(rootKey.key()).thenReturn("mock");
+        when(rootKey.type()).thenReturn(LOCAL);
+        when(rootKey.schemaClass()).thenReturn(schemaClass);
+        when(rootKey.internal()).thenReturn(false);
+
+        SuperRoot superRoot = new SuperRoot(s -> new RootInnerNode(rootKey, cgen.instantiateNode(schemaClass)));
+
+        ConfigObject hoconCfg = ConfigFactory.parseString(annotation.value()).root();
+
+        HoconConverter.hoconSource(hoconCfg).descend(superRoot);
+
+        ConfigurationUtil.addDefaults(superRoot);
+
+        // Reference to the super root is required to make DynamicConfigurationChanger#change method atomic.
+        var superRootRef = new AtomicReference<>(superRoot);
+
+        return cgen.instantiateCfg(rootKey, new DynamicConfigurationChanger() {
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<Void> change(ConfigurationSource change) {
+                while (true) {
+                    SuperRoot sr = superRootRef.get();
+
+                    SuperRoot copy = sr.copy();
+
+                    change.descend(copy);
+
+                    if (superRootRef.compareAndSet(sr, copy))
+                        return completedFuture(null);
+                }
+            }
+
+            /** {@inheritDoc} */
+            @Override public InnerNode getRootNode(RootKey<?, ?> rk) {
+                return superRootRef.get().getRoot(rk);
+            }
+        });
+    }
+
+    /**
+     * Looks for the annotated field inside the given test class.
+     *
+     * @return Annotated fields.
+     */
+    private static List<Field> getMatchingFields(Class<?> testClass) {
+        return AnnotationSupport.findAnnotatedFields(
+            testClass,
+            InjectConfiguration.class,
+            field -> supportType(field.getType()),
+            HierarchyTraversalMode.TOP_DOWN
+        );
+    }
+
+    /**
+     * Checks that instance of the given class can be injected by the extension.
+     *
+     * @param type Field or parameter type.
+     * @return {@code true} if value of the given class can be injected.
+     */
+    private static boolean supportType(Class<?> type) {
+        return type.getCanonicalName().endsWith("Configuration");
+    }
+}
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/ConfigurationExtensionTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/ConfigurationExtensionTest.java
new file mode 100644
index 0000000..922b6f8
--- /dev/null
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/ConfigurationExtensionTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.internal.configuration.testframework;
+
+import java.util.concurrent.ExecutionException;
+import org.apache.ignite.internal.configuration.sample.DiscoveryConfiguration;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Test basic scenarios of {@link ConfigurationExtension}.
+ */
+@ExtendWith(ConfigurationExtension.class)
+class ConfigurationExtensionTest {
+    /** Injected field. */
+    @InjectConfiguration
+    private DiscoveryConfiguration fieldCfg;
+
+    /** Test that contains injected parameter. */
+    @Test
+    public void injectConfiguration(
+        @InjectConfiguration("mock.joinTimeout=100") DiscoveryConfiguration paramCfg
+    ) throws ExecutionException, InterruptedException {
+        assertEquals(5000, fieldCfg.joinTimeout().value());
+
+        assertEquals(100, paramCfg.joinTimeout().value());
+
+        paramCfg.change(d -> d.changeJoinTimeout(200));
+
+        assertEquals(200, paramCfg.joinTimeout().value());
+
+        paramCfg.joinTimeout().update(300);
+
+        assertEquals(300, paramCfg.joinTimeout().value());
+    }
+}
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/InjectConfiguration.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/InjectConfiguration.java
new file mode 100644
index 0000000..d339d1e
--- /dev/null
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/testframework/InjectConfiguration.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.internal.configuration.testframework;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.internal.configuration.ConfigurationChanger;
+import org.apache.ignite.internal.configuration.ConfigurationRegistry;
+
+/**
+ * Annotation for injecting configuration instances into tests.
+ * <p/>
+ * This annotation should be used on either fields or method parameters of the {@code *Configuration} type.
+ * <p/>
+ * Injected instance is initialized with values passed in {@link #value()}, with schema defaults where explicit initial
+ * values are not found.
+ * <p/>
+ * Although configuration instance is mutable, there's no {@link ConfigurationRegistry} and {@link ConfigurationChanger}
+ * underneath. Listeners don't work either, be aware of that. Main point of the extension is to provide mocks.
+ *
+ * @see ConfigurationExtension
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface InjectConfiguration {
+    /**
+     * Configuration values to initialize the instance. Has HOCON syntax. Must have a root value {@code mock}.
+     * <p/>
+     * Examples:
+     * <ul>
+     *     <li>{@code mock.timeout=1000}</li>
+     *     <li>{@code mock{cfg1=50, cfg2=90}}</li>
+     * </ul>
+     * <p/>
+     * Uses only default values by default.
+     *
+     * @return Initial configuration values in HOCON format.
+     */
+    String value() default "mock : {}";
+}
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
index 0f61d65..9df981b 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
@@ -36,7 +36,6 @@
 import org.apache.ignite.internal.configuration.SuperRoot;
 import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
 import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
-import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
 import org.apache.ignite.internal.configuration.tree.ConverterToMapVisitor;
 import org.apache.ignite.internal.configuration.tree.InnerNode;
 import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
@@ -52,6 +51,7 @@
 import static org.apache.ignite.internal.configuration.tree.NamedListNode.NAME;
 import static org.apache.ignite.internal.configuration.tree.NamedListNode.ORDER_IDX;
 import static org.apache.ignite.internal.configuration.util.ConfigurationFlattener.createFlattenedUpdatesMap;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.EMPTY_CFG_SRC;
 import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.addDefaults;
 import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.checkConfigurationType;
 import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.collectSchemas;
@@ -642,7 +642,7 @@
         SuperRoot originalSuperRoot = superRoot.copy();
 
         // Make a copy of the root insode of the superRoot. This copy will be used for further patching.
-        superRoot.construct(ParentConfiguration.KEY.key(), new ConfigurationSource() {}, true);
+        superRoot.construct(ParentConfiguration.KEY.key(), EMPTY_CFG_SRC, true);
 
         // Patch root node.
         patch.accept((ParentChange)superRoot.getRoot(ParentConfiguration.KEY));