Python Testing with pytest: Write Tests That Actually Help
Imagine you wrote a spell-checker. It works great on the word "hello." But what about "HeLLo"? Or an empty string? Or a number? You won't know until something breaks — unless you write tests.
A test is a small piece of code that checks whether your other code works correctly. It's like a safety net for your programs. Every time you make a change, you run your tests, and they instantly tell you if you broke something.
In this tutorial, you'll learn to write tests using pytest-style conventions — the most popular testing approach in Python. We'll use plain assert statements and simple test functions that you can run right here in your browser.
Why Should You Write Tests?
You might think: "I can just run my code and see if it works." That's fine for tiny programs. But real projects have hundreds of functions. You can't manually check every single one after every change.
Tests solve three big problems:
Here's a simple example. Say you write a function that adds two numbers:
That print statement tells you the answer is 5, but you have to check it with your eyes every time. What if someone changes the function later? Nobody will remember to re-run that print.
How Do You Write Your First Test Function?
A test function is just a regular Python function whose name starts with test_. Inside, you use assert to check that something is true. If the assertion passes, the test passes. If it fails, Python raises an error.
Notice the pattern: each test function has a descriptive name that explains what it checks. The name test_add_positive_numbers tells you exactly what scenario is being tested.
In a real project, you'd use the pytest framework to discover and run tests automatically. Here, we call them manually — but the logic is identical.
How Do Assert Statements Work?
The assert statement is the heart of every test. It says: "I believe this thing is true. If it's not, stop everything and tell me."
Here are the most useful types of assertions you'll write:
# Equality
assert result == expected
# Truthiness
assert is_valid
assert not is_empty
# Membership
assert item in collection
assert key in dictionary
# Type checking
assert isinstance(value, int)
# Comparisons
assert count > 0
assert length <= max_lengthWhen an assertion fails, Python tells you exactly what went wrong:
What Are Edge Cases and Why Do They Matter?
An edge case is an unusual or extreme input that your code might not handle well. Think of it like stress-testing a bridge — you don't just drive a car over it, you also check what happens with a full convoy of trucks.
Common edge cases to test:
None?How Should You Organize Your Tests?
As your project grows, you'll have dozens or even hundreds of tests. Keeping them organized is essential. The standard approach is to group related tests together.
One powerful technique is using a test runner — a function that collects and runs all your test functions, reporting which passed and which failed:
def test1():
assert add(1, 2) == 3
def test2():
assert add(-1, 1) == 0
def another_test():
# Won't be found by pytest!
assert add(0, 0) == 0def test_add_positive_numbers():
assert add(1, 2) == 3
def test_add_mixed_signs():
assert add(-1, 1) == 0
def test_add_zeros():
assert add(0, 0) == 0What Is Test-Driven Thinking?
Here's a mindset shift that will make you a better programmer: instead of writing code first and testing later, think about your tests while you design your function.
Before you write a single line of code, ask yourself: "How will I know this works?" The answer to that question is your test.
Practice Exercises
Write a function called square that returns the square of a number. Then write a function called test_square that tests it with at least 3 different assert statements (test with a positive number, zero, and a negative number). Call test_square() and print All tests passed! at the end.
The function is_adult is supposed to return True if age is 18 or older, and False otherwise. The tests are correct, but the function has a bug. Fix the function so all tests pass, then print All tests passed!.
Read the code below carefully. The test runner catches assertion errors and reports results. Predict exactly what gets printed. Think about which tests will pass and which will fail.
The truncate function shortens a string to a maximum length and adds "..." if it was cut. Write a test_truncate function that tests at least 4 edge cases: a string shorter than the limit, a string exactly at the limit, a string longer than the limit, and an empty string. Call the test and print All tests passed!.
Write a function called run_tests that takes a list of test functions, runs each one inside a try/except block, and prints a summary at the end. Use the format shown: print PASS: <function_name> or FAIL: <function_name> for each test, then print a blank line followed by <passed> passed, <failed> failed. Use the provided test functions to demonstrate it.
The tests below work but are repetitive. Refactor them to use a data-driven approach: store test cases as a list of tuples (input, expected) and loop through them. The function, all assertions, and the final print statement should remain. Print Testing: <input> -> <expected> for each case, then All 5 tests passed! at the end.