refactor(test): unified test runner, markers, elemes.sh cleanup
- Add pytest.ini with unit/integration/e2e markers + --strict-markers - Add Makefile as centralized test runner entrypoint - Add @pytest.mark.integration to 7 DB-dependent test files - Add @pytest.mark.unit to 6 pure-logic test files - Refactor elemes.sh: test-* commands delegate to 'python -m pytest -m <marker>' - Move test_elemes_sh.sh -> scripts/ (path-adjusted), add test-shell Makefile target - Add test-worker target for compiler_worker/tests (separate PYTHONPATH) - Update runclearbuild smoke gate to use -m unit + sub-home subset - Add .github/workflows/ci.yml: test-unit + test-integration on PR to dev - 239 tests collected (110 unit, 125 integration) — no count reduction
This commit is contained in:
parent
25269fca08
commit
a5aedf66d4
|
|
@ -0,0 +1,64 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
push:
|
||||
branches: [dev]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_USER: elemes
|
||||
POSTGRES_PASSWORD: elemes
|
||||
POSTGRES_DB: elemes_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U elemes -d elemes_test"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
PYTHONPATH=services python -m pytest -m unit -v
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
PYTHONPATH=services DATABASE_URL="postgresql+psycopg://elemes:elemes@localhost:5432/elemes_test" python -m pytest -m integration -v
|
||||
|
||||
- name: Run compiler worker tests
|
||||
run: |
|
||||
cd compiler_worker && PYTHONPATH=. python -m pytest -v
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
# E2E tests are slow — run async/post-merge or scheduled, not blocking per-PR
|
||||
if: github.event_name == 'schedule' || github.ref == 'refs/heads/dev'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: E2E smoke test
|
||||
run: |
|
||||
echo "E2E tests run on schedule or post-merge (non-blocking per-PR)"
|
||||
echo "See load-test/ for locust-based performance tests"
|
||||
|
|
@ -12,3 +12,4 @@ frontend/node_modules/
|
|||
velxio-deployer-firmware/build
|
||||
velxio-deployer-firmware/managed_components/
|
||||
lms-c-precompiled.tar
|
||||
.browser-test/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
# Elemes Test Suite — Unified Test Runner
|
||||
#
|
||||
# Usage:
|
||||
# make test-unit # Unit tests only (fast, no DB, for dev loop)
|
||||
# make test-integration # Integration tests (needs PostgreSQL test DB)
|
||||
# make test-all # Full suite (CI only)
|
||||
# make test-smoke # Smoke test post-deploy
|
||||
# make test-list # List collected tests
|
||||
# make test-stats # Count tests by marker
|
||||
#
|
||||
# Env:
|
||||
# DATABASE_URL Required for integration tests (set elemes_test DB)
|
||||
# PYTHONPATH Defaults to services for backend tests
|
||||
#
|
||||
# Jika DATABASE_URL tidak diset, integration test yang butuh DB akan otomatis
|
||||
# di-skip via skipif. test-integration tetap berjalan untuk test yang tidak
|
||||
# butuh DB (mis. Flask test client tanpa DB).
|
||||
|
||||
PYTHON ?= python3
|
||||
PYTHONPATH ?= services
|
||||
PYTEST ?= $(PYTHON) -m pytest
|
||||
export PYTHONPATH
|
||||
|
||||
# --- Default (backward-compat): `make test` = test-all ---
|
||||
|
||||
.PHONY: test test-unit test-integration test-all test-worker test-smoke test-shell test-list test-stats help
|
||||
|
||||
help:
|
||||
@echo "Elemes Test Suite:"
|
||||
@echo " make test-unit Unit tests only (fast, no DB needed)"
|
||||
@echo " make test-integration Integration tests (needs postgresql test DB)"
|
||||
@echo " make test-all Full suite (CI only)"
|
||||
@echo " make test-worker Compiler worker tests (separate project)"
|
||||
@echo " make test-smoke # Smoke test post-deploy (unit + sub-home subset)"
|
||||
@echo " make test-shell # Shell regression test (elemes.sh structure)"
|
||||
@echo " make test-list List collected tests"
|
||||
@echo " make test-stats Show test counts by marker"
|
||||
|
||||
## test-all is the default (backward compat: `make test` == `make test-all`)
|
||||
test: test-all
|
||||
|
||||
## Unit tests only — pure logic, no DB, fast
|
||||
test-unit:
|
||||
$(PYTEST) -m unit -v
|
||||
|
||||
## Integration tests — requires PostgreSQL test DB (elemes_test)
|
||||
test-integration:
|
||||
$(PYTEST) -m integration -v
|
||||
|
||||
## Full suite (services) — all markers, CI gate
|
||||
test-all:
|
||||
$(PYTEST) -v
|
||||
|
||||
## Compiler worker tests (separate project, separate PYTHONPATH)
|
||||
test-worker:
|
||||
cd compiler_worker && PYTHONPATH=. $(PYTEST) -v
|
||||
|
||||
|
||||
## Smoke test — minimal subset, runs inside container post-build
|
||||
test-smoke:
|
||||
$(PYTEST) -m unit -v
|
||||
$(PYTEST) services/tests/test_sub_home.py services/tests/test_sub_home_api.py -v
|
||||
|
||||
## Shell regression test — elemes.sh structure (dynamic PROJECT_NAME, no hard-coded names)
|
||||
test-shell:
|
||||
bash scripts/test_elemes_sh.sh
|
||||
|
||||
## List collected tests
|
||||
test-list:
|
||||
$(PYTEST) --collect-only -q
|
||||
|
||||
## Show test counts by marker
|
||||
test-stats:
|
||||
@echo "=== Test counts by marker ==="
|
||||
@$(PYTEST) --collect-only -q -m unit 2>/dev/null | grep -c "test" || echo "0"
|
||||
@echo "^ unit tests"
|
||||
@$(PYTEST) --collect-only -q -m integration 2>/dev/null | grep -c "test" || echo "0"
|
||||
@echo "^ integration tests"
|
||||
@$(PYTEST) --collect-only -q 2>/dev/null | tail -1
|
||||
53
elemes.sh
53
elemes.sh
|
|
@ -191,6 +191,11 @@ runbuild | runclearbuild)
|
|||
# mengubah data produksi. Gagal bila ada tes baru yang rusak.
|
||||
if [ "$1" = "runclearbuild" ]; then
|
||||
echo "🧪 Verifikasi: unit & API test sub-home..."
|
||||
if ! compose_exec -w /app -e PYTHONPATH=services -e DATABASE_URL= \
|
||||
elemes python -m pytest -m unit -q; then
|
||||
echo "❌ Unit test gagal. Periksa output di atas sebelum deploy."
|
||||
exit 1
|
||||
fi
|
||||
if ! compose_exec -w /app -e PYTHONPATH=services -e DATABASE_URL= \
|
||||
elemes python -m pytest services/tests/test_sub_home.py services/tests/test_sub_home_api.py -q; then
|
||||
echo "❌ Tes sub-home gagal. Periksa output di atas sebelum deploy."
|
||||
|
|
@ -205,10 +210,33 @@ run)
|
|||
echo "✅ Elemes berhasil dijalankan!"
|
||||
db_init
|
||||
;;
|
||||
test)
|
||||
echo "🧪 Menjalankan unit & integration test sub-home (DATABASE_URL kosong → tes DB di-skip)..."
|
||||
test-unit)
|
||||
echo "🧪 Menjalankan unit test (cepat, no DB)..."
|
||||
compose_exec -w /app -e PYTHONPATH=services -e DATABASE_URL= \
|
||||
elemes python -m pytest services/tests/test_sub_home.py services/tests/test_sub_home_api.py -q
|
||||
elemes python -m pytest -m unit -v
|
||||
;;
|
||||
test-integration)
|
||||
echo "🧪 Menjalankan integration test (butuh DATABASE_URL)..."
|
||||
compose_exec -w /app -e PYTHONPATH=services \
|
||||
elemes python -m pytest -m integration -v
|
||||
;;
|
||||
test-all)
|
||||
echo "🧪 Menjalankan full test suite..."
|
||||
compose_exec -w /app -e PYTHONPATH=services \
|
||||
elemes python -m pytest -v
|
||||
;;
|
||||
test-smoke)
|
||||
echo "🧪 Smoke test post-deploy (unit + sub-home subset)..."
|
||||
compose_exec -w /app -e PYTHONPATH=services -e DATABASE_URL= \
|
||||
elemes python -m pytest -m unit -v
|
||||
compose_exec -w /app -e PYTHONPATH=services -e DATABASE_URL= \
|
||||
elemes python -m pytest services/tests/test_sub_home.py services/tests/test_sub_home_api.py -v
|
||||
;;
|
||||
test)
|
||||
# Backward-compat alias → test-all
|
||||
echo "🧪 Menjalankan full test suite (alias ke test-all)..."
|
||||
compose_exec -w /app -e PYTHONPATH=services \
|
||||
elemes python -m pytest -v
|
||||
;;
|
||||
exportall)
|
||||
echo "📦 === Mengekspor Semua Image (Pre-Compiled Bundle) ==="
|
||||
|
|
@ -431,6 +459,17 @@ dbrestore)
|
|||
-U "${POSTGRES_USER:-elemes}" -d "${POSTGRES_DB:-elemes}" < "$LATEST"
|
||||
echo "✅ Restore selesai. Bila daftar lesson kosong, jalankan ./elemes.sh run."
|
||||
;;
|
||||
test-worker)
|
||||
echo "🧪 Menjalankan compiler worker test suite..."
|
||||
compose_exec -w /app -e PYTHONPATH=compiler_worker \
|
||||
elemes python -m pytest compiler_worker/tests -v 2>/dev/null || \
|
||||
echo "⚠️ Compiler worker test tidak tersedia di dalam container (jalankan di host: cd compiler_worker && PYTHONPATH=. python -m pytest -v)"
|
||||
;;
|
||||
docs-validate)
|
||||
echo "📄 Validasi dokumentasi (frontmatter + broken link)..."
|
||||
compose_exec -w /app -e PYTHONPATH=services \
|
||||
elemes python scripts/validate_docs.py
|
||||
;;
|
||||
*)
|
||||
echo "💡 Cara Penggunaan elemes.sh:"
|
||||
echo " ./elemes.sh init # Inisialisasi konfigurasi, folder, & template .env"
|
||||
|
|
@ -446,7 +485,13 @@ dbrestore)
|
|||
echo " ./elemes.sh teacher # Buat/update akun guru (upsert, prompt nama & token)"
|
||||
echo " ./elemes.sh dbbackup # Backup database → backups/elemes_<ts>.sql"
|
||||
echo " ./elemes.sh dbrestore # Restore backup terbaru dari backups/"
|
||||
echo " ./elemes.sh test # Jalankan unit & API test sub-home (tanpa DB)"
|
||||
echo " ./elemes.sh test # Jalankan full test suite (alias ke test-all)"
|
||||
echo " ./elemes.sh test-unit # Unit test saja (cepat, no DB)"
|
||||
echo " ./elemes.sh test-integration # Integration test (butuh PostgreSQL test DB)"
|
||||
echo " ./elemes.sh test-all # Full test suite (CI gate)"
|
||||
echo " ./elemes.sh test-smoke # Smoke test post-deploy (unit + sub-home subset)"
|
||||
echo " ./elemes.sh test-worker # Compiler worker test suite"
|
||||
echo " ./elemes.sh docs-validate # Validasi frontmatter & broken link di docs/*.md"
|
||||
echo " ./elemes.sh loadtest # Menjalankan utilitas simulasi Load Test (Locust)"
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
[pytest]
|
||||
markers =
|
||||
unit: Pure unit tests — no DB, no network, fast (<10ms/test)
|
||||
integration: Requires database/services (PostgreSQL, Flask test client, service deps)
|
||||
e2e: End-to-end smoke tests (browser/headless or full-stack container)
|
||||
addopts = -v --tb=short --strict-markers
|
||||
testpaths =
|
||||
services/tests
|
||||
|
|
@ -24,7 +24,7 @@ FAKEBIN="$TESTROOT/fakebin"
|
|||
mkdir -p "$WORKSPACE" "$FAKEBIN"
|
||||
touch "$PARENT_DIR/.env" # PARENT_DIR/.env (dibaca run_compose & db_init)
|
||||
|
||||
cp "$(dirname "$0")/elemes.sh" "$WORKSPACE/elemes.sh"
|
||||
cp "$(dirname "$0")/../elemes.sh" "$WORKSPACE/elemes.sh"
|
||||
export FAKE_PROJECT="$PROJECT_NAME"
|
||||
|
||||
# --- Fake podman-compose: catat semua panggilan, jawab config/exec/restart ----
|
||||
|
|
@ -11,9 +11,12 @@ import pytest
|
|||
|
||||
from services.tests.conftest import STUDENT_TOKEN, TEACHER_TOKEN
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
)
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
),
|
||||
pytest.mark.integration,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Di host tanpa flask, test di-skip otomatis (importorskip).
|
|||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
# Skip seluruh modul bila flask tidak tersedia (host tanpa deps backend)
|
||||
pytest.importorskip("flask")
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import os
|
|||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
from services import lesson_service
|
||||
from services.lesson_registry import lesson_specs, sync_lesson_registry
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
pytestmark = pytest.mark.unit
|
||||
from services.lesson_service import _process_embed_embeds
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
from services.lesson_service import _parse_flashcards
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
"""Metadata model: nama tabel, constraint, relasi — tanpa butuh DB hidup."""
|
||||
|
||||
import pytest
|
||||
|
||||
from sqlalchemy import ForeignKeyConstraint
|
||||
|
||||
from services import models as _models # noqa: F401 (mendaftarkan metadata)
|
||||
from services.database import Base
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _table(name):
|
||||
return Base.metadata.tables[name]
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ import pytest
|
|||
|
||||
from services.tests.conftest import STUDENT_TOKEN, TEACHER_TOKEN
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
)
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
),
|
||||
pytest.mark.integration,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@ from services.database import SessionLocal
|
|||
from services.models import QuizAttempt, StudentProgress
|
||||
from services.tests.conftest import STUDENT_TOKEN
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
)
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
),
|
||||
pytest.mark.integration,
|
||||
]
|
||||
|
||||
LESSON = "hello_world"
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,13 @@ from services.tests.conftest import STUDENT2_TOKEN, STUDENT_TOKEN, TEACHER_TOKEN
|
|||
|
||||
DB_REQUIRED = os.environ.get("DATABASE_URL", "").strip()
|
||||
|
||||
pytestmark = pytest.mark.skipif(not DB_REQUIRED, reason="butuh DATABASE_URL (PostgreSQL nyata)")
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(
|
||||
not DB_REQUIRED,
|
||||
reason="butuh DATABASE_URL (PostgreSQL nyata)",
|
||||
),
|
||||
pytest.mark.integration,
|
||||
]
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
UUID_1 = "7eab651c-5eb1-4eb8-8fd2-17fd77aec6df"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,13 @@ from services.tests.conftest import STUDENT_TOKEN, TEACHER_TOKEN
|
|||
|
||||
DB_REQUIRED = os.environ.get("DATABASE_URL", "").strip()
|
||||
|
||||
pytestmark = pytest.mark.skipif(not DB_REQUIRED, reason="butuh DATABASE_URL (PostgreSQL nyata)")
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(
|
||||
not DB_REQUIRED,
|
||||
reason="butuh DATABASE_URL (PostgreSQL nyata)",
|
||||
),
|
||||
pytest.mark.integration,
|
||||
]
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
from services.progress_status import (
|
||||
ParsedProgress,
|
||||
format_progress_status,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import time
|
|||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
from services import lesson_service
|
||||
from services.lesson_service import (
|
||||
_read_md_cached,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import os
|
|||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
from services import lesson_service
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ def _counts(db):
|
|||
|
||||
|
||||
@needs_db
|
||||
@pytest.mark.integration
|
||||
class TestTeacherBootstrapDB:
|
||||
"""Skenario database — hanya jalan saat DATABASE_URL tersedia."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
"""Unit test hashing token — murni, tanpa DB."""
|
||||
|
||||
import pytest
|
||||
|
||||
from services.token_hashing import hash_token, pepper_set
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def test_hash_deterministic(monkeypatch):
|
||||
monkeypatch.setenv("TOKEN_PEPPER", "pepper-uji")
|
||||
|
|
|
|||
|
|
@ -17,9 +17,12 @@ import pytest
|
|||
from services import token_service as ts
|
||||
from services.tests.conftest import STUDENT2_TOKEN, STUDENT_TOKEN, TEACHER_TOKEN
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
)
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(
|
||||
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
||||
),
|
||||
pytest.mark.integration,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
|
|||
Loading…
Reference in New Issue