diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java b/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java
index 4f511ea2e92dec0b9e3e561fc29aeb65cac641f2..d2e9c8b368f30bcca4faf66f446d7c81ca54a30d 100644
--- a/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java
+++ b/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java
@@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit;
 
 import ch.qos.logback.core.spi.AppenderAttachable;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import nl.tudelft.ewi.auta.common.model.metric.MetricFixturesProvider;
 import nl.tudelft.ewi.auta.core.logging.RingbufferAppender;
 import nl.tudelft.ewi.auta.core.plugin.PluginLoader;
 import nl.tudelft.ewi.auta.core.plugin.PluginRepository;
@@ -352,4 +353,16 @@ public class Core {
         final var root = (AppenderAttachable) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
         return (RingbufferAppender) root.getAppender("ringbuffer");
     }
+
+    /**
+     * A factory returning metric test fixture providers.
+     *
+     * @param gson the GSON instance to deserialize fixtures with
+     *
+     * @return the fixture provider
+     */
+    @Bean
+    public MetricFixturesProvider metricFixturesProviderFactory(final Gson gson) {
+        return new MetricFixturesProvider(gson);
+    }
 }
diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java
index e8852e88723d2a22ffb7a60461f96e2e5c6ddf59..0e10b7fd22a80f1eecbf574195e74fbfe35ad4fb 100644
--- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java
+++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java
@@ -4,6 +4,7 @@ import nl.tudelft.ewi.auta.common.model.metric.MetricName;
 import nl.tudelft.ewi.auta.common.model.metric.MetricSettings;
 import nl.tudelft.ewi.auta.core.database.AssignmentRepository;
 import nl.tudelft.ewi.auta.core.model.Assignment;
+import nl.tudelft.ewi.auta.core.model.PassingScriptTestCasesParser;
 import nl.tudelft.ewi.auta.core.response.Response;
 import nl.tudelft.ewi.auta.core.response.exception.AssignmentAlreadyExistsException;
 import nl.tudelft.ewi.auta.core.response.exception.InvalidAssignmentNameException;
@@ -43,6 +44,10 @@ public class AssignmentController extends ControllerBase {
      */
     private final AssignmentRepository assignmentStore;
 
+    /**
+     * The parser for parsing passing script tests.
+     */
+    private final PassingScriptTestCasesParser passingScriptTestCasesParser;
 
     /**
      * The service that manages course-level security for this controller.
@@ -53,11 +58,16 @@ public class AssignmentController extends ControllerBase {
      * Creates a new assignment controller.
      *
      * @param assignmentStore the assignment repository
+     * @param passingScriptTestCasesParser the parser for parsing passing script tests
      */
-    public AssignmentController(final AssignmentRepository assignmentStore,
-                                final CourseSecuredService securedService) {
+    public AssignmentController(
+            final AssignmentRepository assignmentStore,
+            final PassingScriptTestCasesParser passingScriptTestCasesParser,
+            final CourseSecuredService securedService
+    ) {
         this.assignmentStore = assignmentStore;
         this.securedService = securedService;
+        this.passingScriptTestCasesParser = passingScriptTestCasesParser;
     }
 
     /**
@@ -196,6 +206,7 @@ public class AssignmentController extends ControllerBase {
         return ResponseEntity.ok(res);
     }
 
+    @SuppressWarnings("unchecked")
     private void populateFromRequest(final Map<String, Object> req,
                                      final Assignment assignment) {
         // First extract the mandatory arguments
@@ -223,23 +234,19 @@ public class AssignmentController extends ControllerBase {
         assignment.setName(name);
         assignment.setAllowedLanguages(Collections.singleton(lang));
         assignment.setMetricSettings(stat.stream().map(o -> {
-            if (MetricName.valueOf((String) o.get("name")) == MetricName.DOCKER_LOGS) {
-                return new MetricSettings(
-                        MetricName.valueOf((String) o.get("name")),
-                        (String) o.get("script"),
-                        (String) o.get("formatter"),
-                        0,
-                        0,
-                        (String) o.get("dockerfile")
-                );
-            }
-            return new MetricSettings(
-                        MetricName.valueOf((String) o.get("name")),
-                        (String) o.get("script"),
-                        (String) o.get("formatter"),
-                        0,
-                        0
-                );
+            final var passingScriptTests = this.passingScriptTestCasesParser.parseTests(
+                    (List<Map<String, Object>>) o.get("scriptTests")
+            );
+            final var settings = new MetricSettings(
+                    MetricName.valueOf((String) o.get("name")),
+                    (String) o.get("script"),
+                    (String) o.get("formatter"),
+                    0,
+                    0,
+                    (String) o.get("dockerfile")
+            );
+            settings.setPassingScriptTestCases(passingScriptTests);
+            return settings;
         }).collect(Collectors.toUnmodifiableSet()));
     }
 }
diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java
index d71ff1e8be4baab062640e263607ee7c7d422352..4ee16bfd8149c9bc3ca4ad9f384e3c2282402aa6 100644
--- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java
+++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java
@@ -43,6 +43,10 @@ public class ControllerBase {
         }
     }
 
+    public Object getParam(final Map<String, Object> req, final String key) {
+        return this.getParam(req, key, Object.class);
+    }
+
     /**
      * Extracts a string field from the request body.
      *
diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/MetricsController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/MetricsController.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ac98698ca3a5ddcf780ba7a6172662f862689f6
--- /dev/null
+++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/MetricsController.java
@@ -0,0 +1,112 @@
+package nl.tudelft.ewi.auta.core.controller;
+
+import com.google.gson.Gson;
+import nl.tudelft.ewi.auta.common.model.metric.MetricFixturesProvider;
+import nl.tudelft.ewi.auta.core.communication.MessageReceiver;
+import nl.tudelft.ewi.auta.core.response.Response;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import java.util.Map;
+
+/**
+ * A controller serving information about the metrics supported by the current installation.
+ */
+@Controller
+public class MetricsController extends ControllerBase {
+    /**
+     * The message receiver.
+     */
+    private final MessageReceiver messageReceiver;
+
+    /**
+     * The GSON instance used to serialize the metrics.
+     */
+    private final Gson gson;
+
+    /**
+     * The provider for test fixtures.
+     */
+    private final MetricFixturesProvider fixturesProvider;
+
+    /**
+     * Creates a new metrics controller.
+     *
+     * @param messageReceiver the message receiver providing metrics known by the workers
+     * @param gson the GSON instance used to serialize the metrics
+     * @param fixturesProvider the provider for test fixtures
+     */
+    public MetricsController(
+            final MessageReceiver messageReceiver,
+            final Gson gson,
+            final MetricFixturesProvider fixturesProvider
+    ) {
+        this.messageReceiver = messageReceiver;
+        this.gson = gson;
+        this.fixturesProvider = fixturesProvider;
+    }
+
+    /**
+     * Gets a map of all languages and their metrics.
+     *
+     * @return the response
+     */
+    @GetMapping("/api/v1/worker/metrics")
+    public ResponseEntity<Response> getMetricsAction() {
+        final var res = new Response();
+        res.put("metrics", this.messageReceiver.getMetrics());
+        return ResponseEntity.ok(res);
+    }
+
+    /**
+     * Returns the metrics as a {@code metrics} property of the global {@code MetricEditor} object.
+     *
+     * @return the metrics as an object property
+     *
+     * @deprecated originally a hack for the old UI to allow this resource to load in a blocking
+     *             fashion, no longer necessary
+     */
+    @GetMapping("/api/v1/worker/metrics.js")
+    @Deprecated
+    public ResponseEntity<String> getMetricsJsAction() {
+        final var metrics = this.messageReceiver.getMetrics();
+        return ResponseEntity.ok()
+                .contentType(new MediaType("application", "javascript"))
+                .body("MetricEditor.metrics = " + this.gson.toJson(metrics));
+    }
+
+    /**
+     * Returns the test fixtures for metrics.
+     *
+     * The response is a JSON object resembling the following:
+     * {@code
+     * {
+     *     "fixtures": {
+     *         "CYCLOMATIC_COMPLEXITY": [
+     *             1,
+     *             7,
+     *             14,
+     *             615
+     *         ],
+     *         "DOCKER_LOGS": [
+     *             "BUILD SUCCESSFUL",
+     *             "BUILD FAILED"
+     *         ]
+     *     }
+     * }
+     * }
+     *
+     * It is important to note that the fixtures can be of any type, as long as the type is valid
+     * for the metric it belongs to.
+     *
+     * @return the test fixtures
+     */
+    @GetMapping("/api/v1/worker/metrics/test-fixtures")
+    public ResponseEntity<Response> getMetricTestFixturesAction() {
+        return ResponseEntity.ok(new Response(Map.of(
+                "fixtures", this.fixturesProvider.getFixtures()
+        )));
+    }
+}
diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationController.java
new file mode 100644
index 0000000000000000000000000000000000000000..2925eff18e5f4683ac9ef98c3db10c463f5726aa
--- /dev/null
+++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationController.java
@@ -0,0 +1,267 @@
+package nl.tudelft.ewi.auta.core.controller;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase;
+import nl.tudelft.ewi.auta.core.model.PassingScriptTestCasesParser;
+import nl.tudelft.ewi.auta.srf.iface.Script;
+import nl.tudelft.ewi.auta.srf.iface.ScriptExecutionContextFactory;
+import nl.tudelft.ewi.auta.srf.model.NoteCollection;
+import org.intellij.lang.annotations.Language;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * A controller for criteria script validation actions.
+ */
+@Controller
+public class ScriptValidationController extends ControllerBase {
+    /**
+     * The factory building script execution contexts.
+     */
+    private final ScriptExecutionContextFactory scriptExecutionContextFactory;
+
+    /**
+     * The parser for parsing (criteria) script tests.
+     */
+    private final PassingScriptTestCasesParser passingScriptTestCasesParser;
+
+    /**
+     * Creates a new script validation controller.
+     *
+     * @param scriptExecutionContextFactory the factory building script execution contexts
+     * @param passingScriptTestCasesParser the parser for parsing script tests
+     */
+    public ScriptValidationController(
+            final ScriptExecutionContextFactory scriptExecutionContextFactory,
+            final PassingScriptTestCasesParser passingScriptTestCasesParser
+    ) {
+        this.scriptExecutionContextFactory = scriptExecutionContextFactory;
+        this.passingScriptTestCasesParser = passingScriptTestCasesParser;
+    }
+
+    /**
+     * Validates a passing script.
+     *
+     * @param req the request
+     *
+     * @return a test summary
+     *
+     * @throws Exception if validation fails for reasons unrelated to the script
+     */
+    @PostMapping("/api/v1/script/test/passing-script")
+    @SuppressWarnings("unchecked")
+    @SuppressFBWarnings(
+            value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE",
+            justification = "SpotBugs is not aware of try-with-resources and its null checks"
+    )
+    public ResponseEntity<Object> validateScript(
+            final @RequestBody Map<String, Object> req
+    ) throws Exception {
+        try (var sec = this.scriptExecutionContextFactory.create()) {
+            @Language("ECMAScript 6")
+            final var script = this.getString(req, "script");
+            final var rawTests = this.getParam(req, "tests", List.class);
+            final List<PassingScriptTestCase> tests =
+                    this.passingScriptTestCasesParser.parseTests(rawTests);
+
+            final Script<Object> compiledScript;
+            try {
+                compiledScript = sec.submitScript(script, Object.class);
+            } catch (final Exception ex) {
+                return ResponseEntity.ok(new TestSuiteResults(ex.getMessage()));
+            }
+
+            final var results = tests.stream()
+                    .map(this.buildTestRunner(compiledScript))
+                    .collect(Collectors.toList());
+
+            return ResponseEntity.ok(new TestSuiteResults(results));
+        }
+    }
+
+    /**
+     * Builds a test runner.
+     *
+     * @param compiledScript the script under testing
+     *
+     * @return the function to call on a test case
+     */
+    private Function<PassingScriptTestCase, TestResults> buildTestRunner(
+            final Script<Object> compiledScript
+    ) {
+        return test -> {
+            assert test.getId() != null;
+            assert test.getValue() != null;
+
+            final var notes = new NoteCollection();
+            final Optional<Double> grade;
+            try {
+                grade = compiledScript.execute(test.getValue(), null, notes);
+            } catch (final Exception ex) {
+                return new TestResults(test.getId(), ex.getMessage());
+            }
+
+            return new TestResults(test.getId(), notes, grade.orElse(null));
+        };
+    }
+
+    /**
+     * The results of an entire test suite.
+     */
+    private static final class TestSuiteResults {
+        /**
+         * Whether the script under test compiled.
+         *
+         * Iff the test compiled, this is {@code true}. Otherwise, this is a string containing a
+         * message why compilation failed.
+         */
+        private final Object compiles;
+
+        /**
+         * The list of results for the individual test suites.
+         */
+        private final List<TestResults> results;
+
+        /**
+         * Constructs a new test suite result collection for a script that fails to compile.
+         *
+         * @param compiles the message describing the compilation failure
+         */
+        private TestSuiteResults(final String compiles) {
+            this.compiles = compiles;
+            this.results = Collections.emptyList();
+        }
+
+        /**
+         * Creates a new test suite result collection for a script that compiles.
+         *
+         * @param results the list of results of the test suites
+         */
+        private TestSuiteResults(final List<TestResults> results) {
+            this.compiles = true;
+            this.results = results;
+        }
+
+        /**
+         * Returns the object describing the compilation status of the script.
+         *
+         * @return the compilation status
+         */
+        public Object getCompiles() {
+            return this.compiles;
+        }
+
+        /**
+         * Returns the results of the individual unit tests.
+         *
+         * @return the results
+         */
+        public List<TestResults> getResults() {
+            return this.results;
+        }
+    }
+
+    /**
+     * The results of a single unit test.
+     */
+    private static final class TestResults {
+        /**
+         * The unit test's identifier.
+         */
+        private final String id;
+
+        /**
+         * The runtime error, if any.
+         */
+        @Nullable
+        private final String error;
+
+        /**
+         * The notes generated by the script.
+         */
+        private final NoteCollection notes;
+
+        /**
+         * The grade generated by the script, if any.
+         */
+        @Nullable
+        private final Double grade;
+
+        /**
+         * Creates a new results object.
+         *
+         * @param id the identifier of the test that generated these results
+         * @param notes the notes generated by the script
+         * @param grade the grade generated by the script, if any
+         */
+        private TestResults(
+                final String id, final NoteCollection notes, final @Nullable Double grade
+        ) {
+            this.id = id;
+            this.error = null;
+            this.notes = notes;
+            this.grade = grade;
+        }
+
+        /**
+         * Creates a new results object for a test that generated a run-time error.
+         *
+         * @param id the identifier of the test that errored out
+         * @param error the error generated by the test
+         */
+        private TestResults(final String id, final String error) {
+            this.id = id;
+            this.error = error;
+            this.notes = new NoteCollection();
+            this.grade = null;
+        }
+
+        /**
+         * Returns the identifier of the test.
+         *
+         * @return the identifier
+         */
+        public String getId() {
+            return this.id;
+        }
+
+        /**
+         * Returns the error generated by the test, if any.
+         *
+         * @return the error
+         */
+        @Nullable
+        public String getError() {
+            return this.error;
+        }
+
+        /**
+         * Returns the notes generated by the test.
+         *
+         * @return the notes
+         */
+        public NoteCollection getNotes() {
+            return this.notes;
+        }
+
+        /**
+         * Returns the grade generated by the test, if any.
+         *
+         * @return the grade
+         */
+        @Nullable
+        public Double getGrade() {
+            return this.grade;
+        }
+    }
+}
diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java
index c573cb83c9eb8486c37862a8195a9edd90c1bd53..420117d669c354ea737785910cfd5cae8fc71aaa 100644
--- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java
+++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java
@@ -1,17 +1,12 @@
 package nl.tudelft.ewi.auta.core.controller;
 
 import nl.tudelft.ewi.auta.core.workers.WorkerPool;
-import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.access.annotation.Secured;
-import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RestController;
 
-import com.google.gson.Gson;
-
-import nl.tudelft.ewi.auta.core.communication.MessageReceiver;
 import nl.tudelft.ewi.auta.core.response.Response;
 
 import java.util.HashMap;
@@ -24,19 +19,12 @@ public class WorkerController {
      */
     private final WorkerPool workerPool;
 
-    /**
-     * The message receiver.
-     */
-    private MessageReceiver receiver;
-
     /**
      * Create a new worker controller.
      * @param workerPool the pool of workers currently connected to the core
-     * @param receiver the message receiver.
      */
-    public WorkerController(final WorkerPool workerPool, final MessageReceiver receiver) {
+    public WorkerController(final WorkerPool workerPool) {
         this.workerPool = workerPool;
-        this.receiver = receiver;
     }
 
     /**
@@ -100,22 +88,4 @@ public class WorkerController {
         }).collect(Collectors.toList()));
         return ResponseEntity.ok(res);
     }
-
-    /**
-     * Gets a map of all languages and their metrics.
-     * @return the response
-     */
-    @RequestMapping(path = "/api/v1/worker/metrics", method = RequestMethod.GET)
-    public ResponseEntity<Response> getMetricsAction() {
-        final var res = new Response();
-        res.put("metrics", this.receiver.getMetrics());
-        return ResponseEntity.ok(res);
-    }
-
-    @GetMapping("/api/v1/worker/metrics.js")
-    public ResponseEntity<String> getMetricsJsAction() {
-        return ResponseEntity.ok()
-                .contentType(new MediaType("application", "javascript"))
-                .body("MetricEditor.metrics = " + new Gson().toJson(this.receiver.getMetrics()));
-    }
 }
diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/model/PassingScriptTestCasesParser.java b/core/src/main/java/nl/tudelft/ewi/auta/core/model/PassingScriptTestCasesParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..7636419df3dbd816b3efc465353e8b239a96241c
--- /dev/null
+++ b/core/src/main/java/nl/tudelft/ewi/auta/core/model/PassingScriptTestCasesParser.java
@@ -0,0 +1,42 @@
+package nl.tudelft.ewi.auta.core.model;
+
+import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase;
+import nl.tudelft.ewi.auta.core.controller.ControllerBase;
+import org.jetbrains.annotations.Contract;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A parser for parsing script tests.
+ */
+@Service
+public class PassingScriptTestCasesParser extends ControllerBase {
+    /**
+     * Parses a list of unit tests.
+     *
+     * @param tests the object to parse
+     *
+     * @return the parsed tests
+     */
+    public List<PassingScriptTestCase> parseTests(final List<Map<String, Object>> tests) {
+        return tests.stream().map(this::parseTest).collect(Collectors.toUnmodifiableList());
+    }
+
+    /**
+     * Parses a single unit test.
+     *
+     * @param test the object to parse
+     *
+     * @return the parsed test
+     */
+    @Contract("_ -> new")
+    public PassingScriptTestCase parseTest(final Map<String, Object> test) {
+        final var id = this.getString(test, "id");
+        final var value = this.getParam(test, "value");
+
+        return new PassingScriptTestCase(id, value);
+    }
+}
diff --git a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/MetricsControllerTest.java b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/MetricsControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf46277ae8cb43ac8d80d299feadf94fcdfe0626
--- /dev/null
+++ b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/MetricsControllerTest.java
@@ -0,0 +1,66 @@
+package nl.tudelft.ewi.auta.core.controller;
+
+import com.google.gson.Gson;
+import nl.tudelft.ewi.auta.common.model.metric.MetricFixturesProvider;
+import nl.tudelft.ewi.auta.core.communication.MessageReceiver;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpMethod;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.not;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class MetricsControllerTest {
+    private MessageReceiver receiver;
+    private MetricFixturesProvider fixturesProvider;
+    private MetricsController controller;
+    private MockMvc mvc;
+
+    @BeforeEach
+    public void before() {
+        this.receiver = mock(MessageReceiver.class);
+        this.fixturesProvider = new MetricFixturesProvider(new Gson());
+
+        final var metrics = Map.of(
+                "java", Set.of("cyclomatic complexity"),
+                "*", Set.of("method length")
+        );
+
+        when(this.receiver.getMetrics()).thenReturn((List) Collections.singletonList(metrics));
+
+        this.controller = new MetricsController(this.receiver, new Gson(), this.fixturesProvider);
+
+        this.mvc = MockMvcBuilders.standaloneSetup(this.controller)
+                           .setControllerAdvice(new RestExceptionHandler())
+                           .build();
+    }
+
+    @Test
+    public void testGetMetrics() throws Exception {
+        this.mvc.perform(request(HttpMethod.GET, "/api/v1/worker/metrics"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.errors", hasSize(0)))
+                .andExpect(jsonPath("$.metrics", hasSize(1)));
+    }
+
+    @Test
+    public void testGetMetricFixtures() throws Exception {
+        this.mvc.perform(get("/api/v1/worker/metrics/test-fixtures"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.fixtures", not(empty())));
+    }
+}
diff --git a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationControllerTest.java b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9db563d13f2ffadd6ff8a19cbe395bffe0b6cc80
--- /dev/null
+++ b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationControllerTest.java
@@ -0,0 +1,160 @@
+package nl.tudelft.ewi.auta.core.controller;
+
+import com.google.gson.Gson;
+import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase;
+import nl.tudelft.ewi.auta.core.model.PassingScriptTestCasesParser;
+import nl.tudelft.ewi.auta.srf.iface.Script;
+import nl.tudelft.ewi.auta.srf.iface.ScriptExecutionContext;
+import nl.tudelft.ewi.auta.srf.iface.ScriptExecutionContextFactory;
+import nl.tudelft.ewi.auta.srf.model.Note;
+import nl.tudelft.ewi.auta.srf.model.NoteCollection;
+import nl.tudelft.ewi.auta.srf.model.Severity;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class ScriptValidationControllerTest {
+    private ScriptExecutionContextFactory scriptExecutionContextFactory;
+    private ScriptExecutionContext scriptExecutionContext;
+    private Script<Object> compiledScript;
+    private PassingScriptTestCasesParser passingScriptTestCasesParser;
+    private ScriptValidationController controller;
+    private MockMvc mvc;
+
+    @BeforeEach
+    public void before() {
+        this.scriptExecutionContextFactory = mock(ScriptExecutionContextFactory.class);
+        this.passingScriptTestCasesParser = mock(PassingScriptTestCasesParser.class);
+        this.scriptExecutionContext = mock(ScriptExecutionContext.class);
+        this.compiledScript = mock(Script.class);
+
+        doReturn(this.scriptExecutionContext).when(this.scriptExecutionContextFactory).create();
+
+        this.controller = new ScriptValidationController(
+                this.scriptExecutionContextFactory, this.passingScriptTestCasesParser
+        );
+        this.mvc = MockMvcBuilders.standaloneSetup(this.controller)
+                .setControllerAdvice(new RestExceptionHandler())
+                .build();
+    }
+
+    @Test
+    public void testMissingBody() throws Exception {
+        this.mvc.perform(post("/api/v1/script/test/passing-script"))
+                .andExpect(status().isBadRequest());
+    }
+
+    @Test
+    public void testMissingScript() throws Exception {
+        this.mvc.perform(post("/api/v1/script/test/passing-script")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(new Gson().toJson(Map.of("scriptTests", Collections.emptyList())))
+        ).andExpect(status().isBadRequest());
+    }
+
+    @Test
+    public void testMissingTests() throws Exception {
+        this.mvc.perform(post("/api/v1/script/test/passing-script")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(new Gson().toJson(Map.of("script", "this is Javascript, not Perl")))
+        ).andExpect(status().isBadRequest());
+    }
+
+    @Test
+    public void testEmptyTestSuite() throws Exception {
+        doReturn(this.compiledScript).when(this.scriptExecutionContext).submitScript(any(), any());
+
+        this.mvc.perform(post("/api/v1/script/test/passing-script")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(new Gson().toJson(Map.of(
+                        "script", "this is valid Javascript I swear",
+                        "tests", Collections.emptyList()
+                )))
+        )
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.compiles", equalTo(true)))
+                .andExpect(jsonPath("$.results", hasSize(0)));
+
+        verify(this.compiledScript, never()).execute(any(), any(), any());
+    }
+
+    @Test
+    public void testBadScript() throws Exception {
+        final String complaint = "that is Perl, I ain't gonna touch that";
+
+        doThrow(new RuntimeException(complaint))
+                .when(this.scriptExecutionContext).submitScript(any(), any());
+
+        this.mvc.perform(post("/api/v1/script/test/passing-script")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(new Gson().toJson(Map.of(
+                        "script", "\uD83C\uDDF9\uD83C\uDDF7",
+                        "tests", Collections.emptyList()
+                )))
+        )
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.compiles", equalTo(complaint)))
+                .andExpect(jsonPath("$.results", hasSize(0)));
+    }
+
+    @Test
+    public void testScriptWithTests() throws Exception {
+        final var tests = List.of(
+                new PassingScriptTestCase("first", 12), new PassingScriptTestCase("second", -3)
+        );
+        doReturn(tests).when(this.passingScriptTestCasesParser).parseTests(any());
+        doReturn(this.compiledScript).when(this.scriptExecutionContext).submitScript(any(), any());
+
+        doAnswer(invocation -> {
+            final Object value = invocation.getArgument(0);
+            final NoteCollection notes = invocation.getArgument(2);
+
+            if (Objects.equals(value, -3)) {
+                notes.addNote(new Note(Severity.FAILURE, "it's bad"));
+            } else {
+                throw new RuntimeException("invalid");
+            }
+
+            return Optional.empty();
+        }).when(this.compiledScript).execute(any(), any(), any());
+
+        this.mvc.perform(post("/api/v1/script/test/passing-script")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(new Gson().toJson(Map.of(
+                        "script", "100% pure Javascript",
+                        "tests", tests
+                )))
+        )
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.compiles", equalTo(true)))
+                .andExpect(jsonPath("$.results[0].id", equalTo("first")))
+                .andExpect(jsonPath("$.results[0].error", equalTo("invalid")))
+                .andExpect(jsonPath("$.results[1].id", equalTo("second")))
+                .andExpect(jsonPath("$.results[1].error", nullValue()))
+                .andExpect(jsonPath("$.results[1].notes.notes[0].severity", equalTo("FAILURE")))
+                .andExpect(jsonPath("$.results[1].notes.notes[0].message", equalTo("it's bad")))
+                .andExpect(jsonPath("$.results[1].grade", nullValue()));
+    }
+}
diff --git a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java
index 9013cf575686ac8edb4c5f2c74c9b36ecad299f9..2fc7234cd2a2df198d17b71cd4e0c5be14a06fed 100644
--- a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java
+++ b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java
@@ -7,10 +7,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 import nl.tudelft.ewi.auta.common.jump.Jump;
 import nl.tudelft.ewi.auta.core.model.Assignment;
@@ -28,15 +24,10 @@ import org.springframework.http.HttpMethod;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 
-import nl.tudelft.ewi.auta.core.communication.MessageReceiver;
-
 public class WorkerControllerTest {
 
     private MockMvc mvc;
 
-    @Mock
-    private MessageReceiver receiver;
-
     @Mock
     private WorkerPool workerPool;
 
@@ -53,27 +44,12 @@ public class WorkerControllerTest {
     public void before() {
         MockitoAnnotations.initMocks(this);
 
-        Map<String, Set<String>> metrics = new HashMap<>();
-        metrics.put("java", Set.of("cyclomatic complexity"));
-        metrics.put("*", Set.of("method length"));
-
-        Mockito.when(this.receiver.getMetrics())
-                .thenReturn((List) Collections.singletonList(metrics));
-
         Mockito.when(this.assignment.getId()).thenReturn("123");
         Mockito.when(this.submission.getId()).thenReturn("456");
 
         this.mvc = MockMvcBuilders.standaloneSetup(this.controller).build();
     }
 
-    @Test
-    public void testAddSubmission() throws Exception {
-        this.mvc.perform(request(HttpMethod.GET, "/api/v1/worker/metrics"))
-                .andExpect(status().isOk())
-                .andExpect(jsonPath("$.errors", hasSize(0)))
-                .andExpect(jsonPath("$.metrics", hasSize(1)));
-    }
-
     @Test
     public void testGetWorkers() throws Exception {
         final var testWorker = new WorkerConnection(null, "test #1", new Jump());
diff --git a/doc/api/proposal/index.html b/doc/api/proposal/index.html
index e68ef650fabfc34e1d471b967d50b2784a6069dc..9436dce9c5d7d98f60be9d53267c29508d6e2400 100644
--- a/doc/api/proposal/index.html
+++ b/doc/api/proposal/index.html
@@ -206,35 +206,6 @@ Content-Type: application/json
       <p>Returns a list of normal users.</p>
     </section>
 
-    <section id="f-api-metrisjs">
-      <h1><span class="method">GET</span> /api/v1/worker/metrics.js</h1>
-      <div>
-        <table>
-          <tr>
-            <th>Property</th>
-            <th>Type</th>
-            <th>Required</th>
-            <th>Description</th>
-          </tr>
-          <tr>
-            <td>token</td>
-            <td>string</td>
-            <td>yes</td>
-            <td>the token to invalidate</td>
-          </tr>
-        </table>
-      </div>
-      <h2>Example response</h2>
-      <response>
-{
-  "java": ["LINES_OF_CODE", "CYCLOMATIC_COMPLEXITY"],
-  "python": ["LINES_OF_CODE"],
-  "assembly": ["RECURSION"]
-}
-      </response>
-      <p>Gets a map of all languages and their metrics.</p>
-    </section>
-
     <section id="f-api-user-logout">
       <h1><span class="method">DELETE</span> /api/v1/user/logout</h1>
 
@@ -2150,6 +2121,196 @@ UEsDBBQAAAAAAGdjOEoAAAAAAAAAAAAAAAAFAAAAam...
       </div>
     </section>
 
+    <section id="f-api-worker-metrisjs">
+      <h1><span class="method">GET</span> <del>/api/v1/worker/metrics.js</del></h1>
+      <div>
+        <table>
+          <tr>
+            <th>Property</th>
+            <th>Type</th>
+            <th>Required</th>
+            <th>Description</th>
+          </tr>
+          <tr>
+            <td>token</td>
+            <td>string</td>
+            <td>yes</td>
+            <td>the user's authentication token</td>
+          </tr>
+        </table>
+      </div>
+      <h2>Example response</h2>
+      <response class="javascript">
+MetricEditor.metrics = {
+  "metrics": {
+    "python": [
+      "cyclomatic complexity",
+      "method length"
+    ],
+    "cpp": [
+      "cyclomatic complexity",
+      "method length",
+      "lines of code"
+    ],
+    "*": [
+      "comment count",
+      "line length"
+    ]
+  },
+  "errors": []
+}
+      </response>
+      <p>Gets a map of all languages and their metrics.</p>
+      <p>
+        <strong>Deprecated</strong> - originally a hack for the old UI to allow this resource
+        to load in a blocking fashion, no longer necessary
+      </p>
+    </section>
+
+    <section id="f-api-worker-metrics-test-fixtures">
+      <h1><span class="method">GET</span> /api/v1/worker/metrics/test-fixtures</h1>
+      <div>
+        <table>
+          <tr>
+            <th>Property</th>
+            <th>Type</th>
+            <th>Required</th>
+            <th>Description</th>
+          </tr>
+          <tr>
+            <td>token</td>
+            <td>string</td>
+            <td>yes</td>
+            <td>the user's authentication token</td>
+          </tr>
+        </table>
+      </div>
+      <h2>Example response</h2>
+      <response>
+{
+  "fixtures": {
+    "CYCLOMATIC_COMPLEXITY": [
+      1,
+      7,
+      14,
+      615
+    ],
+    "DOCKER_LOGS": [
+      "BUILD SUCCESSFUL",
+      "BUILD FAILED"
+    ]
+  }
+}
+      </response>
+      <p>Returns a map containing test fixtures for criteria scripts.</p>
+      <p>
+        It is important to note that the fixtures can be of any type, as long as the type is valid
+        for the metric it belongs to.
+      </p>
+    </section>
+
+    <section id="f-api-script-test-passing-script">
+      <h1><span class="method">POST</span> /api/v1/script/test/passing-script</h1>
+      <div>
+        <h2>Request</h2>
+        <table>
+          <tr>
+            <th>Property</th>
+            <th>Type</th>
+            <th>Required</th>
+            <th>Description</th>
+          </tr>
+          <tr>
+            <td>token</td>
+            <td>string</td>
+            <td>yes</td>
+            <td>the user's authentication token</td>
+          </tr>
+          <tr>
+            <td>script</td>
+            <td>string</td>
+            <td>yes</td>
+            <td>the script to test</td>
+          </tr>
+          <tr>
+            <td>tests</td>
+            <td>list&lt;spec&gt;</td>
+            <td>yes</td>
+            <td>the tests to run</td>
+          </tr>
+        </table>
+        <p>
+          A unit test specification ("spec") is an object containing an identifier <tt>id</tt>
+          for the test and a value <tt>value</tt> to pass to the script under test.
+        </p>
+      </div>
+
+      <div>
+        <h2>Example request</h2>
+        <request>
+{
+  "script": "(() => count => {\n    const minWarnThreshold = 4;\n    const minFailThreshold = 2;\n    const maxWarnThreshold = 20;\n    const maxFailThreshold = 30;\n    if (count > 0) {\n        if (count < minFailThreshold) {\n            fail(`Has too few test methods:' ${count} < ${minFailThreshold}`);\n        } else if (count < minWarnThreshold) {\n            warn(`Has few test methods:' ${count} < ${minWarnThreshold}`);\n        } else if (count > maxFailThreshold) {\n            fail(`Has too many test methods:' ${count} > ${maxFailThreshold}`);\n        } else if (count > maxWarnThreshold) {\n            warn(`Has many methods:' ${count} > ${maxWarnThreshold}`);\n        }\n    }\n})();\n",
+  "tests": [
+    {
+      "id": "test:t16oyi-k3m42o-ie108e-318ntg",
+      "value": "3"
+    },
+    {
+      "id": "test:va47q4-7ti1jg-5ubb30-1xllc6",
+      "value": "24"
+    }
+  ]
+}
+        </request>
+      </div>
+
+      <div>
+        <h2>Example response</h2>
+        <response>
+{
+  "compiles": true,
+  "results": [
+    {
+      "id": "test:t16oyi-k3m42o-ie108e-318ntg",
+      "error": null,
+      "notes": {
+        "notes": [
+          {
+            "severity": "WARNING",
+            "message": "Has few test methods:' 3 < 4"
+          }
+        ]
+      },
+      "grade": null
+    },
+    {
+      "id": "test:va47q4-7ti1jg-5ubb30-1xllc6",
+      "error": null,
+      "notes": {
+        "notes": [
+          {
+            "severity": "WARNING",
+            "message": "Has many methods:' 24 > 20"
+          }
+        ]
+      },
+      "grade": null
+    }
+  ]
+}
+        </response>
+      </div>
+
+      <div>
+        <p>Validates a passing script.</p>
+        <p>
+          The script is checked whether it compiles (mostly for syntax errors) and is then run
+          against the provided values. The results are returned, along with any errors generated
+          when running the test, as if the script were run on an entity.
+        </p>
+      </div>
+    </section>
+
     <section id="f-api-get-workers">
       <h1><span class="method">GET</span> /api/v1/workers</h1>
 
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCase.java b/src/main/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCase.java
new file mode 100644
index 0000000000000000000000000000000000000000..7485244cf40d8f3e2326c1e0c489fff15f686753
--- /dev/null
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCase.java
@@ -0,0 +1,94 @@
+package nl.tudelft.ewi.auta.common.model;
+
+import javax.annotation.Nullable;
+import java.util.Objects;
+
+/**
+ * A test for passing (criteria) scripts.
+ */
+public class PassingScriptTestCase {
+    /**
+     * The identifier of the test.
+     */
+    @Nullable
+    private String id;
+
+    /**
+     * The value to pass to the script.
+     */
+    @Nullable
+    private Object value;
+
+    /**
+     * Creates a new passing script test.
+     */
+    public PassingScriptTestCase() {
+        this.id = null;
+        this.value = null;
+    }
+
+    /**
+     * Creates a new passing script test.
+     *
+     * @param id the identifier of the test
+     * @param value the value to pass to the script
+     */
+    public PassingScriptTestCase(final String id, final Object value) {
+        this.id = id;
+        this.value = value;
+    }
+
+    /**
+     * Returns the test's identifier.
+     *
+     * @return the identifier
+     */
+    @Nullable
+    public String getId() {
+        return this.id;
+    }
+
+    /**
+     * Sets the test's identifier.
+     *
+     * @param id the identifier
+     */
+    public void setId(final @Nullable String id) {
+        this.id = id;
+    }
+
+    /**
+     * Returns the value to pass to the script.
+     *
+     * @return the value
+     */
+    @Nullable
+    public Object getValue() {
+        return this.value;
+    }
+
+    /**
+     * Sets the value to pass to the script.
+     *
+     * @param value the value
+     */
+    public void setValue(final @Nullable Object value) {
+        this.value = value;
+    }
+
+    @Override
+    public final boolean equals(final Object obj) {
+        if (!(obj instanceof PassingScriptTestCase)) {
+            return false;
+        }
+
+        final var other = (PassingScriptTestCase) obj;
+
+        return Objects.equals(this.id, other.id) && Objects.equals(this.value, other.value);
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(this.id, this.value);
+    }
+}
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java
index fcaa5fdcdb7f7efdc414739f4f331cbadde2f4f2..1606781f83cac607f29938bf95f8a3fe70a50f44 100644
--- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java
@@ -1,6 +1,6 @@
 package nl.tudelft.ewi.auta.common.model.metric;
 
-public class DoubleMetric extends Metric<Double> {
+public class DoubleMetric extends NumberMetric<Double> {
     /**
      * Create a metric with a value and a name.
      *
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java
index 1dd573753ccc1a2c16c656e2a6eec7b812156395..3a1707fa7866863aa418ba366cd433056f871e7a 100644
--- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java
@@ -1,6 +1,6 @@
 package nl.tudelft.ewi.auta.common.model.metric;
 
-public class IntegerMetric extends Metric<Integer> {
+public class IntegerMetric extends NumberMetric<Integer> {
     /**
      * Create a metric with a value and a name.
      *
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java
index e01c04c8c706fdd56a2f406b9797d5ee67b64025..c5f088eeadd670cbe4d6db654cbf9483a2336a93 100644
--- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java
@@ -1,11 +1,14 @@
 package nl.tudelft.ewi.auta.common.model.metric;
 
 import org.jetbrains.annotations.Contract;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
 
 public abstract class Metric<T> {
+    private static final Logger logger = LoggerFactory.getLogger(Metric.class);
 
     /**
      * The value of the metric.
@@ -32,6 +35,18 @@ public abstract class Metric<T> {
      * @param name the name of the metric
      */
     public Metric(final T value, final MetricName name) {
+        if (!name.isMetricCompatible(this)) {
+            logger.warn(
+                    "A metric instance with name {} was given an instance of {}, "
+                            + "while it expects instances of {}. This is allowed in the worker, "
+                            + "but reporting scripts may fail due to the mismatch.",
+                    name, this.getClass().getCanonicalName(), name.getType().getCanonicalName()
+            );
+            if (logger.isDebugEnabled()) {
+                logger.debug("This warning was issued due to:", new Throwable());
+            }
+        }
+
         this.value = value;
         this.name = name;
     }
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProvider.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..fcc2933a44e1532cbd6052a223615345bc80580f
--- /dev/null
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProvider.java
@@ -0,0 +1,62 @@
+package nl.tudelft.ewi.auta.common.model.metric;
+
+import com.google.gson.Gson;
+import nl.tudelft.ewi.auta.common.threads.CheckedExceptionTunnel;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A provider class for testing metrics.
+ *
+ * These values are meant to be used when generating criteria script unit tests. The values were
+ * picked to be both edge-cases and normal values to fully cover the range of decisions a script
+ * could take.
+ */
+public class MetricFixturesProvider {
+    /**
+     * The fixtures container the values are stored in.
+     */
+    private final MetricFixturesContainer container;
+
+    /**
+     * Creates a new metric fixtures provider.
+     *
+     * @param gson the GSON instance to deserialize the fixtures with
+     */
+    public MetricFixturesProvider(final Gson gson) {
+        try (var in = new InputStreamReader(
+                MetricFixturesProvider.class.getResourceAsStream("test-fixtures.json"),
+                StandardCharsets.UTF_8
+        )) {
+            this.container = gson.fromJson(in, MetricFixturesContainer.class);
+        } catch (final IOException ex) {
+            throw new CheckedExceptionTunnel(ex);
+        }
+    }
+
+    /**
+     * Returns the test fixtures.
+     *
+     * @return the fixtures
+     */
+    public Map<MetricName, List<Object>> getFixtures() {
+        return this.container.fixtures;
+    }
+
+    /**
+     * The container class the fixtures are stored in.
+     *
+     * This class describes the file format for the serialized fixtures.
+     */
+    private static final class MetricFixturesContainer {
+        /**
+         * The map of fixtures.
+         */
+        private final Map<MetricName, List<Object>> fixtures = Collections.emptyMap();
+    }
+}
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java
index 25af88eb0fb337c82469bcb823fc80f5f80798fb..08df3757d2f7f9038de7a456303b3b6a90bbf248 100644
--- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java
@@ -1,5 +1,9 @@
 package nl.tudelft.ewi.auta.common.model.metric;
 
+import org.jetbrains.annotations.Contract;
+
+import javax.annotation.Nullable;
+
 /**
  * The names of the metrics.
  */
@@ -8,388 +12,425 @@ public enum MetricName {
     /**
      * The number of instructions in an assembly file.
      */
-    ASSEMBLY_INSTRUCTION_COUNT,
+    ASSEMBLY_INSTRUCTION_COUNT(IntegerMetric.class),
 
     /**
      * The recursion targets for an assembly file.
      */
-    ASSEMBLY_RECURSION_TARGET,
+    ASSEMBLY_RECURSION_TARGET(StringSetMetric.class),
 
     /**
      * The number of assertions per test method.
      */
-    ASSERTIONS_PER_TEST,
+    ASSERTIONS_PER_TEST(IntegerMetric.class),
 
     /**
      * The number of commented lines in a file.
      */
-    COMMENTED_LINE_COUNT,
+    COMMENTED_LINE_COUNT(IntegerMetric.class),
 
     /**
      * The number of constructors in a file or class.
      */
-    CONSTRUCTOR_COUNT,
+    CONSTRUCTOR_COUNT(IntegerMetric.class),
 
     /**
      * The cyclomatic complexity of a method.
      */
-    CYCLOMATIC_COMPLEXITY,
+    CYCLOMATIC_COMPLEXITY(NumberMetric.class),
 
     /**
      * The logs of the docker container.
      */
-    DOCKER_LOGS,
+    DOCKER_LOGS(StringMetric.class),
 
     /**
      * The number of fields in a file or class.
      */
-    FIELD_COUNT,
+    FIELD_COUNT(IntegerMetric.class),
 
     /**
      * The number of files in a project.
      */
-    FILE_COUNT,
+    FILE_COUNT(IntegerMetric.class),
 
     /**
      * Whether the method has javadocs.
      */
-    JAVADOC_EXISTS,
+    JAVADOC_EXISTS(BooleanMetric.class),
 
     /**
      * The set of javadoc violations (tags).
      */
-    JAVADOC_VIOLATIONS,
+    JAVADOC_VIOLATIONS(StringSetMetric.class),
 
     /**
      * The length of each line in a file.
      */
-    LINE_LENGTH,
+    LINE_LENGTH(IntegerListMetric.class),
 
     /**
      * The number of lines of code in a code block.
      */
-    LINES_OF_CODE,
+    LINES_OF_CODE(IntegerMetric.class),
 
     /**
      * The number of methods in a file or class.
      */
-    METHOD_COUNT,
+    METHOD_COUNT(IntegerMetric.class),
 
     /**
      * The number of effective lines of code in each method. This is excluding commented lines and
      * blank lines.
      */
-    METHOD_EFFECTIVE_LOC,
+    METHOD_EFFECTIVE_LOC(IntegerMetric.class),
 
     /**
      * The number of parameters in a method.
      */
-    PARAMETER_COUNT,
+    PARAMETER_COUNT(IntegerMetric.class),
 
     /**
      * The percentage of commented liens in a file.
      */
-    PERCENTAGE_COMMENTED_LINES,
+    PERCENTAGE_COMMENTED_LINES(DoubleMetric.class),
 
     /**
      * The number of methods with a Test annotation.
      */
-    TEST_METHOD_COUNT,
+    TEST_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * The UML SVG file as a string.
      */
-    UML,
+    UML(StringMetric.class),
 
     /**
      * The maintainability index.
      */
-    MAINTAINABILITY_INDEX,
+    MAINTAINABILITY_INDEX(DoubleMetric.class),
 
     /**
      * The number of unique words in a class or method.
      */
-    UNIQUE_WORDS_COUNT,
+    UNIQUE_WORDS_COUNT(IntegerMetric.class),
 
     /**
      * The number of lambdas in a class or method.
      */
-    LAMBDAS_COUNT,
+    LAMBDAS_COUNT(IntegerMetric.class),
 
     /**
      * The number of subclasses in a class or method.
      */
-    SUBCLASSES_COUNT,
+    SUBCLASSES_COUNT(IntegerMetric.class),
 
     /**
      * The number of anonymous classes in a class or method.
      */
-    ANONYMOUS_CLASSES_COUNT,
+    ANONYMOUS_CLASSES_COUNT(IntegerMetric.class),
 
     /**
      * The maximum number of nested blocks in a class or method.
      */
-    MAX_NESTED_BLOCKS,
+    MAX_NESTED_BLOCKS(IntegerMetric.class),
 
     /**
      * The number of math operations in a class or method.
      */
-    MATH_OPERATIONS_COUNT,
+    MATH_OPERATIONS_COUNT(IntegerMetric.class),
 
     /**
      * The number of assignments in a class or method.
      */
-    ASSIGNMENTS_COUNT,
+    ASSIGNMENTS_COUNT(IntegerMetric.class),
 
     /**
      * The amount of numbers in a class or method.
      */
-    NUMBER_COUNT,
+    NUMBER_COUNT(IntegerMetric.class),
 
     /**
      * The amount of string literals in a class or method.
      */
-    STRING_LITERAL_COUNT,
+    STRING_LITERAL_COUNT(IntegerMetric.class),
 
     /**
      * The number of parenthesized expressions in a class or method.
      */
-    PARENTHESIZED_EXPRESSION_COUNT,
+    PARENTHESIZED_EXPRESSION_COUNT(IntegerMetric.class),
 
     /**
      * The number of try catch blocks in a class or method.
      */
-    TRY_CATCH_COUNT,
+    TRY_CATCH_COUNT(IntegerMetric.class),
 
     /**
      * The number of comparisons in a class or method.
      */
-    COMPARISON_COUNT,
+    COMPARISON_COUNT(IntegerMetric.class),
 
     /**
      * The number of loops in a class or method.
      */
-    LOOP_COUNT,
+    LOOP_COUNT(IntegerMetric.class),
 
     /**
      * The number of variables used in a class or method.
      */
-    VARIABLES_COUNT,
+    VARIABLES_COUNT(IntegerMetric.class),
 
     /**
      * The number of return calls in a class or method.
      */
-    RETURN_COUNT,
+    RETURN_COUNT(IntegerMetric.class),
 
     /**
      *The number of branch instructions in a class or method.
      */
-    WEIGHT_METHOD_CLASS,
+    WEIGHT_METHOD_CLASS(IntegerMetric.class),
 
     /**
      * The number of unique invocations in a class or method.
      */
-    RESPONSE_FOR_A_CLASS,
+    RESPONSE_FOR_A_CLASS(IntegerMetric.class),
 
     /**
      * Counts the number of dependencies a class has.
      */
-    COUPLING_BETWEEN_OBJECTS,
+    COUPLING_BETWEEN_OBJECTS(IntegerMetric.class),
 
     /**
      * Counts how many times each variable is used.
      */
-    VARIABLES_USAGE,
+    VARIABLES_USAGE(StringToIntegerMetric.class),
 
     /**
      * Counts how many times each field is used.
      */
-    FIELD_USAGE,
+    FIELD_USAGE(StringToIntegerMetric.class),
 
     /**
      * Counts the number of static methods.
      */
-    STATIC_METHOD_COUNT,
+    STATIC_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of public methods.
      */
-    PUBLIC_METHOD_COUNT,
+    PUBLIC_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of private methods.
      */
-    PRIVATE_METHOD_COUNT,
+    PRIVATE_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of protected methods.
      */
-    PROTECTED_METHOD_COUNT,
+    PROTECTED_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of synchronized methods.
      */
-    SYNCHRONIZED_METHOD_COUNT,
+    SYNCHRONIZED_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of final methods.
      */
-    FINAL_METHOD_COUNT,
+    FINAL_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of default methods.
      */
-    DEFAULT_METHOD_COUNT,
+    DEFAULT_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of abstract methods.
      */
-    ABSTRACT_METHOD_COUNT,
+    ABSTRACT_METHOD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of public fields.
      */
-    PUBLIC_FIELD_COUNT,
+    PUBLIC_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of private fields.
      */
-    PRIVATE_FIELD_COUNT,
+    PRIVATE_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of protected fields.
      */
-    PROTECTED_FIELD_COUNT,
+    PROTECTED_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of synchronized fields.
      */
-    SYNCHRONIZED_FIELD_COUNT,
+    SYNCHRONIZED_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of final fields.
      */
-    FINAL_FIELD_COUNT,
+    FINAL_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of default fields.
      */
-    DEFAULT_FIELD_COUNT,
+    DEFAULT_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of abstract fields.
      */
-    ABSTRACT_FIELD_COUNT,
+    ABSTRACT_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts the number of static fields.
      */
-    STATIC_FIELD_COUNT,
+    STATIC_FIELD_COUNT(IntegerMetric.class),
 
     /**
      * Counts how many time a static invocation is performed in a class.
      */
-    NUMBER_OF_STATIC_INVOCATIONS,
+    NUMBER_OF_STATIC_INVOCATIONS(IntegerMetric.class),
 
     /**
      * Calculates the lack of cohesion between methods.
      */
-    LACK_OF_COHESION_OF_METHODS,
+    LACK_OF_COHESION_OF_METHODS(IntegerMetric.class),
 
     /**
      * Calculates the depth of the inheritance tree.
      */
-    DEPTH_INHERITANCE_TREE,
+    DEPTH_INHERITANCE_TREE(IntegerMetric.class),
 
     /**
      * A list of test smells.
      */
-    TEST_SMELLS,
+    TEST_SMELLS(StringSetMetric.class),
 
     /**
      * A PyLint result.
      */
-    PYLINT,
+    PYLINT(PyLintResultMetric.class),
 
     /**
      * A list of duplicate code blocks.
      */
-    DUPLICATE_CODE_BLOCKS,
+    DUPLICATE_CODE_BLOCKS(DuplicateCodeBlockListMetric.class),
 
     /**
      * Suppressed files or entities.
      */
-    SUPPRESSIONS,
+    SUPPRESSIONS(SuppressedEntityCollectionMetric.class),
 
     /**
      * The number of effective LOC in a file. That is, excluding both comment lines and blank lines.
      */
-    FILE_EFFECTIVE_LOC,
+    FILE_EFFECTIVE_LOC(IntegerMetric.class),
 
     /**
      * The total number of assertions divided by the effective lines of source code.
      */
-    RELATIVE_NUMBER_OF_ASSERTIONS,
+    RELATIVE_NUMBER_OF_ASSERTIONS(DoubleMetric.class),
 
     /**
      * The total number of testcases divided by the effective lines of source code.
      */
-    RELATIVE_NUMBER_OF_TESTCASES,
+    RELATIVE_NUMBER_OF_TESTCASES(NumberMetric.class),
 
     /**
      * The average number of assertions per testcase.
      */
-    AVERAGE_ASSERTIONS_PER_TESTCASE,
+    AVERAGE_ASSERTIONS_PER_TESTCASE(NumberMetric.class),
 
     /**
      * The total complexity of the test cases divided by the complexity of the source code.
      */
-    RELATIVE_CYCLOMATIC_COMPLEXITY,
+    RELATIVE_CYCLOMATIC_COMPLEXITY(NumberMetric.class),
 
     /**
      * Coupling between objects of the test cases divided by the coupling in the source code.
      */
-    RELATIVE_COUPLING_BETWEEN_OBJECTS,
+    RELATIVE_COUPLING_BETWEEN_OBJECTS(NumberMetric.class),
 
     /**
      * The depth of the test cases divided by the depth of the source code.
      */
-    RELATIVE_DEPTH_INHERITANCE_TREE,
+    RELATIVE_DEPTH_INHERITANCE_TREE(NumberMetric.class),
 
     /**
      * Test WMC divided by source code WMC.
      */
-    RELATIVE_WEIGHT_METHOD_CLASS,
+    RELATIVE_WEIGHT_METHOD_CLASS(NumberMetric.class),
 
     /**
      * The ratio between test code and production code.
      */
-    TEST_PRODUCTION_RATIO,
+    TEST_PRODUCTION_RATIO(NumberMetric.class),
 
     /**
      * The profanity checker metric.
      */
-    PROFANITY,
+    PROFANITY(StringListMetric.class),
 
     /**
      * The list of failing tests.
      */
-    TESTS,
+    TESTS(StringMetric.class),
 
     /**
      * Test coverage percentage.
      */
-    TEST_COVERAGE,
+    TEST_COVERAGE(DoubleMetric.class),
 
     /**
      * Checkstyle complaints.
      */
-    CHECKSTYLE,
+    CHECKSTYLE(Metric.class),
 
     /**
      * SpotBugs/FindBugs complaints.
      */
-    SPOTBUGS;
+    SPOTBUGS(Metric.class);
+
+    /**
+     * The type of the metric produced by instances with this name.
+     */
+    private final Class<? extends Metric> type;
+
+    /**
+     * Creates a new metric name.
+     *
+     * @param type the type of metric produces by instances with this name
+     */
+    MetricName(final Class<? extends Metric> type) {
+        this.type = type;
+    }
+
+    /**
+     * Returns the type of metric instances with this name accept.
+     *
+     * @return the type
+     */
+    @Contract(pure = true)
+    public Class<? extends Metric> getType() {
+        return this.type;
+    }
+
+    /**
+     * Checks whether the given metric instance is compatible with this metric name.
+     *
+     * @param instance the metric instance to check
+     *
+     * @return if the metric instance is {@code null} or if the instance is not compatible with
+     *         the type of instances this metric name expects
+     */
+    @Contract(value = "null -> false", pure = true)
+    public boolean isMetricCompatible(final @Nullable Metric<?> instance) {
+        return instance != null && this.getType().isAssignableFrom(instance.getClass());
+    }
 
     /**
      * Converts the metric name to a name without underscores and in lower case.
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java
index 3c5de3ec94c9a2fae37e5629758fbefc4f4f2fd6..e8755447f02473dc564bf0fa08391cbdc8e4fbe0 100644
--- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java
@@ -1,8 +1,13 @@
 package nl.tudelft.ewi.auta.common.model.metric;
 
+import nl.tudelft.ewi.auta.common.annotation.Unmodifiable;
+import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase;
 import org.intellij.lang.annotations.Language;
 
 import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -27,6 +32,11 @@ public class MetricSettings {
     @Nullable
     private String passingScript;
 
+    /**
+     * Unit tests for {@link #passingScript}.
+     */
+    private Collection<PassingScriptTestCase> passingScriptTestCases = Collections.emptyList();
+
     /**
      * The JavaScript function to use to format the results.
      */
@@ -67,15 +77,7 @@ public class MetricSettings {
             final @Nullable Integer maxWarnings,
             final @Nullable Integer maxFailures
     ) {
-        this.metric = metric;
-        this.passingScript = passingScript;
-        this.formattingScript = formattingScript;
-        if (maxWarnings != null) {
-            this.maxWarnings = maxWarnings;
-        }
-        if (maxFailures != null) {
-            this.maxFailures = maxFailures;
-        }
+        this(metric, passingScript, formattingScript, maxWarnings, maxFailures, null);
     }
 
     /**
@@ -126,9 +128,11 @@ public class MetricSettings {
     public MetricSettings(final MetricSettings other) {
         this.metric = other.metric;
         this.passingScript = other.passingScript;
+        this.passingScriptTestCases = List.copyOf(other.passingScriptTestCases);
         this.formattingScript = other.formattingScript;
         this.maxWarnings = other.maxWarnings;
         this.maxFailures = other.maxFailures;
+        this.dockerfile = other.dockerfile;
     }
 
     /**
@@ -170,6 +174,25 @@ public class MetricSettings {
         this.passingScript = passingScript;
     }
 
+    /**
+     * Returns the unit tests for the metric's passing script.
+     *
+     * @return the unit tests
+     */
+    @Unmodifiable
+    public Collection<PassingScriptTestCase> getPassingScriptTestCases() {
+        return this.passingScriptTestCases;
+    }
+
+    /**
+     * Sets the unit tests for the metric's passing script.
+     *
+     * @param tests the unit tests
+     */
+    public void setPassingScriptTestCases(final Collection<PassingScriptTestCase> tests) {
+        this.passingScriptTestCases = List.copyOf(tests);
+    }
+
     /**
      * Returns the script used to format the results.
      *
@@ -230,13 +253,15 @@ public class MetricSettings {
                 && Objects.equals(this.formattingScript, other.formattingScript)
                 && Objects.equals(this.maxFailures, other.maxFailures)
                 && Objects.equals(this.maxWarnings, other.maxWarnings)
-                && Objects.equals(this.dockerfile, other.dockerfile);
+                && Objects.equals(this.dockerfile, other.dockerfile)
+                && Objects.equals(this.passingScriptTestCases, other.passingScriptTestCases);
     }
 
     @Override
     public final int hashCode() {
         return Objects.hash(this.metric, this.passingScript, this.formattingScript,
-                this.maxWarnings, this.maxFailures, this.dockerfile
+                this.maxWarnings, this.maxFailures, this.dockerfile,
+                this.passingScriptTestCases
         );
     }
 }
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/NumberMetric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/NumberMetric.java
new file mode 100644
index 0000000000000000000000000000000000000000..f2e6d14a3fca176c8e4d2e56c406dfeccc650643
--- /dev/null
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/NumberMetric.java
@@ -0,0 +1,18 @@
+package nl.tudelft.ewi.auta.common.model.metric;
+
+/**
+ * A type for metrics that contain single numbers.
+ *
+ * @param <T> the exact type of number stored
+ */
+public class NumberMetric<T extends Number> extends Metric<T> {
+    /**
+     * Creates a new number metric.
+     *
+     * @param value the value of the metric
+     * @param name the name of the metric
+     */
+    public NumberMetric(final T value, final MetricName name) {
+        super(value, name);
+    }
+}
diff --git a/src/main/resources/nl/tudelft/ewi/auta/common/model/metric/test-fixtures.json b/src/main/resources/nl/tudelft/ewi/auta/common/model/metric/test-fixtures.json
new file mode 100644
index 0000000000000000000000000000000000000000..a7165a9e3c9e8ec8c44548df06a1dbc0733721e6
--- /dev/null
+++ b/src/main/resources/nl/tudelft/ewi/auta/common/model/metric/test-fixtures.json
@@ -0,0 +1,428 @@
+{
+  "fixtures": {
+    "ASSEMBLY_INSTRUCTION_COUNT": [
+      3,
+      1461
+    ],
+    "ASSEMBLY_RECURSION_TARGET": [
+      [],
+      [
+        "factorial"
+      ],
+      [
+        "mult",
+        "main",
+        "factorial"
+      ]
+    ],
+    "ASSERTIONS_PER_TEST": [
+      0,
+      1,
+      3
+    ],
+    "COMMENTED_LINE_COUNT": [
+      0,
+      150
+    ],
+    "CONSTRUCTOR_COUNT": [
+      0,
+      1,
+      3,
+      14
+    ],
+    "CYCLOMATIC_COMPLEXITY": [
+      1,
+      4,
+      9,
+      12,
+      16,
+      759
+    ],
+    "DOCKER_LOGS": [
+      "BUILD FAILED",
+      "BUILD SUCCESS"
+    ],
+    "FIELD_COUNT": [
+      0,
+      3,
+      28
+    ],
+    "FILE_COUNT": [
+      0,
+      1,
+      8,
+      114
+    ],
+    "JAVADOC_EXISTS": [
+      false,
+      true
+    ],
+    "JAVADOC_VIOLATIONS": [
+      [],
+      [
+        "Missing @return statement"
+      ],
+      [
+        "Missing @param tag for parameter arg",
+        "Missing @return statement"
+      ]
+    ],
+    "LINE_LENGTH": [
+      [],
+      [
+        2,
+        2
+      ],
+      [
+        16,
+        0,
+        3,
+        72,
+        0,
+        1
+      ],
+      [
+        16,
+        0,
+        3,
+        214,
+        91,
+        18,
+        22,
+        0
+      ]
+    ],
+    "LINES_OF_CODE": [
+      0,
+      13,
+      182,
+      638,
+      21047
+    ],
+    "METHOD_COUNT": [
+      0,
+      2,
+      38
+    ],
+    "METHOD_EFFECTIVE_LOC": [
+      0,
+      8,
+      9131
+    ],
+    "PARAMETER_COUNT": [
+      0,
+      3,
+      7,
+      225
+    ],
+    "PERCENTAGE_COMMENTED_LINES": [
+      0.0,
+      0.23,
+      0.79,
+      1.0
+    ],
+    "TEST_METHOD_COUNT": [
+      0,
+      5,
+      413
+    ],
+    "MAINTAINABILITY_INDEX": [
+      0.0,
+      0.08,
+      0.13,
+      0.61,
+      1.0
+    ],
+    "UNIQUE_WORDS_COUNT": [
+      5,
+      1047
+    ],
+    "LAMBDAS_COUNT": [
+      0,
+      1,
+      17
+    ],
+    "SUBCLASSES_COUNT": [
+      0,
+      1,
+      12
+    ],
+    "ANONYMOUS_CLASSES_COUNT": [
+      0,
+      1,
+      3,
+      162
+    ],
+    "MAX_NESTED_BLOCKS": [
+      0,
+      2,
+      59
+    ],
+    "MATH_OPERATIONS_COUNT": [
+      0,
+      1,
+      10,
+      12065
+    ],
+    "ASSIGNMENTS_COUNT": [
+      0,
+      4,
+      721
+    ],
+    "NUMBER_COUNT": [
+      0,
+      2,
+      132
+    ],
+    "STRING_LITERAL_COUNT": [
+      0,
+      6,
+      613
+    ],
+    "PARENTHESIZED_EXPRESSION_COUNT": [
+      0,
+      1,
+      75
+    ],
+    "TRY_CATCH_COUNT": [
+      0,
+      1,
+      5,
+      48
+    ],
+    "COMPARISON_COUNT": [
+      0,
+      1,
+      90
+    ],
+    "LOOP_COUNT": [
+      0,
+      1,
+      14
+    ],
+    "VARIABLES_COUNT": [
+      0,
+      3,
+      53
+    ],
+    "RETURN_COUNT": [
+      0,
+      1,
+      3,
+      326
+    ],
+    "WEIGHT_METHOD_CLASS": [
+      0,
+      6,
+      527
+    ],
+    "RESPONSE_FOR_A_CLASS": [
+      0,
+      12,
+      1895
+    ],
+    "COUPLING_BETWEEN_OBJECTS": [
+      0,
+      4,
+      37,
+      172
+    ],
+    "VARIABLES_USAGE": [
+      {},
+      {
+        "i": 14
+      }
+    ],
+    "FIELD_USAGE": [
+      {},
+      {
+        "value": 3
+      }
+    ],
+    "STATIC_METHOD_COUNT": [
+      0,
+      1,
+      9
+    ],
+    "PUBLIC_METHOD_COUNT": [
+      0,
+      1,
+      8,
+      37
+    ],
+    "PRIVATE_METHOD_COUNT": [
+      0,
+      1,
+      5,
+      47
+    ],
+    "PROTECTED_METHOD_COUNT": [
+      0,
+      1,
+      3,
+      39
+    ],
+    "SYNCHRONIZED_METHOD_COUNT": [
+      0,
+      1,
+      20
+    ],
+    "FINAL_METHOD_COUNT": [
+      0,
+      1,
+      7
+    ],
+    "DEFAULT_METHOD_COUNT": [
+      0,
+      1,
+      3
+    ],
+    "ABSTRACT_METHOD_COUNT": [
+      0,
+      1,
+      6
+    ],
+    "PUBLIC_FIELD_COUNT": [
+      0,
+      1,
+      8,
+      37
+    ],
+    "PRIVATE_FIELD_COUNT": [
+      0,
+      1,
+      5,
+      47
+    ],
+    "PROTECTED_FIELD_COUNT": [
+      0,
+      1,
+      3,
+      39
+    ],
+    "SYNCHRONIZED_FIELD_COUNT": [
+      0,
+      1,
+      20
+    ],
+    "FINAL_FIELD_COUNT": [
+      0,
+      1,
+      7
+    ],
+    "DEFAULT_FIELD_COUNT": [
+      0,
+      1,
+      3
+    ],
+    "ABSTRACT_FIELD_COUNT": [
+      0,
+      1,
+      6
+    ],
+    "STATIC_FIELD_COUNT": [
+      0,
+      1,
+      9
+    ],
+    "NUMBER_OF_STATIC_INVOCATIONS": [
+      0,
+      3,
+      27
+    ],
+    "LACK_OF_COHESION_OF_METHODS": [
+      13,
+      2,
+      0
+    ],
+    "DEPTH_INHERITANCE_TREE": [
+      0,
+      1,
+      29
+    ],
+    "TEST_SMELLS": [
+      [],
+      [
+        "sleepy"
+      ]
+    ],
+    "PYLINT": [
+      []
+    ],
+    "DUPLICATE_CODE_BLOCKS": [
+      [],
+      [
+        {
+          "filenames": [],
+          "lineNumbers": [],
+          "lineCount": 3,
+          "tokenCount": 14,
+          "occurences": 2
+        }
+      ]
+    ],
+    "SUPPRESSIONS": [
+      [],
+      [
+        {
+          "name": "TestClass",
+          "justification": "for testing"
+        }
+      ]
+    ],
+    "FILE_EFFECTIVE_LOC": [
+      2,
+      38,
+      2162
+    ],
+    "RELATIVE_NUMBER_OF_ASSERTIONS": [
+      1,
+      0.0,
+      0.0021,
+      0.0153
+    ],
+    "RELATIVE_NUMBER_OF_TESTCASES": [
+      0.0,
+      0.0019,
+      0.0139
+    ],
+    "AVERAGE_ASSERTIONS_PER_TESTCASE": [
+      0.893,
+      1.027,
+      2.103
+    ],
+    "RELATIVE_CYCLOMATIC_COMPLEXITY": [
+      0.00174,
+      0.0248,
+      0.1659
+    ],
+    "RELATIVE_COUPLING_BETWEEN_OBJECTS": [
+      0.0437,
+      0.115,
+      0.721
+    ],
+    "RELATIVE_DEPTH_INHERITANCE_TREE": [
+      0.000274,
+      0.00164,
+      0.0512
+    ],
+    "RELATIVE_WEIGHT_METHOD_CLASS": [
+      0.5
+    ],
+    "TEST_PRODUCTION_RATIO": [
+      0.0,
+      0.8,
+      1.0,
+      1.2
+    ],
+    "PROFANITY": [
+      [],
+      [
+        "Line 6 contains profanity"
+      ]
+    ],
+    "TESTS": [
+      "Assertion failure"
+    ]
+  }
+}
diff --git a/src/test/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCaseTest.java b/src/test/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCaseTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b3615f100fc3e24ead5cf957ddbf880e5557f20f
--- /dev/null
+++ b/src/test/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCaseTest.java
@@ -0,0 +1,14 @@
+package nl.tudelft.ewi.auta.common.model;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import nl.jqno.equalsverifier.Warning;
+import org.junit.jupiter.api.Test;
+
+public class PassingScriptTestCaseTest {
+    @Test
+    public void testEquals() {
+        EqualsVerifier.forClass(PassingScriptTestCase.class)
+                .suppress(Warning.NONFINAL_FIELDS)
+                .verify();
+    }
+}
diff --git a/src/test/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProviderTest.java b/src/test/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProviderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0d815e501c683636f74dc5c84647eba32893ded
--- /dev/null
+++ b/src/test/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProviderTest.java
@@ -0,0 +1,18 @@
+package nl.tudelft.ewi.auta.common.model.metric;
+
+import com.google.gson.Gson;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MetricFixturesProviderTest {
+    @Test
+    public void testGetFixtures() {
+        final var provider = new MetricFixturesProvider(new Gson());
+        final var fixtures = provider.getFixtures();
+
+        // Basic metrics that should have fixtures as a baseline
+        assertThat(fixtures)
+                .containsKeys(MetricName.CYCLOMATIC_COMPLEXITY, MetricName.LINES_OF_CODE);
+    }
+}
diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java b/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java
index ec35bdb299192eff0af2877d4c76a45d1259dd37..04066d3e45d287bcdbe00c1e2a4a17db0e104bd6 100644
--- a/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java
+++ b/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java
@@ -200,6 +200,7 @@ public class ControlProcessor {
 
                     metric.put("name", metricName);
                     metric.put("internal_name", metricName);
+                    metric.put("value_type", metricName.getType().getSimpleName());
                     metric.put("languages", analyzer.getLanguages());
                     metric.put("class", analyzer.getType() == Job.class ? "dynamic" : "static");
                     metric.put("script_presets", scriptPresets.get(metricName));