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/pom.xml b/core/pom.xml
index e34cb44596..532b1d4d04 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -23,36 +23,14 @@
         <dependency>
             <groupId>org.glassfish.jersey.core</groupId>
             <artifactId>jersey-client</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.glassfish.hk2.external</groupId>
-                    <artifactId>javax.inject</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
         <dependency>
             <groupId>org.glassfish.jersey.media</groupId>
             <artifactId>jersey-media-jaxb</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.glassfish.hk2.external</groupId>
-                    <artifactId>javax.inject</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
         <dependency>
             <groupId>org.glassfish.jersey.media</groupId>
             <artifactId>jersey-media-json-jackson</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.glassfish.hk2.external</groupId>
-                    <artifactId>javax.inject</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-        <dependency>
-            <groupId>org.glassfish.jersey.inject</groupId>
-            <artifactId>jersey-hk2</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
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 267267e988..352c43456a 100644
--- a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
+++ b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
@@ -1033,11 +1033,6 @@ public class CoreConfig {
         );
     }
 
-    @Bean
-    public DataInitializer dataInitializer() {
-        return new DataInitializer();
-    }
-
     @Bean
     public FinalSeminarActivityHandler finalSeminarActivityHandler(
         ActivityPlanFacade activityPlanFacade,
diff --git a/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java b/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java
index 9dc48661d9..b21a586607 100755
--- a/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java
+++ b/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java
@@ -176,18 +176,16 @@ public class ApplicationPeriod extends DomainObject {
         return Collections.unmodifiableSet(answerSet);
     }
 
-    public void setProjectTypes(Iterable<ProjectType> projectTypes) {
-        this.projectTypes.clear();
+    public void setProjectTypes(Set<ProjectType> projectTypes) {
+        this.projectTypes.removeIf(appt -> !projectTypes.contains(appt.getProjectType()));
         for (ProjectType pt : projectTypes) {
-            this.projectTypes.add(new ApplicationPeriodProjectType(this, pt));
+            if (this.projectTypes.stream().noneMatch(appt -> appt.getProjectType().equals(pt))) {
+                addProjectType(pt);
+            }
         }
     }
 
     public void addProjectType(ProjectType projectType) {
         this.projectTypes.add(new ApplicationPeriodProjectType(this, projectType));
     }
-
-    public void removeProjectType(ProjectType projectType) {
-        this.projectTypes.removeIf(next -> next.getProjectType().equals(projectType));
-    }
 }
diff --git a/pom.xml b/pom.xml
index a43a6491cd..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>
@@ -295,13 +296,6 @@
             <artifactId>slf4j-api</artifactId>
         </dependency>
 
-        <!-- Additional dependencies -->
-        <dependency>
-            <groupId>javax.inject</groupId>
-            <artifactId>javax.inject</artifactId>
-            <version>1</version>
-        </dependency>
-
         <!-- Test stuff -->
         <dependency>
             <groupId>org.junit.jupiter</groupId>
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 98%
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 466ec822ee..cf01ab2315 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;
@@ -51,13 +51,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 Optional<Collection<TestDataPopulator>> testDataPopulators = Optional.empty();
+
     @Inject
     private UserService userService;
 
@@ -146,6 +149,10 @@ public class DataInitializer implements Lifecycle {
             createRoughDraftApproval();
             createPastFinalSeminar();
             setUpNotifications();
+            Collection<TestDataPopulator> availablePopulators = testDataPopulators.orElseGet(Collections::emptySet);
+            for (TestDataPopulator testDataPopulator : availablePopulators) {
+                testDataPopulator.populate(this, this);
+            }
         }
         if (profile.getCurrentProfile() == Profiles.DEV && noAdminUser()) {
             createAdmin();
@@ -310,13 +317,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();
@@ -2151,6 +2163,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/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java b/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java
index 307d7858f3..b8bb7d9e76 100755
--- a/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java
+++ b/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java
@@ -123,13 +123,7 @@ public class AdminApplicationPeriodsPanel extends Panel {
                     item.add(
                         new DisplayMultiplesPanel<>(
                             s,
-                            new ListAdapterModel<>(
-                                LambdaModel.of(
-                                    iModel,
-                                    ApplicationPeriod::getProjectTypes,
-                                    ApplicationPeriod::setProjectTypes
-                                )
-                            )
+                            new ListAdapterModel<>(iModel.map(ApplicationPeriod::getProjectTypes))
                         ) {
                             @Override
                             public Component getComponent(String componentId, IModel<ProjectType> t) {
diff --git a/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java b/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java
index 2f4d027af5..8c4c98a6d0 100644
--- a/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java
+++ b/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java
@@ -22,12 +22,7 @@ public class AddTargetLinkPanel extends Panel {
     public AddTargetLinkPanel(String id, final IModel<ApplicationPeriod> model) {
         super(id, model);
         add(
-            new ListView<>(
-                "list",
-                new ListAdapterModel<>(
-                    LambdaModel.of(model, ApplicationPeriod::getProjectTypes, ApplicationPeriod::setProjectTypes)
-                )
-            ) {
+            new ListView<>("list", new ListAdapterModel<>(model.map(ApplicationPeriod::getProjectTypes))) {
                 @Override
                 protected void populateItem(ListItem<ProjectType> item) {
                     item.add(new Label("pc", item.getModelObject().getName()));
diff --git a/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java b/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java
index a70588801a..ca3736e027 100755
--- a/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java
+++ b/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java
@@ -76,13 +76,7 @@ public class ProjectPartnerPage extends AbstractIdeaProjectPage implements MenuH
             }
         );
         final IModel<? extends List<ProjectType>> matchableTypes = getMatchableTypes(
-            new ListAdapterModel<>(
-                LambdaModel.of(
-                    applicationPeriod,
-                    ApplicationPeriod::getProjectTypes,
-                    ApplicationPeriod::setProjectTypes
-                )
-            )
+            new ListAdapterModel<>(applicationPeriod.map(ApplicationPeriod::getProjectTypes))
         );
         panelContainer.add(
             new ListView<>("ads", matchableTypes) {
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>
diff --git a/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java b/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
index 6f209f38aa..3d71fd12a3 100644
--- a/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
+++ b/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
@@ -4,6 +4,7 @@ import jakarta.inject.Inject;
 import jakarta.inject.Provider;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import java.io.Serializable;
 import java.security.Principal;
 import java.util.Collections;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -81,7 +82,7 @@ public class CurrentUserFromSpringSecurity implements AuthenticationContext {
         return authentication.getName();
     }
 
-    private static final class WicketControlledPrincipal implements Principal {
+    private static final class WicketControlledPrincipal implements Principal, Serializable {
 
         private final String username;