diff --git a/.gitignore b/.gitignore
index a4397c6105..8393ae6082 100755
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,5 @@ fitnesse/target/
 daisy-integration/target/
 war/target/
 api/target/
+test-data/target/
 node_modules/
diff --git a/Dockerfile b/Dockerfile
index ac53fd07f3..acf3c889a3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,12 +12,14 @@ COPY core/pom.xml core/pom.xml
 COPY view/pom.xml view/pom.xml
 COPY war/pom.xml war/pom.xml
 COPY daisy-integration/pom.xml daisy-integration/pom.xml
+COPY test-data/pom.xml test-data/pom.xml
 
 COPY api/src/ api/src/
 COPY core/src/ core/src/
 COPY view/src/ view/src/
 COPY war/src/ war/src/
 COPY daisy-integration/src/ daisy-integration/src/
+COPY test-data/src/ test-data/src/
 
 RUN ./mvnw package \
     --define skipTests \
diff --git a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
index b7c51b41f2..19b8f04a13 100644
--- a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
+++ b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
@@ -1017,11 +1017,6 @@ public class CoreConfig {
         );
     }
 
-    @Bean
-    public DataInitializer dataInitializer() {
-        return new DataInitializer();
-    }
-
     @Bean
     public FinalSeminarActivityHandler finalSeminarActivityHandler(
         ActivityPlanFacade activityPlanFacade,
diff --git a/pom.xml b/pom.xml
index 1940f2ed25..896b1269e5 100755
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
         <module>daisy-integration</module>
         <module>war</module>
         <module>api</module>
+        <module>test-data</module>
     </modules>
 
     <properties>
diff --git a/test-data/pom.xml b/test-data/pom.xml
new file mode 100644
index 0000000000..127cef4d71
--- /dev/null
+++ b/test-data/pom.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>se.su.dsv.scipro</groupId>
+        <artifactId>SciPro</artifactId>
+        <version>0.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>test-data</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>se.su.dsv.scipro</groupId>
+            <artifactId>core</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
new file mode 100644
index 0000000000..1dd6b18132
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
@@ -0,0 +1,19 @@
+package se.su.dsv.scipro.testdata;
+
+import se.su.dsv.scipro.system.ProjectType;
+
+/// All the base test data that can be re-used in different test cases.
+///
+/// **Do not modify any of this data.** There are many
+/// [TestDataPopulator]s that rely on this data to be in a specific state.
+///
+/// In addition to the data that is available here there is also much additional
+/// data that has been created;
+///
+///   - A grading report template for each [ProjectType]
+///
+public interface BaseData {
+    ProjectType bachelor();
+    ProjectType magister();
+    ProjectType master();
+}
diff --git a/core/src/main/java/se/su/dsv/scipro/DataInitializer.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
similarity index 99%
rename from core/src/main/java/se/su/dsv/scipro/DataInitializer.java
rename to test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
index 15a093006b..8b157598c4 100644
--- a/core/src/main/java/se/su/dsv/scipro/DataInitializer.java
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
@@ -1,4 +1,4 @@
-package se.su.dsv.scipro;
+package se.su.dsv.scipro.testdata;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Provider;
@@ -34,13 +34,16 @@ import se.su.dsv.scipro.security.auth.roles.Roles;
 import se.su.dsv.scipro.system.*;
 import se.su.dsv.scipro.util.Pair;
 
-public class DataInitializer implements Lifecycle {
+public class DataInitializer implements Lifecycle, BaseData, Factory {
 
     public static final int APPLICATION_PERIOD_START_MINUS_DAYS = 1;
     public static final int APPLICATION_PERIOD_END_PLUS_DAYS = 3;
     public static final int APPLICATION_PERIOD_COURSE_START_PLUS_DAYS = 5;
     public static final long RESEARCH_AREA_ID = 12L;
 
+    @Inject
+    private Collection<TestDataPopulator> testDataPopulators = new ArrayList<>();
+
     @Inject
     private UserService userService;
 
@@ -120,6 +123,9 @@ public class DataInitializer implements Lifecycle {
             createTarget();
             createStudentIdea();
             createRoughDraftApproval();
+            for (TestDataPopulator testDataPopulator : testDataPopulators) {
+                testDataPopulator.populate(this, this);
+            }
         }
         if (profile.getCurrentProfile() == Profiles.DEV && noAdminUser()) {
             createAdmin();
@@ -243,13 +249,18 @@ public class DataInitializer implements Lifecycle {
         sofia_student = createStudent("Sofia", 17);
     }
 
-    private User createStudent(String firstName, int identifier) {
+    private User createStudent(String firstName) {
         User user = createUser(firstName, STUDENT_LAST);
-        user.setIdentifier(identifier);
         createBeta(user);
         return user;
     }
 
+    private User createStudent(String firstName, int identifier) {
+        User user = createStudent(firstName);
+        user.setIdentifier(identifier);
+        return user;
+    }
+
     private User createEmployee(String firstName) {
         User user = createUser(firstName, EMPLOYEE_LAST);
         Unit u = createUnit();
@@ -2087,6 +2098,36 @@ public class DataInitializer implements Lifecycle {
         return entity;
     }
 
+    @Override
+    public ProjectType bachelor() {
+        return bachelorClass;
+    }
+
+    @Override
+    public ProjectType magister() {
+        return magisterClass;
+    }
+
+    @Override
+    public ProjectType master() {
+        return masterClass;
+    }
+
+    @Override
+    public User createAuthor(String firstName) {
+        return createStudent(firstName);
+    }
+
+    @Override
+    public User createSupervisor(String firstName) {
+        return createEmployee(firstName);
+    }
+
+    @Override
+    public User createReviewer(String firstName) {
+        return createEmployee(firstName);
+    }
+
     private static final class SimpleTextFile implements FileUpload {
 
         private final User uploader;
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/Factory.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/Factory.java
new file mode 100644
index 0000000000..fc4c0910f1
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/Factory.java
@@ -0,0 +1,46 @@
+package se.su.dsv.scipro.testdata;
+
+import se.su.dsv.scipro.security.auth.roles.Roles;
+import se.su.dsv.scipro.system.User;
+
+/**
+ * A factory to help with repetitive tasks when populating test data.
+ */
+public interface Factory {
+    /**
+     * Creates a user with the given first name and last name "Student".
+     * The user is given the role {@link Roles#AUTHOR}.
+     * <p>
+     * A username is created of the form {@code <first_name>@example.com} that
+     * can be used to log in.
+     */
+    User createAuthor(String firstName);
+
+    /**
+     * Creates a user with the given first name and last name "Employee".
+     * <p>
+     * The user is given the role {@link Roles#SUPERVISOR}, {@link Roles#REVIEWER},
+     * and {@link Roles#EXAMINER}.
+     * <p>
+     * The user gets a default research area, unit, and language. It is also
+     * marked as {@link User#setActiveAsSupervisor(boolean) an active supervisor}.
+     * <p>
+     * A username is created of the form {@code <first_name>@example.com} that
+     * can be used to log in.
+     */
+    User createSupervisor(String firstName);
+
+    /**
+     * Creates a user with the given first name and last name "Employee".
+     * <p>
+     * The user is given the role {@link Roles#SUPERVISOR}, {@link Roles#REVIEWER},
+     * and {@link Roles#EXAMINER}.
+     * <p>
+     * The user gets a default research area, unit, and language. It is also
+     * marked as {@link User#setActiveAsSupervisor(boolean) an active supervisor}.
+     * <p>
+     * A username is created of the form {@code <first_name>@example.com} that
+     * can be used to log in.
+     */
+    User createReviewer(String firstName);
+}
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataConfiguration.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataConfiguration.java
new file mode 100644
index 0000000000..ffd372628b
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataConfiguration.java
@@ -0,0 +1,16 @@
+package se.su.dsv.scipro.testdata;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import se.su.dsv.scipro.war.PluginConfiguration;
+
+@Configuration(proxyBeanMethods = false)
+@ComponentScan(basePackages = "se.su.dsv.scipro.testdata.populators")
+public class TestDataConfiguration implements PluginConfiguration {
+
+    @Bean
+    public DataInitializer dataInitializer() {
+        return new DataInitializer();
+    }
+}
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataPopulator.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataPopulator.java
new file mode 100644
index 0000000000..41d250ac71
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataPopulator.java
@@ -0,0 +1,11 @@
+package se.su.dsv.scipro.testdata;
+
+public interface TestDataPopulator {
+    /**
+     * Add test data to the system to help with testing a specific feature.
+     *
+     * @param baseData the base data already populated
+     * @param factory helper object to make repetitive tasks easier (such as creating users)
+     */
+    void populate(BaseData baseData, Factory factory);
+}
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/package-info.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/package-info.java
new file mode 100644
index 0000000000..6b93353f29
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * This package contains the infrastructure that is used when generating test data for the application. To add new test
+ * data to the system, add a new class to the {@link se.su.dsv.scipro.testdata.populators} package that implements the
+ * {@link se.su.dsv.scipro.testdata.TestDataPopulator} interface and annotate it with
+ * {@link org.springframework.stereotype.Service @Service}. Inject dependencies as needed using
+ * {@link jakarta.inject.Inject @Inject}.
+ */
+package se.su.dsv.scipro.testdata;
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/package-info.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/package-info.java
new file mode 100644
index 0000000000..c9d116ca5a
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * Contains classes that populate the database with test data.
+ * <p>
+ * Prefer to use methods on the various services to create data, rather than directly interacting with the database
+ * using an {@link jakarta.persistence.EntityManager}. This is to make sure all business rules are enforced and that
+ * any additional logic is executed such as sending notifications or calculating statistics.
+ *
+ * @see se.su.dsv.scipro.testdata how to add new populators
+ * @see se.su.dsv.scipro.testdata.TestDataPopulator
+ * @see se.su.dsv.scipro.testdata.BaseData
+ * @see se.su.dsv.scipro.testdata.Factory
+ */
+package se.su.dsv.scipro.testdata.populators;
diff --git a/test-data/src/main/resources/META-INF/services/se.su.dsv.scipro.war.PluginConfiguration b/test-data/src/main/resources/META-INF/services/se.su.dsv.scipro.war.PluginConfiguration
new file mode 100644
index 0000000000..99b97f03e4
--- /dev/null
+++ b/test-data/src/main/resources/META-INF/services/se.su.dsv.scipro.war.PluginConfiguration
@@ -0,0 +1 @@
+se.su.dsv.scipro.testdata.TestDataConfiguration
diff --git a/war/pom.xml b/war/pom.xml
index c1dd889587..6a347c9b09 100644
--- a/war/pom.xml
+++ b/war/pom.xml
@@ -140,5 +140,18 @@
                 <spring.profile.active>branch</spring.profile.active>
             </properties>
         </profile>
+        <profile>
+            <id>DEV</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>se.su.dsv.scipro</groupId>
+                    <artifactId>test-data</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+            </dependencies>
+        </profile>
     </profiles>
 </project>