DEV Community

Cover image for 🔰 pytest, GitHub Actions, and CI — Explained Through a Truck Driver’s Inspection Log
tosane932
tosane932

Posted on Originally published at qiita.com

🔰 pytest, GitHub Actions, and CI — Explained Through a Truck Driver’s Inspection Log

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
Enter fullscreen mode Exit fullscreen mode

At first, I thought I was simply:

Writing pytest tests
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You could write:

def test_add():
    assert add(2, 3) == 5
Enter fullscreen mode Exit fullscreen mode

Here:

assert
Enter fullscreen mode Exit fullscreen mode

means something like:

I expect this to be true.

If the result is correct, the test passes.

PASSED
Enter fullscreen mode Exit fullscreen mode

If the result is wrong, it fails.

FAILED
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

But stronger options might be:

Change the delivery time
Change the delivery order
Use another route
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A weak response would be:

Be careful not to pass dangerous HTML into it.
Enter fullscreen mode Exit fullscreen mode

Instead, I changed the implementation so that:

Even if HTML-like text arrives,
it is not interpreted as HTML.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

can trigger:

GitHub Actions starts
        ↓
Prepare a Python environment
        ↓
Install dependencies
        ↓
Run pytest -v
        ↓
Report the result
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That only executed one specific test file.

So even if I added:

test_security.py
test_sales.py
test_database.py
Enter fullscreen mode Exit fullscreen mode

GitHub Actions would ignore them.

I changed it to:

pytest -v
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

every single time.

Instead, I can record those checks in pytest.

Then:

pytest performs the inspections
Enter fullscreen mode Exit fullscreen mode

for me.

And with GitHub Actions:

Even if I forget to run pytest locally,
GitHub performs another automated check.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.


GitHub

https://github.com/tosane932/sales_data_app

Top comments (0)