Skip to content

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:

Text Only
        /\
       /  \
      /    \     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

PowerShell
# 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:

INI
[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

Text Only
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

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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

PowerShell
# 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

Python
# 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

PowerShell
# 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:

Python
#!/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:

PowerShell
# 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:

YAML
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

  1. AAA Pattern - Arrange, Act, Assert
  2. Single Responsibility - Each test should verify only one functionality
  3. Independence - Tests should not depend on each other
  4. Repeatability - Test results should be deterministic
  5. Fast - Unit tests should execute quickly

Test Naming

Python
# ๅฅฝ็š„ๆต‹่ฏ•ๅ็งฐ
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

Python
# ไฝฟ็”จๆœ‰ๆ„ไน‰็š„ๆต‹่ฏ•ๆ•ฐๆฎ
def test_user_registration():
    user_data = {
        "name": "ๅผ ไธ‰",
        "email": "zhangsan@example.com",
        "age": 25
    }
    # ่€Œไธๆ˜ฏ
    # user_data = {"a": "b", "c": "d"}

Exception Testing

Python
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:

  1. Write high-quality tests - Cover various scenarios and edge cases
  2. Ensure code quality - Discover issues through automated testing
  3. Improve development efficiency - Quickly verify correctness of changes
  4. Maintain code stability - Prevent regression errors

Now start writing tests for your code! Good testing habits will make your code more robust and maintainable. ๐Ÿงชโœจ