From ac903594790b65fcd912f501916a98093b862300 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sun, 27 Feb 2022 00:13:31 +0100
Subject: [PATCH] #663 - use testcontainers to run selenium tests against
 postgres database from docker

---
 pom.xml                                       |  14 ++++
 .../DatabaseConfigurationProperties.java      |   6 +-
 ...reAllIconizableHaveAnIconInstanceTest.java |   1 -
 .../integration/helpers/SeleniumTestBase.java |  62 +++++++++++-------
 .../SeleniumTestExecutionListener.java        |  50 ++++++++++----
 .../helpers/SeleniumTestWatcher.java          |  21 ------
 .../integration/selenium/AccountTest.java     |  11 ++--
 .../selenium/CategorySelectTest.java          |   7 +-
 .../selenium/ChangeTransactionTypeTest.java   |   6 +-
 .../integration/selenium/ChartTest.java       |   6 +-
 .../integration/selenium/FirstUseTest.java    |   6 +-
 .../integration/selenium/HotkeyTest.java      |   6 +-
 .../integration/selenium/ImportTest.java      |  40 -----------
 .../selenium/LoginControllerTest.java         |   7 ++
 .../integration/selenium/MediaTest.java       |   6 +-
 .../NewTransactionFromExistingOneTest.java    |   6 +-
 .../NewTransactionFromTemplateTest.java       |   6 +-
 .../selenium/NewTransactionNormalTest.java    |   6 +-
 .../selenium/NewTransactionRecurringTest.java |   6 +-
 .../selenium/NewTransactionTransferTest.java  |   6 +-
 .../integration/selenium/SearchTest.java      |   6 +-
 .../integration/selenium/TagTest.java         |   6 +-
 .../integration/selenium/WhatsNewTest.java    |   6 +-
 .../unit/TransactionServiceDatabaseTest.java  |   9 ++-
 src/test/resources/repeating_with_tags.mv.db  | Bin 86016 -> 86016 bytes
 25 files changed, 160 insertions(+), 146 deletions(-)
 delete mode 100644 src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestWatcher.java
 delete mode 100644 src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ImportTest.java

diff --git a/pom.xml b/pom.xml
index 1ac34c171..22df26f5a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,6 +74,7 @@
         <vanilla-picker.version>2.12.1</vanilla-picker.version>
         <jacoco-maven-plugin.version>0.8.8</jacoco-maven-plugin.version>
         <dependency-check-maven.version>6.5.3</dependency-check-maven.version>
+        <testcontainer.version>1.16.3</testcontainer.version>
 
         <app.versionDate>${maven.build.timestamp}</app.versionDate>
         <maven.build.timestamp.format>dd.MM.yy</maven.build.timestamp.format>
