Check an aggregation function with four rows you can calculate by hand and 12 unit tests. Verify not only normal values but also empty input, zero, decimals, and invalid input.
Content checked 2026.09.19Example files included
Show contents
This translation was generated by AI. Check the code, units, and numbers against the original. Native-speaker review has not yet been completed for each language. 한국어
Who this is forBeginners who hesitate to use Python code from AI at work just because it runs
What you need
Install Python 3.12 or later and check the version in a terminal with python --version.
Extract the example ZIP into a new folder. Do not run the example from inside the ZIP archive.
Open a terminal in the folder containing example.py. If the python command is unavailable on Windows, use py. On macOS or Linux, use python3 if your environment requires it.
No external packages or accounts are needed. The included files are synthetic data created for this tutorial.
01Decide the answer before running
Code from AI finishing without errors does not mean the calculation is right. This article does not connect to a real AI service; it practices the verification steps with an independently written aggregation function and small synthetic data. You can apply the same method to a function you need to review. No API key or external packages are needed.
Read the four rows of input.csv and calculate quantity × unit price yourself.
Open expected.json and check that it lists 4 rows, quantity 6, and amount 11101.50.
Run the command below. The 12 unit tests run first, followed by the CSV aggregation and comparison with the expected values.
Check outputs/summary.json and verification.txt, which are created only when all comparisons pass.
bash
python example.py
02Compare the calculation for four rows in a table
item
quantity
unit_price
Amount per row
노트
2
2500.00
5000.00
펜
3
1200.50
3601.50
노트
1
2500.00
2500.00
Sample
0
999.99
0.00
There are 4 rows, but the total quantity is 2 + 3 + 1 + 0 = 6. The total amount is 5000.00 + 3601.50 + 2500.00 = 11101.50. The last row with a quantity of 0 is still counted in the number of input rows. By item, 노트 (notebook) has quantity 3 and amount 7500.00, 펜 (pen) has quantity 3 and amount 3601.50, and 샘플 (sample) has quantity 0 and amount 0.00.
So do not compare only the total; look at the row count, total quantity, and per-item values together. Entering the same item in two rows, like the notebook, also lets you check the grouping and summing. The order of per-item results is sorted by name and does not mean sales rank or input order.
03Write the input rules the function accepts
Item
Allowed rule
Rejected example
Column
Three: item, quantity, unit_price
Missing or extra columns
item
Non-empty string after trimming leading and trailing spaces
Empty string or whitespace only
quantity
Integer string from 0 to 1,000,000
-1, 1.5, 1e2
unit_price
0 to 1,000,000,000, up to two decimal places
NaN, Infinity, 0.001, 1,000
Amounts are assumed to be in the same fictional currency. Negative quantities and unit prices are not allowed, so there is no rule for representing returns. Defining the input rules first lets you tell whether the code silently changes values to 0 or skips some rows. This function stops with a ValueError when it finds an invalid row.
04Full code with the aggregation and tests
aggregate_sales returns only the result without changing the input rows. Reading files and writing results are separated into main. This way, tests can pass small dictionaries directly and check only the calculation. Each expected value is compared with assertEqual, and values that should be rejected are checked with assertRaises.
example.py
"""집계 함수를 손계산 기대값과 unittest로 검증합니다. 외부 AI 호출은 없습니다."""
import argparse
import csv
import json
import re
import sys
import unittest
from decimal import Decimal
from pathlib import Path
BASE = Path(__file__).resolve().parent
OUTPUT = BASE / "outputs"
FIELDS = {"item", "quantity", "unit_price"}
def money(cents):
return f"{cents // 100}.{cents % 100:02d}"
def aggregate_sales(rows):
"""문자열 딕셔너리 행을 검증하고 집계합니다. rows와 원본 파일을 변경하지 않습니다."""
totals = {}
row_count = total_quantity = total_cents = 0
for number, row in enumerate(rows, start=1):
if not isinstance(row, dict) or set(row) != FIELDS:
raise ValueError(f"{number}행: item,quantity,unit_price 세 열이 필요합니다.")
if any(not isinstance(value, str) for value in row.values()):
raise ValueError(f"{number}행: 모든 값은 문자열이어야 합니다.")
item, quantity_text, price_text = (row[key].strip() for key in ("item", "quantity", "unit_price"))
if not item:
raise ValueError(f"{number}행: 품목이 비어 있습니다.")
if not re.fullmatch(r"[0-9]{1,7}", quantity_text):
raise ValueError(f"{number}행: 수량은 0 이상의 정수여야 합니다.")
quantity = int(quantity_text)
if quantity > 1_000_000:
raise ValueError(f"{number}행: 수량은 1,000,000 이하여야 합니다.")
if not re.fullmatch(r"[0-9]{1,10}(?:\.[0-9]{1,2})?", price_text):
raise ValueError(f"{number}행: 단가는 0 이상, 소수점 둘째 자리까지 입력하세요.")
price = Decimal(price_text) # 문자열에서 직접 생성해 float 오차를 피합니다.
if price > Decimal("1000000000"):
raise ValueError(f"{number}행: 단가는 1,000,000,000 이하여야 합니다.")
cents = int(price * 100) * quantity
bucket = totals.setdefault(item, {"quantity": 0, "cents": 0})
bucket["quantity"] += quantity
bucket["cents"] += cents
total_quantity += quantity
total_cents += cents
row_count += 1
return {
"row_count": row_count,
"quantity": total_quantity,
"amount": money(total_cents),
"by_item": [
{"item": item, "quantity": totals[item]["quantity"], "amount": money(totals[item]["cents"])}
for item in sorted(totals)
],
}
def sale(item="펜", quantity="1", unit_price="1200.50"):
return {"item": item, "quantity": quantity, "unit_price": unit_price}
class TestAggregate(unittest.TestCase):
# 기대값은 검증 대상 함수로 만들지 않고 손계산한 상수로 적습니다.
def test_empty_input(self):
self.assertEqual(aggregate_sales([]), {"row_count": 0, "quantity": 0, "amount": "0.00", "by_item": []})
def test_single_row(self):
self.assertEqual(aggregate_sales([sale(quantity="3")])["amount"], "3601.50")
def test_zero_quantity(self):
result = aggregate_sales([sale(quantity="0")])
self.assertEqual((result["row_count"], result["quantity"], result["amount"]), (1, 0, "0.00"))
def test_repeated_item(self):
result = aggregate_sales([sale(quantity="2"), sale(quantity="1")])
self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 3, "amount": "3601.50"}])
def test_decimal_addition(self):
result = aggregate_sales([sale(unit_price="0.10"), sale(unit_price="0.20")])
self.assertEqual(result["amount"], "0.30")
def test_whitespace(self):
result = aggregate_sales([sale(item=" 펜 ", quantity=" 1 ", unit_price=" 2.00 ")])
self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 1, "amount": "2.00"}])
def test_bad_quantity(self):
for value in ("-1", "1.5", "", "1000001", "1e2", "1"):
with self.subTest(value=value), self.assertRaises(ValueError):
aggregate_sales([sale(quantity=value)])
def test_bad_price(self):
for value in ("-1", "0.001", "NaN", "Infinity", "", "1,000", "1000000001"):
with self.subTest(value=value), self.assertRaises(ValueError):
aggregate_sales([sale(unit_price=value)])
def test_blank_item(self):
with self.assertRaises(ValueError):
aggregate_sales([sale(item=" ")])
def test_wrong_columns(self):
for row in ({"item": "펜"}, {**sale(), "extra": "x"}):
with self.subTest(row=row), self.assertRaises(ValueError):
aggregate_sales([row])
def test_non_string(self):
with self.assertRaises(ValueError):
aggregate_sales([sale(quantity=True)])
def test_upper_boundary(self):
self.assertEqual(
aggregate_sales([sale(quantity="1000000", unit_price="1000000000")])["amount"],
"1000000000000000.00",
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--test", action="store_true", help="단위 테스트만 실행합니다.")
args = parser.parse_args()
# unittest의 명시적 비교는 python -O에서도 생략되지 않습니다.
result = unittest.TextTestRunner(verbosity=2, stream=sys.stdout).run(
unittest.defaultTestLoader.loadTestsFromTestCase(TestAggregate)
)
if not result.wasSuccessful():
return 1
if args.test:
return 0
if OUTPUT.exists() or OUTPUT.is_symlink():
raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
with (BASE / "input.csv").open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream, strict=True)
if reader.fieldnames != ["item", "quantity", "unit_price"]:
raise ValueError("input.csv의 열은 item,quantity,unit_price 순서여야 합니다.")
actual = aggregate_sales(reader)
expected = json.loads((BASE / "expected.json").read_text(encoding="utf-8"))
if actual != expected:
raise ValueError("예제 집계가 expected.json의 손계산 기대값과 다릅니다.")
OUTPUT.mkdir()
with (OUTPUT / "summary.json").open("x", encoding="utf-8") as stream:
json.dump(actual, stream, ensure_ascii=False, indent=2)
stream.write("\n")
with (OUTPUT / "verification.txt").open("x", encoding="utf-8") as stream:
stream.write(f"unittest: {result.testsRun}개 통과\n예제 기대값: 일치\n외부 AI API 호출: 없음\n")
print(f"완료: {actual['row_count']}행, 수량 {actual['quantity']}, 금액 {actual['amount']}")
print(OUTPUT)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, csv.Error) as error:
print(f"중지: {error}", file=sys.stderr)
raise SystemExit(2)
The unit price is made into a Decimal from the string, multiplied by 100 to convert it to integer units, and then summed. The resulting amount is also returned as a string with two decimal places. This is a choice that fits the example's two-decimal unit price rule, not a universal rule for every numeric calculation.
05Normal, boundary, and error cases the tests check
Category
Built-in tests
What it checks
Normal
One row, repeated item, leading and trailing spaces
Basic calculation and grouping
Boundary
Empty input, quantity 0, upper limit
Definitions at small and large values
Decimals
0.10 + 0.20 = 0.30
Amount representation and addition
Error
Invalid quantity, unit price, blank item, columns, types
Invalid values are not silently accepted
Check ok next to each test name and the final ‘Ran 12 tests’ and ‘OK’. The CSV expected-value comparison has passed only when you also see the following ‘완료: 4행, 수량 6, 금액 11101.50’ (Done: 4 rows, quantity 6, amount 11101.50). If only the unit tests pass and the result differs from expected.json, the default run fails and does not save new results.
bash
python example.py --test
--test repeats only the unit tests and does not create outputs. It can be used even if a results folder already exists, which makes it good for quick checks while editing the function. In contrast, the default command stops without overwriting existing outputs.
06Try making a failing test once
Prepare a copy of example.py in a freshly extracted folder. Keep the original file so you can revert.
In the calculation line int(price * 100) * quantity, delete only the final * quantity to create an error that ignores quantity.
Run python example.py --test and check that tests such as the one-row quantity 3 and repeated-item tests fail.
Restore the original calculation line and run --test again to see that all 12 pass.
The purpose of this experiment is to confirm that the tests catch errors. While making this example, the same kind of error was injected into a temporary copy to verify that the tests fail and no results are written. Seeing one meaningful failure makes it easier to avoid a situation where the test code runs but checks nothing.
07Steps for applying this to a function you got from AI
For new code, first fix examples of the input and output. If any part writes to original files, limit it to copies, and separate the function that does the calculation first. Prepare one set of normal data, empty data, 0, repeated items, and values that should not be allowed, then calculate the expected values yourself.
Write the function's business rules as sentences. For example, decide whether returns are rejected or calculated as negative amounts.
Write a small input and expected value for each rule. Compare the overall total and the per-item results together.
Put the same input into the function under review and read the differences in the results. Do not change the expected values first to make failures go away.
After running the existing tests again with the revised code, keep the new error cases in the tests too.
If you replace input.csv with your own data, you must also calculate and update expected.json separately. Do not conclude that a mismatch with the expected values is a code defect after changing only the input. Checking which rules and which data changed is where reproducible verification starts.
08Read the failure message and narrow it down
Result
Meaning
Next check
FAIL or FAILED
The calculated result differs from the test's expected value
Read the name of the failed test and the two values.
Stopped due to input rules
A CSV value is outside the allowed range
Check the indicated row and the original data.
Differs from the hand-calculated expected value
CSV aggregation does not match expected.json
Recheck both whether the data changed and the hand calculation.
outputs already exists
A previous successful result exists
Run only the checks with --test, or move the results elsewhere to keep them.
Passing the tests in this example is evidence for the stated rules. It is not a measurement of any real AI model's performance or a guarantee of accuracy for all accounting work. Taxes, discounts, exchange rates, returns, multiple currencies, and performance on large data need separate rules and tests. The more failure cases you add, the more clearly you can explain the range where it applies.
Execution and verification record
2026-09-19 · Windows 11 · CPython 3.12.14 · No additional packages · Run in a temporary copy of the distribution
12 built-in unittest tests passed
4 rows of synthetic CSV match the hand-calculated expected value of 11101.50
Stops before creating results when the input and expected values do not match
The tests caught a deliberate calculation error that ignores quantity
Confirmed repeated --test runs and preservation of the original SHA-256
Verification limits
No real AI API was called.
Tax, exchange rate, returns, and performance verification are not included.
Turn “automate this” into an executable task description. Attach a synthetic sample with no sensitive data and a hand-checked expected result to complete a request for a script that totals work logs by team.
Split a plausible summary into facts, calculations, and interpretations. Recalculate ratios and averages from synthetic monthly data, and rewrite sentences with missing sources or overstated causes into checkable statements.