Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on testing external services with VCR.py! ๐ Ever wondered how to test code that makes API calls without hitting the actual API every time? Thatโs where VCR.py comes to the rescue!
Youโll discover how VCR.py can transform your testing experience. Whether youโre building weather apps ๐ฆ๏ธ, e-commerce platforms ๐, or social media integrations ๐ฑ, understanding VCR.py is essential for writing reliable, fast tests without expensive API calls.
By the end of this tutorial, youโll feel confident testing external services like a pro! Letโs dive in! ๐โโ๏ธ
๐ Understanding VCR.py
๐ค What is VCR.py?
VCR.py is like a video recorder for your HTTP interactions! ๐น Think of it as recording a conversation between your code and an API, then replaying that exact conversation during tests instead of making real API calls.
In Python terms, VCR.py intercepts HTTP requests and saves them as โcassettesโ ๐ผ. This means you can:
- โจ Test without internet connection
- ๐ Run tests super fast (no network delays!)
- ๐ก๏ธ Avoid hitting API rate limits
- ๐ฐ Save money on API calls
- ๐ฏ Have deterministic, repeatable tests
๐ก Why Use VCR.py?
Hereโs why developers love VCR.py:
- Speed โก: Tests run instantly with recorded responses
- Reliability ๐: No flaky tests due to network issues
- Cost Savings ๐ต: No API usage charges during testing
- Offline Development ๐๏ธ: Work anywhere, anytime
- Real Responses ๐: Test with actual API data
Real-world example: Imagine building a weather app ๐ค๏ธ. With VCR.py, you can record the weather API response once and replay it thousands of times in your tests!
๐ง Basic Syntax and Usage
๐ Installation and Setup
Letโs start by installing VCR.py:
# ๐ฏ Install VCR.py
# pip install vcrpy
# ๐ Import what we need!
import vcr
import requests
import pytest # ๐งช We'll use pytest for testing
๐ฌ Your First Recording
Hereโs a simple example:
# ๐ฎ Let's record our first API call!
import vcr
import requests
@vcr.use_cassette('weather_api.yaml')
def test_weather_api():
# ๐ค๏ธ This call will be recorded!
response = requests.get('https://api.weatherapi.com/v1/current.json',
params={'key': 'demo', 'q': 'London'})
# โ
Test the response
assert response.status_code == 200
assert 'location' in response.json()
print("Weather recorded! ๐ผ")
๐ก Explanation: The @vcr.use_cassette
decorator tells VCR.py to record/replay HTTP interactions in the specified file!
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Using context manager
def test_with_context_manager():
with vcr.use_cassette('github_api.yaml'):
# ๐ All HTTP calls in this block are recorded
response = requests.get('https://api.github.com/users/python')
assert response.json()['name'] == 'Python'
# ๐จ Pattern 2: Class-based tests
@vcr.use_cassette('multiple_calls.yaml')
class TestAPIIntegration:
def test_user_endpoint(self):
# ๐ค Test user data
response = requests.get('https://jsonplaceholder.typicode.com/users/1')
assert response.json()['name'] == 'Leanne Graham'
def test_posts_endpoint(self):
# ๐ Test posts data
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')
assert 'title' in response.json()
# ๐ Pattern 3: Custom matching
my_vcr = vcr.VCR(
match_on=['uri', 'method', 'body'], # ๐ฏ What to match on
record_mode='once' # ๐ผ Recording strategy
)
@my_vcr.use_cassette('custom_matching.yaml')
def test_with_custom_vcr():
# ๐จ Your test here!
pass
๐ก Practical Examples
๐ Example 1: E-commerce API Testing
Letโs build something real - testing a shopping API:
# ๐๏ธ Testing an e-commerce API
import vcr
import requests
import json
class ShoppingAPIClient:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.shopping.com/v1"
def search_products(self, query):
# ๐ Search for products
response = requests.get(
f"{self.base_url}/search",
params={'q': query, 'api_key': self.api_key}
)
return response.json()
def get_product_details(self, product_id):
# ๐ฆ Get product info
response = requests.get(
f"{self.base_url}/products/{product_id}",
headers={'Authorization': f'Bearer {self.api_key}'}
)
return response.json()
# ๐งช Now let's test it!
@vcr.use_cassette('shopping_tests.yaml',
filter_headers=['Authorization'], # ๐ Hide sensitive data!
record_mode='new_episodes') # ๐ผ Smart recording
class TestShoppingAPI:
def setup_method(self):
# ๐ฏ Setup our test client
self.client = ShoppingAPIClient('test-api-key')
def test_search_products(self):
# ๐ Test product search
results = self.client.search_products('laptop')
assert len(results['products']) > 0
assert results['products'][0]['name']
assert results['products'][0]['price'] > 0
print("Found products! ๐")
def test_product_details(self):
# ๐ฆ Test getting specific product
product = self.client.get_product_details('12345')
assert product['id'] == '12345'
assert 'description' in product
assert product['in_stock'] is not None
print("Product details work! โจ")
๐ฏ Try it yourself: Add a test for adding products to cart and checking out!
๐ Example 2: Social Media Integration
Letโs test a social media integration:
# ๐ฆ Testing Twitter-like API
import vcr
import requests
from datetime import datetime
class SocialMediaClient:
def __init__(self, api_token):
self.token = api_token
self.base_url = "https://api.socialplatform.com/v2"
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json'
})
def post_update(self, message, emoji='๐'):
# ๐ Post a status update
data = {
'text': f"{message} {emoji}",
'timestamp': datetime.now().isoformat()
}
response = self.session.post(f"{self.base_url}/posts", json=data)
return response.json()
def get_timeline(self, count=10):
# ๐ฐ Get user's timeline
response = self.session.get(
f"{self.base_url}/timeline",
params={'count': count}
)
return response.json()
def like_post(self, post_id):
# โค๏ธ Like a post
response = self.session.post(f"{self.base_url}/posts/{post_id}/like")
return response.json()
# ๐งช Test with VCR.py magic!
@pytest.fixture
def social_client():
# ๐ง Setup client for tests
return SocialMediaClient('test-token-12345')
@vcr.use_cassette('social_media_tests.yaml',
filter_headers=['Authorization'],
filter_post_data_parameters=['timestamp'])
def test_social_media_workflow(social_client):
# ๐ฌ Test complete workflow
# 1๏ธโฃ Post an update
post = social_client.post_update("Testing with VCR.py!", "๐")
assert post['id']
assert post['status'] == 'published'
# 2๏ธโฃ Check timeline
timeline = social_client.get_timeline(count=5)
assert len(timeline['posts']) <= 5
assert timeline['posts'][0]['id'] == post['id']
# 3๏ธโฃ Like the post
like_result = social_client.like_post(post['id'])
assert like_result['liked'] == True
assert like_result['total_likes'] > 0
print("Social media integration works! ๐")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Recording Modes
When youโre ready to level up, try these recording modes:
# ๐ฏ Different recording strategies
import vcr
# ๐ผ Mode 1: Record once (default)
@vcr.use_cassette('once_mode.yaml', record_mode='once')
def test_record_once():
# Records on first run, replays afterwards
response = requests.get('https://api.example.com/data')
assert response.ok
# ๐ Mode 2: New episodes
@vcr.use_cassette('new_episodes.yaml', record_mode='new_episodes')
def test_new_episodes():
# Records new requests, replays existing ones
response1 = requests.get('https://api.example.com/data')
response2 = requests.get('https://api.example.com/new-endpoint') # ๐ Gets recorded!
# ๐ซ Mode 3: None (never record)
@vcr.use_cassette('none_mode.yaml', record_mode='none')
def test_replay_only():
# Only replays, never records (good for CI/CD)
response = requests.get('https://api.example.com/data')
# ๐ฅ Mode 4: All (always record)
@vcr.use_cassette('all_mode.yaml', record_mode='all')
def test_always_record():
# โ ๏ธ Always makes real requests! Use carefully!
response = requests.get('https://api.example.com/data')
๐๏ธ Custom Request Matching
For the brave developers:
# ๐ Advanced matching strategies
import vcr
from urllib.parse import urlparse, parse_qs
def custom_matcher(r1, r2):
# ๐ฏ Match requests by custom logic
# Ignore query parameter order
url1 = urlparse(r1.uri)
url2 = urlparse(r2.uri)
return (url1.scheme == url2.scheme and
url1.netloc == url2.netloc and
url1.path == url2.path and
parse_qs(url1.query) == parse_qs(url2.query))
# ๐ช Use custom matcher
my_vcr = vcr.VCR()
my_vcr.register_matcher('custom', custom_matcher)
@my_vcr.use_cassette('custom_matching.yaml', match_on=['custom', 'method'])
def test_with_custom_matcher():
# Order of query params doesn't matter!
response1 = requests.get('https://api.example.com?a=1&b=2')
response2 = requests.get('https://api.example.com?b=2&a=1')
# Both match the same recording! โจ
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Exposing Sensitive Data
# โ Wrong way - API keys in cassettes!
@vcr.use_cassette('unsafe.yaml')
def test_with_secrets():
response = requests.get('https://api.example.com',
headers={'API-Key': 'super-secret-key-123'})
# ๐ฐ Your secret is now saved in the cassette!
# โ
Correct way - filter sensitive data!
@vcr.use_cassette('safe.yaml',
filter_headers=['API-Key', 'Authorization'],
filter_query_parameters=['api_key', 'token'])
def test_without_secrets():
response = requests.get('https://api.example.com',
headers={'API-Key': 'super-secret-key-123'})
# ๐ก๏ธ Secrets are replaced with placeholders!
๐คฏ Pitfall 2: Dynamic Data Problems
# โ Dangerous - timestamps always change!
def test_with_timestamp():
data = {
'message': 'Hello',
'timestamp': datetime.now().isoformat() # ๐ฅ Different every time!
}
response = requests.post('https://api.example.com', json=data)
# โ
Safe - use consistent test data!
@vcr.use_cassette('consistent.yaml',
before_record_request=lambda request: None if 'timestamp' in request.body else request)
def test_with_consistent_data():
data = {
'message': 'Hello',
'timestamp': '2024-01-01T00:00:00Z' # โ
Fixed timestamp for tests
}
response = requests.post('https://api.example.com', json=data)
๐ ๏ธ Best Practices
- ๐ฏ Filter Sensitive Data: Always hide API keys and tokens!
- ๐ Organize Cassettes: One cassette per test class or feature
- ๐ Use Appropriate Record Mode:
once
for stable APIs,new_episodes
for evolving ones - ๐ Version Control Cassettes: Commit them with your tests
- ๐งน Clean Old Cassettes: Remove unused recordings periodically
- โก Mock Time-Sensitive Data: Use fixed timestamps in tests
- ๐ฌ Document Recording Process: Tell team how to update cassettes
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Weather Service Test Suite
Create a comprehensive test suite for a weather service:
๐ Requirements:
- โ Test fetching current weather for multiple cities
- ๐ก๏ธ Test temperature unit conversion (Celsius/Fahrenheit)
- ๐ Test weekly forecast retrieval
- ๐๏ธ Handle city not found errors gracefully
- ๐ Test with different regions/languages
๐ Bonus Points:
- Add retry logic for failed requests
- Implement caching to avoid duplicate API calls
- Create a test fixture for common weather data
- Test extreme weather alerts
๐ก Solution
๐ Click to see solution
# ๐ค๏ธ Comprehensive weather service testing!
import vcr
import pytest
import requests
from datetime import datetime, timedelta
class WeatherService:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.weatherservice.com/v1"
self.cache = {} # ๐พ Simple cache
def get_current_weather(self, city, units='metric'):
# ๐ก๏ธ Get current weather
cache_key = f"{city}_{units}_current"
if cache_key in self.cache:
return self.cache[cache_key]
response = requests.get(
f"{self.base_url}/current",
params={
'city': city,
'units': units,
'api_key': self.api_key
}
)
if response.status_code == 404:
raise CityNotFoundError(f"City '{city}' not found! ๐๏ธโ")
data = response.json()
self.cache[cache_key] = data
return data
def get_weekly_forecast(self, city, units='metric'):
# ๐
Get 7-day forecast
response = requests.get(
f"{self.base_url}/forecast/weekly",
params={
'city': city,
'units': units,
'api_key': self.api_key
}
)
return response.json()
def get_weather_alerts(self, city):
# โ ๏ธ Get severe weather alerts
response = requests.get(
f"{self.base_url}/alerts",
params={'city': city, 'api_key': self.api_key}
)
return response.json()
def convert_temperature(self, temp, from_unit, to_unit):
# ๐ Convert temperature units
if from_unit == to_unit:
return temp
if from_unit == 'celsius' and to_unit == 'fahrenheit':
return (temp * 9/5) + 32
elif from_unit == 'fahrenheit' and to_unit == 'celsius':
return (temp - 32) * 5/9
else:
raise ValueError("Unsupported unit conversion! ๐
")
class CityNotFoundError(Exception):
pass
# ๐งช Test suite with VCR.py
@pytest.fixture
def weather_service():
return WeatherService('test-api-key-123')
class TestWeatherService:
@vcr.use_cassette('weather_current.yaml',
filter_query_parameters=['api_key'])
def test_current_weather_multiple_cities(self, weather_service):
# ๐ Test multiple cities
cities = ['London', 'Tokyo', 'New York', 'Sydney']
for city in cities:
weather = weather_service.get_current_weather(city)
assert weather['city'] == city
assert 'temperature' in weather
assert 'conditions' in weather
print(f"โ
Got weather for {city}: {weather['temperature']}ยฐC")
@vcr.use_cassette('weather_units.yaml',
filter_query_parameters=['api_key'])
def test_temperature_units(self, weather_service):
# ๐ก๏ธ Test unit conversion
city = 'Paris'
# Get in Celsius
weather_c = weather_service.get_current_weather(city, 'metric')
temp_c = weather_c['temperature']
# Get in Fahrenheit
weather_f = weather_service.get_current_weather(city, 'imperial')
temp_f = weather_f['temperature']
# Convert and compare
converted = weather_service.convert_temperature(temp_c, 'celsius', 'fahrenheit')
assert abs(converted - temp_f) < 0.1 # ๐ฏ Allow small rounding difference
print(f"โ
Temperature conversion works: {temp_c}ยฐC = {temp_f}ยฐF")
@vcr.use_cassette('weather_forecast.yaml',
filter_query_parameters=['api_key'])
def test_weekly_forecast(self, weather_service):
# ๐
Test weekly forecast
forecast = weather_service.get_weekly_forecast('Berlin')
assert len(forecast['days']) == 7
for day in forecast['days']:
assert 'date' in day
assert 'high' in day
assert 'low' in day
assert 'conditions' in day
print(f"๐
{day['date']}: {day['conditions']} ({day['low']}ยฐ-{day['high']}ยฐ)")
@vcr.use_cassette('weather_errors.yaml',
filter_query_parameters=['api_key'])
def test_city_not_found(self, weather_service):
# ๐๏ธ Test error handling
with pytest.raises(CityNotFoundError) as exc_info:
weather_service.get_current_weather('Atlantis')
assert "City 'Atlantis' not found!" in str(exc_info.value)
print("โ
Error handling works!")
@vcr.use_cassette('weather_alerts.yaml',
filter_query_parameters=['api_key'])
def test_weather_alerts(self, weather_service):
# โ ๏ธ Test severe weather alerts
alerts = weather_service.get_weather_alerts('Miami')
if alerts['active_alerts']:
for alert in alerts['alerts']:
assert 'severity' in alert
assert 'description' in alert
assert alert['severity'] in ['low', 'medium', 'high', 'extreme']
print(f"โ ๏ธ Alert: {alert['severity']} - {alert['description']}")
else:
print("โ๏ธ No weather alerts!")
def test_caching(self, weather_service):
# ๐พ Test caching (no VCR needed for this)
weather_service.cache['London_metric_current'] = {
'city': 'London',
'temperature': 15,
'conditions': 'Cloudy'
}
# Should return cached data without API call
result = weather_service.get_current_weather('London')
assert result['temperature'] == 15
print("โ
Caching works!")
# ๐ฎ Run the tests!
if __name__ == "__main__":
pytest.main([__file__, "-v"])
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Record HTTP interactions with VCR.py cassettes ๐ผ
- โ Test external APIs without making real calls ๐
- โ Protect sensitive data with filters ๐ก๏ธ
- โ Handle dynamic data in recordings ๐
- โ Speed up your test suite dramatically โก
- โ Test offline with confidence ๐๏ธ
Remember: VCR.py is your testing superhero, saving you time, money, and headaches! ๐ฆธโโ๏ธ
๐ค Next Steps
Congratulations! ๐ Youโve mastered testing external services with VCR.py!
Hereโs what to do next:
- ๐ป Install VCR.py and try the examples above
- ๐๏ธ Add VCR.py to your existing test suite
- ๐ Move on to our next tutorial on mocking with unittest.mock
- ๐ Share your VCR.py success stories with the community!
Remember: Every test you write with VCR.py makes your application more reliable. Keep testing, keep learning, and most importantly, have fun! ๐
Happy testing! ๐๐โจ