@@ -253,6 +254,19 @@
             <artifactId>mockito-inline</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>${testcontainer.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>postgresql</artifactId>
+            <version>${testcontainer.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/de/deadlocker8/budgetmaster/utils/DatabaseConfigurationProperties.java b/src/main/java/de/deadlocker8/budgetmaster/utils/DatabaseConfigurationProperties.java
index 987558e5f..5861dea82 100644
--- a/src/main/java/de/deadlocker8/budgetmaster/utils/DatabaseConfigurationProperties.java
+++ b/src/main/java/de/deadlocker8/budgetmaster/utils/DatabaseConfigurationProperties.java
@@ -17,7 +17,7 @@ public class DatabaseConfigurationProperties
 
 	@Min(1)
 	@Max(65536)
-	private int port;
+	private Integer port;
 
 	@NotBlank
 	private String databaseName;
@@ -48,12 +48,12 @@ public class DatabaseConfigurationProperties
 		this.hostname = hostname;
 	}
 
-	public int getPort()
+	public Integer getPort()
 	{
 		return port;
 	}
 
-	public void setPort(int port)
+	public void setPort(Integer port)
 	{
 		this.port = port;
 	}
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/EnsureAllIconizableHaveAnIconInstanceTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/EnsureAllIconizableHaveAnIconInstanceTest.java
index 2f790b17f..9f96e87e5 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/EnsureAllIconizableHaveAnIconInstanceTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/EnsureAllIconizableHaveAnIconInstanceTest.java
@@ -30,7 +30,6 @@ import static org.assertj.core.api.Assertions.assertThat;
 @SpringBootTest(classes = Main.class)
 @Import(EnsureAllIconizableHaveAnIconInstanceTest.TestDatabaseConfiguration.class)
 @ActiveProfiles("test")
-@SeleniumTest
 @Transactional
 class EnsureAllIconizableHaveAnIconInstanceTest
 {
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestBase.java b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestBase.java
index cab15704c..cea7734e1 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestBase.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestBase.java
@@ -1,45 +1,63 @@
 package de.deadlocker8.budgetmaster.integration.helpers;
 
 import de.deadlocker8.budgetmaster.Main;
-import org.junit.jupiter.api.*;
-import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.BeforeEach;
 import org.openqa.selenium.WebDriver;
-import org.openqa.selenium.firefox.FirefoxDriver;
-import org.openqa.selenium.firefox.FirefoxOptions;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.web.server.LocalServerPort;
 import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
 
+
+@Testcontainers
 @SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-@ExtendWith(SeleniumTestWatcher.class)
 @DirtiesContext
 @SeleniumTest
-@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-public class SeleniumTestBase
+@ActiveProfiles("test")
+public abstract class SeleniumTestBase
 {
+	@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+	@Autowired
 	protected WebDriver driver;
 
 	@LocalServerPort
 	protected int port;
 
-	@Order(1)
-	@BeforeAll
-	public void init()
-	{
-		FirefoxOptions options = new FirefoxOptions();
-		options.setHeadless(false);
-		options.addPreference("devtools.console.stdout.content", true);
-		driver = new FirefoxDriver(options);
-		driver.manage().window().maximize();
-	}
+	private static boolean isDatabaseAlreadyImported = false;
+
+	@Container
+	static PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>("postgres:14.2")
+			.withDatabaseName("budgetmaster-tests-db")
+			.withUsername("budgetmaster")
+			.withPassword("BudgetMaster");
+
 
-	@AfterAll
-	public void afterAll() {
-		driver.quit();
+	@DynamicPropertySource
+	static void properties(DynamicPropertyRegistry registry)
+	{
+		registry.add("spring.datasource.url", postgresDB::getJdbcUrl);
+		registry.add("spring.datasource.username", postgresDB::getUsername);
+		registry.add("spring.datasource.password", postgresDB::getPassword);
 	}
 
-	public WebDriver getDriver()
+	@BeforeEach
+	public void beforeEach()
 	{
-		return driver;
+		if(isDatabaseAlreadyImported)
+		{
+			return;
+		}
+
+		importDatabaseOnce();
+
+		isDatabaseAlreadyImported = true;
 	}
+
+	protected abstract void importDatabaseOnce();
 }
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestExecutionListener.java b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestExecutionListener.java
index 6e47428c6..8cac01aad 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestExecutionListener.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestExecutionListener.java
@@ -1,18 +1,20 @@
 package de.deadlocker8.budgetmaster.integration.helpers;
 
-import de.thecodelabs.utils.util.Localization;
-import de.thecodelabs.utils.util.SystemUtils;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.firefox.FirefoxDriver;
+import org.openqa.selenium.firefox.FirefoxOptions;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.core.Ordered;
 import org.springframework.test.context.TestContext;
 import org.springframework.test.context.support.AbstractTestExecutionListener;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
 
 public class SeleniumTestExecutionListener extends AbstractTestExecutionListener
 {
+	private WebDriver driver;
+
 	@Override
 	public int getOrder()
 	{
@@ -26,19 +28,45 @@ public class SeleniumTestExecutionListener extends AbstractTestExecutionListener
 		{
 			throw new RuntimeException("Test profile not activated. Skipping tests. (Set -DtestProfile=true in your VM arguments)");
 		}
+
+		if(driver != null)
+		{
+			return;
+		}
+
+		// allow driver to be Autowired
+		final ApplicationContext context = testContext.getApplicationContext();
+		if(context instanceof final ConfigurableApplicationContext configurableApplicationContext)
+		{
+			FirefoxOptions options = new FirefoxOptions();
+			options.setHeadless(false);
+			options.addPreference("devtools.console.stdout.content", true);
+			driver = new FirefoxDriver(options);
+			driver.manage().window().maximize();
+
+			ConfigurableListableBeanFactory factory = configurableApplicationContext.getBeanFactory();
+			factory.registerResolvableDependency(WebDriver.class, driver);
+		}
 	}
 
 	@Override
 	public void afterTestClass(TestContext testContext)
 	{
-		final Path path = SystemUtils.getApplicationSupportDirectoryPath(Localization.getString("folder"), "test", "budgetmaster.mv.db");
-		try
+		if(driver != null)
 		{
-			Files.deleteIfExists(path);
+			driver.quit();
 		}
-		catch(IOException e)
+	}
+
+	@Override
+	public void afterTestMethod(TestContext testContext)
+	{
+		final boolean isSuccess = testContext.getTestException() == null;
+		if(isSuccess)
 		{
-			e.printStackTrace();
+			return;
 		}
+
+		IntegrationTestHelper.saveScreenshots(driver, testContext.getTestMethod().getName(), testContext.getTestClass().getSimpleName());
 	}
 }
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestWatcher.java b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestWatcher.java
deleted file mode 100644
index be160519b..000000000
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/SeleniumTestWatcher.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.deadlocker8.budgetmaster.integration.helpers;
-
-import org.junit.jupiter.api.extension.ExtensionContext;
-import org.junit.jupiter.api.extension.TestWatcher;
-import org.openqa.selenium.WebDriver;
-
-public class SeleniumTestWatcher implements TestWatcher
-{
-	@Override
-	public void testFailed(ExtensionContext context, Throwable cause)
-	{
-		final WebDriver driver = getDriver(context);
-		IntegrationTestHelper.saveScreenshots(driver, context.getRequiredTestMethod().getName(), context.getRequiredTestClass().getSimpleName());
-	}
-
-	private WebDriver getDriver(ExtensionContext context)
-	{
-		final SeleniumTestBase testInstance = (SeleniumTestBase) context.getRequiredTestInstance();
-		return testInstance.getDriver();
-	}
-}
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/AccountTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/AccountTest.java
index 6e157e180..a2046e61c 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/AccountTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/AccountTest.java
@@ -8,7 +8,6 @@ import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper;
 import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTestBase;
 import de.deadlocker8.budgetmaster.integration.helpers.TransactionTestHelper;
 import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.openqa.selenium.By;
 import org.openqa.selenium.JavascriptExecutor;
@@ -24,12 +23,13 @@ import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+
 class AccountTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
@@ -37,7 +37,7 @@ class AccountTest extends SeleniumTestBase
 		helper.hideBackupReminder();
 		helper.hideWhatsNewDialog();
 
-		String path = getClass().getClassLoader().getResource("AccountDatabase.json").getFile().replace("/", File.separator);
+		String path = Account.class.getClassLoader().getResource("AccountDatabase.json").getFile().replace("/", File.separator);
 
 		final Account account1 = new Account("DefaultAccount0815", AccountType.CUSTOM);
 		final Account account2 = new Account("sfsdf", AccountType.CUSTOM);
@@ -79,7 +79,6 @@ class AccountTest extends SeleniumTestBase
 		}
 	}
 
-
 	@Test
 	void test_newAccount_cancel()
 	{
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CategorySelectTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CategorySelectTest.java
index a9d6698e7..f10ceaf05 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CategorySelectTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CategorySelectTest.java
@@ -5,7 +5,6 @@ import de.deadlocker8.budgetmaster.accounts.AccountType;
 import de.deadlocker8.budgetmaster.authentication.UserService;
 import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper;
 import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTestBase;
-import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.openqa.selenium.By;
@@ -23,10 +22,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class CategorySelectTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChangeTransactionTypeTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChangeTransactionTypeTest.java
index d7240a35b..b90cd5e35 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChangeTransactionTypeTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChangeTransactionTypeTest.java
@@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class ChangeTransactionTypeTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
 	private void openTransferTypeModal(int transactionID)
 	{
@@ -52,8 +52,8 @@ class ChangeTransactionTypeTest extends SeleniumTestBase
 		assertThat(driver.findElement(By.id("modalChangeTransactionType")).isDisplayed()).isTrue();
 	}
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChartTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChartTest.java
index d6a4adffa..9174c3d42 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChartTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChartTest.java
@@ -29,10 +29,10 @@ class ChartTest extends SeleniumTestBase
 	private final String SELECTOR_VISIBLE_CHART_PREVIEWS = ".chart-preview-column:not(.hidden)";
 	private final String SELECTOR_ACTIVE_CHART_PREVIEWS = ".chart-preview.active";
 
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/FirstUseTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/FirstUseTest.java
index 928010194..cb7ea85af 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/FirstUseTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/FirstUseTest.java
@@ -15,10 +15,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class FirstUseTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void prepare()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/HotkeyTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/HotkeyTest.java
index 14b6770ed..48abdb7d1 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/HotkeyTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/HotkeyTest.java
@@ -28,10 +28,10 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 class HotkeyTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void prepare()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ImportTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ImportTest.java
deleted file mode 100644
index 13622fe6e..000000000
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ImportTest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package de.deadlocker8.budgetmaster.integration.selenium;
-
-import de.deadlocker8.budgetmaster.accounts.Account;
-import de.deadlocker8.budgetmaster.accounts.AccountType;
-import de.deadlocker8.budgetmaster.authentication.UserService;
-import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper;
-import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTestBase;
-import org.junit.jupiter.api.Test;
-import org.openqa.selenium.By;
-import org.openqa.selenium.WebElement;
-
-import java.io.File;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class ImportTest extends SeleniumTestBase
-{
-	@Test
-	void requestImport()
-	{
-		IntegrationTestHelper helper = new IntegrationTestHelper(driver, port);
-		helper.start();
-		helper.login(UserService.DEFAULT_PASSWORD);
-		helper.hideBackupReminder();
-		helper.hideWhatsNewDialog();
-
-		String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator);
-		List<String> sourceAccounts = Arrays.asList("DefaultAccount0815", "sfsdf");
-		final Account account1 = new Account("DefaultAccount0815", AccountType.CUSTOM);
-		final Account account2 = new Account("Account2", AccountType.CUSTOM);
-		helper.uploadDatabase(path, sourceAccounts, List.of(account1, account2));
-
-		// assert
-		driver.get(helper.getUrl() + "/accounts");
-		List<WebElement> accountRows = driver.findElements(By.cssSelector(".account-container tr"));
-		assertThat(accountRows).hasSize(3);
-	}
-}
\ No newline at end of file
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/LoginControllerTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/LoginControllerTest.java
index b12bd58e5..0b3bdfb8a 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/LoginControllerTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/LoginControllerTest.java
@@ -18,6 +18,13 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class LoginControllerTest extends SeleniumTestBase
 {
+	private static IntegrationTestHelper helper;
+
+	@Override
+	protected void importDatabaseOnce()
+	{
+	}
+
 	@Test
 	void test_getLoginPage()
 	{
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/MediaTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/MediaTest.java
index 67d47041e..79f7f8e10 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/MediaTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/MediaTest.java
@@ -23,10 +23,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class MediaTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromExistingOneTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromExistingOneTest.java
index ccad71306..34931249f 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromExistingOneTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromExistingOneTest.java
@@ -24,10 +24,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class NewTransactionFromExistingOneTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromTemplateTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromTemplateTest.java
index 21d125159..78c9dee87 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromTemplateTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromTemplateTest.java
@@ -24,10 +24,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class NewTransactionFromTemplateTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionNormalTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionNormalTest.java
index ff6a77a1b..aac60ef3a 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionNormalTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionNormalTest.java
@@ -26,10 +26,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class NewTransactionNormalTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionRecurringTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionRecurringTest.java
index 894dc4cf7..2a0149ff5 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionRecurringTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionRecurringTest.java
@@ -27,10 +27,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class NewTransactionRecurringTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeALl()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionTransferTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionTransferTest.java
index b02047e0c..09de62e76 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionTransferTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionTransferTest.java
@@ -28,10 +28,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class NewTransactionTransferTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void prepare()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/SearchTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/SearchTest.java
index 1c9064330..bc733b0ab 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/SearchTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/SearchTest.java
@@ -21,8 +21,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class SearchTest extends SeleniumTestBase
 {
-	@BeforeAll
-	public void prepare()
+	private static IntegrationTestHelper helper;
+
+	@Override
+	protected void importDatabaseOnce()
 	{
 		IntegrationTestHelper helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/TagTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/TagTest.java
index 40f63a8c0..ea028e26f 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/TagTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/TagTest.java
@@ -19,10 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class TagTest extends SeleniumTestBase
 {
-	private IntegrationTestHelper helper;
+	private static IntegrationTestHelper helper;
 
-	@BeforeAll
-	public void beforeAll()
+	@Override
+	protected void importDatabaseOnce()
 	{
 		helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/WhatsNewTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/WhatsNewTest.java
index 2cd3316de..246129690 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/WhatsNewTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/WhatsNewTest.java
@@ -15,8 +15,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class WhatsNewTest extends SeleniumTestBase
 {
-	@BeforeAll
-	public void prepare()
+	private static IntegrationTestHelper helper;
+
+	@Override
+	protected void importDatabaseOnce()
 	{
 		IntegrationTestHelper helper = new IntegrationTestHelper(driver, port);
 		helper.start();
diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceDatabaseTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceDatabaseTest.java
index 2338726d6..4055c139d 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceDatabaseTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceDatabaseTest.java
@@ -6,8 +6,11 @@ import de.deadlocker8.budgetmaster.accounts.AccountType;
 import de.deadlocker8.budgetmaster.filter.FilterConfiguration;
 import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest;
 import de.deadlocker8.budgetmaster.transactions.Transaction;
+import de.deadlocker8.budgetmaster.transactions.TransactionRepository;
 import de.deadlocker8.budgetmaster.transactions.TransactionService;
 import de.deadlocker8.budgetmaster.utils.DateHelper;
+import de.thecodelabs.utils.util.SystemUtils;
+import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
@@ -23,8 +26,10 @@ import org.springframework.core.io.Resource;
 import org.springframework.test.context.ActiveProfiles;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.persistence.EntityManager;
 import javax.sql.DataSource;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.time.LocalDate;
 import java.util.List;
 
@@ -33,7 +38,6 @@ import static org.assertj.core.api.Assertions.assertThat;
 @SpringBootTest(classes = Main.class)
 @Import(TransactionServiceDatabaseTest.TestDatabaseConfiguration.class)
 @ActiveProfiles("test")
-@SeleniumTest
 @Transactional
 class TransactionServiceDatabaseTest
 {
@@ -59,6 +63,9 @@ class TransactionServiceDatabaseTest
 	@Autowired
 	private AccountRepository accountRepository;
 
+	@Autowired
+	DataSource dataSource;
+
 	@Test
 	void test_deleteAll()
 	{
diff --git a/src/test/resources/repeating_with_tags.mv.db b/src/test/resources/repeating_with_tags.mv.db
index a412f66a685ceaf5a5c6990e38798e1ca0c3f139..773368dcd7040eee3822562ee75236b3036ea45b 100644
GIT binary patch
delta 3070
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
E00&?sM*si-

delta 4431
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

-- 
GitLab