While refactoring my retry-logic decorator, I stumbled onto something unsettling: the KRW/USD currency-splitting logic I’d fixed earlier had quietly disappeared from the code. I caught it before deploying, but the fact that a fixed bug can silently come back with zero warning was enough to change my priorities for the day.
Testing had been sitting on my roadmap for a while, unstarted. That scare finally pushed me to add pytest. Here’s what I tested, what I found along the way, and a small Windows headache that cost me ten minutes I’ll never get back.
Toss Auto-Trading Dev Journal series (Phase 1 of 5 posts)
- Phase 0 — Laying the foundation
- Phase 0-1 — The currency bug fix
- Phase 1 — Config separation & retry logic
- Phase 1-2 — Config/retry follow-up
- Phase 1-3 (this post) — Adding pytest
← Previous: Phase 1-2 | Next: added after publishing
Why now
pytest had been on the roadmap from the start, but it kept losing to the classic “make it work first, test it later” excuse. That changed the moment I found the currency-splitting logic missing mid-refactor. Manual code review clearly wasn’t going to catch every regression — I needed a machine to do it for me.
What I decided to test
The original plan only covered display-formatting helpers like get_display_width and fit_to_width. I widened the scope considerably:
safe_int/safe_float/center_korean— small pure utility functionsextract_amounts_by_currency— the exact logic behind the USD-dropping bug I’d fixed recently. This one absolutely needed a regression test.- The
retry_on_network_errordecorator — success, failure, partial failure, backoff growth, and exception propagation - Safety-critical constants like
DRY_RUNandTARGET_BUY_PRICE— pinned so that any accidental change fails the test suite immediately
Adding pytest coverage for pure functions, retry decorators, and safety constants lets a trading bot catch refactor regressions automatically, before they reach production.
Pinning safety constants as tests
My favorite part of this whole exercise was treating safety values like DRY_RUN and TARGET_BUY_PRICE as things worth testing, not just configuring. Simplified example:
def test_dry_run_is_true_by_default():
# if DRY_RUN accidentally flips to False, this fails immediately
assert config.DRY_RUN is True
def test_target_buy_price_masked_example():
# illustrative ticker/price, not a real holding
assert config.TARGET_BUY_PRICE["005930"] == 70000
Testing a config value instead of logic felt odd at first, but it’s the cheapest insurance against ever accidentally shipping with safety guards disabled.
The regression test that mattered most
Since this function was the actual site of the recent incident, I put the most care into this one:
def test_extract_amounts_separates_krw_and_usd():
raw_text = "Balance: 1,000,000 KRW / $500.00"
result = extract_amounts_by_currency(raw_text)
assert result["KRW"] == 1000000
assert result["USD"] == 500.00
Now if future-me (or anyone) touches this function again and breaks the currency split, this test will scream before it ever reaches a live run.
Two “not bugs” I found along the way
Writing tests always surfaces a few “wait, why does it do that?” moments. This time, two came up — both turned out to be intentional, existing behavior, not bugs.
safe_int("1234.0")fails to convert and returns the default 0. It looked wrong until I remembered the real API always returns KRW amounts as plain integer strings, so this never actually triggers in production.fit_to_widthsometimes pads Korean strings with a trailing space to hit an exact width, so truncated text doesn’t always end in an ellipsis (…). Also confirmed as intentional design.
In both cases, instead of “fixing” the code, I adjusted the tests to accurately describe the actual behavior. When the test’s expectation and the code’s behavior disagree, checking intent first — before blindly rewriting either one — turned out to matter more than I expected.
The small thing that ate ten minutes: Windows PATH
pip install pytest went fine. Running pytest -v afterward threw a “‘pytest’ is not recognized” error — a pretty common symptom of pip’s installed scripts not being on the Windows PATH.
The fix was simple: python -m pytest -v worked immediately, and that’s now the standard way I run tests in this project going forward.
Results and what’s next
Final tally: 33 tests total across test_config.py (3), test_toss_common.py (5), and test_toss_portfolio.py (25) — all passing. Verified in both the local working folder and the project repo.
The last item left in Phase 1 is fixing unbounded log growth with a RotatingFileHandler. Once that’s done, Phase 1’s stability work wraps up, and Phase 2 (strategy modularization) begins.
Pingback: [Toss Auto-Trading Dev Journal #7] Extracting the Buy Condition into a Strategy Class (Phase 2 Kickoff) - Orbit - Space & ETF Investing