From fc7fecca9b8c4173502e7db2e7fcbca4e6133eaa Mon Sep 17 00:00:00 2001 From: podlLev Date: Tue, 4 Aug 2026 10:00:24 +0300 Subject: [PATCH 1/4] test: add Testcontainers support for integration tests --- pom.xml | 14 ++++++- .../TestcontainersConfiguration.java | 39 +++++++++++++++++++ src/test/resources/application.properties | 10 +---- 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/weatherviewer/testcontainers/TestcontainersConfiguration.java diff --git a/pom.xml b/pom.xml index ada8686..e4ae47a 100644 --- a/pom.xml +++ b/pom.xml @@ -166,8 +166,18 @@ 4.0.0 - com.h2database - h2 + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql test 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 From 447174338edab425d7be96916cd03bb1bbb6b14e Mon Sep 17 00:00:00 2001 From: podlLev Date: Tue, 4 Aug 2026 10:00:58 +0300 Subject: [PATCH 2/4] test: add controller integration tests --- .../java/com/weatherviewer/controller/HomeControllerTest.java | 3 +++ .../com/weatherviewer/controller/ProfileControllerTest.java | 3 +++ .../com/weatherviewer/controller/SearchControllerTest.java | 3 +++ 3 files changed, 9 insertions(+) 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 From 2d3a2a46c196c454eee40251426060bc10784c31 Mon Sep 17 00:00:00 2001 From: podlLev Date: Tue, 4 Aug 2026 10:01:14 +0300 Subject: [PATCH 3/4] test: add repository integration tests --- .../com/weatherviewer/repository/LocationRepositoryTest.java | 3 +++ .../java/com/weatherviewer/repository/UserRepositoryTest.java | 3 +++ .../repository/VerificationTokenRepositoryTest.java | 3 +++ 3 files changed, 9 insertions(+) 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 From 959b3f7fff842de36fa9e8e04a4e6da8462985f1 Mon Sep 17 00:00:00 2001 From: podlLev Date: Tue, 4 Aug 2026 10:02:32 +0300 Subject: [PATCH 4/4] test: add integration tests with Testcontainers --- README.md | 8 +-- .../WeatherViewerApplicationTests.java | 23 +++--- .../integration/ForecastIntegrationTest.java | 3 + .../integration/LocationIntegrationTest.java | 3 + .../OpenApiDocumentationIntegrationTest.java | 3 + .../integration/ProfileIntegrationTest.java | 3 + .../integration/SearchIntegrationTest.java | 3 + .../integration/SecurityIntegrationTest.java | 3 + .../integration/SignInIntegrationTest.java | 3 + .../UserRegistrationIntegrationTest.java | 3 + .../WeatherApiCacheIntegrationTest.java | 71 +++++++++++++------ .../WeatherApiIntegrationTest.java | 3 + 12 files changed, 94 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index a1fe036..04b3586 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ flowchart TB Cache[(Redis)] Weather[(OpenWeatherMap API)] Mail[(SMTP)] - + Client --> RL --> Sec --> Web Client -. live updates .-> WS Web --> Svc @@ -264,14 +264,14 @@ Every log line is tagged with a request correlation ID, and HTTP request latency ./mvnw test ``` -Tests run against an in-memory H2 database, so no external services are required. The suite includes unit tests, MVC/REST controller tests, repository tests, and full integration tests for auth (including verification, password reset, and remember-me), search, profile, and weather flows. JaCoCo generates a coverage report at `target/site/jacoco/index.html` after running tests. +Every test that boots a Spring context (`@SpringBootTest`, `@DataJpaTest`) runs against real Postgres and Redis via [Testcontainers](https://testcontainers.com/) — `TestcontainersConfiguration` wires both in via `@ServiceConnection`, so no manual datasource/Redis properties are needed. This needs a running Docker daemon; without one, those tests fail to start. Pure unit tests (model/DTO/enum tests, Mockito-based service tests) don't start a Spring context at all, so they're unaffected either way. The suite includes unit tests, MVC/REST controller tests (`@WebMvcTest`, which slice the web layer and don't touch a real database), repository tests, and full integration tests for auth (including verification, password reset, and remember-me), search, profile, and weather flows. JaCoCo generates a coverage report at `target/site/jacoco/index.html` after running tests. ## CI/CD Every push to `main` and every pull request into `main`/`dev` runs through GitHub Actions: -1. **Build & Test** — compiles the project and runs the full test suite against H2, publishing a JUnit test report and a JaCoCo coverage report as workflow artifacts. -2. **Update coverage badge** — on pushes to `main`, regenerates the `.github/badges/jacoco.svg` badge from that JaCoCo report and commits it back to the repo. +1. **Build & Test** — compiles the project and runs the full test suite (Spring-context tests against real Postgres/Redis via Testcontainers), publishing a JUnit test report and a JaCoCo coverage report as workflow artifacts. +2. **Update coverage badge** — on pushes to `main` or `dev`, regenerates that branch's `.github/badges/jacoco.svg` badge from the JaCoCo report and commits it back. 3. **Docker build & push** — on pushes to `main`, builds the application image and pushes it to Docker Hub as `podllev/weather-viewer`. See `.github/workflows/ci.yml` for the full pipeline. diff --git a/src/test/java/com/weatherviewer/WeatherViewerApplicationTests.java b/src/test/java/com/weatherviewer/WeatherViewerApplicationTests.java index d7a0168..f94b381 100644 --- a/src/test/java/com/weatherviewer/WeatherViewerApplicationTests.java +++ b/src/test/java/com/weatherviewer/WeatherViewerApplicationTests.java @@ -1,18 +1,25 @@ package com.weatherviewer; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; +import org.mockito.MockedStatic; +import org.springframework.boot.SpringApplication; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; -@SpringBootTest class WeatherViewerApplicationTests { @Test - void contextLoads() { - } + void mainStartsApplication() { + try (MockedStatic 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/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