Incremental Testing and Debugging

Incremental development means building one small part, testing it, then adding the next part.

The goal is not to write perfect professional test suites. The goal is simpler and more urgent:

Keep the problem small enough to understand and fix.

In a lab task, many students lose control because they write too much code before running anything. When the final program fails, they do not know whether the problem is in the input, the processing, the database, the template, the file path, or the final output.

Incremental testing prevents this. It gives you evidence after each small step.

Beginner Mental Model

A weak workflow looks like this:

read task -> code everything -> run once -> many errors -> panic-edit

A stronger workflow looks like this:

read task -> build one small part -> test it -> save it -> build the next part

The main idea is:

The newest untested change is the most likely place to look first.

If the program worked five minutes ago and now fails, the error is probably in the part you just changed.

Testing Ladder

Figure: The testing ladder rises from one function to a final task; each new layer should be run, inspected, and saved before the next layer is added.

Do not jump straight from a blank file to a full solution.

LevelExample testWhat it proves
one expression0 <= mark <= 100 gives the expected resulta condition is correct
one functionis_valid_mark(101) should return Falsea reusable unit works
one featureinvalid form input should show an errorone user-facing behaviour works
integrated pathform input should be stored in the database and displayedseveral layers work together
final taskall required inputs, outputs, files, and edge cases checkedthe submitted solution is ready

The ladder matters because each level gives different evidence. A function test does not prove that the Flask form uses the correct field name. A successful database insert does not prove that the template displays the row.

Why Small Tests Matter

Suppose you write 80 lines before running the program and it fails. The error could be anywhere.

If you run after every small feature, the likely error is in the newest change.

large untested change -> many possible causes
small tested change -> few possible causes

This is the main reason incremental testing saves time.

It also protects confidence. A beginner who sees ten errors at once may feel the whole solution is wrong. A beginner who tests one step at a time can usually say:

The route worked.
The template worked.
The form submission broke after I changed the form method.

That is a much easier problem.

Evidence Before Moving On

Do not move to the next part just because the code “looks right”. Move on when you have evidence.

ClaimWeak evidenceBetter evidence
the function worksno red underline in editorsmall test or assert passes
the form workspage displays input boxsubmitted value appears in Flask
validation worksnormal input acceptednormal, boundary, and invalid inputs tested
database insert worksno crashinserted row is selected back
template worksroute returns a pageexpected variable appears on the page
final solution is readyit ran once earlierrerun from the submitted folder and check required output

This does not mean every step needs a long test script. Under exam conditions, even one print check or one small input-output check is useful.

Normal, Extreme, and Abnormal Tests

Use tests from Test Case Design when practical.

Test typeMeaningExample for mark 0-100
normalvalid ordinary value75
extremevalid boundary value0, 100
abnormalinvalid value-1, 101, "abc"

For each test, write the expected result before running it. Otherwise you may accept a wrong result just because it looks plausible.

Example validation rule:

A mark must be an integer from 0 to 100 inclusive.

Useful tests:

InputTypeExpected resultWhy it matters
75normalvalidordinary accepted value
0extremevalidlower boundary
100extremevalidupper boundary
-1abnormalinvalidjust below lower boundary
101abnormalinvalidjust above upper boundary
"abc"abnormalinvalidwrong data type
""abnormalinvalidblank input

Boundary tests are especially important because comparison errors often happen at cutoffs.

Small Python Example

When writing a function, test it immediately.

def grade(mark):
    if mark < 0 or mark > 100:
        return "invalid"
    if mark >= 70:
        return "A"
    if mark >= 60:
        return "B"
    return "C"
 
 
assert grade(70) == "A"
assert grade(69) == "B"
assert grade(0) == "C"
assert grade(101) == "invalid"

The boundary tests 70 and 69 are important because comparison errors often happen at grade cutoffs.

This small test is not the full solution. It simply proves that the grading logic works before it is connected to input, files, forms, or databases.

Failed-Test Example

Suppose the code accidentally uses > instead of >=:

def grade(mark):
    if mark > 70:
        return "A"
    return "B"

Test:

assert grade(70) == "A"

This test fails because 70 > 70 is false.

The important point is not just that the test failed. The useful point is that the test tells you where to look:

EvidenceMeaning
grade(71) gives "A"high marks above 70 work
grade(70) gives "B"the boundary value is mishandled
condition is mark > 70comparison should probably include equality

