A pytest-based API test suite for the AquaSense FastAPI backend (Smart Irrigation Advisory System), built as a standalone portfolio project to demonstrate API test automation practices: fixtures, parametrization, markers, and HTML reporting.
Built as a separate project (not living inside AquaSense itself) so it reads as a genuine "I test other people's/my own APIs" project rather than being buried inside the app repo.
The suite covers every endpoint AquaSense exposes:
| Area | Endpoints | What's verified |
|---|---|---|
| Auth | POST /auth/register, POST /auth/login |
registration, duplicate-email rejection, invalid email format, login success/failure, that every protected endpoint actually enforces its token |
| Crops | GET /crops |
seeded catalog shape, auth requirement |
| Fields | POST/GET/PATCH/DELETE /fields, POST /fields/{id}/crops |
full CRUD, input validation (soil type, irrigation method, lat/lon/area bounds), 404s for missing resources, ownership isolation (user A can never see or touch user B's fields) |
| Recommendations | .../recommendation, /history, /forecast, /water-savings, /outlook, /report.pdf |
validation paths (404/400) always run; happy paths (marked external) hit the real Open-Meteo weather API AquaSense depends on |
The ownership isolation tests (test_fields.py) are the ones worth
pointing out in an interview — they register a second, independent user and
assert they get a 404 (not just an empty list) when trying to reach the
first user's data by ID. That's the difference between "the happy path
works" and "I checked whether this API leaks other people's data."
By default, this project assumes it's checked out as a sibling of
aquasense/:
Documents/
├── api-test-framework-pytest/ ← this project
└── aquasense/
└── backend/ ← the system under test
If your layout differs, set an environment variable instead:
export AQUASENSE_BACKEND_PATH=/path/to/your/aquasense/backendAquaSense's backend uses pydantic-core (Rust-compiled), which doesn't yet
have prebuilt wheels for very new Python versions — use Python 3.11 (or
whatever version AquaSense's own backend/requirements.txt was built
against) to avoid a from-source build failure.
cd api-test-framework-pytest
python3.11 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt# Everything
pytest
# Fast subset — one representative test per feature area
pytest -m smoke
# Full regression suite
pytest -m regression
# Only negative/validation-path tests
pytest -m negative
# Skip the tests that hit the live Open-Meteo API (e.g. offline / flaky network)
pytest -m "not external"
# Combine markers
pytest -m "crud and negative"Markers are declared in pytest.ini: smoke, regression, auth, crud,
negative, external.
Every run writes an HTML report to reports/report.html (configured via
addopts in pytest.ini, using pytest-html):
xdg-open reports/report.html # Linux
open reports/report.html # macOS.github/workflows/pytest-ci.yml runs on every push and pull request to
main:
- Checks out two repos: this project, and
RISHITASHARMA01/aquasense(into a siblingaquasense/folder). No server or database service container is started —conftest.pyimports the AquaSense app directly in-process viaTestClientwith a fresh in-memory SQLite DB per test, so having the source on disk is all CI needs.AQUASENSE_BACKEND_PATHis set to point at the checked-out path. - Sets up Python 3.11 (pinned — see "Setup" above for why) and
installs
requirements.txt. - Runs the deterministic suite (
pytest -m "not external", 46 tests) as a normal, build-blocking step. - Runs the
externalsuite separately (7 tests hitting the live Open-Meteo weather API) as a non-blocking step (continue-on-error: true) — a third-party outage or transient network issue on the runner shouldn't fail the build for code that didn't change. It still runs and reports on every build so regressions are visible, just not gating. - Uploads both HTML reports (
reports/report.htmlandreports/external-report.html) as a downloadable build artifact, whether the job passed or failed (if: always()), so a failure can be debugged from the Actions tab without re-running locally.
What's excluded and why: nothing is skipped outright — the split is
about blocking vs. non-blocking, not about coverage. The only thing CI
can't fully guarantee is that the external group's result reflects the
AquaSense API itself rather than Open-Meteo's uptime at that moment; that
tradeoff is the whole reason those 7 tests are marked and isolated.
TestClient, not requests against a live server. AquaSense's backend
needs no external services to run — it defaults to a local SQLite file and
has no required environment variables (see app/config.py). That means
there's no reason to pay the cost of spinning up uvicorn and hitting it
over real HTTP: FastAPI's TestClient runs the app in-process (via ASGI),
which is faster and gives cleaner tracebacks on failure. (If AquaSense's
backend instead required a running Postgres instance or other
infrastructure, requests against a real running instance would have been
the better call — that tradeoff is a deliberate one, not a default.)
Fresh in-memory SQLite DB per test. The client fixture in
conftest.py creates a brand-new sqlite:// (in-memory) database and
overrides FastAPI's get_db dependency before each test function. This
means every test starts from an identical, empty-of-users-and-fields state
— no test can pass or fail because of leftover data from a previous test,
and tests can run in any order (verified: they do).
Fixture layering to avoid repeated setup boilerplate. Rather than every test re-implementing "register a user, log in, grab the token" or "create a field to test against," fixtures build on each other:
client → registered_user → auth_headers → test_field → test_field_with_crop
A test that needs a field with an active crop just asks for
test_field_with_crop and gets a fully-set-up scenario in one line.
The external marker is an honest boundary, not a workaround. The
recommendation/forecast/history/water-savings/outlook endpoints call the
real Open-Meteo weather API (see app/services/weather.py). Open-Meteo is
free and requires no API key, so these tests run out of the box — but they
still depend on network access and a third party being up, which is a
different risk profile than the rest of the suite. Marking them lets CI (or
you, offline) choose whether to include that risk rather than silently
mixing flaky and deterministic tests together.
All 53 tests pass, including the 7 marked external (verified against
the live Open-Meteo API at the time this suite was written).
Nothing was skipped due to missing credentials — AquaSense's auth uses a
dev-only-secret-change-me default JWT secret and SQLite by default, so
this suite can register and log in its own throwaway test users freely
without needing any real account or API key.
The one honest limitation: the external-marked tests are only as
reliable as Open-Meteo's uptime and your network connection at test-run
time. If you see failures only in that group, that's the third-party
dependency, not the AquaSense API or this test suite — rerun with
pytest -m "not external" to confirm the rest of the suite is unaffected.