Skip to content

第15章:测试

15.2 集成测试

LiveServerTestCase

LiveServerTestCase用于运行需要真实HTTP服务器的集成测试:

python
# test_integration.py
from django.test import LiveServerTestCase
from django.contrib.auth.models import User
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
import time

class BlogIntegrationTest(LiveServerTestCase):
    """博客集成测试"""
    
    @classmethod
    def setUpClass(cls):
        """测试类初始化"""
        super().setUpClass()
        
        # 配置Chrome选项
        chrome_options = Options()
        chrome_options.add_argument('--headless')  # 无头模式
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        
        # 启动Chrome浏览器
        cls.browser = webdriver.Chrome(options=chrome_options)
        cls.browser.implicitly_wait(10)
    
    @classmethod
    def tearDownClass(cls):
        """测试类清理"""
        cls.browser.quit()
        super().tearDownClass()
    
    def setUp(self):
        """每个测试前的设置"""
        # 创建测试用户
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        self.user.save()
        
        # 创建测试文章
        from .models import Article, Category
        self.category = Category.objects.create(name='测试分类')
        self.article = Article.objects.create(
            title='测试文章',
            author=self.user,
            category=self.category,
            content='这是测试文章的内容',
            status='published'
        )
        
        super().setUp()
    
    def test_user_can_view_article_list(self):
        """测试用户可以查看文章列表"""
        # 访问文章列表页面
        self.browser.get(f'{self.live_server_url}/')
        
        # 等待页面加载
        WebDriverWait(self.browser, 10).until(
            EC.presence_of_element_located((By.TAG_NAME, "h1"))
        )
        
        # 检查页面标题
        self.assertIn('博客', self.browser.title)
        
        # 检查文章标题是否显示
        page_text = self.browser.find_element(By.TAG_NAME, "body").text
        self.assertIn('测试文章', page_text)
    
    def test_user_can_view_article_detail(self):
        """测试用户可以查看文章详情"""
        # 访问文章详情页面
        self.browser.get(
            f'{self.live_server_url}/articles/{self.article.slug}/'
        )
        
        # 等待页面加载
        WebDriverWait(self.browser, 10).until(
            EC.presence_of_element_located((By.TAG_NAME, "h1"))
        )
        
        # 检查文章标题
        title_element = self.browser.find_element(By.TAG_NAME, "h1")
        self.assertEqual(title_element.text, '测试文章')
        
        # 检查文章内容
        content_element = self.browser.find_element(By.CLASS_NAME, "article-content")
        self.assertIn('这是测试文章的内容', content_element.text)
    
    def test_user_can_login_and_create_article(self):
        """测试用户可以登录并创建文章"""
        # 访问登录页面
        self.browser.get(f'{self.live_server_url}/accounts/login/')
        
        # 填写登录表单
        username_input = self.browser.find_element(By.NAME, "username")
        password_input = self.browser.find_element(By.NAME, "password")
        login_button = self.browser.find_element(By.XPATH, "//button[@type='submit']")
        
        username_input.send_keys('testuser')
        password_input.send_keys('testpass123')
        login_button.click()
        
        # 等待登录成功并重定向
        WebDriverWait(self.browser, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "alert-success"))
        )
        
        # 访问创建文章页面
        self.browser.get(f'{self.live_server_url}/articles/new/')
        
        # 填写文章表单
        title_input = self.browser.find_element(By.NAME, "title")
        content_input = self.browser.find_element(By.NAME, "content")
        submit_button = self.browser.find_element(By.XPATH, "//button[@type='submit']")
        
        title_input.send_keys('新测试文章')
        content_input.send_keys('这是新测试文章的内容')
        submit_button.click()
        
        # 等待文章创建成功
        WebDriverWait(self.browser, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "alert-success"))
        )
        
        # 验证文章已创建
        self.assertIn('文章创建成功', self.browser.page_source)

# 使用Playwright进行更现代的浏览器测试
# test_playwright.py
import pytest
from django.test import LiveServerTestCase
from playwright.sync_api import sync_playwright

