Testing your code

AESB2122 - Signals and Systems with Python

Geet George

Testing in Python

  • Tests verify that your code does what you think it does.
  • They help you:
    • Catch bugs early
    • Refactor safely
    • Collaborate with confidence
  • Think of tests as a safety net for your codebase.

Example function

Let’s define a simple function that creates a sine wave.

import numpy as np

def generate_sine_wave(frequency, amplitude, duration, sample_rate=1000):
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    y = amplitude * np.sin(2 * np.pi * frequency * t)
    return t, y

# Example usage
t, y = generate_sine_wave(1, 2, 1)  # 1 Hz, amplitude 2, 1 second long
print(t[:5], y[:5])  # Print first 5 samples
[0.    0.001 0.002 0.003 0.004] [0.         0.01256629 0.02513208 0.03769688 0.05026019]

Testing with if — (❌)

We could, in theory, test our function using if statements.

t, y = generate_sine_wave(1, 2, 1)  # 1 Hz, amplitude 2, 1 second long
if len(t) == 1000:
    print("Test passed ✅")
else:
    print("Test failed ❌")
Test passed ✅

Another Test

Let’s check the first value of y.

t, y = generate_sine_wave(1, 2, 1)
if y[0] == 0:
    print("Test passed ✅")
else:
    print("Test failed ❌")
Test passed ✅

Testing Amplitude

_, y = generate_sine_wave(5, 3, 1)
if np.isclose(max(y), 3, atol=1e-6):
    print("Test passed ✅")
else:
    print("Test failed ❌")
Test passed ✅

✅ Works fine.

But this gets repetitive fast…

Testing with if works, but…

  • It’s verbose
  • You must read each message
  • It doesn’t stop automatically when something breaks

👉 There’s a better way.

Testing with assert — (✅)

Using assert

Let’s rewrite those tests using assert.

t, y = generate_sine_wave(1, 2, 1)
assert len(t) == 1000

t, y = generate_sine_wave(1, 2, 1)
assert y[0] == 0

_, y = generate_sine_wave(5, 3, 1)
assert np.isclose(max(y), 3, atol=1e-6)

What Happens?

  • ✅ If all assertions are true, nothing happens.
  • ❌ If one is false, you get an AssertionError.
  • No print statements needed!

Break the function on purpose:

def generate_sine_wave(frequency, amplitude, duration, sample_rate=1000):
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    y = np.sin(2 * np.pi * frequency * t)  # Forgot amplitude!
    return t, y

t, y = generate_sine_wave(1, 2, 1)
assert len(t) == 1000

t, y = generate_sine_wave(1, 2, 1)
assert y[0] == 0

_, y = generate_sine_wave(5, 3, 1)
assert np.isclose(max(y), 3, atol=1e-6)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[18], line 13
     10 assert y[0] == 0
     12 _, y = generate_sine_wave(5, 3, 1)
---> 13 assert np.isclose(max(y), 3, atol=1e-6)

AssertionError: 

Why assert Is Better

  • Minimal boilerplate
  • Immediate feedback
  • Scales easily for multiple tests
  • Works great with frameworks like pytest

Edge Cases Matter

Good tests include normal cases and edge cases.

Edge Case: Negative Duration

def generate_sine_wave(frequency, amplitude, duration, sample_rate=1000):
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    y = amplitude * np.sin(2 * np.pi * frequency * t)
    return t, y

t, y = generate_sine_wave(1, 2, -1)  # duration = -1
assert len(t) == 0
assert len(y) == 0

Edge Cases Matter

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[19], line 6
      3     y = amplitude * np.sin(2 * np.pi * frequency * t)
      4     return t, y
----> 6 t, y = generate_sine_wave(1, 2, -1)  # duration = -1
      7 assert len(t) == 0
      8 assert len(y) == 0

Cell In[19], line 2, in generate_sine_wave(frequency, amplitude, duration, sample_rate)
      1 def generate_sine_wave(frequency, amplitude, duration, sample_rate=1000):
----> 2     t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
      3     y = amplitude * np.sin(2 * np.pi * frequency * t)
      4     return t, y

File ~/miniforge3/envs/test-doc-pack_env/lib/python3.10/site-packages/numpy/_core/function_base.py:130, in linspace(start, stop, num, endpoint, retstep, dtype, axis, device)
    128 num = operator.index(num)
    129 if num < 0:
--> 130     raise ValueError(
    131         "Number of samples, %s, must be non-negative." % num
    132     )
    133 div = (num - 1) if endpoint else num
    135 conv = _array_converter(start, stop)

ValueError: Number of samples, -1000, must be non-negative.

Fixing Edge Cases

Let’s make the function handle negative durations.

def generate_sine_wave(frequency, amplitude, duration, sample_rate=1000):
    if duration < 0:
        return np.array([]), np.array([])
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    y = amplitude * np.sin(2 * np.pi * frequency * t)
    return t, y

t, y = generate_sine_wave(1, 2, -1)  # duration = -1
assert len(t) == 0
assert len(y) == 0

✅ Now all tests pass.

Another Edge Case

What if amplitude is 0?

t, y = generate_sine_wave(5, 0, 1)
assert np.allclose(y, 0)

✅ Works fine — sine wave is just flat zero.

Test-Driven Development (TDD)

TDD mindset:

  1. Write the test first.
  2. Run it (it fails).
  3. Write the code to make it pass.
  4. Refactor if needed.

What you just did in the previous couple of slides was TDD without realizing it! 🎉

Why TDD Helps

  • Forces you to define expected behavior first
  • Encourages modular design
  • Reduces debugging time
  • Builds confidence in your code

Simple checklist for writing tests

  1. Call the function (that you want to test) with known inputs.
  2. Use assert statements to check outputs against expected results.
  3. Cover normal cases and edge cases.
  4. Keep tests independent of each other, i.e. call the function afresh in each test.
  5. Try using descriptive names for test functions.

Using pytest

First, let’s make a function that contains all our tests.

def test_generate_sine_wave():
    t, y = generate_sine_wave(1, 2, 1)
    assert len(t) == 1000
    assert y[0] == 0

    t, y = generate_sine_wave(5, 3, 1)
    assert np.isclose(max(y), 3, atol=1e-6)

    t, y = generate_sine_wave(1, 2, -1)
    assert len(t) == 0 and len(y) == 0

    t, y = generate_sine_wave(5, 0, 1)
    assert np.allclose(y, 0)

Run It Manually

Call the test function in your Python file:

test_generate_sine_wave()

And run the Python file. If nothing happens, all tests passed.

✅ All tests pass.
But what if we have dozens of functions? (maybe spread across multiple files?) We need automation.

👉 Enter pytest.

Installing pytest

In your terminal:

pip install pytest

Running pytest

Add your test_generate_sine_wave function to a file named test_my_function.py.

Then run:

pytest test_my_function.py

pytest Output Example

============================= test session starts ==============================
collected 1 item

test_my_function.py .                                               [100%]
============================== 1 passed in 0.05s ===============================

. = test passed ✅

F = test failed ❌

Seeing a Failure

Let’s add a broken test to the same file see what happens. The time-step is wrong on purpose.

def test_we_want_to_see_a_fail():
    t, y = generate_sine_wave(1, 2, 1)
    assert len(t) == 999 

Seeing a Failure

Run again with pytest. You’ll see an F and details about the failure.

=========================================================== test session starts ============================================================
platform darwin -- Python 3.10.13, pytest-8.3.3, pluggy-1.5.0
rootdir: /Users/geetgeorge/Documents/Work/Teaching/AESB2122-Signals_and_Systems_with_Python/2025-26_Q1/123456-sands-python
plugins: anyio-4.7.0
collected 2 items                                                                                                                          

functions.py .F                                                                                                                      [100%]

================================================================= FAILURES =================================================================
________________________________________________________ test_we_want_to_see_a_fail ________________________________________________________

    def test_we_want_to_see_a_fail():
        t, y = generate_sine_wave(1, 2, 1)
>       assert len(t) == 999
E       assert 1000 == 999
E        +  where 1000 = len(array([0.   , 0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008,\n       0.009, 0.01 , 0.011, 0.012, 0.013, 0.014,...0.985, 0.986, 0.987, 0.988, 0.989,\n       0.99 , 0.991, 0.992, 0.993, 0.994, 0.995, 0.996, 0.997, 0.998,\n       0.999]))

functions.py:29: AssertionError
========================================================= short test summary info ==========================================================
FAILED functions.py::test_we_want_to_see_a_fail - assert 1000 == 999
======================================================= 1 failed, 1 passed in 0.17s ========================================================

Reading the Output

pytest tells you: - Which test failed
- The exact line and values
- A summary of all passed/failed tests

This makes debugging incredibly fast.

What should I do now?

  • Start small, but always test.
  • Use assertions early and often.
  • Automate with pytest.
  • Test your code like you’ll never see it again — because future-you won’t remember what it did! 😄