diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c7d42cdb85ddc55d98908db8e313e36c47c5e3b5..a4809bd6690274cea3db131f6ed49289898027cb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,10 +12,13 @@ variables:
 #  # Improve performance with overlayfs.
 #  DOCKER_DRIVER: overlay2
   GRADLE_OPTS: "-Dorg.gradle.daemon=false"
-  TEST_REPORTS: "/builds/OpenLicht/eraser/eraser-base/build/reports/tests/test/"
-  TEST_LOG: "/builds/OpenLicht/eraser/eraser-base/logs/eraser-test.log"
-  JACOCO_REPORT: "/builds/OpenLicht/eraser/eraser-base/build/reports/jacoco/test/jacocoTestReport.xml"
-  TESTCONTAINERS_RYUK_DISABLED: "true"
+  TEST_REPORTS: "eraser-base/build/reports/tests/"
+  TEST_LOG: "eraser-base/logs/eraser-test.log"
+  JACOCO_REPORT: "*/build/reports/jacoco/all-tests/jacoco*Report.xml"
+  # settings for influxdb
+  INFLUXDB_DB: "jastaddHistory"
+  INFLUXDB_USER: "root"
+  INFLUXDB_USER_PASSWORD: "root"
 
 before_script:
   - export GRADLE_USER_HOME=`pwd`/.gradle
@@ -29,7 +32,7 @@ build:
   image: openjdk:8
   stage: build
   script:
-    - ./gradlew --console=plain --build-cache assemble
+    - ./gradlew --console=plain assemble
   artifacts:
     paths:
       - "eraser-base/src/gen"
@@ -39,10 +42,15 @@ test:
   tags:
     - docker
   stage: test
+  services:
+    - name: "eclipse-mosquitto:1.6.12"
+      alias: "mqtt"
+    - name: "influxdb:1.8.4"
+      alias: "influx"
   needs:
     - build
   script:
-    - ./gradlew --continue --console=plain --info check jacocoTestReport
+    - ./gradlew --console=plain --info allTests
   artifacts:
     when: always
     paths:
@@ -56,7 +64,6 @@ coverage:
   needs:
     - test
   script:
-#    - ./gradlew --continue --console=plain -x test jacocoTestReport
     - pip install --user untangle
     - python print-coverage.py
   coverage: "/Covered (\\d{1,3}\\.\\d{2}%) of instructions for all projects\\./"
diff --git a/buildSrc/src/main/groovy/eraser.java-common-conventions.gradle b/buildSrc/src/main/groovy/eraser.java-common-conventions.gradle
index 21077d8d8a54b9e6bee47df5b45df1ca96142a1f..35d106e2f655c9e9d6b627913c4e9d68ba82480f 100644
--- a/buildSrc/src/main/groovy/eraser.java-common-conventions.gradle
+++ b/buildSrc/src/main/groovy/eraser.java-common-conventions.gradle
@@ -2,6 +2,7 @@ plugins {
   id 'java'
   id 'idea'
   id 'com.github.ben-manes.versions'
+  id 'jacoco'
 }
 
 repositories {
@@ -17,5 +18,31 @@ dependencies {
 }
 
 tasks.named('test') {
-  useJUnitPlatform()
+  useJUnitPlatform {
+    excludeTags 'mqtt | influx'
+  }
+}
+
+task allTests(type: Test, dependsOn: testClasses) {
+  description = 'Run every test'
+  group = 'verification'
+
+  useJUnitPlatform {
+  }
+}
+
+// extension for all Test tasks similar to https://stackoverflow.com/a/57330075/2493208
+jacocoTestReport {
+    executionData tasks.withType(Test).findAll { it.state.executed }
+    getExecutionData().setFrom(fileTree(buildDir).include("/jacoco/*.exec"))
+
+    reports {
+        xml.enabled true
+        xml.destination(file("${jacoco.reportsDir}/all-tests/jacocoAllTestReport.xml"))
+        html.enabled false
+    }
+}
+
+tasks.withType(Test) {
+    finalizedBy jacocoTestReport
 }
diff --git a/eraser-base/build.gradle b/eraser-base/build.gradle
index 7b5aa8060bbd083c4e05b41fa9bc77a85c6642ff..75c4c27c466bb8f89a14b9174e4e8d29af2dc4dd 100644
--- a/eraser-base/build.gradle
+++ b/eraser-base/build.gradle
@@ -9,7 +9,6 @@ buildscript {
 plugins {
     id 'eraser.java-application-conventions'
     id 'eraser.java-jastadd-conventions'
-    id 'jacoco'
 }
 
 apply plugin: 'jastadd'
@@ -29,31 +28,6 @@ dependencies {
 application {
     mainClass = 'de.tudresden.inf.st.eraser.Main'
 }
-//
-//test {
-//    testLogging {
-//        events "passed", "skipped", "failed"
-//        exceptionFormat "full"
-//    }
-//}
-
-//jacoco {
-//    toolVersion = '0.7.9'
-//    applyTo junitPlatformTest
-//}
-
-jacocoTestReport {
-    reports {
-        xml.enabled true
-        html.enabled false
-    }
-}
-
-//junitPlatformTest {
-//    jacoco {
-//        destinationFile = file("${buildDir}/jacoco/test.exec")
-//    }
-//}
 
 def relastFiles = fileTree('src/main/jastadd/') {
     include '**/*.relast' }.toList().toArray()
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java
index 8d9d5a4c50382b63b0e43324dad3968219903f63..a5647fe4ee23ee8825a17ec5d59de80fb94dd9c7 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java
@@ -3,16 +3,10 @@ package de.tudresden.inf.st.eraser;
 import de.tudresden.inf.st.eraser.jastadd.model.*;
 import de.tudresden.inf.st.eraser.util.TestUtils;
 import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem;
-import org.junit.ClassRule;
 import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
-import org.testcontainers.containers.InfluxDBContainer;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
 
 import java.time.Instant;
 import java.util.ArrayList;
@@ -29,9 +23,19 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
  *
  * @author rschoene - Initial contribution
  */
-@Testcontainers
+@Tag("influx")
 public class InfluxTest {
 
+  public static String getInfluxHost() {
+    if (System.getenv("GITLAB_CI") != null) {
+      // we are in the CI, so use "influx" as host
+      return "influx";
+    } {
+      // else assume a locally running influx container
+      return "localhost";
+    }
+  }
+
   private static final double DELTA = 0.001;
   private final List<DoubleStatePoint> points = new ArrayList<>();
   private static final double firstState = 2.0;
@@ -40,16 +44,6 @@ public class InfluxTest {
 
   private ModelAndItem mai;
 
-  @Container
-  private static final InfluxDBContainer<?> influxDbContainer =
-      new InfluxDBContainer<>(
-          DockerImageName
-              .parse("influxdb")
-              .withTag("1.8.3"))
-          .withDatabase(InfluxRoot.createDefault().getDbName())
-          .withAdmin(InfluxRoot.createDefault().getUser())
-          .withAdminPassword(InfluxRoot.createDefault().getPassword());
-
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   public void oneItem(boolean useStub) {
@@ -176,8 +170,7 @@ public class InfluxTest {
       influxRoot = InfluxRoot.createDefault();
       // use container running influx
       influxRoot.setDbName(InfluxTest.class.getSimpleName());
-      System.out.println("ports: " + influxDbContainer.getPortBindings() + " url: '" + influxDbContainer.getUrl() + "'");
-      influxRoot.setHostByName(influxDbContainer.getUrl().replaceAll("^http://", ""));
+      influxRoot.setHostByName(getInfluxHost());
     }
     mai.model.getRoot().setInfluxRoot(influxRoot);
     assumeTrue(influxRoot.influxAdapter().isConnected());
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java
index cca050f08b6065e208ccd9c6732c03eae251724b..9b6a4ab492036278416ef1d2e2ff8d435a68d39e 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java
@@ -5,12 +5,9 @@ import de.tudresden.inf.st.eraser.util.MqttReceiver;
 import de.tudresden.inf.st.eraser.util.TestUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.wait.strategy.Wait;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -29,9 +26,19 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
  *
  * @author rschoene - Initial contribution
  */
-@Testcontainers
+@Tag("mqtt")
 public class MqttTests {
 
+  public static String getMqttHost() {
+    if (System.getenv("GITLAB_CI") != null) {
+      // we are in the CI, so use "mqtt" as host
+      return "mqtt";
+    } {
+      // else assume a locally running mqtt broker
+      return "localhost";
+    }
+  }
+
   private static final String outgoingPrefix = "out";
   private static final String firstPart = "a";
   private static final String alternativeFirstPart = "x";
@@ -43,10 +50,6 @@ public class MqttTests {
   private final List<String> messages = new ArrayList<>();
   private static final Logger logger = LogManager.getLogger(MqttTests.class);
 
-  @Container
-  public static GenericContainer<?> mqttBroker = new GenericContainer<>("eclipse-mosquitto:1.5")
-      .withExposedPorts(1883);
-
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   public void resolve1(boolean useStub) {
@@ -137,7 +140,6 @@ public class MqttTests {
 
   private void assertSenderConnected(ModelItemAndTwoTopics modelAB) {
     MqttRoot mqttRoot = modelAB.model.getRoot().getMqttRoot();
-    mqttBroker.waitingFor(Wait.forHealthcheck());
     if (!mqttRoot.getMqttSender().isConnected()) {
       try {
         Thread.sleep(1000);
@@ -156,8 +158,7 @@ public class MqttTests {
     }
     MqttReceiver receiver = new MqttReceiver();
     List<String> expectedTopicList = Arrays.asList(expectedTopics);
-//    receiver.setHost("localhost", 1883);
-    receiver.setHost(mqttBroker.getContainerIpAddress(), mqttBroker.getFirstMappedPort());
+    receiver.setHost(getMqttHost(), 1883);
     receiver.setTopicsForSubscription(expectedTopics);
     receiver.setOnMessage((topic, message) -> {
       assertThat(expectedTopicList).contains(topic);
@@ -178,8 +179,7 @@ public class MqttTests {
       // now a SenderStub is being used
       ((MQTTSenderStub) mqttRoot.getMqttSender()).setCallback(((topic, message, qos) -> messages.add(message)));
     } else {
-//      mqttRoot.setHostByName("localhost");
-      mqttRoot.setHost(ExternalHost.of(mqttBroker.getContainerIpAddress(), mqttBroker.getFirstMappedPort()));
+      mqttRoot.setHost(ExternalHost.of(getMqttHost(), 1883));
     }
     MqttTopic a = createAndAddMqttTopic(mqttRoot, firstPart);
     MqttTopic ab = createAndAddMqttTopic(mqttRoot, firstPart + "/" + secondPart);
diff --git a/print-coverage.py b/print-coverage.py
index 30ee86794382a8e055ce3fa0e0746cd872981b34..951043a82b101393a0486c342a12c7b619f4338e 100644
--- a/print-coverage.py
+++ b/print-coverage.py
@@ -1,8 +1,16 @@
+import glob
 import os
 import untangle
-print('Current path: ' + os.path.abspath(os.curdir))
-obj = untangle.parse('eraser-base/build/reports/jacoco/test/jacocoTestReport.xml')
-instructions = [o for o in obj.report.counter if o['type'] == 'INSTRUCTION'][0]
-missed, covered = int(instructions['missed']), int(instructions['covered'])
+
+print(f'Current path: {os.path.abspath(os.curdir)}')
+missed, covered = 0, 0
+for f in glob.iglob(os.getenv('JACOCO_REPORT')):
+    print(f'Checking {f}')
+    obj = untangle.parse(f)
+    instructions = [o for o in obj.report.counter if o['type'] == 'INSTRUCTION'][0]
+    missed += int(instructions['missed'])
+    covered += int(instructions['covered'])
 # print missed / (missed + covered)
-print('Covered %.2f%% of instructions for all projects.' % (missed * 100.0 / (missed + covered)))
+if missed == covered == 0:
+    covered = 1
+print('Covered %.2f%% of instructions for all projects.' % (covered * 100.0 / (missed + covered)))