class PlaywrightBlogTest(LiveServerTestCase):
    """使用Playwright的博客测试"""
    
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.playwright = sync_playwright().start()
        cls.browser = cls.playwright.chromium.launch(headless=True)
        cls.page = cls.browser.new_page()
    
    @classmethod
    def tearDownClass(cls):
        cls.browser.close()
        cls.playwright.stop()
        super().tearDownClass()
    
    def test_article_search_functionality(self):
        """测试文章搜索功能"""
        # 创建测试数据
        from .models import Article, Category
        from django.contrib.auth.models import User
        
        user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        category = Category.objects.create(name='测试分类')
        Article.objects.create(
            title='Python编程指南',
            author=user,
            category=category,
            content='Python是一门强大的编程语言',
            status='published'
        )
        Article.objects.create(
            title='JavaScript入门',
            author=user,
            category=category,
            content='JavaScript是前端开发的核心语言',
            status='published'
        )
        
        # 访问首页
        self.page.goto(f"{self.live_server_url}/")
        
        # 使用搜索功能
        search_input = self.page.locator("input[name='q']")
        search_input.fill("Python")
        search_input.press("Enter")
        
        # 验证搜索结果
        self.page.wait_for_selector(".article-list")
        search_results = self.page.locator(".article-item")
        assert search_results.count() == 1
        assert "Python编程指南" in self.page.content()

Selenium集成

Selenium集成测试的更多示例:

python
# test_selenium_advanced.py
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options

class AdvancedSeleniumTest(LiveServerTestCase):
    """高级Selenium测试"""
    
    def setUp(self):
        """测试设置"""
        chrome_options = Options()
        chrome_options.add_argument('--headless')
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        
        self.browser = webdriver.Chrome(options=chrome_options)
        self.browser.implicitly_wait(10)
        
        # 创建测试数据
        from django.contrib.auth.models import User
        from .models import Article, Category, Comment
        
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.category = Category.objects.create(name='测试分类')
        self.article = Article.objects.create(
            title='测试文章',
            author=self.user,
            category=self.category,
            content='测试文章内容',
            status='published'
        )
        
        super().setUp()
    
    def tearDown(self):
        """测试清理"""
        self.browser.quit()
        super().tearDown()
    
    def test_comment_system(self):
        """测试评论系统"""
        # 登录
        self.browser.get(f'{self.live_server_url}/accounts/login/')
        self.browser.find_element(By.NAME, "username").send_keys('testuser')
        self.browser.find_element(By.NAME, "password").send_keys('testpass123')
        self.browser.find_element(By.XPATH, "//button[@type='submit']").click()
        
        # 访问文章详情
        self.browser.get(
            f'{self.live_server_url}/articles/{self.article.slug}/'
        )
        
        # 添加评论
        comment_textarea = self.browser.find_element(By.NAME, "content")
        comment_textarea.send_keys('这是一条测试评论')
        
        submit_button = self.browser.find_element(
            By.XPATH, "//button[contains(text(), '发表评论')]"
        )
        submit_button.click()
        
        # 验证评论已添加
        WebDriverWait(self.browser, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "comment-content"))
        )
        
        comment_content = self.browser.find_element(
            By.CLASS_NAME, "comment-content"
        ).text
        self.assertIn('这是一条测试评论', comment_content)
    
    def test_responsive_design(self):
        """测试响应式设计"""
        # 测试桌面版本
        self.browser.set_window_size(1920, 1080)
        self.browser.get(f'{self.live_server_url}/')
        
        # 检查桌面导航
        desktop_nav = self.browser.find_element(By.CLASS_NAME, "navbar-desktop")
        self.assertTrue(desktop_nav.is_displayed())
        
        # 测试移动版本
        self.browser.set_window_size(375, 667)
        self.browser.refresh()
        
        # 检查移动导航
        mobile_nav_toggle = self.browser.find_element(By.CLASS_NAME, "navbar-toggle")
        self.assertTrue(mobile_nav_toggle.is_displayed())
    
    def test_form_validation(self):
        """测试表单验证"""
        # 登录
        self.browser.get(f'{self.live_server_url}/accounts/login/')
        self.browser.find_element(By.NAME, "username").send_keys('testuser')
        self.browser.find_element(By.NAME, "password").send_keys('testpass123')
        self.browser.find_element(By.XPATH, "//button[@type='submit']").click()
        
        # 访问创建文章页面
        self.browser.get(f'{self.live_server_url}/articles/new/')
        
        # 提交空表单
        submit_button = self.browser.find_element(By.XPATH, "//button[@type='submit']")
        submit_button.click()
        
        # 验证错误消息
        error_messages = self.browser.find_elements(By.CLASS_NAME, "error-message")
        self.assertGreater(len(error_messages), 0)
        
        # 填写有效数据并提交
        self.browser.find_element(By.NAME, "title").send_keys('有效标题')
        self.browser.find_element(By.NAME, "content").send_keys('有效内容')
        submit_button.click()
        
        # 验证成功消息
        success_message = self.browser.find_element(By.CLASS_NAME, "alert-success")
        self.assertTrue(success_message.is_displayed())

