Quiz: Testing and Debugging
Test your understanding before moving on. Target: 70% or higher to proceed confidently.
Section 1: Multiple Choice (1 point each)
1. What is a unit test?
- A) A test that checks the entire program end-to-end
- B) A test that checks a single, small piece of code in isolation
- C) A test that checks how multiple modules work together
- D) A test that measures how fast your code runs
Answer
**B)** A test that checks a single, small piece of code in isolation. *Why B:* Unit tests verify individual functions or methods with specific inputs and expected outputs. *Why not A:* That describes end-to-end testing. *Why not C:* That describes integration testing. *Why not D:* That describes performance/benchmark testing. *Reference:* Section 13.22. Which pytest naming convention is REQUIRED for test discovery?
- A) Test files must end with
.pyand test functions must contain the word "test" - B) Test files must start with
test_(or end with_test.py) and test functions must start withtest_ - C) Test files and functions can be named anything as long as they contain assertions
- D) Test files must be in a directory called
tests/
Answer
**B)** Test files must start with `test_` (or end with `_test.py`) and test functions must start with `test_`. *Why B:* This is pytest's standard discovery convention. *Why not A:* Just containing "test" somewhere isn't sufficient. *Why not C:* pytest relies on naming conventions for automatic discovery. *Why not D:* While common, a `tests/` directory is not required — pytest discovers tests anywhere. *Reference:* Section 13.33. What does the following test verify?
def test_empty_list():
with pytest.raises(ValueError):
calculate_average([])
- A) That
calculate_averagereturnsValueErrorwhen given an empty list - B) That
calculate_averageraises aValueErrorexception when given an empty list - C) That
calculate_averagereturns an empty list - D) That
calculate_averagehandles empty lists without crashing
Answer
**B)** That `calculate_average` raises a `ValueError` exception when given an empty list. *Why B:* `pytest.raises(ValueError)` asserts that the code inside the `with` block raises a `ValueError`. If it doesn't raise, the test fails. *Why not A:* Functions raise exceptions; they don't return them. *Why not C:* The test isn't checking a return value. *Why not D:* The test specifically requires a crash (a `ValueError`) — handling it silently would cause the test to fail. *Reference:* Section 13.44. In test-driven development, what is the correct order of the three steps?
- A) Green, Red, Refactor
- B) Refactor, Red, Green
- C) Red, Green, Refactor
- D) Red, Refactor, Green
Answer
**C)** Red, Green, Refactor. *Why C:* Write a failing test (Red), write minimum code to pass it (Green), then clean up the code (Refactor). *Why not A/B/D:* The sequence matters — you must see the test fail first to be confident it's testing something real. *Reference:* Section 13.55. Why is it important that a new test FAILS before you write the implementation?
- A) Because failing tests run faster
- B) Because it proves the test is actually checking something meaningful
- C) Because pytest requires at least one failure per session
- D) Because it helps you memorize the error messages
Answer
**B)** Because it proves the test is actually checking something meaningful. *Why B:* A test that passes immediately might be testing nothing (wrong assertion, testing the wrong function, etc.). Seeing it fail first confirms it can detect the absence of the behavior you're about to implement. *Reference:* Section 13.56. Which of the following is an edge case for a function that finds the maximum value in a list?
- A) A list of 10 random positive integers
- B) A list with a single element
- C) A list of numbers in ascending order
- D) A list of numbers where the max is in the middle
Answer
**B)** A list with a single element. *Why B:* A single-element list is at the extreme boundary of valid input — the function must handle the case where there's nothing to compare. *Why not A:* A list of 10 random integers is a normal/typical case. *Why not C/D:* These test ordering scenarios but aren't at the boundary of what the function can accept. *Reference:* Section 13.67. What is a pytest fixture?
- A) A special assertion that checks multiple conditions at once
- B) A function decorated with
@pytest.fixturethat provides shared test data or setup - C) A configuration file that tells pytest where to find tests
- D) A plugin that adds extra assertion methods
Answer
**B)** A function decorated with `@pytest.fixture` that provides shared test data or setup. *Why B:* Fixtures replace repeated setup code by providing test data through function parameters, with each test getting a fresh copy. *Reference:* Section 13.78. What debugging strategy involves checking the midpoint of your code first, then narrowing down which half contains the bug?
- A) Rubber duck debugging
- B) Print debugging
- C) Binary search debugging
- D) Breakpoint debugging
Answer
**C)** Binary search debugging. *Why C:* Just like binary search halves the search space with each step, binary search debugging narrows the location of a bug by checking intermediate points. *Why not A:* Rubber duck debugging involves explaining code out loud. *Why not B:* Print debugging adds print statements but doesn't specify a strategy for where to place them. *Why not D:* Breakpoint debugging uses IDE tools to pause execution. *Reference:* Section 13.89. Why is this function HARD to test?
def get_user_age():
age = int(input("Enter your age: "))
if age >= 18:
print("You are an adult.")
else:
print("You are a minor.")
- A) Because it uses
int()which can crash - B) Because it uses
input()andprint()instead of parameters and return values - C) Because it doesn't have a docstring
- D) Because it doesn't handle negative ages
Answer
**B)** Because it uses `input()` and `print()` instead of parameters and return values. *Why B:* Functions that read from `input()` require interactive user input and can't be called from automated tests. Functions that use `print()` instead of returning values don't produce testable output. *Why not A:* While `int()` can crash, that's a separate issue from testability. *Why not C:* Docstrings are good practice but don't affect testability. *Why not D:* Missing validation is a bug, not a testability problem. *Reference:* Section 13.910. What does code coverage measure?
- A) How many bugs your tests have found
- B) What percentage of your code is executed by your test suite
- C) How many test functions you've written
- D) How fast your tests run
Answer
**B)** What percentage of your code is executed by your test suite. *Why B:* Coverage tools track which lines of code are actually executed during test runs and report the percentage. *Why not A:* Coverage doesn't count bugs found — it counts lines executed. *Why not C:* The number of test functions doesn't directly relate to coverage. *Why not D:* That would be performance metrics, not coverage. *Reference:* Section 13.10Section 2: Short Answer (2 points each)
11. Explain why 100% code coverage does not guarantee that your code is correct. Give a specific example.
Answer
100% coverage means every line of code was *executed* during testing, but it doesn't mean the results were *verified*. For example:def add(a, b):
return a + b
def test_add():
add(2, 3) # Executes add() but never checks the result
This achieves 100% coverage of `add()` but asserts nothing — the function could return any value and the test would still pass.
*Reference:* Section 13.10
12. What is the difference between a regression test and a unit test?
Answer
A **unit test** verifies that a specific function or method produces the correct output for given inputs. A **regression test** is any test (often a unit test) that was written to prevent a specific bug from recurring. When you find and fix a bug, you write a regression test that would have caught it — ensuring the bug never comes back. All regression tests are tests, but the term emphasizes their purpose: guarding against previously fixed bugs reappearing when code changes. *Reference:* Section 13.1113. Describe the "separate logic from I/O" principle. How would you refactor a function that both reads user input and performs a calculation?
Answer
Put the computation (math, logic, data transformation) in a pure function that takes parameters and returns a value. Put the I/O (input/output) in a separate function (usually `main()`). For example: - **Before:** One function calls `input()`, does math, calls `print()` - **After:** `calculate(values)` takes a list and returns a result (pure, testable). `main()` calls `input()`, passes data to `calculate()`, and `print()`s the result. This way, the logic can be tested by calling `calculate()` directly with known inputs and asserting expected outputs. *Reference:* Section 13.914. Write three boundary condition tests for a function is_passing(score) that returns True if score >= 60 and False otherwise. Explain why each test is important.
Answer
def test_exactly_at_boundary():
assert is_passing(60) is True # The exact cutoff — often where off-by-one bugs hide
def test_just_below_boundary():
assert is_passing(59) is False # One below the cutoff — catches >= vs > errors
def test_just_above_boundary():
assert is_passing(61) is True # One above — confirms the boundary isn't shifted
These three tests together verify the transition point is exactly at 60, catching off-by-one errors like `>` instead of `>=` or a cutoff at 59 or 61 instead of 60.
*Reference:* Section 13.6
Section 3: Code Analysis (3 points each)
15. Read the following code and predict exactly what pytest will output (pass/fail for each test, and what error if any):
def multiply(a, b):
return a * b
def test_multiply_integers():
assert multiply(3, 4) == 12
def test_multiply_by_zero():
assert multiply(5, 0) == 0
def test_multiply_strings():
assert multiply("ha", 3) == "hahaha"
def test_multiply_negative():
assert multiply(-2, 3) == 6
Answer
- `test_multiply_integers`: **PASS** — `3 * 4 == 12` is True - `test_multiply_by_zero`: **PASS** — `5 * 0 == 0` is True - `test_multiply_strings`: **PASS** — `"ha" * 3 == "hahaha"` is True (Python string repetition) - `test_multiply_negative`: **FAIL** — `(-2) * 3 == -6`, not `6`. The assertion `assert -6 == 6` fails. Output: 3 passed, 1 failed. *Reference:* Section 13.416. This fixture has a subtle problem. What is it, and how would you fix it?
@pytest.fixture
def shared_list():
return [1, 2, 3]
def test_append(shared_list):
shared_list.append(4)
assert len(shared_list) == 4
def test_length(shared_list):
assert len(shared_list) == 3
Answer
**There is no problem.** This is actually fine with pytest fixtures. Each test function gets a *fresh* call to the fixture, so `test_append` gets its own `[1, 2, 3]` and `test_length` gets a separate `[1, 2, 3]`. The mutation in `test_append` doesn't affect `test_length`. This would be a problem if the list were created at module level (a global variable), because both tests would share the same list. The fixture approach correctly isolates test state. **The trick question:** If you answered "test_length might fail because test_append modified the shared list," you're thinking about the problem that fixtures *solve*. Understanding that fixtures provide fresh instances is a key insight. *Reference:* Section 13.717. Identify all the problems with this test suite:
def test_everything():
# Test addition
assert 2 + 3 == 5
# Test string upper
assert "hello".upper() == "HELLO"
# Test list append
data = [1, 2]
data.append(3)
assert len(data) == 3
# Test dictionary
d = {"a": 1}
assert d["a"] == 1
Answer
Problems: 1. **Tests multiple unrelated things in one function** — if the addition assertion fails, you don't know if the string, list, or dict tests would also fail. Each test should verify one behavior. 2. **Tests built-in Python features, not your own code** — these tests verify that Python's `+`, `upper()`, `append()`, and dict indexing work. You should be testing *your* functions, not the language itself. 3. **Vague name** — `test_everything` gives no information about what's being verified. Good names describe the specific behavior: `test_add_returns_sum`, `test_upper_converts_lowercase`. 4. **No test isolation** — if the second assertion fails, the third and fourth never run. With separate test functions, all tests run independently. *Reference:* Sections 13.3, 13.4Section 4: Applied Problem (4 points)
18. Write a complete test suite (at least 6 tests) for this function. Include normal cases, edge cases, and error cases:
def find_longest_word(sentence):
"""Return the longest word in a sentence. If there's a tie, return the first one."""
if not sentence or not sentence.strip():
raise ValueError("Sentence cannot be empty")
words = sentence.split()
longest = words[0]
for word in words[1:]:
if len(word) > len(longest):
longest = word
return longest
Answer
import pytest
def test_single_word():
assert find_longest_word("hello") == "hello"
def test_multiple_words():
assert find_longest_word("the quick fox") == "quick"
def test_tie_returns_first():
assert find_longest_word("cat dog rat") == "cat" # All length 3, return first
def test_longest_at_end():
assert find_longest_word("a bb ccc") == "ccc"
def test_longest_at_start():
assert find_longest_word("elephant is big") == "elephant"
def test_empty_string_raises():
with pytest.raises(ValueError):
find_longest_word("")
def test_whitespace_only_raises():
with pytest.raises(ValueError):
find_longest_word(" ")
def test_extra_whitespace():
assert find_longest_word(" hello world ") == "hello"
*Reference:* Sections 13.4, 13.6
19. You have a function parse_date(date_string) that converts strings like "2025-03-15" into a dictionary {"year": 2025, "month": 3, "day": 15}. Using TDD, show the Red-Green-Refactor process: write your first three tests, then write the implementation. Show each step.
Answer
**Step 1 — Red (Write tests first):**def test_parse_standard_date():
assert parse_date("2025-03-15") == {"year": 2025, "month": 3, "day": 15}
def test_parse_january_first():
assert parse_date("2024-01-01") == {"year": 2024, "month": 1, "day": 1}
def test_parse_invalid_format_raises():
with pytest.raises(ValueError):
parse_date("March 15, 2025")
All three fail because `parse_date` doesn't exist yet.
**Step 2 — Green (Minimum implementation):**
def parse_date(date_string):
parts = date_string.split("-")
if len(parts) != 3:
raise ValueError(f"Invalid date format: {date_string}")
try:
return {
"year": int(parts[0]),
"month": int(parts[1]),
"day": int(parts[2])
}
except ValueError:
raise ValueError(f"Invalid date format: {date_string}")
All three tests pass.
**Step 3 — Refactor:** Code is clean enough for now. Add more tests as needed (empty string, extra dashes, invalid month values, etc.).
*Reference:* Section 13.5
20. Read the following buggy function and, without running it, predict what process_scores([85, 92, 78, 90, 88]) returns. Then identify the bug and fix it.
def process_scores(scores):
"""Return a dict with min, max, and average of the scores,
excluding the lowest score."""
sorted_scores = sorted(scores)
trimmed = sorted_scores[1:] # Remove lowest
return {
"min": min(scores),
"max": max(scores),
"average": sum(trimmed) / len(trimmed)
}
Answer
The function returns `{"min": 78, "max": 92, "average": 86.25}`. **The bug:** The `"min"` key uses `min(scores)` — the *original* list including the lowest score. But the average is calculated on `trimmed`, which excludes the lowest. The result is inconsistent: `min` reports a value that isn't included in the average calculation. **Fix:** Use `trimmed` for all statistics:def process_scores(scores):
sorted_scores = sorted(scores)
trimmed = sorted_scores[1:]
return {
"min": min(trimmed),
"max": max(trimmed),
"average": sum(trimmed) / len(trimmed)
}
Now `min` correctly reflects the lowest score after trimming.
*Reference:* Sections 13.6, 13.8