Update
This article records how I understood the relationship between pytest, GitHub Actions, and CI at the point when I had expanded my test suite from 3 tests to 9.
Since then, I have continued developing pytest as what I call an “incident-prevention ledger.”
The regression suite has expanded into areas such as sales input, product registration, database constraints, rollback behavior, historical-data preservation, and dashboard aggregation.
Therefore, references to “9 tests” and “areas I want to test next” describe the state of the project at the time this article was written.
Introduction
Hello from Japan! 🇯🇵
This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.
I currently work as a truck driver while teaching myself web application development with Python and Flask.
At the time I wrote this article, my total learning time had reached 159 hours.
GitHub:
https://github.com/tosane932/sales_data_app
In my personal project, I had been working on:
- XSS protection
- Database migration repairs
- Strengthening pytest
- Automated testing with GitHub Actions
But because I was still a beginner, I kept running into very basic questions:
What exactly is pytest doing?
What is the difference between pytest and GitHub Actions?
And where did this thing called CI suddenly come from?
Rather than memorizing the terminology, I tried to understand it using concepts from my day job:
Truck inspection logs and workplace safety management.
This article is my beginner-level explanation of pytest, GitHub Actions, CI, and regression testing through that analogy.
What I Was Actually Building
At the time, my development workflow was starting to look like this:
Discover a bug or near miss
↓
Investigate the cause
↓
Fix the code
↓
Add a regression test to pytest
↓
Push to GitHub
↓
GitHub Actions automatically runs pytest
↓
If the dangerous state returns, CI fails
At first, I thought I was simply:
Writing pytest tests
But gradually, I realized I was building something more useful:
A system that records previously discovered problems and automatically warns me if the application returns to the same dangerous state.
What Is pytest?
pytest is a tool for writing and running tests in Python.
Very simply:
It automatically checks whether your program still behaves the way you expect.
For example, imagine this function:
def add(a, b):
return a + b
You could write:
def test_add():
assert add(2, 3) == 5
Here:
assert
means something like:
I expect this to be true.
If the result is correct, the test passes.
PASSED
If the result is wrong, it fails.
FAILED
That's the basic idea.
In Trucking Terms, a pytest Test Is an Inspection Item
Before taking a truck onto the road, there are things that must be checked.
For example:
□ Are the tires in good condition?
□ Do the lights work?
□ Is the oil level okay?
□ Are the brakes working normally?
pytest started making more sense to me when I thought about it the same way.
For a software system, the inspection items might be:
□ Is the AI prompt still structured correctly?
□ Is untrusted text still rendered safely?
□ Is product data processed correctly?
□ Does invalid sales input leave the database unchanged?
So in my head:
One pytest test ≈ one inspection item.
That simple analogy made the concept much easier to understand.
A Test Suite Feels Like an Inspection Logbook
When multiple pytest tests are collected together, they start to resemble an inspection logbook.
At the time this article was written, I had expanded the project from 3 pytest tests to 9.
But I was not simply trying to increase the number.
For example, I had previously discovered a place where AI-generated responses were displayed using JavaScript:
innerHTML
After fixing that XSS risk, I added a pytest regression test to check:
Has this rendering path accidentally returned to the dangerous implementation?
The workflow became:
Discover an XSS risk
↓
Fix it
↓
Record the fix as a pytest regression test
Even if I forget the details six months later, pytest can still remember the inspection item for me.
I have continued expanding the test suite using this idea.
“I'll Be More Careful Next Time” Is Not Enough
This is another idea that comes from my day job.
After an accident or mistake, saying:
I'll be more careful next time.
is weak as a prevention strategy.
It still depends entirely on human attention.
I think an important distinction is between:
Never make the same mistake again.
and:
Prevent the same dangerous situation from existing again.
Suppose there is a location on a delivery route where a dangerous situation repeatedly occurs at the same time of day.
One response is:
Drive more carefully there.
But stronger options might be:
Change the delivery time
Change the delivery order
Use another route
Instead of relying only on attention, change the conditions that create the risk.
I found the same idea useful in software development.
XSS Is a Good Example
My application previously displayed AI-generated text using:
innerHTML
A weak response would be:
Be careful not to pass dangerous HTML into it.
Instead, I changed the implementation so that:
Even if HTML-like text arrives,
it is not interpreted as HTML.
Then I added a pytest regression test that checks whether the relevant code path has returned to its previous dangerous form.
This changes the strategy from:
Be careful.
to:
Build the system so that the unsafe state is harder to recreate.
That feels very similar to workplace safety management.
So What Is GitHub Actions?
This is where I originally became confused.
To run pytest locally, I type:
pytest -v
So if pytest already runs the tests, what does GitHub Actions do?
In my repository, GitHub Actions is configured so that when I push code to GitHub:
GitHub automatically prepares an environment and runs pytest for me.
For example:
git push origin main
can trigger:
GitHub Actions starts
↓
Prepare a Python environment
↓
Install dependencies
↓
Run pytest -v
↓
Report the result
In my current configuration, tests also run for Pull Requests targeting main.
So pytest is the testing tool.
GitHub Actions is the automation platform that runs those tests in response to GitHub events.
Pre-Trip Inspection and the Shipping Gate
The trucking analogy helped here too.
When I run:
pytest -v
on my own machine, I think of it as:
A pre-trip inspection I perform myself.
Then I push the code.
GitHub Actions runs pytest again in GitHub's environment.
I think of that as:
Another automated inspection at the shipping gate.
Conceptually:
Developer:
"I inspected it locally."
↓
push
↓
GitHub Actions:
"We'll inspect it here too."
↓
Tests passed
↓
OK
The useful part is that the code is checked not only in my own development environment, but also in another automated environment.
Then What Is CI?
This brings us to:
CI
which stands for:
Continuous Integration
When I first heard that term, I thought:
Okay... but what does it actually do?
For my current level and project, the easiest way to understand CI is:
A workflow that automatically checks code changes when they are integrated into the shared repository.
CI can include more than testing.
For example, it may also include:
- Linting
- Static analysis
- Builds
- Security checks
- Formatting checks
But in my repository, the central CI activity is:
GitHub Actions automatically running pytest
So I stopped trying to understand CI as an abstract buzzword and instead connected it to what my project was actually doing.
pytest vs. GitHub Actions vs. CI
This is how I currently organize the concepts in my head:
| Name | My Understanding |
|---|---|
| pytest | The inspection tool |
| One pytest test | One inspection item |
| Test files | The inspection logbook |
| GitHub Actions | The equipment that runs inspections automatically |
| CI | The ongoing process of automatically checking code changes |
Once I separated the responsibilities this way, everything became much easier to understand.
My Actual GitHub Actions Configuration
The important part of my workflow looks roughly like this:
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: pytest -v
A push to main or a Pull Request targeting main causes GitHub Actions to run pytest automatically.
Previously, the workflow used:
pytest test_prompts.py -v
That only executed one specific test file.
So even if I added:
test_security.py
test_sales.py
test_database.py
GitHub Actions would ignore them.
I changed it to:
pytest -v
Now pytest uses its normal discovery rules and automatically includes newly added tests.
Why Is This Useful?
Imagine I change a completely different feature in the future.
The new feature itself may work correctly.
But without realizing it, I could accidentally break an XSS protection that I fixed months earlier.
It would be difficult for a human to remember every previous issue:
Check XSS
Check prompts
Check Jinja
Check Gemini
Check sales behavior
Check database state
...
every single time.
Instead, I can record those checks in pytest.
Then:
pytest performs the inspections
for me.
And with GitHub Actions:
Even if I forget to run pytest locally,
GitHub performs another automated check.
That does not mean GitHub Actions makes local testing unnecessary.
My mental model is still:
Local pytest = pre-trip inspection
GitHub Actions = automated inspection at the shipping gate
Two layers are better than relying on one.
Turning Small Near Misses into Tests
At the time I wrote this article, I wanted to continue this cycle:
Discover a small bug
↓
Investigate the cause
↓
Fix it
↓
Write a regression test
↓
Let GitHub Actions check it every time
This is why I started calling pytest:
An incident-prevention ledger.
Every time I discover another problem, another inspection item can be added to the logbook.
Since then, I have expanded the idea into database-changing operations such as:
- Sales entry
- Product registration
For those tests, I no longer want to check only:
Did the HTTP request return the expected response?
I also want to check:
What happened to the database after the request failed?
That is a much stronger inspection.
pytest Alone Does Not Make a System Safe
This is important.
At the time this article was written, I had 9 pytest tests.
That did not mean:
This application is now completely safe!
Many areas were still untested.
At the time, I wanted to expand coverage into areas such as:
- PostgreSQL migrations
- Authentication
- CSRF
- Invalid sales input
- Gemini API failures
- Real browser DOM behavior
Since then, I have added regression coverage for areas including:
- Invalid sales input
- Product registration
- Database uniqueness constraints
- Rollback behavior
- Historical-data preservation after soft deletion
- Dashboard aggregation
Some other areas, particularly:
- Authentication
- Authorization
- CSRF
are still planned for separate stages.
Having an inspection logbook does not guarantee that an accident can never happen.
But it does make it harder to overlook a dangerous state that has already been discovered once.
That alone is valuable.
Conclusion
If someone had told me when I first started programming:
pytest
GitHub Actions
CI
regression tests
I probably would not have understood much.
But translating those concepts into my own work experience made them much clearer.
My current mental model is:
pytest
= inspection
pytest tests
= inspection items
test files
= inspection logbook
GitHub Actions
= automated inspection equipment
CI
= a process that continuously checks code changes
My development workflow has gradually become:
Discover a problem
↓
Investigate the cause
↓
Fix it
↓
Add a regression rule to pytest
↓
Verify locally
↓
Push
↓
GitHub Actions performs another automated inspection
At the time I wrote this article, the suite contained 9 tests.
Since then, I have continued adding real bugs and specifications that I want to protect.
My goal is for pytest to become not merely:
A tool for checking whether things work
but:
An incident-prevention ledger containing lessons from previous bugs and near misses.
I am still learning.
But I have found that understanding technical terminology does not always mean memorizing definitions.
Sometimes the most useful question is:
What is this actually doing?
And then translating that answer into words I already understand.