测试覆盖率

配置和使用测试覆盖率工具:

python
# 安装coverage工具
# pip install coverage

# .coveragerc 配置文件
"""
[run]
source = .
omit = 
    */venv/*
    */env/*
    */migrations/*
    */tests/*
    manage.py
    */settings/*
    */wsgi.py
    */asgi.py

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:

[html]
directory = coverage_html_report
"""

# 运行带覆盖率的测试
# coverage run --source='.' manage.py test
# coverage report
# coverage html

# pytest配置文件 pytest.ini
"""
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = tests.py test_*.py *_tests.py
addopts = --cov=. --cov-report=html --cov-report=term
"""

# 使用pytest进行测试
# test_pytest.py
import pytest
from django.test import Client
from django.contrib.auth.models import User
from .models import Article, Category

@pytest.fixture
def client():
    """测试客户端fixture"""
    return Client()

@pytest.fixture
def user():
    """用户fixture"""
    return User.objects.create_user(
        username='testuser',
        password='testpass123'
    )

@pytest.fixture
def article(user):
    """文章fixture"""
    category = Category.objects.create(name='测试分类')
    return Article.objects.create(
        title='测试文章',
        author=user,
        category=category,
        content='测试内容',
        status='published'
    )

def test_article_list_view(client, article):
    """测试文章列表视图"""
    response = client.get('/')
    assert response.status_code == 200
    assert '测试文章' in str(response.content)

def test_article_detail_view(client, article):
    """测试文章详情视图"""
    response = client.get(f'/articles/{article.slug}/')
    assert response.status_code == 200
    assert '测试文章' in str(response.content)

@pytest.mark.django_db
def test_article_creation():
    """测试文章创建"""
    user = User.objects.create_user(
        username='testuser',
        password='testpass123'
    )
    category = Category.objects.create(name='测试分类')
    
    article = Article.objects.create(
        title='新文章',
        author=user,
        category=category,
        content='新文章内容',
        status='published'
    )
    
    assert article.title == '新文章'
    assert article.is_published

# 性能测试示例
import time
from django.test import TestCase

class PerformanceTest(TestCase):
    """性能测试"""
    
    def setUp(self):
        # 创建大量测试数据
        from django.contrib.auth.models import User
        from .models import Article, Category
        
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.category = Category.objects.create(name='测试分类')
        
        # 创建1000篇文章用于性能测试
        for i in range(1000):
            Article.objects.create(
                title=f'测试文章{i}',
                author=self.user,
                category=self.category,
                content=f'测试内容{i}',
                status='published'
            )
    
    def test_article_list_performance(self):
        """测试文章列表性能"""
        start_time = time.time()
        
        # 执行100次查询
        for _ in range(100):
            response = self.client.get('/articles/')
            self.assertEqual(response.status_code, 200)
        
        end_time = time.time()
        total_time = end_time - start_time
        
        # 断言平均响应时间小于100ms
        average_time = total_time / 100
        self.assertLess(average_time, 0.1)

持续集成

配置持续集成环境:

yaml
# .github/workflows/test.yml
name: Django CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      
      redis:
        image: redis:6
        ports:
          - 6379:6379
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: 3.9
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install coverage
    
    - name: Run migrations
      run: python manage.py migrate
    
    - name: Run tests with coverage
      run: coverage run --source='.' manage.py test
    
    - name: Generate coverage report
      run: |
        coverage report
        coverage xml
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v1
      with:
        file: ./coverage.xml
        flags: unittests
        name: codecov-umbrella

通过这些集成测试实践,可以确保应用程序在真实环境中的功能和性能表现符合预期。

小结

Django集成测试的核心要点:

  1. ✅ 使用LiveServerTestCase进行浏览器集成测试
  2. ✅ 掌握Selenium和Playwright等工具的使用
  3. ✅ 实施测试覆盖率分析确保代码质量
  4. ✅ 配置持续集成环境自动化测试流程
  5. ✅ 关注性能测试和响应时间优化

集成测试能够验证系统各组件间的协作是否正常。

下一篇

我们将学习数据库优化技术。

16.1 数据库优化 →

目录

返回课程目录

Released under the Apache 2.0 License.