The fix is small:

if mark >= 70:

A good boundary test turns a vague wrong answer into a specific correction.

Do Not Debug by Guessing

When something fails, it is tempting to change many lines and hope the error disappears. This is risky.

Weak debugging:

change condition
rename variable
move file
restart server
change SQL
try again

After this, even if the program works, you may not know which change fixed it. If it still fails, you have created more possible causes.

Stronger debugging:

1. Reproduce the problem.
2. Observe the exact error or wrong output.
3. Identify the smallest likely failing part.
4. Add one print, assertion, query, or route test.
5. Change one thing.
6. Retest.

The rule is:

Change one thing at a time whenever possible.

Debugging Process

When something fails, do not rewrite the whole program. Narrow the problem.

1. Reproduce the error.
2. Read the exact error message or wrong output.
3. Identify the smallest part that could cause it.
4. Add a print, assertion, or tiny test.
5. Change one thing.
6. Retest.
7. Check that earlier working parts still work.

The last step is important. A fix for one part can accidentally break another part.

Reading an Error Message

An error message is not just noise. It usually tells you:

PartWhat it tells you
error typesyntax, name, type, file, import, database, template, etc.
file namewhich file was running
line numberwhere Python noticed the problem
messagewhat Python, Flask, SQLite, or another tool could not do

Start at the first useful error location in your own file. Later lines may show library internals that are less useful for beginners.

Example:

NameError: name 'mark' is not defined

Possible reasoning:

EvidenceMeaning
error type is NameErrorPython cannot find a variable with that name
message mentions markthe problem is probably the name mark
line says grade(mark)mark is used here
earlier code uses scorethe value may have been stored under a different variable name

Possible fix:

score = int(input("Enter mark: "))
print(grade(score))

or rename consistently:

mark = int(input("Enter mark: "))
print(grade(mark))

Do not fix this by changing unrelated code.

Error Types in Lab Work

Error typeBeginner symptomExampleUseful first check
syntax errorprogram does not startmissing colon or bracketline number, nearby punctuation
runtime errorprogram starts but crashesfile not found, wrong typeexact error message and value
logic errorprogram runs but answer is wrongwrong condition or formulasmall input with known expected output
integration errorindividual parts work but not togetherform field name does not match Flask codehandoff between layers
file/path errorcode works on one machine but not anotherwrong relative pathcurrent working folder and submitted folder
database errorrow missing or query failswrong table name, missing commit()table names, column names, database path
template errorroute runs but page is wrongwrong Jinja variablevalues passed to render_template

The fix depends on the type. A logic error will not be solved by repeatedly checking indentation if the condition is wrong.

Debugging by Data Flow

For many practical tasks, trace the data.

Example:

input value -> variable -> validation -> processing -> storage -> output

At each arrow, ask:

  • what value do I expect here?
  • what value is actually here?
  • is the type correct?
  • is the name spelled correctly?
  • did this step run?

This is especially useful when there is no obvious error message.

Data-Flow Trace Example: Form to Grade

Task: read a mark from a form and display a grade.

StageExpected valuePossible bugQuick check
form fieldmark input existsHTML uses name="score"inspect form or print request.form
Flask routerequest.form.get("mark") returns textroute handles only GETcheck route methods and request method
conversiontext becomes integerblank input causes conversion errorprint repr(mark_text) before converting
grade functionreturns A, B, C, or invalidboundary comparison is wrongassert function output for boundary cases
templatedisplays grade variableJinja variable name mismatchprint value before render_template

This table gives you a debugging order.

Do not debug the template before checking that the route has actually received the form value.

Data-Flow Trace Example: Web and Database

Task: add a book title using a Flask form, store it in SQLite, and display all saved books.

Expected path:

"Dune" -> form field -> Flask route -> validation -> INSERT -> SELECT -> Jinja loop -> browser list

A useful integrated test:

StepCheckExpected evidence
submit formenter Duneroute receives Dune
validatetitle is not blankno error message
insertSQL INSERT runsno database error
commitcon.commit() calledrow persists after request
selectquery returns rowsDune appears in result list
displaytemplate loops through rowsbrowser shows Dune

If the browser does not show Dune, do not immediately rewrite the whole database section. Locate the last step where the evidence was correct.

Print checks are simple but effective under exam pressure.

name = request.form.get("name")
print("DEBUG name:", repr(name))

