Lý thuyết
30 phút
Bài 9/12

Testing với Copilot

Generate unit tests, integration tests, và improve test coverage với AI assistance

Testing với Copilot

1. Tại sao Testing quan trọng?

Benefits of Testing

AspectWithout TestsWith Tests
ConfidenceLowHigh
RefactoringRiskySafe
Bug DetectionLate (production)Early (development)
DocumentationMissingCode as docs
DeploymentScaryConfident

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

CommandDescriptionWhen to Use
/testsGenerate tests cho selectionQuick test generation
/testGenerate test fileNew test file
Ctrl+I > "write tests"Inline test generationSpecific tests

Using /tests

Select một function và dùng /tests:

Python
1# Select this function
2def 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 pytest
2from calculator import calculate_discount
3
4class TestCalculateDiscount:
5 def test_basic_discount(self):
6 assert calculate_discount(100, 10) == 90
7
8 def test_zero_discount(self):
9 assert calculate_discount(100, 0) == 100
10
11 def test_full_discount(self):
12 assert calculate_discount(100, 100) == 0
13
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.py
2import pytest
3from user_service import UserService
4
5class 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 None
13 assert user.created_at is not None
14
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.py
2# Type comment describing what you need:
3
4# UserService class with create_user method
5# - Validates email format
6# - Validates password strength (8+ chars, number, special char)
7# - Creates User object with id, email, created_at
8# - Raises ValidationError for invalid inputs
9
10# Copilot generates implementation...

Step 3: Run Tests & Refactor

Bash
1pytest test_user_service.py -v

4. Unit Test Patterns

4.1 Testing Pure Functions

Prompt:

Text
1Generate comprehensive unit tests for this function including:
2- Normal cases
3- Edge cases
4- Boundary values
5- Error cases
Python
1def parse_date(date_string: str) -> datetime:
2 """Parse date string in format YYYY-MM-DD"""
3 # implementation

4.2 Testing Classes

Prompt:

Text
1Generate tests for this class covering:
2- Initialization
3- All public methods
4- State changes
5- Edge cases
Python
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.fixture
3 def cart(self):
4 return ShoppingCart()
5
6 @pytest.fixture
7 def sample_product(self):
8 return Product(id="123", name="Test", price=10.0)
9
10 # Initialization
11 def test_new_cart_is_empty(self, cart):
12 assert len(cart.items) == 0
13 assert cart.get_total() == 0
14
15 # Add item tests
16 def test_add_single_item(self, cart, sample_product):
17 cart.add_item(sample_product)
18 assert len(cart.items) == 1
19
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 == 3
23
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 == 5
28
29 # Remove item tests
30 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) == 0
34
35 def test_remove_nonexistent_item_raises_error(self, cart):
36 with pytest.raises(ItemNotFoundError):
37 cart.remove_item("nonexistent")
38
39 # Total calculation
40 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() == 35

4.3 Testing Async Functions

Prompt:

Text
1Generate async tests using pytest-asyncio
Python
1import pytest
2
3@pytest.mark.asyncio
4async def test_fetch_user_data():
5 service = UserService()
6 user = await service.get_user(123)
7
8 assert user is not None
9 assert user.id == 123
10
11@pytest.mark.asyncio
12async 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 connection
3- Mock the email service
4- Mock the payment gateway

Example:

Python
1from unittest.mock import Mock, patch, AsyncMock
2
3class 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 # Arrange
8 mock_payment.return_value = {'status': 'success', 'transaction_id': 'tx123'}
9 mock_email.return_value = True
10
11 service = OrderService()
12 order = Order(total=100, customer_email='test@test.com')
13
14 # Act
15 result = service.place_order(order)
16
17 # Assert
18 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 database
3- Include setup and teardown
4- Test CRUD operations
Python
1import pytest
2from sqlalchemy import create_engine
3from sqlalchemy.orm import sessionmaker
4
5class TestUserRepository:
6 @pytest.fixture(scope='class')
7 def db_session(self):
8 # Setup test database
9 engine = create_engine('sqlite:///:memory:')
10 Base.metadata.create_all(engine)
11 Session = sessionmaker(bind=engine)
12 session = Session()
13
14 yield session
15
16 # Teardown
17 session.close()
18
19 @pytest.fixture
20 def repo(self, db_session):
21 return UserRepository(db_session)
22
23 def test_create_and_get_user(self, repo):
24 # Create
25 user = repo.create(email='test@test.com', name='Test')
26
27 # Get
28 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 None

API Integration

Prompt:

Text
1Generate API integration tests using FastAPI TestClient
Python
1from fastapi.testclient import TestClient
2
3class TestUserAPI:
4 @pytest.fixture
5 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 == 201
15 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 == 422
25 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 == 404

7. Test Coverage

Check Coverage

Bash
1# Install coverage
2pip install pytest-cov
3
4# Run with coverage
5pytest --cov=src --cov-report=html

Improve Coverage với Copilot

Prompt:

Text
1@workspace analyze test coverage for src/services/
2Identify untested functions and generate tests for them

Hoặc:

Text
1This function has 60% coverage. Generate tests for uncovered branches:
2- Line 45: else branch
3- Line 52: exception handling
4- Line 67: empty list case

8. Property-Based Testing

Using Hypothesis

Prompt:

Text
1Generate property-based tests using hypothesis for this function
Python
1from hypothesis import given, strategies as st
2
3@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 price
8 assert result <= price
9
10 # Property: Result should never be negative
11 assert result >= 0
12
13 # Property: Higher discount = lower result
14 if discount > 0:
15 higher_discount = min(discount + 10, 100)
16 assert calculate_discount(price, higher_discount) <= result
17
18@given(st.text())
19def test_email_validation_never_crashes(email):
20 # Should never raise unexpected exception
21 try:
22 validate_email(email)
23 except ValidationError:
24 pass # Expected for invalid emails

9. Snapshot Testing

For UI Components

Prompt:

Text
1Generate snapshot tests for this React component
JavaScript
1// Button.test.jsx
2import { render } from '@testing-library/react';
3
4describe('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.py
5│ │ └── order_service.py
6│ └── utils/
7│ └── validators.py
8└── tests/
9 ├── conftest.py # Shared fixtures
10 ├── unit/
11 │ ├── services/
12 │ │ ├── test_user_service.py
13 │ │ └── test_order_service.py
14 │ └── utils/
15 │ └── test_validators.py
16 └── integration/
17 ├── test_api.py
18 └── test_database.py

conftest.py

Prompt:

Text
1Generate a conftest.py with common fixtures for:
2- Database session
3- Test client
4- Sample user data
5- Authentication headers
Python
1# tests/conftest.py
2import pytest
3from app import create_app
4from database import db
5
6@pytest.fixture(scope='session')
7def app():
8 app = create_app(testing=True)
9 with app.app_context():
10 db.create_all()
11 yield app
12 db.drop_all()
13
14@pytest.fixture
15def client(app):
16 return app.test_client()
17
18@pytest.fixture
19def sample_user():
20 return {
21 'email': 'test@example.com',
22 'password': 'Test123!',
23 'name': 'Test User'
24 }
25
26@pytest.fixture
27def auth_headers(client, sample_user):
28 # Create user and login
29 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 = gateway
4 self.logger = logger
5
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_id
22 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') and
29 len(card_info['number']) == 16 and
30 card_info.get('cvv') and
31 card_info.get('expiry')
32 )

Task:

  1. Generate unit tests với mocks
  2. Cover all branches
  3. Test error scenarios
  4. 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!