Download tests/test_api.py from kr4phy/SchoolSustainabilityPrediction: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/spaces/kr4phy/SchoolSustainabilityPrediction/resolve/main/tests/test_api.py
- Command line
-
hf download hf://spaces/kr4phy/SchoolSustainabilityPrediction/tests/test_api.py
-
curl -L -o test_api.py https://huggingface.co/spaces/kr4phy/SchoolSustainabilityPrediction/resolve/main/tests/test_api.py
12 kB
| """ | |
| FastAPI μλν¬μΈνΈ ν΅ν© ν μ€νΈ. | |
| μΈλΆ API (NEIS, νμλΆ) νΈμΆμ mock νμ¬ μ€μ μΈν°λ· μ°κ²° μμ΄ μ€νν©λλ€. | |
| """ | |
| from __future__ import annotations | |
| from unittest.mock import AsyncMock, patch | |
| import pandas as pd | |
| from fastapi.testclient import TestClient | |
| from src.analytics.diagnostics import DiagnosticsResult | |
| from src.analytics.predictor import ForecastResult | |
| from src.analytics.scorer import SustainabilityScore | |
| from src.analytics.xai import ShapResult | |
| from src.api.main import app | |
| client = TestClient(app) | |
| # ββ κ³΅ν΅ mock λ°μ΄ν° βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MOCK_SCHOOL_INFO_ROWS = [ | |
| { | |
| "ATPT_OFCDC_SC_CODE": "P10", | |
| "ATPT_OFCDC_SC_NM": "κ²½μλ¨λκ΅μ‘μ²", | |
| "SD_SCHUL_CODE": "9290079", | |
| "SCHUL_NM": "κ³€λͺ μ΄λ±νκ΅", | |
| "SCHUL_KND_SC_NM": "μ΄λ±νκ΅", | |
| "LCTN_SC_NM": "λμ΄ν", | |
| "FOND_SC_NM": "곡립", | |
| "COEDU_SC_NM": "λ¨μ¬κ³΅ν", | |
| "XTLN_YN": "N", | |
| "SCHUL_FOND_YMD": "19620301", | |
| } | |
| ] | |
| MOCK_STUDENT_ROWS = [ | |
| {"SD_SCHUL_CODE": "9290079", "SCHUL_NM": "κ³€λͺ μ΄λ±νκ΅", "AY": "2023", "STUDENT_COUNT": "8", "SE_COUNT": "6"}, | |
| ] | |
| MOCK_TEACHER_ROWS = [ | |
| {"SD_SCHUL_CODE": "9290079", "SCHUL_NM": "κ³€λͺ μ΄λ±νκ΅", "AY": "2023", "TEACHER_COUNT": "4", "TEMP_TEACHER_COUNT": "2"}, | |
| ] | |
| MOCK_YEARLY_STUDENT_ROWS = [ | |
| [{"SD_SCHUL_CODE": "9290079", "AY": "2017", "STUDENT_COUNT": "20", "SE_COUNT": "6"}], | |
| [{"SD_SCHUL_CODE": "9290079", "AY": "2018", "STUDENT_COUNT": "18", "SE_COUNT": "6"}], | |
| [{"SD_SCHUL_CODE": "9290079", "AY": "2019", "STUDENT_COUNT": "16", "SE_COUNT": "6"}], | |
| [{"SD_SCHUL_CODE": "9290079", "AY": "2020", "STUDENT_COUNT": "14", "SE_COUNT": "6"}], | |
| [{"SD_SCHUL_CODE": "9290079", "AY": "2021", "STUDENT_COUNT": "12", "SE_COUNT": "6"}], | |
| [{"SD_SCHUL_CODE": "9290079", "AY": "2022", "STUDENT_COUNT": "10", "SE_COUNT": "6"}], | |
| [{"SD_SCHUL_CODE": "9290079", "AY": "2023", "STUDENT_COUNT": "8", "SE_COUNT": "6"}], | |
| ] | |
| class TestHealthEndpoint: | |
| def test_health_returns_ok(self) -> None: | |
| response = client.get("/health") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["status"] == "ok" | |
| assert "env" in data | |
| assert "granite_model" in data | |
| class TestSchoolInfoEndpoint: | |
| def test_school_not_found_returns_404(self) -> None: | |
| with patch( | |
| "src.api.routes.schools.DataGoKrClient.fetch_school_info", | |
| new_callable=AsyncMock, | |
| return_value=[], | |
| ): | |
| response = client.get("/schools/NOTEXIST") | |
| assert response.status_code == 404 | |
| def test_school_found_returns_info(self) -> None: | |
| with patch( | |
| "src.api.routes.schools.DataGoKrClient.fetch_school_info", | |
| new_callable=AsyncMock, | |
| return_value=MOCK_SCHOOL_INFO_ROWS, | |
| ): | |
| response = client.get("/schools/9290079") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["sd_schul_code"] == "9290079" | |
| assert data["school_name"] == "κ³€λͺ μ΄λ±νκ΅" | |
| class TestSchoolStatsEndpoint: | |
| def test_stats_no_data_returns_404(self) -> None: | |
| with ( | |
| patch( | |
| "src.api.routes.schools.DataGoKrClient.fetch_student_count", | |
| new_callable=AsyncMock, | |
| return_value=[], | |
| ), | |
| patch( | |
| "src.api.routes.schools.DataGoKrClient.fetch_teacher_count", | |
| new_callable=AsyncMock, | |
| return_value=[], | |
| ), | |
| ): | |
| response = client.get("/schools/NOTEXIST/stats?year=2023") | |
| assert response.status_code == 404 | |
| def test_stats_returns_data(self) -> None: | |
| with ( | |
| patch( | |
| "src.api.routes.schools.DataGoKrClient.fetch_student_count", | |
| new_callable=AsyncMock, | |
| return_value=MOCK_STUDENT_ROWS, | |
| ), | |
| patch( | |
| "src.api.routes.schools.DataGoKrClient.fetch_teacher_count", | |
| new_callable=AsyncMock, | |
| return_value=MOCK_TEACHER_ROWS, | |
| ), | |
| ): | |
| response = client.get("/schools/9290079/stats?year=2023") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["year"] == 2023 | |
| assert float(data["student_count"]) == 8.0 | |
| class TestDemoEndpoint: | |
| """λ°λͺ¨ μλν¬μΈνΈ ν μ€νΈ (mock λ°μ΄ν° λ°ν).""" | |
| def test_demo_diagnose_returns_200(self) -> None: | |
| response = client.get("/demo/diagnose/9290079") | |
| assert response.status_code == 200 | |
| def test_demo_diagnose_has_required_fields(self) -> None: | |
| response = client.get("/demo/diagnose/9290079") | |
| data = response.json() | |
| required = {"schul_code", "school_name", "status_label", "sustainability_score", "forecast"} | |
| assert required.issubset(data.keys()) | |
| def test_demo_forecast_years_sequential(self) -> None: | |
| response = client.get("/demo/diagnose/9290079") | |
| forecast = response.json()["forecast"] | |
| years = forecast["forecast_years"] | |
| for i in range(1, len(years)): | |
| assert years[i] == years[i - 1] + 1 | |
| def test_demo_shap_has_top_factors(self) -> None: | |
| response = client.get("/demo/diagnose/9290079") | |
| shap = response.json()["shap_result"] | |
| assert len(shap["top_factors"]) > 0 | |
| def test_demo_score_in_range(self) -> None: | |
| response = client.get("/demo/diagnose/9290079") | |
| score = response.json()["sustainability_score"] | |
| assert 0 <= score <= 100 | |
| def test_demo_simulations_present(self) -> None: | |
| response = client.get("/demo/diagnose/9290079") | |
| sims = response.json()["simulations"] | |
| assert len(sims) >= 1 | |
| class TestPredictionEndpoint: | |
| def test_predict_returns_forecast(self) -> None: | |
| fake_forecast = ForecastResult( | |
| schul_code="9290079", | |
| target_col="student_count", | |
| forecast_years=[2024, 2025, 2026], | |
| point_forecast=[7.0, 6.0, 5.0], | |
| lower_bound=[6.0, 5.0, 4.0], | |
| upper_bound=[8.0, 7.0, 6.0], | |
| model_version="arima-fallback", | |
| context_years=[2017, 2018, 2019, 2020, 2021, 2022, 2023], | |
| context_values=[20.0, 18.0, 16.0, 14.0, 12.0, 10.0, 8.0], | |
| ) | |
| with ( | |
| patch( | |
| "src.api.routes.predictions.DataGoKrClient.fetch_student_count", | |
| new_callable=AsyncMock, | |
| side_effect=MOCK_YEARLY_STUDENT_ROWS, | |
| ), | |
| patch( | |
| "src.api.routes.predictions._predictor.predict", | |
| return_value=fake_forecast, | |
| ), | |
| ): | |
| response = client.get("/schools/9290079/predict?horizon_years=3&end_year=2023") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["schul_code"] == "9290079" | |
| assert len(data["forecast_years"]) == 3 | |
| class TestDiagnosticsAndSimulationEndpoint: | |
| def test_diagnose_returns_integrated_payload(self) -> None: | |
| fake_result = DiagnosticsResult( | |
| schul_code="9290079", | |
| school_name="κ³€λͺ μ΄λ±νκ΅", | |
| status_label="μν", | |
| status_code=2, | |
| sustainability_score=38.5, | |
| forecast=ForecastResult( | |
| schul_code="9290079", | |
| target_col="student_count", | |
| forecast_years=[2024, 2025], | |
| point_forecast=[7.0, 6.0], | |
| lower_bound=[6.0, 5.0], | |
| upper_bound=[8.0, 7.0], | |
| model_version="arima-fallback", | |
| context_years=[2022, 2023], | |
| context_values=[10.0, 8.0], | |
| ), | |
| shap_result=ShapResult( | |
| schul_code="9290079", | |
| status_label="μν", | |
| status_code=2, | |
| shap_values={"student_count_latest": 1.0}, | |
| top_factors=[ | |
| { | |
| "feature": "student_count_latest", | |
| "feature_kr": "νμ¬ νμ μ", | |
| "shap_value": 1.0, | |
| "feature_value": 8.0, | |
| } | |
| ], | |
| base_value=0.0, | |
| model_accuracy=None, | |
| ), | |
| score_detail=SustainabilityScore( | |
| schul_code="9290079", | |
| total_score=38.5, | |
| curriculum_score=20.0, | |
| personnel_score=30.0, | |
| facility_score=40.0, | |
| community_score=60.0, | |
| sub_scores={}, | |
| warnings=["νμ μκ° λ§€μ° μ μ΅λλ€."], | |
| ), | |
| simulations={ | |
| "improve_teacher_ratio": { | |
| "label": "κ΅μ¬ 1μΈλΉ νμ μ κ°μ ", | |
| "description": "κ΅μ μ¦μ", | |
| "base_total_score": 38.5, | |
| "sim_total_score": 41.0, | |
| "delta_score": 2.5, | |
| } | |
| }, | |
| metadata={"diagnosed_at": "2026-01-01T00:00:00", "model_version": "arima-fallback"}, | |
| ) | |
| with ( | |
| patch( | |
| "src.api.routes.diagnostics._collect_school_data", | |
| new_callable=AsyncMock, | |
| return_value=( | |
| {"sd_schul_code": "9290079", "school_name": "κ³€λͺ μ΄λ±νκ΅"}, | |
| pd.Series({2022: 10.0, 2023: 8.0}), | |
| ), | |
| ), | |
| patch( | |
| "src.api.routes.diagnostics._engine.diagnose", | |
| return_value=fake_result, | |
| ), | |
| ): | |
| response = client.get("/schools/9290079/diagnose?horizon_years=2&end_year=2023") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["schul_code"] == "9290079" | |
| assert "forecast" in data | |
| assert "shap_result" in data | |
| assert "score_detail" in data | |
| def test_simulate_returns_score(self) -> None: | |
| with patch( | |
| "src.api.routes.diagnostics._collect_school_data", | |
| new_callable=AsyncMock, | |
| return_value=( | |
| {"sd_schul_code": "9290079", "teacher_count": 4, "student_count": 8}, | |
| pd.Series({2023: 8.0}), | |
| ), | |
| ): | |
| response = client.post( | |
| "/schools/9290079/simulate?end_year=2023", | |
| json={"school_data_overrides": {"teacher_count": 6}}, | |
| ) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["schul_code"] == "9290079" | |
| assert "total_score" in data | |
| class TestRegionRiskEndpoint: | |
| def test_region_risk_returns_summary(self) -> None: | |
| with ( | |
| patch( | |
| "src.api.routes.regions.DataGoKrClient.fetch_school_info", | |
| new_callable=AsyncMock, | |
| return_value=MOCK_SCHOOL_INFO_ROWS, | |
| ), | |
| patch( | |
| "src.api.routes.regions.DataGoKrClient.fetch_student_count", | |
| new_callable=AsyncMock, | |
| return_value=MOCK_STUDENT_ROWS, | |
| ), | |
| patch( | |
| "src.api.routes.regions.DataGoKrClient.fetch_teacher_count", | |
| new_callable=AsyncMock, | |
| return_value=MOCK_TEACHER_ROWS, | |
| ), | |
| ): | |
| response = client.get("/regions/P10/risk?year=2023") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["region_code"] == "P10" | |
| assert "schools" in data | |
| assert "summary" in data | |