|
| 1 | +Project: Weather App Java – Development Guidelines (Advanced) |
| 2 | + |
| 3 | +This document captures project-specific knowledge for advanced development, build, testing, and troubleshooting. |
| 4 | + |
| 5 | +1. Build and Configuration |
| 6 | +- Toolchain |
| 7 | + - Java: 21 (configured via Gradle Toolchains). Ensure a JDK 21 is available; Gradle will auto-provision if required. |
| 8 | + - Build: Gradle with Spring Boot plugin 3.5.5 and io.spring.dependency-management 1.1.7. |
| 9 | +- Dependencies (high-level) |
| 10 | + - Web/UI: spring-boot-starter-web, spring-boot-starter-thymeleaf |
| 11 | + - AI: langchain4j 0.35.0, langchain4j-google-ai-gemini 0.35.0 |
| 12 | + - Tests: spring-boot-starter-test (JUnit 5 / Jupiter) |
| 13 | +- Build commands |
| 14 | + - Full build (no tests): ./gradlew assemble |
| 15 | + - Build with tests: ./gradlew build |
| 16 | + - Run app locally: ./gradlew bootRun |
| 17 | + - Docker: docker build -t weather-app-java . && docker run -p 8080:8080 -e AI_API_KEY=... weather-app-java |
| 18 | +- Runtime configuration |
| 19 | + - AI config (Google Gemini via Google AI Studio): |
| 20 | + - Property: ai.gemini.api-key (String). If blank/missing, AI summary endpoints respond with fallback message and UI shows unavailable summary. |
| 21 | + - Property: ai.gemini.model (String). Default is gemini-1.5-flash; empty/blank coerced to default. |
| 22 | + - How to set: |
| 23 | + - application.properties (src/main/resources) or runtime environment e.g., -Dai.gemini.api-key=... or SPRING_APPLICATION_JSON; README shows examples too. |
| 24 | + - Ports: Spring Boot default 8080. Override with server.port if needed. |
| 25 | + - Data: City suggestions are loaded from src/main/resources/cities.json in CitySearchService. |
| 26 | +- Devcontainer |
| 27 | + - .devcontainer/devcontainer.json exists (if using VS Code / JetBrains Gateway). It provides a consistent dev environment; match Java 21. |
| 28 | + |
| 29 | +2. Testing – Running, Adding, and Examples |
| 30 | +- Test profile |
| 31 | + - Active profile for tests: "test" (via @ActiveProfiles("test") in several integration tests). |
| 32 | + - src/test/resources/application-test.properties sets ai.gemini.api-key=test-key to avoid real external calls. AI beans can be further overridden/mocked within @TestConfiguration in tests where needed (see EndToEndIntegrationTest and web/AiSummaryControllerIntegrationTest for patterns). |
| 33 | +- Commands |
| 34 | + - Run all tests: ./gradlew test |
| 35 | + - Run a single class: ./gradlew test --tests "com.example.weatherapp.web.ReportControllerIntegrationTest" |
| 36 | + - Run a single method: ./gradlew test --tests "com.example.weatherapp.web.ReportControllerIntegrationTest.methodName" |
| 37 | + - Show stacktraces: add -i or --stacktrace |
| 38 | + - Continuous testing (watch mode): ./gradlew test --continuous (re-runs on changes) |
| 39 | +- Test types present |
| 40 | + - Controller Integration Tests (MockMvc): |
| 41 | + - web/CitySearchControllerIntegrationTest |
| 42 | + - web/ReportControllerIntegrationTest |
| 43 | + - web/HomeControllerIntegrationTest |
| 44 | + - web/AiSummaryControllerIntegrationTest (uses @TestConfiguration to override AiSummaryService with a mock) |
| 45 | + - Service Integration Tests: |
| 46 | + - city/CitySearchServiceIntegrationTest |
| 47 | + - ai/AiSummaryServiceIntegrationTest |
| 48 | + - End-to-End Integration Test: |
| 49 | + - EndToEndIntegrationTest bootstraps the full Spring context, overrides both CitySearchService and AiSummaryService via @TestConfiguration, and exercises a realistic flow: GET /, city search endpoint, /report page, then POST /api/ai-summary with a weather report JSON payload. This is a good blueprint for authoring system-level tests. |
| 50 | +- How to add a new test |
| 51 | + - Place tests under src/test/java mirroring main package structure. |
| 52 | + - For Spring tests, annotate with @SpringBootTest (or @WebMvcTest for slice tests) and @AutoConfigureMockMvc if you need MockMvc. |
| 53 | + - If the test should avoid external AI calls, either rely on the test profile (ai.gemini.api-key=test-key ensures AiSummaryService.isConfigured() true) or provide a @TestConfiguration with a @Primary bean overriding AiSummaryService to a Mockito mock and stub summarize(). Pattern: |
| 54 | + |
| 55 | + @SpringBootTest |
| 56 | + @AutoConfigureMockMvc |
| 57 | + @ActiveProfiles("test") |
| 58 | + class MyControllerIT { |
| 59 | + @TestConfiguration |
| 60 | + static class Cfg { |
| 61 | + @Bean @Primary AiSummaryService ai() { |
| 62 | + var mock = org.mockito.Mockito.mock(AiSummaryService.class); |
| 63 | + org.mockito.Mockito.when(mock.isConfigured()).thenReturn(true); |
| 64 | + org.mockito.Mockito.when(mock.summarize(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString())) |
| 65 | + .thenReturn("Stubbed summary"); |
| 66 | + return mock; |
| 67 | + } |
| 68 | + } |
| 69 | + @Autowired MockMvc mvc; |
| 70 | + @Test void responds200() throws Exception { |
| 71 | + mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get("/")) |
| 72 | + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + - Run only this test to validate quickly: ./gradlew test --tests "com.example.weatherapp.MyControllerIT" |
| 77 | +- Creating and running a simple test example that works |
| 78 | + - Use any of the existing tests as a working example; they build and run with the provided test profile. For a minimal sanity test that does not require the web context: |
| 79 | + |
| 80 | + package com.example.weatherapp; |
| 81 | + |
| 82 | + import org.junit.jupiter.api.Test; |
| 83 | + import static org.junit.jupiter.api.Assertions.*; |
| 84 | + |
| 85 | + class SanityTest { |
| 86 | + @Test void sanity() { |
| 87 | + assertTrue(21 >= 17, "Java toolchain is at least 17"); |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + - Place it at src/test/java/com/example/weatherapp/SanityTest.java. |
| 92 | + - Run: ./gradlew test --tests "com.example.weatherapp.SanityTest". |
| 93 | + - Note: This example is not committed to the repository per instructions; create locally if needed and delete afterward. |
| 94 | + |
| 95 | +3. Project Architecture and Development Notes |
| 96 | +- Packages |
| 97 | + - com.example.weatherapp.web – Controllers for endpoints and pages (Thymeleaf views: templates/index.html, templates/report.html). |
| 98 | + - com.example.weatherapp.city – CitySearchService uses cities.json; exposes searchSuggestions(query, limit) used by controller and tests. |
| 99 | + - com.example.weatherapp.ai – AiSummaryService integrates LangChain4j Google Gemini model; designed to degrade gracefully when not configured. |
| 100 | + - com.example.weatherapp.weather – WeatherReport is the domain model consumed by the AI summary endpoint. |
| 101 | +- AiSummaryService specifics |
| 102 | + - Configuration: ai.gemini.api-key and ai.gemini.model (default "gemini-1.5-flash"). |
| 103 | + - Behavior: |
| 104 | + - isConfigured(): returns true if api key is non-blank. |
| 105 | + - summarize(report, timezone, city): |
| 106 | + - If not configured: returns an explicit "AI summary unavailable: missing Google Gemini API key." message. |
| 107 | + - Builds a compact prompt including timezone and location. JSON serialization uses Jackson. On serialization failure, falls back to a minimal safe JSON via safeReport(). |
| 108 | + - Uses a lazily initialized ChatLanguageModel with double-checked locking and a volatile cachedModel field. |
| 109 | + - Any RuntimeException from model.generate() returns a controlled fallback: "AI summary unavailable at the moment. Reason: ..." to avoid failing the request. |
| 110 | +- Controller/API behavior |
| 111 | + - /api/ai-summary: Accepts WeatherReport JSON in body, optional timezone and city parameters. Response includes fields summary (String), model ("gemini"), configured (boolean). Integration tests assert these. |
| 112 | + - /api/cities/search: Validates parameters; limit parameter has defaults and caps (see tests for behavior and edge cases). |
| 113 | + - /report: Validates mandatory lat/lon; passes city and values to the Thymeleaf template. |
| 114 | +- Code style and testing style |
| 115 | + - Prefer Spring Boot test slices (@WebMvcTest) for controller-only units; use @SpringBootTest + MockMvc for integrated flows. |
| 116 | + - Mock external integrations using @TestConfiguration with @Primary bean overrides. Prefer Mockito stubs for clarity, as shown in EndToEndIntegrationTest and AiSummaryControllerIntegrationTest. |
| 117 | + - Keep tests deterministic; avoid network calls in tests. City data is static. |
| 118 | + - use final var instead of type where possible for local variables |
| 119 | + - prefer wrapper types instead of primitives |
| 120 | + - use List.of instead of Arrays.asList |
| 121 | +- Troubleshooting |
| 122 | + - Tests fail with missing AI key: Ensure ActiveProfiles("test") and application-test.properties is on the classpath. Alternatively, set -Dspring.profiles.active=test or define ai.gemini.api-key. |
| 123 | + - Unsupported Java version: Ensure local JDK 21; Gradle toolchain should provision, but corporate networks can block downloads—preinstall JDK 21 in that case. |
| 124 | + - Port conflicts when running bootRun: Set -Dserver.port=0 or another port. |
| 125 | + - Slow tests due to full context: Consider @WebMvcTest slices and mock only required beans. |
| 126 | + - Flaky AI tests: Do not rely on external AI responses; always stub summarize(). |
| 127 | + |
| 128 | +4. Commands Quick Reference |
| 129 | +- Build: ./gradlew build |
| 130 | +- Run app: ./gradlew bootRun |
| 131 | +- Run all tests: ./gradlew test |
| 132 | +- Run one class: ./gradlew test --tests "com.example.weatherapp.web.ReportControllerIntegrationTest" |
| 133 | +- Run one method: ./gradlew test --tests "com.example.weatherapp.web.ReportControllerIntegrationTest.methodName" |
| 134 | +- With profile override: ./gradlew test -Dspring.profiles.active=test |
| 135 | + |
| 136 | +Notes |
| 137 | +- The examples above have been validated against current project structure and dependencies. For demonstration tests, create them locally and delete before committing, as per repository policy. |
0 commit comments