Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
303: Writing Effective Test Suites
I've seen this exact scenario play out in countless code reviews. A developer spends three hours writing a "comprehensive" test suite, only to have the entire thing turn red the moment they rename a private variable or change a list to a set. They end up hating the tests because the tests feel like a burden, not a safety net.
# The implementation
class UserRegistry:
def __init__(self):
self._users = [] # Internal storage
def add_user(self, username):
if username not in self._users:
self._users.append(username)
def get_all_users(self):
return sorted(self._users)
# The "Fragile" Test
def test_add_user():
registry = UserRegistry()
registry.add_user("alice")
# The mistake: Testing the internal attribute directly
assert registry._users == ["alice"]
When Tests Break for the Wrong Reasons
At first glance, the test above looks perfect. It's fast, it's simple, and it passes. But notice that the test is reaching inside the UserRegistry to check the _users list. By doing this, you've coupled your test to the implementation rather than the behavior.
Imagine you realize that as your user base grows, checking if username not in self._users becomes painfully slow. You decide to change self._users from a list to a set for O(1) lookups. The code still works perfectly—the behavior hasn't changed—but suddenly your test fails because a set is not equal to a list. You didn't break the feature, but you broke the test. This is how "test fatigue" starts; you stop trusting your suite because it screams at you even when you've actually improved the code.
Shifting Focus to Public Behavior
To write an effective suite, you have to treat the class you're testing as a black box. You should only care about what goes in and what comes out through the public API. If the UserRegistry provides a method to get the users, use that. If it doesn't, you might need to add a method or a property to make the class observable without exposing its guts.
# The improved, robust test
def test_add_user_behavior():
registry = UserRegistry()
registry.add_user("alice")
# We test the public outcome: can we actually retrieve the user?
assert "alice" in registry.get_all_users()
Now, if I change the internal storage to a set, a dictionary, or even a database call, this test stays green. It doesn't care how the registry stores the user, only that the user is stored. This is the difference between a suite that hinders you and one that empowers you to refactor with confidence.
The Danger of Over-Mocking
While we're talking about effectiveness, let's touch on mocks. I see a lot of developers who mock every single dependency in their system. If you're testing a PaymentProcessor and you mock the BankAPI, the Database, and the Logger, you aren't actually testing if your system works—you're testing if your mocks behave the way you told them to behave. You've essentially written a mirror of your code in your test file.
My rule of thumb: mock the boundaries of your system (like external HTTP APIs or the filesystem), but avoid mocking your own internal logic. Use "fakes" or small in-memory versions of your services instead. If your test suite is 90% mocks, you'll be shocked to find that everything passes in CI, but the app crashes in production because the real objects don't actually talk to each other the way your mocks assumed they did.
Designing for Testability
If you find it impossible to test a function without reaching into its private variables or mocking half the world, it's usually a signal that your function is doing too much. This is the "test-driven" realization: difficult-to-test code is almost always poorly designed code.
Instead of creating a massive class that handles database connections, business logic, and email notifications, break it apart. Pass the database connection as an argument (Dependency Injection). When the dependencies are explicit, your test suite becomes a series of small, focused checks rather than a fragile web of setup and teardown logic.
📋 Practical Task
Refactoring the OrderValidation Suite
You've inherited a codebase for an e-commerce system. The previous developer wrote a test suite for the OrderValidator class, but it's incredibly fragile. Every time the internal logic for how orders are tracked changes, the tests break, even if the validation results remain correct.
Your Goal: Refactor the following test suite to focus on behavior rather than implementation. Remove all assertions that access private attributes (those starting with _) and replace them with assertions that use the public methods of the class.
class OrderValidator:
def __init__(self):
self._invalid_orders = []
self._processed_count = 0
def validate(self, order):
self._processed_count += 1
if order.get('amount', 0) <= 0:
self._invalid_orders.append(order['id'])
return False
return True
def get_invalid_order_ids(self):
return self._invalid_orders
def get_total_processed(self):
return self._processed_count
# --- BROKEN/FRAGILE TESTS TO FIX ---
def test_validate_negative_amount():
validator = OrderValidator()
order = {'id': 101, 'amount': -50}
validator.validate(order)
# FIX THIS: This reaches into internals
assert 101 in validator._invalid_orders
def test_validate_increments_counter():
validator = OrderValidator()
validator.validate({'id': 102, 'amount': 100})
validator.validate({'id': 103, 'amount': 200})
# FIX THIS: This reaches into internals
assert validator._processed_count == 2
Rewrite the two test functions so that they only use get_invalid_order_ids() and get_total_processed(). Ensure the tests still pass and would remain passing even if the OrderValidator changed _invalid_orders from a list to a set.
There are no comments for now.