Skip to content

Instantly share code, notes, and snippets.

@MuellerConstantin
Created November 15, 2019 21:28
Show Gist options
  • Save MuellerConstantin/c0ba4d7f2cc1fe6e5b321e5f226afb73 to your computer and use it in GitHub Desktop.
Save MuellerConstantin/c0ba4d7f2cc1fe6e5b321e5f226afb73 to your computer and use it in GitHub Desktop.
Execution listener for loading initial data into MongoDB database
import org.springframework.test.context.TestExecutionListeners;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Enables {@link MongoData @MongoData} related {@code TestExecutionListener} for supporting
* MongoDB database seeding.
*
* @author 0x1C1B
* @see MongoData
* @see MongoDataTestExecutionListener
*/
@Documented
@Inherited
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@TestExecutionListeners(
value = MongoDataTestExecutionListener.class,
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public @interface EnableMongoData {
}
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* {@link MongoData @MongoData} is used to annotate a test class or test method to configure
* insertion of JSON based documents into a given database during integration tests. The actual
* insertion is performed by the {@link MongoDataTestExecutionListener}, which is
* <i>disabled</i> by default.
*
* <p>
* Each collection requires a separate JSON file and therefore an own {@link MongoData @MongoData}
* annotation for populating data. However it's possible to populate multiple documents of the
* same collection using a single one. Please note: Method-level declarations of the annotation
* overrides class-level declarations.
* </p>
*
* <p>
* Since the release of Java 8, {@code @MongoData} can be used as a
* <em>{@linkplain Repeatable repeatable}</em> annotation. Otherwise,
* {@link MongoDataGroup @MongoDataGroup} can be used as an explicit container for declaring
* multiple instances of {@code @MongoData}.
* </p>
*
* @author 0x1C1B
* @see MongoDataTestExecutionListener
* @see MongoDataGroup
*/
@Documented
@Inherited
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@Repeatable(MongoDataGroup.class)
public @interface MongoData {
/**
* Path to the JSON file to insert.
*
* <p>
* The path will be interpreted as a string and relative to the classpath root, this means
* it is <b>not</b> possible to load external resources. A plain path
* &mdash; for example, {@code "data.json"} &mdash; will be treated as a
* classpath resource that is <em>relative</em> to the package in which the actual test class
* is defined. All other paths, respectively paths starting with a slash, will be treated as an
* <em>absolute</em> classpath resource, for example: {@code "/example/data.json"}.
* </p>
*
* @return Returns the JSON file path as string
*/
String value();
/**
* Collection's name to insert provided records.
*
* <p>
* It's assumed that a collection named like the given one already exists in the used MongoDB
* database instance.
* </p>
*
* @return Returns the collection's name
*/
String collection();
}
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Container annotation that aggregates multiple {@link MongoData @MongoData} annotations.
*
* <p>
* Usage required for Java 7 and below without support for
* <em>{@linkplain Repeatable repeatable}</em> annotation. For higher versions
* this annotation can also be used in conjunction with Java 8's support for repeatable ones,
* where {@link MongoData @MongoData} can simply be declared several times.
* </p>
*
* @author 0x1C1B
* @see MongoData
*/
@Documented
@Inherited
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface MongoDataGroup {
MongoData[] value();
}
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.Document;
import org.bson.codecs.BsonArrayCodec;
import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.DecoderContext;
import org.bson.json.JsonParseException;
import org.bson.json.JsonReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import java.io.File;
import java.io.FileReader;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* {@code TestExecutionListener} that provides support for inserting JSON
* {@link MongoData#value() files} configured via the {@link MongoData @MongoData} annotation.
*
* <p>
* Each time an annotated method is called, the JSON data specified using
* {@link MongoData @MongoData} is inserted before by this class. If an method is called
* without {@link MongoData @MongoData} annotation, the implementation will try to fetch it
* from the class level. If the annotation is also missing at class level, the execution
* listener is skipped.
* </p>
*
* @author 0x1C1B
* @see MongoData
*/
public class MongoDataTestExecutionListener extends DependencyInjectionTestExecutionListener {
private static final Logger log = LoggerFactory.getLogger(MongoDataTestExecutionListener.class);
private MongoOperations mongoOperations;
@Override
protected void injectDependencies(TestContext testContext) {
mongoOperations = testContext.getApplicationContext().getBean(MongoOperations.class);
}
@Override
public void beforeTestMethod(TestContext testContext) {
insertMongoData(testContext);
}
private void insertMongoData(TestContext testContext) {
MongoData[] annotations = testContext.getTestMethod().getAnnotationsByType(MongoData.class);
if (0 == annotations.length) {
annotations = testContext.getTestClass().getAnnotationsByType(MongoData.class);
if (0 == annotations.length) return;
}
Arrays.stream(annotations).forEach(annotation -> insertMongoData(annotation, testContext));
}
private void insertMongoData(MongoData annotation, TestContext testContext) {
try {
File file = new File(testContext.getTestClass().getResource(annotation.value()).toURI());
try (JsonReader reader = new JsonReader(new FileReader(file))) {
switch (reader.readBsonType()) {
case ARRAY: {
BsonArrayCodec arrayCodec = new BsonArrayCodec();
BsonArray bsonArray = arrayCodec.decode(reader, DecoderContext.builder().build());
List<Document> documents = bsonArray.stream()
.map(bsonValue -> Document.parse(bsonValue.asDocument().toJson()))
.collect(Collectors.toList());
mongoOperations.getCollection(annotation.collection()).insertMany(documents);
break;
}
case DOCUMENT: {
BsonDocumentCodec documentCodec = new BsonDocumentCodec();
BsonDocument bsonDocument = documentCodec.decode(reader, DecoderContext.builder().build());
Document document = Document.parse(bsonDocument.toJson());
mongoOperations.getCollection(annotation.collection()).insertOne(document);
break;
}
default: {
throw new JsonParseException("Root element must be either DOCUMENT or ARRAY");
}
}
}
log.info(String.format("Populated JSON source '%s' for collection '%s' successfully",
annotation.value(), annotation.collection()));
} catch (Exception exc) {
log.error(String.format("Populating JSON source '%s' for collection '%s' failed",
annotation.value(), annotation.collection()), exc);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment