Skip to Content
Course content

295: Testing with pytest: Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was working on a pricing engine for a subscription service. I spent about an hour "optimizing" a function that calculated pro-rated refunds. I felt great about the code—it was leaner, faster, and looked elegant. I pushed it to staging, and within ten minutes, the QA lead pinged me. The system was accidentally refunding customers 110% of their payment in specific edge cases. I had spent an hour optimizing the code and four hours manually trying to recreate the bug because I didn't have a repeatable way to verify that my "optimization" hadn't broken the original logic.

That's the moment I stopped viewing testing as a chore and started seeing it as a safety net. In Python, while the built-in unittest module exists, almost everyone I know uses pytest. It's less boilerplate, more intuitive, and frankly, it gets out of your way so you can actually write code.

The Magic of Simple Asserts

The biggest shift you'll notice moving to pytest is that you don't need to wrap everything in classes or learn a dozen different assertion methods like self.assertEqual() or self.assertTrue(). You just use the standard Python assert statement. If the expression following assert is true, the test passes. If it's false, pytest catches the exception and tells you exactly why it failed.

Let's say we have a simple function in a file called shopping_cart.py that calculates a total after applying a discount:

def apply_discount(total, discount_percent):
    if not (0 <= discount_percent <= 100):
        raise ValueError("Discount must be between 0 and 100")
    return total * (1 - discount_percent / 100)

To test this, you just write a function. No fancy setup required. I usually keep my tests in a separate file to keep the production code clean:

# test_shopping_cart.py
from shopping_cart import apply_discount

def test_apply_discount_standard():
    assert apply_discount(100, 20) == 80

def test_apply_discount_zero():
    assert apply_discount(100, 0) == 100

def test_apply_discount_full():
    assert apply_discount(100, 100) == 0

Organizing Your Tests for Discovery

You might be wondering how pytest knows which functions are actually tests and which are just helper functions. It uses a naming convention called "test discovery." By default, pytest looks for files that start with test_ or end with _test.py. Inside those files, it looks for functions that start with test_.

I've seen developers try to name their tests things like check_logic() or verify_discount(). pytest will simply ignore those. Stick to the test_ prefix. It feels repetitive at first, but it saves you from having to manually maintain a list of every test case in your project. As your project grows to hundreds of tests, this automatic discovery is a lifesaver.

Running the Suite and Reading Failures

To run your tests, you don't call the script directly. Instead, you run pytest from your terminal in the root directory of your project. You can just type pytest, and it will scan your folders, find every test file, and execute every test function.

The real power, however, is in the failure reports. When a standard assert fails in a normal script, you just get an AssertionError. When it fails in pytest, the tool performs "assertion introspection." It will show you the exact values of the variables at the moment of failure. If you expected 80 but got 85, it doesn't just say "False"; it shows you the math that led to 85. This eliminates the need to pepper your test code with print() statements just to see what went wrong.




📋 Practical Task

Build a Validator Test Suite for an Inventory System

You have been handed a piece of legacy code that manages warehouse stock. The function validate_stock_withdrawal(current_stock, request_amount) is supposed to return True if the withdrawal is possible and False if the request is invalid or exceeds stock. However, the original developer left some bugs in the logic.

The Source Code (inventory.py):

def validate_stock_withdrawal(current_stock, request_amount):
    # Bug: This doesn't handle negative request amounts correctly
    if request_amount < 0:
        return True 
    if request_amount <= current_stock:
        return True
    return False

Your Task:

  1. Create a file named test_inventory.py.
  2. Write a test test_sufficient_stock that asserts True when stock is 10 and request is 5.
  3. Write a test test_insufficient_stock that asserts False when stock is 10 and request is 15.
  4. Write a test test_negative_request that asserts False when a negative amount is requested (this test should currently fail, revealing the bug in the source code).
  5. Run pytest in your terminal and observe the failure report for the negative request test.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.