Testing Guide¶
This guide introduces the testing strategy, framework usage, and best practices for the EmailWidget project to help ensure code quality and stability.
๐ฏ Testing Strategy¶
Testing Pyramid¶
EmailWidget adopts the classic testing pyramid strategy:
/\
/ \
/ \ E2E Tests (Few)
/______\ - Complete workflow tests
/ \ - Email generation tests
/ \
/____________\ Integration Tests (Some)
/ \ - Component integration tests
/________________\ - Template rendering tests
Unit Tests (Many)
- Individual component tests
- Utility function tests
- Validator tests
Testing Goals¶
- Unit Test Coverage โฅ 90%
- Integration Test Coverage โฅ 80%
- Critical Path Testing 100%
- Performance Regression Testing Continuous monitoring
๐ ๏ธ Testing Framework¶
Main Tools¶
Tool | Purpose | Version Requirement |
---|---|---|
pytest | Testing framework | โฅ 7.0 |
pytest-cov | Coverage statistics | โฅ 4.0 |
pytest-mock | Mock support | โฅ 3.10 |
pytest-xdist | Parallel testing | โฅ 3.0 |
pytest-html | HTML reports | โฅ 3.1 |
Installing Test Dependencies¶
# Install in Windows PowerShell
pip install pytest pytest-cov pytest-mock pytest-xdist pytest-html
# Or install from requirements-test.txt
pip install -r requirements-test.txt
pytest Configuration¶
pytest.ini
configuration in project root:
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--strict-markers
--strict-config
--verbose
--cov=email_widget
--cov-report=html
--cov-report=term-missing
--cov-fail-under=90
markers =
unit: Unit tests
integration: Integration tests
e2e: End-to-end tests
slow: Slow tests
performance: Performance tests
๐ Test Directory Structure¶
tests/
โโโ conftest.py # pytest configuration and fixtures
โโโ test_email.py # Email main class tests
โโโ test_core/ # Core module tests
โ โโโ __init__.py
โ โโโ test_base.py # BaseWidget tests
โ โโโ test_config.py # Configuration tests
โ โโโ test_validators.py # Validator tests
โ โโโ test_template_engine.py # Template engine tests
โ โโโ test_cache.py # Cache tests
โโโ test_widgets/ # Widget component tests
โ โโโ __init__.py
โ โโโ test_text_widget.py # Text component tests
โ โโโ test_table_widget.py # Table component tests
โ โโโ test_progress_widget.py # Progress component tests
โ โโโ test_*.py # Other component tests
โโโ test_utils/ # Utility module tests
โ โโโ __init__.py
โ โโโ test_image_utils.py # Image utility tests
โ โโโ test_optional_deps.py # Optional dependency tests
โโโ integration/ # Integration tests
โ โโโ test_email_generation.py
โ โโโ test_template_rendering.py
โ โโโ test_widget_interaction.py
โโโ e2e/ # End-to-end tests
โ โโโ test_complete_workflows.py
โ โโโ test_email_output.py
โโโ performance/ # Performance tests
โ โโโ test_rendering_speed.py
โ โโโ test_memory_usage.py
โโโ fixtures/ # Test data
โโโ sample_data.json
โโโ test_images/
โโโ expected_outputs/
๐งช Unit Testing¶
Basic Test Structure¶
import pytest
from email_widget.widgets.text_widget import TextWidget
from email_widget.core.enums import TextType, TextAlign
class TestTextWidget:
"""ๆๆฌ็ปไปถๅๅ
ๆต่ฏ"""
def setup_method(self):
"""ๆฏไธชๆต่ฏๆนๆณๅ็ๅๅงๅ"""
self.widget = TextWidget()
def test_initialization(self):
"""ๆต่ฏๅๅงๅ็ถๆ"""
assert self.widget._content == ""
assert self.widget._text_type == TextType.BODY
assert self.widget._align == TextAlign.LEFT
def test_set_content(self):
"""ๆต่ฏ่ฎพ็ฝฎๅ
ๅฎน"""
content = "ๆต่ฏๅ
ๅฎน"
result = self.widget.set_content(content)
# ๆต่ฏ่ฟๅๅผ๏ผ้พๅผ่ฐ็จ๏ผ
assert result is self.widget
# ๆต่ฏ็ถๆๅๅ
assert self.widget._content == content
def test_set_content_validation(self):
"""ๆต่ฏๅ
ๅฎน้ช่ฏ"""
# ๆต่ฏๆๆ่พๅ
ฅ
self.widget.set_content("ๆๆๅ
ๅฎน")
assert self.widget._content == "ๆๆๅ
ๅฎน"
# ๆต่ฏๆ ๆ่พๅ
ฅ
with pytest.raises(TypeError):
self.widget.set_content(123) # ไธๆฏๅญ็ฌฆไธฒ
with pytest.raises(ValueError):
self.widget.set_content("") # ็ฉบๅญ็ฌฆไธฒ
def test_set_type(self):
"""ๆต่ฏ่ฎพ็ฝฎๆๆฌ็ฑปๅ"""
self.widget.set_type(TextType.TITLE_LARGE)
assert self.widget._text_type == TextType.TITLE_LARGE
def test_set_align(self):
"""ๆต่ฏ่ฎพ็ฝฎๅฏน้ฝๆนๅผ"""
self.widget.set_align(TextAlign.CENTER)
assert self.widget._align == TextAlign.CENTER
def test_render_basic(self):
"""ๆต่ฏๅบๆฌๆธฒๆ"""
self.widget.set_content("ๆต่ฏๆๆฌ")
html = self.widget.render()
assert "ๆต่ฏๆๆฌ" in html
assert "<" in html and ">" in html # ๅ
ๅซ HTML ๆ ็ญพ
def test_render_with_styling(self):
"""ๆต่ฏๅธฆๆ ทๅผ็ๆธฒๆ"""
self.widget.set_content("ๆ ้ขๆๆฌ") \
.set_type(TextType.TITLE_LARGE) \
.set_align(TextAlign.CENTER) \
.set_color("#ff0000")
html = self.widget.render()
assert "ๆ ้ขๆๆฌ" in html
assert "text-align: center" in html
assert "color: #ff0000" in html
@pytest.mark.parametrize("text_type,expected_tag", [
(TextType.TITLE_LARGE, "h1"),
(TextType.TITLE_SMALL, "h2"),
(TextType.SECTION_H2, "h2"),
(TextType.SECTION_H3, "h3"),
(TextType.BODY, "p"),
(TextType.CAPTION, "small")
])
def test_render_html_tags(self, text_type, expected_tag):
"""ๆต่ฏไธๅๆๆฌ็ฑปๅ็ HTML ๆ ็ญพ"""
self.widget.set_content("ๆต่ฏ").set_type(text_type)
html = self.widget.render()
assert f"<{expected_tag}" in html
def test_chain_methods(self):
"""ๆต่ฏ้พๅผ่ฐ็จ"""
result = self.widget.set_content("ๆต่ฏ") \
.set_type(TextType.TITLE_LARGE) \
.set_align(TextAlign.CENTER) \
.set_color("#blue")
assert result is self.widget
assert self.widget._content == "ๆต่ฏ"
assert self.widget._text_type == TextType.TITLE_LARGE
assert self.widget._align == TextAlign.CENTER
assert self.widget._color == "#blue"
Using Fixtures¶
Define common fixtures in conftest.py
:
import pytest
import pandas as pd
from pathlib import Path
from email_widget import Email
from email_widget.widgets import TextWidget, TableWidget
@pytest.fixture
def sample_email():
"""ๅๅปบ็คบไพ้ฎไปถๅฏน่ฑก"""
return Email("ๆต่ฏ้ฎไปถ")
@pytest.fixture
def sample_text_widget():
"""ๅๅปบ็คบไพๆๆฌ็ปไปถ"""
widget = TextWidget()
widget.set_content("ๆต่ฏๅ
ๅฎน")
return widget
@pytest.fixture
def sample_dataframe():
"""ๅๅปบ็คบไพ DataFrame"""
return pd.DataFrame({
'Name': ['Alice', 'Bob', 'Charlie'],
'Age': [25, 30, 35],
'City': ['New York', 'London', 'Tokyo']
})
@pytest.fixture
def temp_output_dir(tmp_path):
"""ๅๅปบไธดๆถ่พๅบ็ฎๅฝ"""
output_dir = tmp_path / "output"
output_dir.mkdir()
return output_dir
@pytest.fixture
def mock_image_path():
"""ๆจกๆๅพ็่ทฏๅพ"""
return "tests/fixtures/test_images/sample.png"
# ไฝฟ็จ fixture ็ๆต่ฏ
class TestEmailGeneration:
def test_add_widget(self, sample_email, sample_text_widget):
"""ๆต่ฏๆทปๅ ็ปไปถ"""
sample_email.add_widget(sample_text_widget)
assert len(sample_email._widgets) == 1
assert sample_email._widgets[0] is sample_text_widget
def test_export_html(self, sample_email, sample_text_widget, temp_output_dir):
"""ๆต่ฏๅฏผๅบ HTML"""
sample_email.add_widget(sample_text_widget)
output_path = temp_output_dir / "test.html"
sample_email.export_html(str(output_path))
assert output_path.exists()
content = output_path.read_text(encoding='utf-8')
assert "ๆต่ฏๅ
ๅฎน" in content
Mock and Stub¶
Using pytest-mock
for mock testing:
import pytest
from unittest.mock import Mock, patch
from email_widget.utils.image_utils import ImageUtils
class TestImageUtils:
"""ๅพ็ๅทฅๅ
ทๆต่ฏ"""
@patch('requests.get')
def test_download_image_success(self, mock_get):
"""ๆต่ฏๆๅไธ่ฝฝๅพ็"""
# ่ฎพ็ฝฎ mock ่ฟๅๅผ
mock_response = Mock()
mock_response.status_code = 200
mock_response.content = b'fake_image_data'
mock_get.return_value = mock_response
# ๆง่กๆต่ฏ
result = ImageUtils.download_image("http://example.com/image.jpg")
# ้ช่ฏ็ปๆ
assert result == b'fake_image_data'
mock_get.assert_called_once_with("http://example.com/image.jpg")
@patch('requests.get')
def test_download_image_failure(self, mock_get):
"""ๆต่ฏไธ่ฝฝๅพ็ๅคฑ่ดฅ"""
# ่ฎพ็ฝฎ mock ๆๅบๅผๅธธ
mock_get.side_effect = ConnectionError("็ฝ็ป้่ฏฏ")
# ้ช่ฏๅผๅธธ
with pytest.raises(ConnectionError):
ImageUtils.download_image("http://example.com/image.jpg")
def test_validate_image_format(self, mocker):
"""ๆต่ฏๅพ็ๆ ผๅผ้ช่ฏ"""
# ไฝฟ็จ mocker fixture
mock_is_valid = mocker.patch.object(ImageUtils, '_is_valid_format')
mock_is_valid.return_value = True
result = ImageUtils.validate_format("image.jpg")
assert result is True
mock_is_valid.assert_called_once_with("image.jpg")
Parametrized Testing¶
Using @pytest.mark.parametrize
for parametrized testing:
import pytest
from email_widget.core.validators import ColorValidator
class TestColorValidator:
"""้ข่ฒ้ช่ฏๅจๆต่ฏ"""
@pytest.mark.parametrize("color,expected", [
("#ff0000", True), # ๆ ๅๅๅ
ญ่ฟๅถ
("#FF0000", True), # ๅคงๅๅๅ
ญ่ฟๅถ
("#f00", True), # ็ญๅๅ
ญ่ฟๅถ
("red", True), # ้ข่ฒๅ็งฐ
("rgb(255,0,0)", True), # RGB ๆ ผๅผ
("rgba(255,0,0,0.5)", True), # RGBA ๆ ผๅผ
("invalid", False), # ๆ ๆ้ข่ฒ
("", False), # ็ฉบๅญ็ฌฆไธฒ
("#gggggg", False), # ๆ ๆๅๅ
ญ่ฟๅถ
])
def test_color_validation(self, color, expected):
"""ๆต่ฏๅ็ง้ข่ฒๆ ผๅผ็้ช่ฏ"""
validator = ColorValidator()
if expected:
# ๅบ่ฏฅ้่ฟ้ช่ฏ
validator.validate(color) # ไธๅบ่ฏฅๆๅบๅผๅธธ
else:
# ๅบ่ฏฅ้ช่ฏๅคฑ่ดฅ
with pytest.raises(ValueError):
validator.validate(color)
@pytest.mark.parametrize("rgb_value", [0, 128, 255])
def test_rgb_values(self, rgb_value):
"""ๆต่ฏ RGB ๅผ่ๅด"""
color = f"rgb({rgb_value},{rgb_value},{rgb_value})"
validator = ColorValidator()
validator.validate(color) # ๅบ่ฏฅ้่ฟ้ช่ฏ
๐ Integration Testing¶
Integration tests verify multiple components working together:
import pytest
import pandas as pd
from email_widget import Email
from email_widget.widgets import TextWidget, TableWidget, ProgressWidget
class TestWidgetIntegration:
"""็ปไปถ้ๆๆต่ฏ"""
def test_email_with_multiple_widgets(self):
"""ๆต่ฏ้ฎไปถๅ
ๅซๅคไธช็ปไปถ"""
email = Email("้ๆๆต่ฏ้ฎไปถ")
# ๆทปๅ ๆ ้ข
title = TextWidget()
title.set_content("ๆต่ฏๆฅๅ").set_type(TextType.TITLE_LARGE)
email.add_widget(title)
# ๆทปๅ ่กจๆ ผ
table = TableWidget()
table.set_headers(["ๅงๅ", "ๅนด้พ"])
table.add_row(["ๅผ ไธ", "25"])
table.add_row(["ๆๅ", "30"])
email.add_widget(table)
# ๆทปๅ ่ฟๅบฆๆก
progress = ProgressWidget()
progress.set_value(75).set_label("ๅฎๆๅบฆ")
email.add_widget(progress)
# ๆธฒๆ้ฎไปถ
html = email.export_str()
# ้ช่ฏๆๆ็ปไปถ้ฝๅจ่พๅบไธญ
assert "ๆต่ฏๆฅๅ" in html
assert "ๅผ ไธ" in html
assert "ๆๅ" in html
assert "75%" in html or "75.0%" in html
def test_dataframe_to_table_integration(self):
"""ๆต่ฏ DataFrame ไธ่กจๆ ผ็ปไปถ้ๆ"""
# ๅๅปบๆต่ฏๆฐๆฎ
df = pd.DataFrame({
'ไบงๅ': ['A', 'B', 'C'],
'้้': [100, 200, 150],
'ไปทๆ ผ': [10.5, 20.0, 15.8]
})
email = Email("ๆฐๆฎๆฅๅ")
# ไฝฟ็จไพฟๆทๆนๆณไป DataFrame ๅๅปบ่กจๆ ผ
email.add_table_from_df(df, title="ไบงๅ้ๅฎๆฐๆฎ")
html = email.export_str()
# ้ช่ฏๆฐๆฎๆญฃ็กฎๆธฒๆ
assert "ไบงๅ้ๅฎๆฐๆฎ" in html
assert "ไบงๅ" in html and "้้" in html and "ไปทๆ ผ" in html
assert "100" in html and "200" in html and "150" in html
@pytest.mark.integration
def test_template_engine_integration(self):
"""ๆต่ฏๆจกๆฟๅผๆ้ๆ"""
email = Email("ๆจกๆฟๆต่ฏ")
# ไฝฟ็จ่ชๅฎไนๆจกๆฟ
custom_widget = CustomTemplateWidget()
custom_widget.set_template("Hello {{name}}!")
custom_widget.set_data(name="World")
email.add_widget(custom_widget)
html = email.export_str()
assert "Hello World!" in html
๐ End-to-End Testing¶
End-to-end tests verify complete user workflows:
import pytest
from pathlib import Path
import tempfile
from email_widget import Email
class TestE2EWorkflows:
"""็ซฏๅฐ็ซฏๆต่ฏ"""
@pytest.mark.e2e
def test_complete_report_generation(self):
"""ๆต่ฏๅฎๆดๆฅๅ็ๆๆต็จ"""
# 1. ๅๅปบ้ฎไปถ
email = Email("ๆๅบฆไธๅกๆฅๅ")
# 2. ๆทปๅ ๆ ้ขๅ่ฏดๆ
email.add_title("2024ๅนด1ๆไธๅกๆฅๅ", TextType.TITLE_LARGE)
email.add_text("ๆฌๆฅๅๅ
ๅซไธป่ฆไธๅกๆๆ ๅๅๆใ")
# 3. ๆทปๅ ๅ
ณ้ฎๆๆ
email.add_card("ๆปๆถๅ
ฅ", "ยฅ1,250,000", "๐ฐ")
email.add_card("ๆฐ็จๆท", "2,847", "๐ฅ")
# 4. ๆทปๅ ่ฏฆ็ปๆฐๆฎ่กจๆ ผ
data = [
["ไบงๅA", "ยฅ500,000", "1,200"],
["ไบงๅB", "ยฅ750,000", "1,647"]
]
email.add_table_from_data(data, ["ไบงๅ", "ๆถๅ
ฅ", "้้"])
# 5. ๆทปๅ ่ฟๅบฆๆๆ
email.add_progress(85, "็ฎๆ ๅฎๆๅบฆ", ProgressTheme.SUCCESS)
# 6. ๆทปๅ ๆ้
email.add_alert("ไธๆ้่ฆ้็นๅ
ณๆณจไบงๅA็ๅบๅญๆ
ๅต", AlertType.WARNING)
# 7. ๅฏผๅบไธบ HTML
with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as f:
email.export_html(f.name)
# 8. ้ช่ฏๆไปถ็ๆ
output_path = Path(f.name)
assert output_path.exists()
# 9. ้ช่ฏๅ
ๅฎนๅฎๆดๆง
content = output_path.read_text(encoding='utf-8')
assert "ๆๅบฆไธๅกๆฅๅ" in content
assert "ยฅ1,250,000" in content
assert "ไบงๅA" in content
assert "85%" in content or "85.0%" in content
assert "ๅบๅญๆ
ๅต" in content
# 10. ้ช่ฏ HTML ็ปๆ
assert "<html" in content
assert "</html>" in content
assert "<head>" in content
assert "<body>" in content
@pytest.mark.e2e
@pytest.mark.slow
def test_large_dataset_performance(self):
"""ๆต่ฏๅคงๆฐๆฎ้ๆง่ฝ"""
import time
# ๅๅปบๅคง้ๆฐๆฎ
email = Email("ๅคงๆฐๆฎๆต่ฏ")
# ๆทปๅ ๅคง่กจๆ ผ
large_data = []
for i in range(1000):
large_data.append([f"้กน็ฎ{i}", f"ๅผ{i}", f"ๆ่ฟฐ{i}"])
start_time = time.time()
email.add_table_from_data(large_data, ["้กน็ฎ", "ๅผ", "ๆ่ฟฐ"])
# ๆธฒๆๆถ้ดๅบ่ฏฅๅจๅ็่ๅดๅ
html = email.export_str()
end_time = time.time()
# ๆง่ฝๆญ่จ๏ผๆ นๆฎๅฎ้
ๆ
ๅต่ฐๆด๏ผ
assert (end_time - start_time) < 10.0 # ๅบ่ฏฅๅจ10็งๅ
ๅฎๆ
assert len(html) > 10000 # ็กฎไฟๅ
ๅฎนๅทฒ็ๆ
assert "้กน็ฎ999" in html # ็กฎไฟๆๆๆฐๆฎ้ฝๅ
ๅซ
โก Performance Testing¶
Monitor key performance metrics:
import pytest
import time
import psutil
import os
from email_widget import Email
class TestPerformance:
"""ๆง่ฝๆต่ฏ"""
@pytest.mark.performance
def test_rendering_speed(self):
"""ๆต่ฏๆธฒๆ้ๅบฆ"""
email = Email("ๆง่ฝๆต่ฏ")
# ๆทปๅ ๅคไธช็ปไปถ
for i in range(100):
email.add_text(f"ๆๆฌๅ
ๅฎน {i}")
# ๆต้ๆธฒๆๆถ้ด
start_time = time.perf_counter()
html = email.export_str()
end_time = time.perf_counter()
render_time = end_time - start_time
# ๆญ่จๆธฒๆๆถ้ด
assert render_time < 1.0, f"ๆธฒๆๆถ้ด่ฟ้ฟ: {render_time:.3f}็ง"
assert len(html) > 1000, "่พๅบๅ
ๅฎนๅคชๅฐ"
@pytest.mark.performance
def test_memory_usage(self):
"""ๆต่ฏๅ
ๅญไฝฟ็จ"""
process = psutil.Process(os.getpid())
initial_memory = process.memory_info().rss
# ๅๅปบๅคง้ๅฏน่ฑก
emails = []
for i in range(50):
email = Email(f"ๆต่ฏ้ฎไปถ {i}")
for j in range(20):
email.add_text(f"ๅ
ๅฎน {i}-{j}")
emails.append(email)
# ๆฃๆฅๅ
ๅญๅข้ฟ
peak_memory = process.memory_info().rss
memory_increase = peak_memory - initial_memory
# ๆญ่จๅ
ๅญไฝฟ็จๅ็๏ผๆ นๆฎๅฎ้
ๆ
ๅต่ฐๆด๏ผ
assert memory_increase < 100 * 1024 * 1024, f"ๅ
ๅญไฝฟ็จ่ฟๅค: {memory_increase / 1024 / 1024:.1f}MB"
@pytest.mark.performance
def test_cache_effectiveness(self):
"""ๆต่ฏ็ผๅญๆๆ"""
from email_widget.core.cache import Cache
cache = Cache(max_size=100)
# ็ฌฌไธๆฌก่ฎฟ้ฎ๏ผๆช็ผๅญ๏ผ
start_time = time.perf_counter()
result1 = cache.get_or_set("test_key", lambda: expensive_operation())
first_time = time.perf_counter() - start_time
# ็ฌฌไบๆฌก่ฎฟ้ฎ๏ผๅทฒ็ผๅญ๏ผ
start_time = time.perf_counter()
result2 = cache.get("test_key")
second_time = time.perf_counter() - start_time
# ็ผๅญๅบ่ฏฅๆพ่ๆๅๆง่ฝ
assert result1 == result2
assert second_time < first_time / 10, "็ผๅญๆฒกๆๆพ่ๆๅๆง่ฝ"
def expensive_operation():
"""ๆจกๆ่ๆถๆไฝ"""
time.sleep(0.1)
return "expensive_result"
๐ Test Coverage¶
Generate Coverage Reports¶
# Run tests and generate coverage report
python -m pytest --cov=email_widget --cov-report=html --cov-report=term
# View HTML report
start htmlcov/index.html
# View only missing coverage lines
python -m pytest --cov=email_widget --cov-report=term-missing
Coverage Goals¶
# pytest.ini ไธญ่ฎพ็ฝฎ่ฆ็็่ฆๆฑ
[tool:pytest]
addopts = --cov-fail-under=90
# ๆ้คๆไบๆไปถ
--cov-config=.coveragerc
# .coveragerc ๆไปถๅ
ๅฎน
[run]
source = email_widget
omit =
*/tests/*
*/venv/*
setup.py
*/migrations/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
๐ง Testing Tools and Commands¶
Common Test Commands¶
# Basic test run
python -m pytest
# Verbose output
python -m pytest -v
# Run specific test file
python -m pytest tests/test_email.py
# Run specific test method
python -m pytest tests/test_email.py::TestEmail::test_add_widget
# Run marked tests
python -m pytest -m unit
python -m pytest -m "not slow"
# Run tests in parallel
python -m pytest -n auto
# Generate HTML report
python -m pytest --html=report.html --self-contained-html
# Run only failed tests
python -m pytest --lf
# Stop at first failure
python -m pytest -x
# Detailed failure information
python -m pytest -vv --tb=long
Test Scripts¶
Create scripts/run_tests.py
script:
#!/usr/bin/env python
"""
ๆต่ฏ่ฟ่ก่ๆฌ
"""
import subprocess
import sys
import argparse
from pathlib import Path
def run_command(cmd, description):
"""่ฟ่กๅฝไปคๅนถๆฃๆฅ็ปๆ"""
print(f"\n๐ {description}...")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
print(f"โ
{description} ๆๅ")
if result.stdout:
print(result.stdout)
else:
print(f"โ {description} ๅคฑ่ดฅ")
print(result.stderr)
return False
return True
def main():
parser = argparse.ArgumentParser(description="่ฟ่ก EmailWidget ๆต่ฏ")
parser.add_argument("--unit", action="store_true", help="ๅช่ฟ่กๅๅ
ๆต่ฏ")
parser.add_argument("--integration", action="store_true", help="ๅช่ฟ่ก้ๆๆต่ฏ")
parser.add_argument("--e2e", action="store_true", help="ๅช่ฟ่ก็ซฏๅฐ็ซฏๆต่ฏ")
parser.add_argument("--performance", action="store_true", help="ๅช่ฟ่กๆง่ฝๆต่ฏ")
parser.add_argument("--coverage", action="store_true", help="็ๆ่ฆ็็ๆฅๅ")
parser.add_argument("--html", action="store_true", help="็ๆ HTML ๆฅๅ")
args = parser.parse_args()
# ๅบๆฌๆต่ฏๅฝไปค
pytest_cmd = "python -m pytest"
if args.unit:
pytest_cmd += " -m unit"
elif args.integration:
pytest_cmd += " -m integration"
elif args.e2e:
pytest_cmd += " -m e2e"
elif args.performance:
pytest_cmd += " -m performance"
if args.coverage:
pytest_cmd += " --cov=email_widget --cov-report=term-missing"
if args.html:
pytest_cmd += " --cov-report=html"
if args.html:
pytest_cmd += " --html=reports/test_report.html --self-contained-html"
# ็กฎไฟๆฅๅ็ฎๅฝๅญๅจ
Path("reports").mkdir(exist_ok=True)
# ่ฟ่กๆต่ฏ
success = run_command(pytest_cmd, "่ฟ่กๆต่ฏ")
if success:
print("\n๐ ๆๆๆต่ฏ้่ฟ!")
else:
print("\n๐ฅ ๆต่ฏๅคฑ่ดฅ!")
sys.exit(1)
if __name__ == "__main__":
main()
Using the script:
# Run all tests
python scripts/run_tests.py
# Run only unit tests
python scripts/run_tests.py --unit
# Run tests and generate coverage report
python scripts/run_tests.py --coverage --html
๐ Continuous Integration¶
GitHub Actions Configuration¶
.github/workflows/test.yml
:
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: windows-latest
strategy:
matrix:
python-version: [3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-test.txt
pip install -e .
- name: Run tests
run: |
python -m pytest --cov=email_widget --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
๐ Testing Best Practices¶
Testing Principles¶
- AAA Pattern - Arrange, Act, Assert
- Single Responsibility - Each test should verify only one functionality
- Independence - Tests should not depend on each other
- Repeatability - Test results should be deterministic
- Fast - Unit tests should execute quickly
Test Naming¶
# ๅฅฝ็ๆต่ฏๅ็งฐ
def test_set_title_with_valid_string_updates_title():
pass
def test_render_returns_html_with_title_content():
pass
def test_add_widget_with_none_raises_type_error():
pass
# ้ฟๅ
็ๆต่ฏๅ็งฐ
def test_title(): # ๅคชๆจก็ณ
pass
def test_1(): # ๆฒกๆๆไน
pass
Test Data¶
# ไฝฟ็จๆๆไน็ๆต่ฏๆฐๆฎ
def test_user_registration():
user_data = {
"name": "ๅผ ไธ",
"email": "zhangsan@example.com",
"age": 25
}
# ่ไธๆฏ
# user_data = {"a": "b", "c": "d"}
Exception Testing¶
def test_invalid_input_handling():
"""ๆต่ฏๆ ๆ่พๅ
ฅ็ๅค็"""
widget = TextWidget()
# ๆต่ฏๅ
ทไฝ็ๅผๅธธ็ฑปๅๅๆถๆฏ
with pytest.raises(ValueError, match="ๅ
ๅฎนไธ่ฝไธบ็ฉบ"):
widget.set_content("")
with pytest.raises(TypeError, match="ๅ
ๅฎนๅฟ
้กปๆฏๅญ็ฌฆไธฒ"):
widget.set_content(123)
๐ Summary¶
Following this testing guide, you will be able to:
- Write high-quality tests - Cover various scenarios and edge cases
- Ensure code quality - Discover issues through automated testing
- Improve development efficiency - Quickly verify correctness of changes
- Maintain code stability - Prevent regression errors
Now start writing tests for your code! Good testing habits will make your code more robust and maintainable. ๐งชโจ