Testing với Copilot
1. Tại sao Testing quan trọng?
Benefits of Testing
| Aspect | Without Tests | With Tests |
|---|---|---|
| Confidence | Low | High |
| Refactoring | Risky | Safe |
| Bug Detection | Late (production) | Early (development) |
| Documentation | Missing | Code as docs |
| Deployment | Scary | Confident |
Testing Pyramid
Testing Pyramid
Test Types
Unit Tests (70%)
Fast execution
Isolated
Many tests
Integration Tests (20%)
Multiple units
Slower
E2E Tests (10%)
Full system
Slowest
2. Copilot Testing Commands
Quick Commands
| Command | Description | When to Use |
|---|---|---|
/tests | Generate tests cho selection | Quick test generation |
/test | Generate test file | New test file |
Ctrl+I > "write tests" | Inline test generation | Specific tests |
Using /tests
Select một function và dùng /tests:
Python
1# Select this function2def calculate_discount(price, discount_percent):3 if price < 0:4 raise ValueError("Price cannot be negative")5 if not 0 <= discount_percent <= 100:6 raise ValueError("Discount must be between 0 and 100")7 return price * (1 - discount_percent / 100)Copilot generates:
Python
1import pytest2from calculator import calculate_discount34class TestCalculateDiscount:5 def test_basic_discount(self):6 assert calculate_discount(100, 10) == 907 8 def test_zero_discount(self):9 assert calculate_discount(100, 0) == 10010 11 def test_full_discount(self):12 assert calculate_discount(100, 100) == 013 14 def test_negative_price_raises_error(self):15 with pytest.raises(ValueError, match="Price cannot be negative"):16 calculate_discount(-10, 10)17 18 def test_invalid_discount_too_high(self):19 with pytest.raises(ValueError, match="Discount must be between"):20 calculate_discount(100, 150)21 22 def test_invalid_discount_negative(self):23 with pytest.raises(ValueError, match="Discount must be between"):24 calculate_discount(100, -10)25 26 def test_decimal_values(self):27 assert calculate_discount(99.99, 15) == pytest.approx(84.99, 0.01)3. Test-Driven Development (TDD) với Copilot
TDD Workflow
TDD Cycle
TDD với Copilot
Step 1: Write Test First (Describe what you want)
Python
1# test_user_service.py2import pytest3from user_service import UserService45class TestUserService:6 def test_create_user_with_valid_data(self):7 """User should be created with valid email and password"""8 service = UserService()9 user = service.create_user("test@example.com", "StrongPass123!")10 11 assert user.email == "test@example.com"12 assert user.id is not None13 assert user.created_at is not None14 15 def test_create_user_invalid_email_raises_error(self):16 """Should raise ValidationError for invalid email"""17 service = UserService()18 19 with pytest.raises(ValidationError, match="Invalid email"):20 service.create_user("invalid-email", "StrongPass123!")21 22 def test_create_user_weak_password_raises_error(self):23 """Password must be at least 8 chars with number and special char"""24 service = UserService()25 26 with pytest.raises(ValidationError, match="Password too weak"):27 service.create_user("test@example.com", "weak")Step 2: Use Copilot to Implement
Python
1# user_service.py2# Type comment describing what you need:34# UserService class with create_user method5# - Validates email format6# - Validates password strength (8+ chars, number, special char)7# - Creates User object with id, email, created_at8# - Raises ValidationError for invalid inputs910# Copilot generates implementation...Step 3: Run Tests & Refactor
Bash
1pytest test_user_service.py -v4. Unit Test Patterns
4.1 Testing Pure Functions
Prompt:
Text
1Generate comprehensive unit tests for this function including:2- Normal cases3- Edge cases4- Boundary values5- Error casesPython
1def parse_date(date_string: str) -> datetime:2 """Parse date string in format YYYY-MM-DD"""3 # implementation4.2 Testing Classes
Prompt:
Text
1Generate tests for this class covering:2- Initialization3- All public methods4- State changes5- Edge casesPython
1class ShoppingCart:2 def __init__(self):3 self.items = []4 5 def add_item(self, product, quantity=1):6 # ...7 8 def remove_item(self, product_id):9 # ...10 11 def get_total(self):12 # ...Generated Tests:
Python
1class TestShoppingCart:2 @pytest.fixture3 def cart(self):4 return ShoppingCart()5 6 @pytest.fixture7 def sample_product(self):8 return Product(id="123", name="Test", price=10.0)9 10 # Initialization11 def test_new_cart_is_empty(self, cart):12 assert len(cart.items) == 013 assert cart.get_total() == 014 15 # Add item tests16 def test_add_single_item(self, cart, sample_product):17 cart.add_item(sample_product)18 assert len(cart.items) == 119 20 def test_add_item_with_quantity(self, cart, sample_product):21 cart.add_item(sample_product, quantity=3)22 assert cart.items[0].quantity == 323 24 def test_add_same_item_increases_quantity(self, cart, sample_product):25 cart.add_item(sample_product, 2)26 cart.add_item(sample_product, 3)27 assert cart.items[0].quantity == 528 29 # Remove item tests30 def test_remove_existing_item(self, cart, sample_product):31 cart.add_item(sample_product)32 cart.remove_item(sample_product.id)33 assert len(cart.items) == 034 35 def test_remove_nonexistent_item_raises_error(self, cart):36 with pytest.raises(ItemNotFoundError):37 cart.remove_item("nonexistent")38 39 # Total calculation40 def test_total_with_multiple_items(self, cart):41 cart.add_item(Product(id="1", name="A", price=10), 2)42 cart.add_item(Product(id="2", name="B", price=15), 1)43 assert cart.get_total() == 354.3 Testing Async Functions
Prompt:
Text
1Generate async tests using pytest-asyncioPython
1import pytest23@pytest.mark.asyncio4async def test_fetch_user_data():5 service = UserService()6 user = await service.get_user(123)7 8 assert user is not None9 assert user.id == 1231011@pytest.mark.asyncio12async def test_fetch_nonexistent_user():13 service = UserService()14 15 with pytest.raises(UserNotFoundError):16 await service.get_user(99999)5. Mocking với Copilot
When to Mock
- External APIs
- Databases
- File systems
- Time-dependent code
- Random values
Mock Generation
Prompt:
Text
1Generate tests with mocks for external dependencies:2- Mock the database connection3- Mock the email service4- Mock the payment gatewayExample:
Python
1from unittest.mock import Mock, patch, AsyncMock23class TestOrderService:4 @patch('services.email_service.send_email')5 @patch('services.payment_gateway.process_payment')6 def test_place_order_success(self, mock_payment, mock_email):7 # Arrange8 mock_payment.return_value = {'status': 'success', 'transaction_id': 'tx123'}9 mock_email.return_value = True10 11 service = OrderService()12 order = Order(total=100, customer_email='test@test.com')13 14 # Act15 result = service.place_order(order)16 17 # Assert18 assert result.status == 'confirmed'19 mock_payment.assert_called_once_with(amount=100)20 mock_email.assert_called_once()21 22 @patch('services.payment_gateway.process_payment')23 def test_place_order_payment_failure(self, mock_payment):24 mock_payment.side_effect = PaymentError("Card declined")25 26 service = OrderService()27 order = Order(total=100, customer_email='test@test.com')28 29 with pytest.raises(OrderError, match="Payment failed"):30 service.place_order(order)6. Integration Tests
Database Integration
Prompt:
Text
1Generate integration tests for the user repository:2- Use a test database3- Include setup and teardown4- Test CRUD operationsPython
1import pytest2from sqlalchemy import create_engine3from sqlalchemy.orm import sessionmaker45class TestUserRepository:6 @pytest.fixture(scope='class')7 def db_session(self):8 # Setup test database9 engine = create_engine('sqlite:///:memory:')10 Base.metadata.create_all(engine)11 Session = sessionmaker(bind=engine)12 session = Session()13 14 yield session15 16 # Teardown17 session.close()18 19 @pytest.fixture20 def repo(self, db_session):21 return UserRepository(db_session)22 23 def test_create_and_get_user(self, repo):24 # Create25 user = repo.create(email='test@test.com', name='Test')26 27 # Get28 fetched = repo.get_by_id(user.id)29 30 assert fetched.email == 'test@test.com'31 assert fetched.name == 'Test'32 33 def test_update_user(self, repo):34 user = repo.create(email='old@test.com', name='Old')35 36 repo.update(user.id, name='New')37 38 updated = repo.get_by_id(user.id)39 assert updated.name == 'New'40 41 def test_delete_user(self, repo):42 user = repo.create(email='delete@test.com', name='Delete')43 44 repo.delete(user.id)45 46 assert repo.get_by_id(user.id) is NoneAPI Integration
Prompt:
Text
1Generate API integration tests using FastAPI TestClientPython
1from fastapi.testclient import TestClient23class TestUserAPI:4 @pytest.fixture5 def client(self):6 return TestClient(app)7 8 def test_create_user_endpoint(self, client):9 response = client.post(10 "/users",11 json={"email": "test@test.com", "password": "Test123!"}12 )13 14 assert response.status_code == 20115 assert response.json()["email"] == "test@test.com"16 assert "id" in response.json()17 18 def test_create_user_invalid_email(self, client):19 response = client.post(20 "/users",21 json={"email": "invalid", "password": "Test123!"}22 )23 24 assert response.status_code == 42225 assert "email" in response.json()["detail"][0]["loc"]26 27 def test_get_user_not_found(self, client):28 response = client.get("/users/99999")29 30 assert response.status_code == 4047. Test Coverage
Check Coverage
Bash
1# Install coverage2pip install pytest-cov3 4# Run with coverage5pytest --cov=src --cov-report=htmlImprove Coverage với Copilot
Prompt:
Text
1@workspace analyze test coverage for src/services/2Identify untested functions and generate tests for themHoặc:
Text
1This function has 60% coverage. Generate tests for uncovered branches:2- Line 45: else branch3- Line 52: exception handling4- Line 67: empty list case8. Property-Based Testing
Using Hypothesis
Prompt:
Text
1Generate property-based tests using hypothesis for this functionPython
1from hypothesis import given, strategies as st23@given(st.integers(min_value=0), st.integers(min_value=0, max_value=100))4def test_discount_properties(price, discount):5 result = calculate_discount(price, discount)6 7 # Property: Result should never exceed original price8 assert result <= price9 10 # Property: Result should never be negative11 assert result >= 012 13 # Property: Higher discount = lower result14 if discount > 0:15 higher_discount = min(discount + 10, 100)16 assert calculate_discount(price, higher_discount) <= result1718@given(st.text())19def test_email_validation_never_crashes(email):20 # Should never raise unexpected exception21 try:22 validate_email(email)23 except ValidationError:24 pass # Expected for invalid emails9. Snapshot Testing
For UI Components
Prompt:
Text
1Generate snapshot tests for this React componentJavaScript
1// Button.test.jsx2import { render } from '@testing-library/react';34describe('Button', () => {5 it('renders primary button correctly', () => {6 const { container } = render(7 <Button variant="primary">Click me</Button>8 );9 expect(container).toMatchSnapshot();10 });11 12 it('renders disabled state correctly', () => {13 const { container } = render(14 <Button disabled>Disabled</Button>15 );16 expect(container).toMatchSnapshot();17 });18 19 it('renders with icon correctly', () => {20 const { container } = render(21 <Button icon={<SearchIcon />}>Search</Button>22 );23 expect(container).toMatchSnapshot();24 });25});10. Test Organization
File Structure
Text
1project/2├── src/3│ ├── services/4│ │ ├── user_service.py5│ │ └── order_service.py6│ └── utils/7│ └── validators.py8└── tests/9 ├── conftest.py # Shared fixtures10 ├── unit/11 │ ├── services/12 │ │ ├── test_user_service.py13 │ │ └── test_order_service.py14 │ └── utils/15 │ └── test_validators.py16 └── integration/17 ├── test_api.py18 └── test_database.pyconftest.py
Prompt:
Text
1Generate a conftest.py with common fixtures for:2- Database session3- Test client4- Sample user data5- Authentication headersPython
1# tests/conftest.py2import pytest3from app import create_app4from database import db56@pytest.fixture(scope='session')7def app():8 app = create_app(testing=True)9 with app.app_context():10 db.create_all()11 yield app12 db.drop_all()1314@pytest.fixture15def client(app):16 return app.test_client()1718@pytest.fixture19def sample_user():20 return {21 'email': 'test@example.com',22 'password': 'Test123!',23 'name': 'Test User'24 }2526@pytest.fixture27def auth_headers(client, sample_user):28 # Create user and login29 client.post('/users', json=sample_user)30 response = client.post('/login', json={31 'email': sample_user['email'],32 'password': sample_user['password']33 })34 token = response.json()['access_token']35 return {'Authorization': f'Bearer {token}'}11. Hands-on Exercise
Challenge: Test a Complete Feature
Viết tests cho PaymentProcessor class:
Python
1class PaymentProcessor:2 def __init__(self, gateway, logger):3 self.gateway = gateway4 self.logger = logger5 6 def process_payment(self, amount, card_info):7 """8 Process payment through gateway.9 Returns transaction_id on success.10 Raises PaymentError on failure.11 """12 if amount <= 0:13 raise ValueError("Amount must be positive")14 15 if not self._validate_card(card_info):16 raise InvalidCardError("Invalid card information")17 18 try:19 result = self.gateway.charge(amount, card_info)20 self.logger.info(f"Payment processed: {result.transaction_id}")21 return result.transaction_id22 except GatewayError as e:23 self.logger.error(f"Payment failed: {e}")24 raise PaymentError(f"Payment failed: {e}")25 26 def _validate_card(self, card_info):27 return (28 card_info.get('number') and29 len(card_info['number']) == 16 and30 card_info.get('cvv') and31 card_info.get('expiry')32 )Task:
- Generate unit tests với mocks
- Cover all branches
- Test error scenarios
- Achieve 100% coverage
Tiếp theo
Bài tiếp theo: Build Project: Todo App (Part 1) - xây dựng ứng dụng thực tế từ đầu đến cuối với Copilot!
