diff --git a/build.sbt b/build.sbt
index e2e7600bc881b0dd50aebf685b2d5fbb90450811..13a20cca573a7a4da9c725045f83be66ee9a7f82 100644
--- a/build.sbt
+++ b/build.sbt
@@ -34,7 +34,7 @@ lazy val generator = (project in file("."))
       "org.junit.vintage" % "junit-vintage-engine" % "4.12.0" % "test",
       "org.assertj" % "assertj-core" % "3.12.2" % "test",
 
-      "com.novocode" % "junit-interface" % "0.11" % "test"
+      "net.aichler" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % Test
     ),
     scalacOptions ++= Seq(
       "-language:implicitConversions"
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 45a6f02d1880ea4c9bff889a194ab87adf467611..4a65abadaf7e792155072db22cce49a4abbf4d54 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,2 +1,6 @@
+
+resolvers += Resolver.jcenterRepo
+
+addSbtPlugin("net.aichler" % "sbt-jupiter-interface" % "0.8.2")
 addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4")
 addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0")
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e253dafd1feee21a329b2a2f65b4cb47488627f
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java
@@ -0,0 +1,20 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.List;
+import javax.annotation.Nonnull;
+
+/**
+ * A {@code CompoundKeepExpression} is a special kind of {@code KeepExpression} which in turn may
+ * contain a number of other {@code KeepExpression}s.
+ *
+ * @author Rico Bergmann
+ */
+public abstract class CompoundKeepExpression extends KeepExpression {
+
+  /**
+   * Provides all the keep expressions that this expression is build of.
+   */
+  @Nonnull
+  public abstract List<KeepExpression> getKeeps();
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java
index b111ca3e3bead3cc6299424bd930c31a74dc841b..5d6f0f32924f2f495b21b9ab3107dcbf15c044b2 100644
--- a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java
@@ -28,7 +28,7 @@ import org.rosi_project.model_sync.model_join.representation.util.Assert;
  *
  * @author Rico Bergmann
  */
-public class KeepReferenceExpression extends KeepExpression {
+public class KeepReferenceExpression extends CompoundKeepExpression {
 
   /**
    * The {@code ReferenceDirection} defines whether a reference is owned by the containing model
@@ -188,6 +188,7 @@ public class KeepReferenceExpression extends KeepExpression {
    * the referenced instances.
    */
   @Nonnull
+  @Override
   public List<KeepExpression> getKeeps() {
     return new ArrayList<>(keeps);
   }
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java
index fd732649f7fdb5337a49007a18edea44d04f3947..76e8d6027462a00d341906020ca57c638b1ed074 100644
--- a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java
@@ -17,7 +17,7 @@ import org.rosi_project.model_sync.model_join.representation.util.Assert;
  *
  * @author Rico Bergmann
  */
-public class KeepSubTypeExpression extends KeepExpression {
+public class KeepSubTypeExpression extends CompoundKeepExpression {
 
   /**
    * The {@code KeepSubTypeBuilder} enables the construction of new {@code KeepSubTypeExpression}
@@ -132,6 +132,7 @@ public class KeepSubTypeExpression extends KeepExpression {
    * the instances of the subclass instances.
    */
   @Nonnull
+  @Override
   public List<KeepExpression> getKeeps() {
     return new ArrayList<>(keeps);
   }
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java
index 703adefd8e24e401f39d7d68c20fa8d1449150ab..6c72a3d9a42fdef32eafe621ceba232af271b803 100644
--- a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java
@@ -16,7 +16,7 @@ import org.rosi_project.model_sync.model_join.representation.util.Assert;
  *
  * @author Rico Bergmann
  */
-public class KeepSuperTypeExpression extends KeepExpression {
+public class KeepSuperTypeExpression extends CompoundKeepExpression {
 
   /**
    * The {@code KeepSuperTypeBuilder} enables the construction of new {@code KeepSuperTypeExpression}
@@ -131,6 +131,7 @@ public class KeepSuperTypeExpression extends KeepExpression {
    * the instances of the superclass instances.
    */
   @Nonnull
+  @Override
   public List<KeepExpression> getKeeps() {
     return new ArrayList<>(keeps);
   }
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..79cc9ee12917ec4384333b595f7656cf172f0981
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java
@@ -0,0 +1,34 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+
+class FileBasedModelJoinWriter implements ModelJoinWriter<Boolean> {
+
+  private final File outputFile;
+  private final StringBasedModelJoinWriter toStringWriter;
+
+  FileBasedModelJoinWriter(File outputFile) {
+    this.outputFile = outputFile;
+    this.toStringWriter = StringBasedModelJoinWriter.withNewlines();
+  }
+
+  @Override
+  public Boolean write(ModelJoinExpression modelJoin) {
+    try (BufferedWriter writer = Files.newBufferedWriter(
+        outputFile.toPath(),
+        StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+
+      String modelJoinTextRepresentation = toStringWriter.write(modelJoin);
+      writer.write(modelJoinTextRepresentation);
+
+      return true;
+    } catch (IOException ioe) {
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriterFactory.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..0fe9dff02df00c925616403303f8dccb5fd75174
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriterFactory.java
@@ -0,0 +1,16 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import java.io.File;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+
+public class ModelJoinWriterFactory {
+
+  public static String writeToString(ModelJoinExpression modelJoin) {
+    return StringBasedModelJoinWriter.createDefault().write(modelJoin);
+  }
+
+  public static boolean writeToFile(File outputFile, ModelJoinExpression modelJoin) {
+    return new FileBasedModelJoinWriter(outputFile).write(modelJoin);
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java
index aca5a8a5869661ac49cc4c8a766d2d552331a7b3..15870e0275f2a39e95390e77f51882f2c309d2cb 100644
--- a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java
@@ -2,6 +2,7 @@ package org.rosi_project.model_sync.model_join.representation.writer;
 
 import java.util.StringJoiner;
 import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.grammar.CompoundKeepExpression;
 import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression;
 import org.rosi_project.model_sync.model_join.representation.grammar.KeepAggregateExpression;
 import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression;
@@ -17,11 +18,37 @@ import org.rosi_project.model_sync.model_join.representation.grammar.ThetaJoinEx
 import org.rosi_project.model_sync.model_join.representation.util.Functional;
 import org.rosi_project.model_sync.model_join.representation.util.Nothing;
 
-public class StringBasedModelJoinWriter implements ModelJoinWriter<String> {
+class StringBasedModelJoinWriter implements ModelJoinWriter<String> {
+
+  private static final String NEWLINE_DELIM = "\n";
+  private static final String EMPTY_DELIM = " ";
+  private static final String DEFAULT_INDENTATION = "  ";
+
+  public static StringBasedModelJoinWriter createDefault() {
+    return new StringBasedModelJoinWriter(false);
+  }
+
+  public static StringBasedModelJoinWriter withNewlines() {
+    return new StringBasedModelJoinWriter(true);
+  }
+
+  private final String delimiter;
+  private final boolean useNewlineDelimiters;
+
+  private String currentIndentation = "";
+
+  private StringBasedModelJoinWriter(boolean useNewlinesDelimiters) {
+    if (useNewlinesDelimiters) {
+      this.delimiter = NEWLINE_DELIM;
+    } else {
+      this.delimiter = EMPTY_DELIM;
+    }
+    this.useNewlineDelimiters = useNewlinesDelimiters;
+  }
 
   @Override
   public String write(@Nonnull ModelJoinExpression modelJoin) {
-    StringJoiner joiner = new StringJoiner("\n\n");
+    StringJoiner joiner = new StringJoiner(delimiter + delimiter);
 
     modelJoin.getJoins().forEach(
         joinStatement -> joiner.merge(writeJoin(joinStatement))
@@ -47,7 +74,7 @@ public class StringBasedModelJoinWriter implements ModelJoinWriter<String> {
           }
         })
         .runOrThrow();
-    joiner.add(String.format("%s %s with %s as %s", joinHeader, join.getLeft(), join.getRight(), join.getTarget()));
+    joiner.add(String.format(currentIndentation + "%s %s with %s as %s", joinHeader, join.getLeft(), join.getRight(), join.getTarget()));
     Functional.<Nothing>match(join)
         .caseOf(ThetaJoinExpression.class, thetaJoin -> {
           joiner.add("where").add(thetaJoin.getCondition().toString());
@@ -57,8 +84,10 @@ public class StringBasedModelJoinWriter implements ModelJoinWriter<String> {
         .runOrThrow();
 
     joiner.add("{\n");
-    join.getKeeps().forEach(keep -> joiner.add(writeKeep(keep) + "\n"));
-    joiner.add("}");
+    increaseIndentationIfNecessary();
+    join.getKeeps().forEach(keep -> joiner.add(currentIndentation + writeKeep(keep) + delimiter));
+    decreaseIndentationIfNecessary();
+    joiner.add(currentIndentation + "}");
 
     return joiner;
   }
@@ -81,47 +110,45 @@ public class StringBasedModelJoinWriter implements ModelJoinWriter<String> {
                 keepAggregate.getAggregatedAttribute(),
                 keepAggregate.getSource(), keepAggregate.getTarget())
             )
-        .caseOf(KeepReferenceExpression.class, this::writeKeepReference)
-        .caseOf(KeepSubTypeExpression.class, this::writeKeepSubType)
-        .caseOf(KeepSuperTypeExpression.class, this::writeKeepSuperType)
+        .caseOf(KeepReferenceExpression.class, keepRef -> {
+          String header = String.format("keep %s %s as type %s {" + delimiter, keepRef.getReferenceDirection().name().toLowerCase(), keepRef.getAttribute(), keepRef.getTarget());
+          return writeCompound(keepRef, header);
+        })
+        .caseOf(KeepSubTypeExpression.class, keepSubtype -> {
+          String header = String.format("keep subtype %s as type %s {" + delimiter, keepSubtype.getType(), keepSubtype.getTarget());
+          return writeCompound(keepSubtype, header);
+        })
+        .caseOf(KeepSuperTypeExpression.class, keepSupertype -> {
+          String header = String.format("keep supertype %s as type %s {" + delimiter, keepSupertype.getType(), keepSupertype.getTarget());
+          return writeCompound(keepSupertype, header);
+        })
         .runOrThrow();
 
   }
 
-  private String writeKeepReference(KeepReferenceExpression keepRef) {
+  private String writeCompound(CompoundKeepExpression compoundKeep, String header) {
     StringJoiner refJoiner = new StringJoiner(" ");
 
-    String header = String.format("keep %s %s as type %s {\n", keepRef.getReferenceDirection().name().toLowerCase(), keepRef.getAttribute(), keepRef.getTarget());
-
     refJoiner.add(header);
-    keepRef.getKeeps().forEach(keep -> refJoiner.add(writeKeep(keep) + "\n"));
-    refJoiner.add("}");
+    increaseIndentationIfNecessary();
+    compoundKeep.getKeeps().forEach(keep -> refJoiner.add(currentIndentation + writeKeep(keep) + delimiter));
+    decreaseIndentationIfNecessary();
+    refJoiner.add(currentIndentation + "}");
 
     return refJoiner.toString();
   }
 
-  private String writeKeepSubType(KeepSubTypeExpression keepSubType) {
-    StringJoiner refJoiner = new StringJoiner(" ");
-
-    String header = String.format("keep subtype %s as type %s {\n", keepSubType.getType(), keepSubType.getTarget());
-
-    refJoiner.add(header);
-    keepSubType.getKeeps().forEach(keep -> refJoiner.add(writeKeep(keep) + "\n"));
-    refJoiner.add("}");
-
-    return refJoiner.toString();
+  private void increaseIndentationIfNecessary() {
+    if (useNewlineDelimiters) {
+      currentIndentation += DEFAULT_INDENTATION;
+    }
   }
 
-  private String writeKeepSuperType(KeepSuperTypeExpression keepSuperType) {
-    StringJoiner refJoiner = new StringJoiner(" ");
-
-    String header = String.format("keep supertype %s as type %s {\n", keepSuperType.getType(), keepSuperType.getTarget());
-
-    refJoiner.add(header);
-    keepSuperType.getKeeps().forEach(keep -> refJoiner.add(writeKeep(keep) + "\n"));
-    refJoiner.add("}");
-
-    return refJoiner.toString();
+  private  void decreaseIndentationIfNecessary() {
+    if (useNewlineDelimiters) {
+      int deletionIdx = currentIndentation.lastIndexOf(DEFAULT_INDENTATION);
+      currentIndentation = currentIndentation.substring(0, deletionIdx);
+    }
   }
 
 }
diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..115556369bdefdd7850579d6c81c04ceb348390f
--- /dev/null
+++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java
@@ -0,0 +1,64 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.rosi_project.model_sync.model_join.representation.writer.TestedModels.Listing2;
+
+class FileBasedModelJoinWriterAcceptanceTests {
+
+  private static final Logger log = Logger.getLogger(FileBasedModelJoinWriterAcceptanceTests.class.getSimpleName());
+
+  private List<File> createdFiles = new ArrayList<>();
+
+  @AfterEach
+  void tearDown() {
+    createdFiles.forEach(file -> {
+      boolean result = file.delete();
+      log.info(String.format("Deleting file %s, successfull=%s", file, result));
+    });
+    createdFiles.clear();
+  }
+
+  /**
+   * Depends on {@link StringBasedModelJoinWriterAcceptanceTests#writeProducesListing2FromTechnicalReport()}
+   */
+  @Test
+  void writeProducesListing2FromTechnicalReport() {
+    File outputFile = new File("listing2.modeljoin");
+    registerCreatedFile(outputFile);
+
+    FileBasedModelJoinWriter writer = new FileBasedModelJoinWriter(outputFile);
+    writer.write(Listing2.asModelJoin);
+
+    logFileContent(outputFile);
+
+    String expectedContent = StringBasedModelJoinWriter.withNewlines().write(Listing2.asModelJoin);
+    assertThat(outputFile).exists();
+    assertThat(outputFile).hasContent(expectedContent);
+  }
+
+  private void registerCreatedFile(File newFile) {
+    createdFiles.add(newFile);
+    log.info(String.format("Created new file %s", newFile));
+  }
+
+  private void logFileContent(File file) {
+    try {
+      String contents = Files.lines(file.toPath()).collect(Collectors.joining());
+      log.info(String.format("Content of file %s:", file));
+      log.info(contents);
+    } catch (IOException e) {
+      log.severe(String.format("Could not read file %s", file));
+    }
+  }
+
+}
diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java
index 3136ebe15d60112418cf4ecfd7bf70fd426c730f..716d8d54e9644a7c9eaa2e0f0c6e9e09d8c9e474 100644
--- a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java
+++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java
@@ -1,84 +1,31 @@
 package org.rosi_project.model_sync.model_join.representation.writer;
 
-import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.attributes;
-import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.outgoing;
-import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.supertype;
-import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.thetaJoin;
 import static org.assertj.core.api.Assertions.*;
 
+import java.util.logging.Logger;
 import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
-import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
-import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
-import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
-import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder;
-import org.rosi_project.model_sync.util.NeedsCleanup;
+import org.rosi_project.model_sync.model_join.representation.writer.TestedModels.Listing2;
 
-@RunWith(JUnitPlatform.class)
-@NeedsCleanup
 class StringBasedModelJoinWriterAcceptanceTests {
 
-  /*
-   * The technical report which forms the basis of the tests here is
-   *
-   * Burger, E. et Al.: "ModelJoin - A Textual Domain-Specific Language for the Combination of
-   *    Heterogeneous Models"; Karslruhe Institute of Technololgy 2014
-   */
+  private static final Logger log = Logger.getLogger(StringBasedModelJoinWriterAcceptanceTests.class.getSimpleName());
 
+  /**
+   * @see TestedModels
+   */
   @Test
   void writeProducesListing2FromTechnicalReport() {
+    StringBasedModelJoinWriter writer = StringBasedModelJoinWriter.withNewlines();
+    String writerOutput = writer.write(Listing2.asModelJoin);
 
-    ClassResource imdbFilm = ClassResource.from("imdb", "Film");
-    ClassResource libraryVideoCassette = ClassResource.from("library", "VideoCassette");
-    ClassResource libraryAudioVisualItem = ClassResource.from("library", "AudioVisualItem");
-    ClassResource jointargetMovie = ClassResource.from("jointarget", "Movie");
-    ClassResource jointargetVote = ClassResource.from("jointarget", "Vote");
-    ClassResource jointargetMediaItem = ClassResource.from("jointarget", "MediaItem");
-
-    AttributePath imdbFilmYear = AttributePath.from(imdbFilm, "year");
-    AttributePath imdbFilmVotes = AttributePath.from(imdbFilm, "votes");
-    AttributePath imdbVoteScore = AttributePath.from("imdb.Vote", "score");
-    AttributePath libraryAudioVisualItemMinutesLength = AttributePath.from(libraryAudioVisualItem, "minutesLength");
-
-    ModelJoinExpression listing2 = ModelJoinBuilder.createNewModelJoin()
-        .add(thetaJoin()
-            .join(imdbFilm)
-            .with(libraryVideoCassette)
-            .as(jointargetMovie)
-            .where(
-                "library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.firstname.concat(\" \") .concat(p.lastName) == a.name))")
-            .keep(attributes(imdbFilmYear))
-            .keep(outgoing(imdbFilmVotes)
-                .as(jointargetVote)
-                .keep(attributes(imdbVoteScore))
-                .buildExpression()
-            )
-            .keep(supertype(libraryAudioVisualItem)
-                .as(jointargetMediaItem)
-                .keep(attributes(libraryAudioVisualItemMinutesLength))
-                .buildExpression()
-            )
-            .done())
-        .build();
-
-    StringBasedModelJoinWriter writer = new StringBasedModelJoinWriter();
-    String writerOutput = writer.write(listing2);
+    log.info("Writer output:");
+    log.info(writerOutput);
 
     String sanitizedWriterOutput = sanitize(writerOutput);
 
-    String expectedOutput = sanitize("theta join imdb.Film with library.VideoCassette as jointarget.Movie\n"
-        + "where library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.\n"
-        + "firstname.concat(\" \") .concat(p.lastName) == a.name)) {\n"
-        + "keep attributes imdb.Film.year\n"
-        + "keep outgoing imdb.Film.votes as type jointarget.Vote {\n"
-        + "keep attributes imdb.Vote.score\n"
-        + "}\n"
-        + "keep supertype library.AudioVisualItem as type jointarget.MediaItem {\n"
-        + " keep attributes library.AudioVisualItem.minutesLength\n"
-        + "}\n"
-        + "}");
-
+    String expectedOutput = sanitize(Listing2.asString);
 
     // Although AssertJ provides an isEqualToIgnoringWhitespace as well as an
     // isEqualToIgnoringNewline method, there's none that ignores both. Thus we reside to manual
diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a00cb911e8d353c283070350f6c26dd96f0f310
--- /dev/null
+++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java
@@ -0,0 +1,69 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.attributes;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.outgoing;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.supertype;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.thetaJoin;
+
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder;
+
+class TestedModels {
+
+  /*
+   * The technical report which forms the basis of the tests here is
+   *
+   * Burger, E. et Al.: "ModelJoin - A Textual Domain-Specific Language for the Combination of
+   *    Heterogeneous Models"; Karslruhe Institute of Technololgy 2014
+   */
+
+  static class Listing2 {
+    static final ClassResource imdbFilm = ClassResource.from("imdb", "Film");
+    static final ClassResource libraryVideoCassette = ClassResource.from("library", "VideoCassette");
+    static final ClassResource libraryAudioVisualItem = ClassResource.from("library", "AudioVisualItem");
+    static final ClassResource jointargetMovie = ClassResource.from("jointarget", "Movie");
+    static final ClassResource jointargetVote = ClassResource.from("jointarget", "Vote");
+    static final ClassResource jointargetMediaItem = ClassResource.from("jointarget", "MediaItem");
+
+    static final AttributePath imdbFilmYear = AttributePath.from(imdbFilm, "year");
+    static final AttributePath imdbFilmVotes = AttributePath.from(imdbFilm, "votes");
+    static final AttributePath imdbVoteScore = AttributePath.from("imdb.Vote", "score");
+    static final AttributePath libraryAudioVisualItemMinutesLength = AttributePath.from(libraryAudioVisualItem, "minutesLength");
+
+    static final ModelJoinExpression asModelJoin = ModelJoinBuilder.createNewModelJoin()
+        .add(thetaJoin()
+            .join(imdbFilm)
+            .with(libraryVideoCassette)
+            .as(jointargetMovie)
+            .where(
+                "library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.firstname.concat(\" \") .concat(p.lastName) == a.name))")
+            .keep(attributes(imdbFilmYear))
+            .keep(outgoing(imdbFilmVotes)
+                .as(jointargetVote)
+                .keep(attributes(imdbVoteScore))
+                .buildExpression()
+            )
+            .keep(supertype(libraryAudioVisualItem)
+                .as(jointargetMediaItem)
+                .keep(attributes(libraryAudioVisualItemMinutesLength))
+                .buildExpression()
+            )
+            .done())
+        .build();
+
+    static final String asString = "theta join imdb.Film with library.VideoCassette as jointarget.Movie\n"
+        + "where library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.\n"
+        + "firstname.concat(\" \") .concat(p.lastName) == a.name)) {\n"
+        + "keep attributes imdb.Film.year\n"
+        + "keep outgoing imdb.Film.votes as type jointarget.Vote {\n"
+        + "keep attributes imdb.Vote.score\n"
+        + "}\n"
+        + "keep supertype library.AudioVisualItem as type jointarget.MediaItem {\n"
+        + " keep attributes library.AudioVisualItem.minutesLength\n"
+        + "}\n"
+        + "}";
+  }
+
+}