Testing Basics
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand why testing is important
- - Understand unit testing concepts
- - Use the unittest module
- - Write test cases
- - Use test fixtures
- - Organize test suites
- - Run tests
- - Understand test assertions
- - Apply testing best practices
- - Debug test failures
Lesson 17.1: Testing Basics
Learning Objectives
By the end of this lesson, you will be able to:
- Understand why testing is important
- Understand unit testing concepts
- Use the unittest module
- Write test cases
- Use test fixtures
- Organize test suites
- Run tests
- Understand test assertions
- Apply testing best practices
- Debug test failures
Introduction to Testing
Testing is the process of verifying that your code works as expected. Writing tests helps ensure code quality, catch bugs early, and maintain confidence when making changes.
Why Test?
- Catch bugs early: Find issues before they reach production
- Documentation: Tests serve as executable documentation
- Confidence: Make changes with confidence
- Refactoring: Safely refactor code knowing tests will catch issues
- Regression prevention: Prevent old bugs from returning
- Code quality: Encourages better code design
What is Testing?
Testing involves writing code that verifies your application code behaves correctly under various conditions.
Why Test?
Benefits of Testing
- Early Bug Detection: Find bugs during development
- Documentation: Tests show how code should be used
- Confidence: Know your code works
- Refactoring Safety: Change code without fear
- Regression Prevention: Prevent bugs from returning
- Better Design: Writing tests improves code design
Testing Pyramid
/\
/ \ E2E Tests (Few)
/____\
/ \ Integration Tests (Some)
/________\
/ \ Unit Tests (Many)
/____________\
- Unit Tests: Test individual components (most tests)
- Integration Tests: Test component interactions (some tests)
- E2E Tests: Test entire system (few tests)
Unit Testing Concepts
What is Unit Testing?
Unit testing is testing individual units of code (functions, methods, classes) in isolation.
Test Case
A test case is a single test that verifies a specific behavior:
def test_add():
assert add(2, 3) == 5
Test Suite
A test suite is a collection of test cases:
class TestCalculator:
def test_add(self):
assert add(2, 3) == 5
def test_subtract(self):
assert subtract(5, 3) == 2
Test Fixtures
Fixtures are setup and teardown code that runs before/after tests:
def setup():
# Setup code
pass
def teardown():
# Cleanup code
pass
Assertions
Assertions verify expected behavior:
assert condition, "Error message"
unittest Module
Basic Test Case
The unittest module provides a testing framework:
import unittest
class TestCalculator(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
def test_subtract(self):
self.assertEqual(subtract(5, 3), 2)
if __name__ == '__main__':
unittest.main()
Test Structure
import unittest
class TestMyFunction(unittest.TestCase):
def setUp(self):
"""Run before each test method."""
pass
def tearDown(self):
"""Run after each test method."""
pass
def test_something(self):
"""Test method - must start with 'test'."""
pass
Running Tests
# Run from command line
python -m unittest test_module.py
# Run specific test
python -m unittest test_module.TestClass.test_method
# Run with verbosity
python -m unittest -v test_module.py
Test Assertions
Common Assertions
import unittest
class TestAssertions(unittest.TestCase):
def test_assert_equal(self):
self.assertEqual(2 + 2, 4)
def test_assert_not_equal(self):
self.assertNotEqual(2 + 2, 5)
def test_assert_true(self):
self.assertTrue(True)
def test_assert_false(self):
self.assertFalse(False)
def test_assert_is(self):
a = [1, 2, 3]
b = a
self.assertIs(a, b)
def test_assert_is_not(self):
a = [1, 2, 3]
b = [1, 2, 3]
self.assertIsNot(a, b)
def test_assert_is_none(self):
self.assertIsNone(None)
def test_assert_is_not_none(self):
self.assertIsNotNone(42)
def test_assert_in(self):
self.assertIn(2, [1, 2, 3])
def test_assert_not_in(self):
self.assertNotIn(4, [1, 2, 3])
def test_assert_is_instance(self):
self.assertIsInstance([1, 2, 3], list)
def test_assert_raises(self):
with self.assertRaises(ValueError):
int("not a number")
Assertion Methods
| Method | Checks |
|---|---|
assertEqual(a, b) |
a == b |
assertNotEqual(a, b) |
a != b |
assertTrue(x) |
bool(x) is True |
assertFalse(x) |
bool(x) is False |
assertIs(a, b) |
a is b |
assertIsNot(a, b) |
a is not b |
assertIsNone(x) |
x is None |
assertIsNotNone(x) |
x is not None |
assertIn(a, b) |
a in b |
assertNotIn(a, b) |
a not in b |
assertIsInstance(a, b) |
isinstance(a, b) |
assertRaises(exception) |
Raises exception |
Test Fixtures
setUp and tearDown
import unittest
class TestDatabase(unittest.TestCase):
def setUp(self):
"""Run before each test."""
self.db = create_test_database()
def tearDown(self):
"""Run after each test."""
self.db.close()
def test_query(self):
result = self.db.query("SELECT * FROM users")
self.assertIsNotNone(result)
setUpClass and tearDownClass
import unittest
class TestDatabase(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Run once before all tests."""
cls.db = create_test_database()
@classmethod
def tearDownClass(cls):
"""Run once after all tests."""
cls.db.close()
def test_query1(self):
result = self.db.query("SELECT * FROM users")
self.assertIsNotNone(result)
def test_query2(self):
result = self.db.query("SELECT * FROM posts")
self.assertIsNotNone(result)
setUpModule and tearDownModule
import unittest
def setUpModule():
"""Run once before all test classes."""
print("Setting up module")
def tearDownModule():
"""Run once after all test classes."""
print("Tearing down module")
class TestClass1(unittest.TestCase):
def test_something(self):
pass
class TestClass2(unittest.TestCase):
def test_something_else(self):
pass
Organizing Tests
Test Discovery
unittest can automatically discover tests:
# Run all tests in current directory
python -m unittest discover
# Run tests in specific directory
python -m unittest discover tests/
# Run tests with pattern
python -m unittest discover -p "test_*.py"
Test File Structure
project/
├── src/
│ └── calculator.py
└── tests/
├── __init__.py
├── test_calculator.py
└── test_advanced.py
Test Class Organization
import unittest
from calculator import Calculator
class TestCalculatorBasic(unittest.TestCase):
"""Test basic calculator operations."""
def test_add(self):
calc = Calculator()
self.assertEqual(calc.add(2, 3), 5)
class TestCalculatorAdvanced(unittest.TestCase):
"""Test advanced calculator operations."""
def test_power(self):
calc = Calculator()
self.assertEqual(calc.power(2, 3), 8)
Practical Examples
Example 1: Testing a Simple Function
# calculator.py
def add(x, y):
return x + y
def subtract(x, y):
return x - y
# test_calculator.py
import unittest
from calculator import add, subtract
class TestCalculator(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-2, -3), -5)
def test_add_zero(self):
self.assertEqual(add(5, 0), 5)
def test_subtract_positive(self):
self.assertEqual(subtract(5, 3), 2)
def test_subtract_negative(self):
self.assertEqual(subtract(5, -3), 8)
if __name__ == '__main__':
unittest.main()
Example 2: Testing a Class
# user.py
class User:
def __init__(self, name, email):
self.name = name
self.email = email
self.active = True
def deactivate(self):
self.active = False
def activate(self):
self.active = True
# test_user.py
import unittest
from user import User
class TestUser(unittest.TestCase):
def setUp(self):
self.user = User("Alice", "alice@example.com")
def test_user_creation(self):
self.assertEqual(self.user.name, "Alice")
self.assertEqual(self.user.email, "alice@example.com")
self.assertTrue(self.user.active)
def test_deactivate(self):
self.user.deactivate()
self.assertFalse(self.user.active)
def test_activate(self):
self.user.deactivate()
self.user.activate()
self.assertTrue(self.user.active)
Example 3: Testing Exceptions
# validator.py
def validate_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age cannot exceed 150")
return True
# test_validator.py
import unittest
from validator import validate_age
class TestValidator(unittest.TestCase):
def test_valid_age(self):
self.assertTrue(validate_age(25))
def test_negative_age(self):
with self.assertRaises(ValueError):
validate_age(-5)
def test_too_old(self):
with self.assertRaises(ValueError):
validate_age(200)
def test_non_integer(self):
with self.assertRaises(TypeError):
validate_age("25")
Example 4: Testing with Mock Data
import unittest
from unittest.mock import Mock, patch
class TestAPI(unittest.TestCase):
@patch('requests.get')
def test_fetch_data(self, mock_get):
# Mock the response
mock_response = Mock()
mock_response.json.return_value = {'data': 'test'}
mock_get.return_value = mock_response
# Test the function
from api import fetch_data
result = fetch_data('https://api.example.com')
self.assertEqual(result, {'data': 'test'})
mock_get.assert_called_once_with('https://api.example.com')
Common Testing Patterns
Pattern 1: Arrange-Act-Assert (AAA)
class TestCalculator(unittest.TestCase):
def test_add(self):
# Arrange
calc = Calculator()
x, y = 2, 3
# Act
result = calc.add(x, y)
# Assert
self.assertEqual(result, 5)
Pattern 2: Test Edge Cases
class TestCalculator(unittest.TestCase):
def test_add_zero(self):
self.assertEqual(add(5, 0), 5)
def test_add_negative(self):
self.assertEqual(add(-2, -3), -5)
def test_add_large_numbers(self):
self.assertEqual(add(1000000, 2000000), 3000000)
Pattern 3: Test Error Cases
class TestValidator(unittest.TestCase):
def test_invalid_input(self):
with self.assertRaises(ValueError):
validate_age(-1)
def test_wrong_type(self):
with self.assertRaises(TypeError):
validate_age("25")
Common Mistakes and Pitfalls
1. Not Testing Edge Cases
# WRONG: Only testing happy path
def test_add(self):
self.assertEqual(add(2, 3), 5)
# CORRECT: Test edge cases
def test_add(self):
self.assertEqual(add(2, 3), 5)
self.assertEqual(add(0, 0), 0)
self.assertEqual(add(-2, -3), -5)
2. Testing Implementation Instead of Behavior
# WRONG: Testing implementation details
def test_internal_variable(self):
self.assertEqual(obj._internal_var, 5)
# CORRECT: Test behavior
def test_public_method(self):
result = obj.public_method()
self.assertEqual(result, expected)
3. Not Isolating Tests
# WRONG: Tests depend on each other
def test_step1(self):
self.obj.value = 5
def test_step2(self):
self.assertEqual(self.obj.value, 5) # Depends on test_step1
# CORRECT: Each test is independent
def test_step1(self):
obj = MyClass()
obj.value = 5
self.assertEqual(obj.value, 5)
def test_step2(self):
obj = MyClass()
obj.value = 10
self.assertEqual(obj.value, 10)
4. Not Cleaning Up
# WRONG: No cleanup
def test_file_operation(self):
with open('test.txt', 'w') as f:
f.write('test')
# File not cleaned up
# CORRECT: Clean up in tearDown
def tearDown(self):
if os.path.exists('test.txt'):
os.remove('test.txt')
Best Practices
1. Write Tests First (TDD)
Test-Driven Development (TDD):
- Write a failing test
- Write code to make it pass
- Refactor
2. Test One Thing at a Time
# Good: One assertion per test concept
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-2, -3), -5)
3. Use Descriptive Test Names
# Good: Descriptive name
def test_add_returns_sum_of_two_positive_numbers(self):
pass
# Bad: Unclear name
def test_add(self):
pass
4. Keep Tests Simple
# Good: Simple and clear
def test_add(self):
result = add(2, 3)
self.assertEqual(result, 5)
# Bad: Complex and hard to understand
def test_add(self):
result = add(2, 3)
if result == 5:
self.assertTrue(True)
else:
self.assertTrue(False)
5. Test Edge Cases
def test_divide(self):
self.assertEqual(divide(10, 2), 5)
self.assertEqual(divide(10, -2), -5)
with self.assertRaises(ZeroDivisionError):
divide(10, 0)
6. Use Fixtures for Setup
def setUp(self):
self.calculator = Calculator()
self.test_data = [1, 2, 3, 4, 5]
Practice Exercise
Exercise: Writing Tests
Objective: Create a Python program that demonstrates testing basics.
Instructions:
-
Create a file called
test_practice.py -
Write a program that:
- Tests simple functions
- Tests classes
- Uses test fixtures
- Tests exceptions
- Demonstrates test organization
-
Your program should include:
- Basic test cases
- setUp and tearDown
- Multiple test methods
- Exception testing
- Edge case testing
- Real-world examples
Example Solution:
"""
Testing Basics Practice
This program demonstrates unittest module.
"""
import unittest
import os
# Code to test
def add(x, y):
return x + y
def divide(x, y):
if y == 0:
raise ZeroDivisionError("Cannot divide by zero")
return x / y
class Calculator:
def __init__(self):
self.history = []
def add(self, x, y):
result = x + y
self.history.append(f"{x} + {y} = {result}")
return result
def subtract(self, x, y):
result = x - y
self.history.append(f"{x} - {y} = {result}")
return result
def get_history(self):
return self.history
# Test cases
class TestAddFunction(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-2, -3), -5)
def test_add_zero(self):
self.assertEqual(add(5, 0), 5)
self.assertEqual(add(0, 5), 5)
def test_add_mixed(self):
self.assertEqual(add(-2, 3), 1)
class TestDivideFunction(unittest.TestCase):
def test_divide_positive(self):
self.assertEqual(divide(10, 2), 5)
def test_divide_negative(self):
self.assertEqual(divide(10, -2), -5)
def test_divide_zero(self):
with self.assertRaises(ZeroDivisionError):
divide(10, 0)
def test_divide_float_result(self):
self.assertEqual(divide(7, 2), 3.5)
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Run before each test."""
self.calc = Calculator()
def tearDown(self):
"""Run after each test."""
self.calc.history.clear()
def test_add(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
self.assertEqual(len(self.calc.history), 1)
def test_subtract(self):
result = self.calc.subtract(5, 3)
self.assertEqual(result, 2)
self.assertEqual(len(self.calc.history), 1)
def test_history(self):
self.calc.add(1, 2)
self.calc.subtract(5, 3)
history = self.calc.get_history()
self.assertEqual(len(history), 2)
self.assertIn("1 + 2 = 3", history)
self.assertIn("5 - 3 = 2", history)
class TestCalculatorClass(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Run once before all tests."""
cls.shared_calc = Calculator()
def test_shared_calculator(self):
result = self.shared_calc.add(10, 20)
self.assertEqual(result, 30)
class TestFileOperations(unittest.TestCase):
def setUp(self):
self.test_file = 'test_file.txt'
def tearDown(self):
if os.path.exists(self.test_file):
os.remove(self.test_file)
def test_file_creation(self):
with open(self.test_file, 'w') as f:
f.write('test content')
self.assertTrue(os.path.exists(self.test_file))
def test_file_content(self):
with open(self.test_file, 'w') as f:
f.write('test content')
with open(self.test_file, 'r') as f:
content = f.read()
self.assertEqual(content, 'test content')
class TestAssertions(unittest.TestCase):
def test_assert_equal(self):
self.assertEqual(2 + 2, 4)
def test_assert_not_equal(self):
self.assertNotEqual(2 + 2, 5)
def test_assert_true(self):
self.assertTrue(True)
self.assertTrue(1)
self.assertTrue([1, 2, 3])
def test_assert_false(self):
self.assertFalse(False)
self.assertFalse(0)
self.assertFalse([])
def test_assert_is(self):
a = [1, 2, 3]
b = a
self.assertIs(a, b)
def test_assert_is_not(self):
a = [1, 2, 3]
b = [1, 2, 3]
self.assertIsNot(a, b)
def test_assert_is_none(self):
self.assertIsNone(None)
def test_assert_is_not_none(self):
self.assertIsNotNone(42)
self.assertIsNotNone([1, 2, 3])
def test_assert_in(self):
self.assertIn(2, [1, 2, 3])
self.assertIn('a', 'abc')
def test_assert_not_in(self):
self.assertNotIn(4, [1, 2, 3])
self.assertNotIn('d', 'abc')
def test_assert_is_instance(self):
self.assertIsInstance([1, 2, 3], list)
self.assertIsInstance('hello', str)
self.assertIsInstance(42, int)
def test_assert_raises(self):
with self.assertRaises(ValueError):
int("not a number")
with self.assertRaises(ZeroDivisionError):
1 / 0
if __name__ == '__main__':
unittest.main(verbosity=2)
Expected Output (truncated):
test_add_negative (__main__.TestAddFunction) ... ok
test_add_positive (__main__.TestAddFunction) ... ok
test_add_zero (__main__.TestAddFunction) ... ok
test_divide_negative (__main__.TestDivideFunction) ... ok
test_divide_positive (__main__.TestDivideFunction) ... ok
test_divide_zero (__main__.TestDivideFunction) ... ok
[... rest of output ...]
----------------------------------------------------------------------
Ran 25 tests in 0.001s
OK
Challenge (Optional):
- Create a test suite for a complete module
- Write tests for edge cases and error conditions
- Implement test fixtures for complex setup
- Create integration tests that test multiple components together
Key Takeaways
- Why test - catch bugs, document code, enable refactoring
- Unit testing - test individual components in isolation
- unittest module - Python's built-in testing framework
- Test structure - TestCase class with test methods
- Assertions - verify expected behavior
- Fixtures - setUp, tearDown, setUpClass, tearDownClass
- Test organization - organize tests in classes and modules
- Test discovery - unittest can automatically find tests
- Best practices - write tests first, test one thing, use descriptive names
- Edge cases - test boundary conditions and error cases
- Isolation - each test should be independent
- Cleanup - always clean up in tearDown
- AAA pattern - Arrange-Act-Assert
- Test names - use descriptive test method names
- Test coverage - aim for good test coverage
Quiz: Testing Basics
Test your understanding with these questions:
-
What is unit testing?
- A) Testing the entire system
- B) Testing individual components in isolation
- C) Testing user interfaces
- D) Testing databases
-
What module provides testing functionality?
- A) test
- B) unittest
- C) testing
- D) pytest
-
What must test methods start with?
- A) test_
- B) check_
- C) verify_
- D) assert_
-
What does setUp do?
- A) Runs after each test
- B) Runs before each test
- C) Runs once before all tests
- D) Nothing
-
What does tearDown do?
- A) Runs after each test
- B) Runs before each test
- C) Runs once after all tests
- D) Nothing
-
What is an assertion?
- A) A test method
- B) A verification of expected behavior
- C) A test class
- D) A fixture
-
What is the AAA pattern?
- A) Arrange, Act, Assert
- B) Act, Arrange, Assert
- C) Assert, Arrange, Act
- D) Arrange, Assert, Act
-
What should test names be?
- A) Short
- B) Descriptive
- C) Random
- D) Numbers
-
Should tests depend on each other?
- A) Yes
- B) No
- C) Sometimes
- D) Only in unittest
-
What is TDD?
- A) Test-Driven Development
- B) Test Design Document
- C) Test Data Definition
- D) Test Development Document
Answers:
- B) Testing individual components in isolation (unit testing definition)
- B) unittest (Python's testing module)
- A) test_ (test method naming convention)
- B) Runs before each test (setUp purpose)
- A) Runs after each test (tearDown purpose)
- B) A verification of expected behavior (assertion definition)
- A) Arrange, Act, Assert (AAA pattern)
- B) Descriptive (test naming best practice)
- B) No (tests should be independent)
- A) Test-Driven Development (TDD definition)
Next Steps
Excellent work! You've mastered testing basics. You now understand:
- Why testing is important
- Unit testing concepts
- The unittest module
- How to write and organize tests
What's Next?
- Lesson 17.2: pytest Framework
- Learn pytest basics
- Understand pytest features
- Explore advanced pytest patterns
Additional Resources
- unittest: docs.python.org/3/library/unittest.html
- Test-Driven Development: en.wikipedia.org/wiki/Test-driven_development
- Testing Best Practices: docs.python-guide.org/writing/tests/
Lesson completed! You're ready to move on to the next lesson.
Course Navigation
- Testing Basics
- pytest Framework
- Test-Driven Development (TDD)
- Mocking and Patching
- Testing Basics
- pytest Framework
- Test-Driven Development (TDD)
- Mocking and Patching