Use repr when spaces or empty strings matter.

Examples:

Printed valuePossible meaning
Nonemissing key, wrong form name, wrong request method
''input was submitted but blank
' 'spaces were entered; use .strip() if appropriate
'80'value is text and may need conversion

Remove noisy debug prints before final submission if they affect required output. In a Flask app, harmless terminal prints are usually less serious than unwanted text in the browser page, but the final solution should still be tidy.

Assertion Checks

Assertions are useful for checking assumptions inside small examples.

total = 35 + 40
assert total == 75

Use assertions for small self-checks while developing:

assert grade(70) == "A"
assert grade(69) == "B"

In a final user-facing program, do not rely only on assertions for validation because users need clear error handling.

For example, a web form should usually show a helpful message instead of crashing with an assertion error.

SQL Debugging Checks

When SQL output is wrong, isolate the query.

rows = con.execute(
    "SELECT title FROM Book WHERE title LIKE ?",
    ("%network%",)
).fetchall()
print(rows)

Check:

  • table name;
  • column names;
  • parameter order;
  • placeholder count;
  • whether commit() was called after inserts or updates;
  • whether the program is opening the database file you think it is opening.

A common mistake is to test the wrong database file. Add a temporary path check if needed:

from pathlib import Path
 
DB_PATH = Path("books.db")
print("Using database:", DB_PATH.resolve())

If the path is not the database you expected, the SQL may be correct but the file path is wrong.

Debugging Wrong Output With No Error

A program can run without crashing and still be wrong.

Example rule:

A mark of 70 or above should receive A.

Wrong code:

if mark > 70:
    return "A"

There is no syntax error and no runtime error. The logic is wrong only at the boundary.

Test:

assert grade(70) == "A"

This catches the error.

When the output is wrong but there is no error message, use known input-output pairs:

Known inputExpected outputWhy useful
simple normal caseobvious resultcatches basic formula or condition error
boundary caseexact cutoff behaviourcatches <, <=, >, >= mistakes
invalid caserejection or error messagecatches missing validation

Regression Check

After fixing an error, retest earlier features.

Example:

Fix: changed validation for blank title.
Retest:
- blank title rejected
- normal title accepted
- saved title still appears in list
- database still contains expected row

A fix for one feature can break another. Regression testing catches that.

For a small lab task, regression testing does not need to be long. It can be a short checklist:

After each fix:
1. Test the failing case again.
2. Test one normal case that worked before.
3. Check the required output still appears.

When You Are Stuck

Use this sequence if you have been stuck for several minutes.

1. Stop changing code randomly.
2. State the symptom in one sentence.
3. Identify the layer: input, processing, file, database, route, template, or output.
4. Create the smallest test for that layer.
5. Check the evidence.
6. Fix only the most likely cause.
7. Retest.

Examples:

SymptomBetter first question
page is blankdid the route return data to the template?
form value is missingdoes the HTML name match request.form.get(...)?
database row missingdid the insert run, commit, and use the right database path?
file not foundwhat is the working folder and relative path?
output is off by onewhich boundary value fails?

A precise symptom is easier to debug than a general feeling that “the program is broken”.

Tiny Tests for Common Lab Layers

LayerTiny test
Python functioncall it with one normal and one boundary input
file readingprint the first line or first record
CSV processingprint the header and one parsed row
JSON processingprint the top-level type and one key
SQLite tablerun one SELECT and print rows
Flask routereturn plain text first
HTML formprint request.form after submission
Jinja templatepass one simple variable and display it

Tiny tests are not a waste of time. They prevent larger debugging later.

Common Mistakes

  • Testing only the happy path.
  • Writing the full solution before running anything.
  • Changing many parts before retesting.
  • Ignoring the exact line number in an error message.
  • Looking only at the last line of a long traceback and missing the useful line in your own file.
  • Assuming a database insert worked without checking the database.
  • Debugging the template when the route never passed the variable.
  • Debugging SQL when the real problem is the HTML form field name.
  • Keeping old test data that hides a bug.
  • Forgetting to retest earlier features after a fix.

Final Takeaway

Incremental testing is not about writing more code. It is about staying in control.

Use this reasoning pattern:

What should happen?
What actually happened?
Where is the first point where the two differ?
What is the smallest test that proves this?
What is the smallest change that could fix it?
Did the fix break anything else?

Return to Lab Exam and Project Skills.