mocked = mockStatic(SpringApplication.class)) {
- @Test
- void mainMethodStartsApplication() {
- WeatherViewerApplication.main(new String[] {});
- }
+ WeatherViewerApplication.main(new String[0]);
+ mocked.verify(() ->
+ SpringApplication.run(
+ eq(WeatherViewerApplication.class),
+ eq(new String[0])
+ ));
+ }
+ }
}
diff --git a/src/test/java/com/weatherviewer/controller/HomeControllerTest.java b/src/test/java/com/weatherviewer/controller/HomeControllerTest.java
index ec5458a..13e8902 100644
--- a/src/test/java/com/weatherviewer/controller/HomeControllerTest.java
+++ b/src/test/java/com/weatherviewer/controller/HomeControllerTest.java
@@ -8,10 +8,12 @@
import com.weatherviewer.security.SecUser;
import com.weatherviewer.service.LocationService;
import com.weatherviewer.service.WeatherApiService;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
@@ -34,6 +36,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class HomeControllerTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/controller/ProfileControllerTest.java b/src/test/java/com/weatherviewer/controller/ProfileControllerTest.java
index 1c10e96..581efe8 100644
--- a/src/test/java/com/weatherviewer/controller/ProfileControllerTest.java
+++ b/src/test/java/com/weatherviewer/controller/ProfileControllerTest.java
@@ -6,6 +6,7 @@
import com.weatherviewer.model.enums.UserStatus;
import com.weatherviewer.security.SecUser;
import com.weatherviewer.service.UserService;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import com.weatherviewer.validation.validator.PasswordMatchesValidator;
import com.weatherviewer.validation.validator.UniqueEmailValidator;
import org.junit.jupiter.api.BeforeEach;
@@ -13,6 +14,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@@ -30,6 +32,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class ProfileControllerTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/controller/SearchControllerTest.java b/src/test/java/com/weatherviewer/controller/SearchControllerTest.java
index 332655a..6da305e 100644
--- a/src/test/java/com/weatherviewer/controller/SearchControllerTest.java
+++ b/src/test/java/com/weatherviewer/controller/SearchControllerTest.java
@@ -7,6 +7,7 @@
import com.weatherviewer.security.SecUser;
import com.weatherviewer.service.LocationService;
import com.weatherviewer.service.WeatherApiService;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import com.weatherviewer.validation.validator.PasswordMatchesValidator;
import com.weatherviewer.validation.validator.UniqueEmailValidator;
import com.weatherviewer.validation.validator.UniqueLocationValidator;
@@ -15,6 +16,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@@ -32,6 +34,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class SearchControllerTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/ForecastIntegrationTest.java b/src/test/java/com/weatherviewer/integration/ForecastIntegrationTest.java
index fc64bde..fd23fc5 100644
--- a/src/test/java/com/weatherviewer/integration/ForecastIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/ForecastIntegrationTest.java
@@ -7,12 +7,14 @@
import com.weatherviewer.repository.LocationRepository;
import com.weatherviewer.repository.UserRepository;
import com.weatherviewer.security.SecUser;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
@@ -25,6 +27,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class ForecastIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/LocationIntegrationTest.java b/src/test/java/com/weatherviewer/integration/LocationIntegrationTest.java
index efa8110..e366975 100644
--- a/src/test/java/com/weatherviewer/integration/LocationIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/LocationIntegrationTest.java
@@ -8,12 +8,14 @@
import com.weatherviewer.repository.LocationRepository;
import com.weatherviewer.repository.UserRepository;
import com.weatherviewer.security.SecUser;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
@@ -29,6 +31,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class LocationIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/OpenApiDocumentationIntegrationTest.java b/src/test/java/com/weatherviewer/integration/OpenApiDocumentationIntegrationTest.java
index 9b74df8..282ff80 100644
--- a/src/test/java/com/weatherviewer/integration/OpenApiDocumentationIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/OpenApiDocumentationIntegrationTest.java
@@ -1,9 +1,11 @@
package com.weatherviewer.integration;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -11,6 +13,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class OpenApiDocumentationIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/ProfileIntegrationTest.java b/src/test/java/com/weatherviewer/integration/ProfileIntegrationTest.java
index c412a1d..4329872 100644
--- a/src/test/java/com/weatherviewer/integration/ProfileIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/ProfileIntegrationTest.java
@@ -6,12 +6,14 @@
import com.weatherviewer.model.enums.UserStatus;
import com.weatherviewer.repository.UserRepository;
import com.weatherviewer.security.SecUser;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
@@ -26,6 +28,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class ProfileIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/SearchIntegrationTest.java b/src/test/java/com/weatherviewer/integration/SearchIntegrationTest.java
index 64d536b..af3c21c 100644
--- a/src/test/java/com/weatherviewer/integration/SearchIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/SearchIntegrationTest.java
@@ -10,12 +10,14 @@
import com.weatherviewer.repository.UserRepository;
import com.weatherviewer.security.SecUser;
import com.weatherviewer.service.WeatherApiService;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@@ -33,6 +35,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class SearchIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/SecurityIntegrationTest.java b/src/test/java/com/weatherviewer/integration/SecurityIntegrationTest.java
index 1b774e2..a97c4ad 100644
--- a/src/test/java/com/weatherviewer/integration/SecurityIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/SecurityIntegrationTest.java
@@ -1,9 +1,11 @@
package com.weatherviewer.integration;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@@ -15,6 +17,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class SecurityIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/SignInIntegrationTest.java b/src/test/java/com/weatherviewer/integration/SignInIntegrationTest.java
index dcfb16e..947d06f 100644
--- a/src/test/java/com/weatherviewer/integration/SignInIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/SignInIntegrationTest.java
@@ -4,12 +4,14 @@
import com.weatherviewer.model.enums.Role;
import com.weatherviewer.model.enums.UserStatus;
import com.weatherviewer.repository.UserRepository;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
@@ -22,6 +24,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class SignInIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/UserRegistrationIntegrationTest.java b/src/test/java/com/weatherviewer/integration/UserRegistrationIntegrationTest.java
index 5f0eeec..1e25b67 100644
--- a/src/test/java/com/weatherviewer/integration/UserRegistrationIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/UserRegistrationIntegrationTest.java
@@ -2,11 +2,13 @@
import com.weatherviewer.model.User;
import com.weatherviewer.repository.UserRepository;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
@@ -18,6 +20,7 @@
@SpringBootTest
@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
class UserRegistrationIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/integration/WeatherApiCacheIntegrationTest.java b/src/test/java/com/weatherviewer/integration/WeatherApiCacheIntegrationTest.java
index 117c25d..015e2f4 100644
--- a/src/test/java/com/weatherviewer/integration/WeatherApiCacheIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/WeatherApiCacheIntegrationTest.java
@@ -1,22 +1,38 @@
package com.weatherviewer.integration;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.weatherviewer.service.integration.WeatherApiCache;
import com.weatherviewer.service.integration.WeatherApiClient;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.CacheManager;
+import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.util.Objects;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
+/**
+ * Runs against a real Redis cache (via {@link TestcontainersConfiguration}),
+ * not an in-memory {@code simple} cache — so a cached value actually has to
+ * survive a serialize/deserialize round trip to come back on a cache hit,
+ * the way it would in production. That's why every cached value here is a
+ * real, Jackson-parsed {@link JsonNode} rather than a Mockito mock: a mock
+ * has nothing meaningful to serialize and would fail (or silently prove
+ * nothing) the moment the cache manager tries to write it to Redis.
+ */
@SpringBootTest
+@Import(TestcontainersConfiguration.class)
class WeatherApiCacheIntegrationTest {
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
@MockitoBean
private WeatherApiClient weatherApiClient;
@@ -32,21 +48,30 @@ void clearCaches() {
.forEach(name -> Objects.requireNonNull(cacheManager.getCache(name)).clear());
}
+ private JsonNode weatherNode(String city) throws Exception {
+ return objectMapper.readTree("""
+ {"name":"%s","main":{"temp":21.5},"weather":[{"main":"Clear"}]}
+ """.formatted(city));
+ }
+
@Test
- void fetchCurrentWeatherByCity_cachedOnSecondCall() {
- JsonNode mockNode = mock(JsonNode.class);
- when(weatherApiClient.fetchCurrentWeatherByCity("Kyiv")).thenReturn(mockNode);
+ void fetchCurrentWeatherByCity_cachedOnSecondCall() throws Exception {
+ JsonNode realNode = weatherNode("Kyiv");
+ when(weatherApiClient.fetchCurrentWeatherByCity("Kyiv")).thenReturn(realNode);
- weatherApiCache.fetchCurrentWeatherByCity("Kyiv");
- weatherApiCache.fetchCurrentWeatherByCity("Kyiv");
+ JsonNode first = weatherApiCache.fetchCurrentWeatherByCity("Kyiv");
+ JsonNode second = weatherApiCache.fetchCurrentWeatherByCity("Kyiv");
verify(weatherApiClient, times(1)).fetchCurrentWeatherByCity("Kyiv");
+
+ assertThat(second).isEqualTo(realNode);
+ assertThat(second).isNotSameAs(first);
}
@Test
- void fetchCurrentWeatherByCoordinates_cachedOnSecondCall() {
- JsonNode mockNode = mock(JsonNode.class);
- when(weatherApiClient.fetchCurrentWeatherByCoordinates(50.45, 30.52)).thenReturn(mockNode);
+ void fetchCurrentWeatherByCoordinates_cachedOnSecondCall() throws Exception {
+ JsonNode realNode = weatherNode("Kyiv");
+ when(weatherApiClient.fetchCurrentWeatherByCoordinates(50.45, 30.52)).thenReturn(realNode);
weatherApiCache.fetchCurrentWeatherByCoordinates(50.45, 30.52);
weatherApiCache.fetchCurrentWeatherByCoordinates(50.45, 30.52);
@@ -55,9 +80,9 @@ void fetchCurrentWeatherByCoordinates_cachedOnSecondCall() {
}
@Test
- void fetchForecastByCity_cachedOnSecondCall() {
- JsonNode mockNode = mock(JsonNode.class);
- when(weatherApiClient.fetchForecastByCity("Kyiv")).thenReturn(mockNode);
+ void fetchForecastByCity_cachedOnSecondCall() throws Exception {
+ JsonNode realNode = weatherNode("Kyiv");
+ when(weatherApiClient.fetchForecastByCity("Kyiv")).thenReturn(realNode);
weatherApiCache.fetchForecastByCity("Kyiv");
weatherApiCache.fetchForecastByCity("Kyiv");
@@ -66,9 +91,9 @@ void fetchForecastByCity_cachedOnSecondCall() {
}
@Test
- void fetchForecastByCoordinates_cachedOnSecondCall() {
- JsonNode mockNode = mock(JsonNode.class);
- when(weatherApiClient.fetchForecastByCoordinates(50.45, 30.52)).thenReturn(mockNode);
+ void fetchForecastByCoordinates_cachedOnSecondCall() throws Exception {
+ JsonNode realNode = weatherNode("Kyiv");
+ when(weatherApiClient.fetchForecastByCoordinates(50.45, 30.52)).thenReturn(realNode);
weatherApiCache.fetchForecastByCoordinates(50.45, 30.52);
weatherApiCache.fetchForecastByCoordinates(50.45, 30.52);
@@ -77,9 +102,9 @@ void fetchForecastByCoordinates_cachedOnSecondCall() {
}
@Test
- void fetchGeocodingByCity_cachedOnSecondCall() {
- JsonNode mockNode = mock(JsonNode.class);
- when(weatherApiClient.fetchGeocodingByCity("Kyiv")).thenReturn(mockNode);
+ void fetchGeocodingByCity_cachedOnSecondCall() throws Exception {
+ JsonNode realNode = weatherNode("Kyiv");
+ when(weatherApiClient.fetchGeocodingByCity("Kyiv")).thenReturn(realNode);
weatherApiCache.fetchGeocodingByCity("Kyiv");
weatherApiCache.fetchGeocodingByCity("Kyiv");
@@ -88,9 +113,9 @@ void fetchGeocodingByCity_cachedOnSecondCall() {
}
@Test
- void fetchCurrentWeatherByCity_differentCities_notCached() {
- JsonNode mockNode = mock(JsonNode.class);
- when(weatherApiClient.fetchCurrentWeatherByCity(any())).thenReturn(mockNode);
+ void fetchCurrentWeatherByCity_differentCities_notCached() throws Exception {
+ when(weatherApiClient.fetchCurrentWeatherByCity(anyString()))
+ .thenAnswer(invocation -> weatherNode(invocation.getArgument(0)));
weatherApiCache.fetchCurrentWeatherByCity("Kyiv");
weatherApiCache.fetchCurrentWeatherByCity("Lviv");
@@ -100,9 +125,9 @@ void fetchCurrentWeatherByCity_differentCities_notCached() {
}
@Test
- void fetchCurrentWeatherByCoordinates_differentCoords_notCached() {
- JsonNode mockNode = mock(JsonNode.class);
- when(weatherApiClient.fetchCurrentWeatherByCoordinates(anyDouble(), anyDouble())).thenReturn(mockNode);
+ void fetchCurrentWeatherByCoordinates_differentCoords_notCached() throws Exception {
+ JsonNode realNode = weatherNode("Kyiv");
+ when(weatherApiClient.fetchCurrentWeatherByCoordinates(anyDouble(), anyDouble())).thenReturn(realNode);
weatherApiCache.fetchCurrentWeatherByCoordinates(50.45, 30.52);
weatherApiCache.fetchCurrentWeatherByCoordinates(48.92, 24.71);
diff --git a/src/test/java/com/weatherviewer/integration/WeatherApiIntegrationTest.java b/src/test/java/com/weatherviewer/integration/WeatherApiIntegrationTest.java
index 9745893..6ad4efb 100644
--- a/src/test/java/com/weatherviewer/integration/WeatherApiIntegrationTest.java
+++ b/src/test/java/com/weatherviewer/integration/WeatherApiIntegrationTest.java
@@ -5,11 +5,13 @@
import com.weatherviewer.exception.ExternalHttpCallException;
import com.weatherviewer.service.WeatherApiService;
import com.weatherviewer.service.integration.WeatherApiClient;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.CacheManager;
+import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.client.ExpectedCount;
@@ -27,6 +29,7 @@
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@SpringBootTest
+@Import(TestcontainersConfiguration.class)
class WeatherApiIntegrationTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/repository/LocationRepositoryTest.java b/src/test/java/com/weatherviewer/repository/LocationRepositoryTest.java
index 982e9e8..f685679 100644
--- a/src/test/java/com/weatherviewer/repository/LocationRepositoryTest.java
+++ b/src/test/java/com/weatherviewer/repository/LocationRepositoryTest.java
@@ -4,17 +4,20 @@
import com.weatherviewer.model.User;
import com.weatherviewer.model.enums.Role;
import com.weatherviewer.model.enums.UserStatus;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
+@Import(TestcontainersConfiguration.class)
class LocationRepositoryTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/repository/UserRepositoryTest.java b/src/test/java/com/weatherviewer/repository/UserRepositoryTest.java
index ee54518..ec18104 100644
--- a/src/test/java/com/weatherviewer/repository/UserRepositoryTest.java
+++ b/src/test/java/com/weatherviewer/repository/UserRepositoryTest.java
@@ -4,11 +4,13 @@
import com.weatherviewer.model.User;
import com.weatherviewer.model.enums.Role;
import com.weatherviewer.model.enums.UserStatus;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
import java.util.List;
import java.util.Optional;
@@ -16,6 +18,7 @@
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
+@Import(TestcontainersConfiguration.class)
class UserRepositoryTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/repository/VerificationTokenRepositoryTest.java b/src/test/java/com/weatherviewer/repository/VerificationTokenRepositoryTest.java
index 08de6de..7e16c1e 100644
--- a/src/test/java/com/weatherviewer/repository/VerificationTokenRepositoryTest.java
+++ b/src/test/java/com/weatherviewer/repository/VerificationTokenRepositoryTest.java
@@ -5,11 +5,13 @@
import com.weatherviewer.model.enums.Role;
import com.weatherviewer.model.enums.TokenType;
import com.weatherviewer.model.enums.UserStatus;
+import com.weatherviewer.testcontainers.TestcontainersConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
import java.time.LocalDateTime;
import java.util.Optional;
@@ -17,6 +19,7 @@
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
+@Import(TestcontainersConfiguration.class)
class VerificationTokenRepositoryTest {
@Autowired
diff --git a/src/test/java/com/weatherviewer/testcontainers/TestcontainersConfiguration.java b/src/test/java/com/weatherviewer/testcontainers/TestcontainersConfiguration.java
new file mode 100644
index 0000000..5bf43d4
--- /dev/null
+++ b/src/test/java/com/weatherviewer/testcontainers/TestcontainersConfiguration.java
@@ -0,0 +1,39 @@
+package com.weatherviewer.testcontainers;
+
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Boots real Postgres and Redis containers for integration tests, instead of
+ * H2 (Postgres compatibility mode) and {@code spring.cache.type=simple}.
+ *
+ * Both containers are wired via Spring Boot's {@link ServiceConnection}, so
+ * no {@code spring.datasource.*} or {@code spring.data.redis.*} properties
+ * need to be set by hand — Spring Boot autoconfigures the datasource and
+ * Redis connection factory directly from the running containers.
+ *
+ * Import this into a test with {@code @Import(TestcontainersConfiguration.class)}.
+ * Spring's test context caching means all tests that import this identical
+ * configuration share the same pair of containers instead of starting a new per test.
+ */
+@TestConfiguration(proxyBeanMethods = false)
+public class TestcontainersConfiguration {
+
+ @Bean
+ @ServiceConnection
+ PostgreSQLContainer> postgresContainer() {
+ return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"));
+ }
+
+ @Bean
+ @ServiceConnection("redis")
+ GenericContainer> redisContainer() {
+ return new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
+ .withExposedPorts(6379);
+ }
+
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index f9c966f..6e347d3 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -1,8 +1,4 @@
-spring.datasource.url=jdbc:h2:mem:weather_test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
-spring.datasource.driver-class-name=org.h2.Driver
-spring.datasource.username=sa
-spring.datasource.password=
-spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none
spring.liquibase.change-log=classpath:liquibase/changelog.xml
@@ -16,9 +12,7 @@ geo.api.url.suffix=/geo/1.0/direct
weather.api.key=${WEATHER_API_KEY}
weather.tile.base.url=https://tile.openweathermap.org/map
-spring.data.redis.host=localhost
-spring.data.redis.port=6379
-spring.cache.type=simple
+spring.cache.type=redis
# --- Mail ---
spring.mail.host=localhost