Unit Testing#
Unit testing is a powerful tool that helps to achieve and maintain a solid code quality. For it to be effective, unit tests should be tied to the development cycle and run automatically, so that during the development, one can directly check the outcome of the unit tests without having to take care of initiating them.
A suitable approach for such automated setup is to use CI/CD pipelines. This project templates has a unit testing pipeline set up and here are all you need to know bout it:
File location: .github/workflows/unitTests.yml
This GitHub Actions workflow is for running Python unit tests and reporting test coverage directly on Pull Requests.
With uv and the py-cov-action, this workflow is highly optimized for speed and automated feedback.
Here is the breakdown of exactly what this workflow does, section by section.
1. The Triggers (on)#
This section dictates when the workflow runs.
Push: It triggers on direct pushes to the
mainbranch.Pull Request: It triggers when PRs targeting
mainare opened, updated (synchronize), reopened, edited, or marked as “ready for review”.
2. Job 1: unit-tests#
This job handles the actual execution of your testing suite.
The Guardrail (
if): Even though the workflow triggers on pushes tomain, this specific job is explicitly restricted to Pull Requests only, and it strictly ignores Draft Pull Requests. If someone opens a draft, it won’t waste runner minutes.Setup & Checkout: It checks out your code on an
ubuntu-24.04machine and explicitly pulls in any Git submodules if you have them.Environment Setup (The Modern Way): Instead of using standard
setup-pythonandpip, it usesastral-sh/setup-uv@v7to installuvwith automatic caching. Then, it runsuv sync --group testto instantly create the environment and fetch dependencies based on yourpyproject.toml.Execution: It runs
pytestviauv run.-s: Disables output capturing (shows print statements).-vvv: Maximum verbosity.--cov: Measures code coverage.It sets an environment variable to name the coverage file
.coverage.unittests.Artifact Storage: Regardless of whether the tests pass or fail (
if: always()), it takes the generated.coverage.unittestsfile and uploads it to GitHub’s temporary storage so the next job can access it.
3. Job 2: coverage#
This job takes the results from the first job, analyzes them, and posts a comment on the Pull Request.
Dependencies (
needs): It waits forunit-teststo finish. It will run whether the tests passed or failed, ensuring you still get a coverage report even if a test breaks.Permissions: It grants the GitHub Actions bot permission to write comments on PRs and read repository contents.
Download & Combine: It downloads the coverage artifact uploaded in the previous step. The
merge-multiple: trueflag is a great touch—if you ever expand this workflow to run tests on multiple operating systems or Python versions, this will combine all their coverage files together.The Commenter: It uses
python-coverage-comment-actionto parse the.coveragefile and automatically post (or update) a highly visual comment on the active Pull Request showing exactly which lines of code were covered or missed.Fallback Storage: It saves the raw text of the comment as an artifact. This is often used in advanced setups where PRs from forks don’t have permission to comment directly, allowing a separate workflow to post it later.
This setup is:
Cost-efficient: It entirely skips Draft PRs.
Fast: It relies on
uvfor dependency resolution, which shaves significant time off the setup phase compared to standardpip.Decoupled: By separating the test running from the coverage reporting, you keep the logs clean and make it easier to add more testing jobs (like integration tests or UI tests) later without rewriting the coverage logic.