Django测试框架实战:从单元测试到CI/CD的完整工程实践

Django测试框架实战:从单元测试到CI/CD的完整工程实践
1. 项目概述当Django项目开始“摇摇欲坠”如果你用Django做过几个项目尤其是那些从“玩具”逐渐演变成“产品”的项目大概率经历过这样的场景某个风和日丽的下午你信心满满地推送了一个看似简单的功能更新。几分钟后用户的抱怨、系统的报警邮件开始像雪花一样飞来。你手忙脚乱地回滚代码打开日志发现罪魁祸首可能只是一个不起眼的边界条件判断或者是一次数据库查询的意外联表。那一刻你看着满屏的500错误心里想的恐怕不是“下次注意”而是“如果有个东西能在上线前就告诉我这里会崩该多好”。这就是测试框架的价值它不是锦上添花而是悬崖边的护栏。很多开发者包括早期的我对Django测试的态度是“先实现功能等有空了再补测试”。但现实是“有空”的那一天永远不会到来。项目在迭代中依赖越来越复杂数据流像一团乱麻任何微小的改动都可能引发连锁反应。没有测试覆盖每一次部署都像一次赌博。Django自诞生之初就内置了强大的测试框架它基于Python的unittest模块并针对Web开发做了大量贴心封装。但很多人仅仅用它来跑几个简单的模型测试远远没有发挥其威力。从“崩溃”到“稳健”的转变核心在于将测试从“事后补救”转变为“开发前置”的工程习惯。这不仅仅是写几个TestCase类而是建立起一套从模型、视图、API到前端交互的自动化验证体系让代码在合入主分支前就经过重重考验。接下来我会结合我踩过的无数个坑带你拆解Django测试框架如何真正成为你Web项目的“守护神”。2. 核心需求解析我们到底在测试什么在动手写测试之前必须先想清楚测试的目标。一个典型的Django Web项目是分层的我们的测试也应该分层进行有的放矢。2.1 分层测试策略构建安全网模型层测试这是最基础也是最重要的一层。模型是数据的基石它的方法如save、clean、自定义管理器方法和属性逻辑必须绝对可靠。测试点包括字段约束如唯一性、可选性、模型方法的返回值、业务逻辑的正确性。例如一个User模型的get_full_name方法是否能正确处理中间名为空的情况视图层测试Django的视图无论是函数视图还是类视图处理HTTP请求并返回响应。这里需要测试状态码、模板使用、上下文数据、重定向以及表单处理。特别是涉及用户权限的视图如login_required,PermissionRequiredMixin必须测试未授权访问是否被正确拦截。表单与序列化器测试Django Form和DRF Serializer负责数据的清洗与验证。测试应覆盖有效数据提交、各种无效数据的验证错误、自定义验证逻辑等。一个常见的坑是只测试了前端传来的数据格式却忘了测试通过API或其他途径传入的恶意数据。API端点测试如果你使用了Django REST framework那么对API的测试需要更细致。除了状态码和返回数据还要测试认证Token、Session、权限、限流、不同HTTP方法GET、POST、PUT、DELETE的行为以及过滤、排序、分页等功能的正确性。集成测试与端到端测试这是模拟真实用户操作的最高层级测试。例如测试用户从登录、填写表单、提交到查看结果这一完整流程。Django的测试客户端可以模拟大部分行为但对于复杂的JavaScript交互可能需要结合pytest、Playwright或Selenium。这层测试运行较慢但能发现跨模块交互产生的问题。2.2 非功能性需求测试除了“功能对不对”我们还要关心“性能行不行”、“安不安全”。性能测试利用django-debug-toolbar或编写测试来监控关键视图的数据库查询次数N1查询问题是重灾区确保没有意外的性能退化。虽然Django测试框架不直接做压力测试但你可以为性能关键路径编写基准测试。安全测试测试常见的Web漏洞场景如CSRF保护是否生效、权限绕过、SQL注入虽然ORM已很大程度上避免但原生SQL查询仍需警惕、XSS防护等。Django内置了许多安全机制但错误配置或自定义代码可能引入弱点。3. 环境准备与工具链选型工欲善其事必先利其器。一个高效的测试环境能让你事半功倍。3.1 测试数据库配置这是第一个关键决策。绝对不要使用生产数据库跑测试。Django测试框架的默认行为是为测试创建一个全新的、独立的测试数据库通常以test_为前缀并在所有测试运行完毕后销毁它。这保证了测试的隔离性。在你的settings.py中或专门的settings/test.py里可以进行优化配置# settings/test.py from .base import * DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: :memory:, # 使用内存数据库速度极快 } } # 加速测试关闭不必要的中间件和调试工具 DEBUG False PASSWORD_HASHERS [ django.contrib.auth.hashers.MD5PasswordHasher, # 测试时使用快速的哈希器 ]注意使用SQLite内存数据库虽然快但要注意它和PostgreSQL/MySQL等生产数据库可能存在细微的语法或行为差异如某些聚合函数、日期处理。一种折中方案是使用与生产环境相同的数据库引擎但通过Docker在本地或CI中快速启动一个实例。3.2 测试运行器与插件Django默认的测试运行器够用但社区有更强大的选择。pytest-django这是目前最主流的增强方案。pytest提供了更简洁的语法无需继承TestCase使用普通函数和assert语句、强大的夹具fixture系统、丰富的插件生态以及更清晰的测试输出。安装与基础配置pip install pytest pytest-django创建pytest.ini配置文件[pytest] DJANGO_SETTINGS_MODULE myproject.settings.test python_files tests.py test_*.py *_tests.py addopts -v --tbshort # 输出详细信息使用简短回溯使用pytest写一个测试# test_models.py import pytest from myapp.models import Product pytest.mark.django_db def test_product_str_representation(): product Product.objects.create(name测试商品, price100) assert str(product) 测试商品 - 100元可以看到代码比传统的unittest风格更简洁。常用插件pytest-cov: 生成测试覆盖率报告直观看到哪些代码未被测试。pytest-xdist: 并行运行测试大幅缩短测试套件执行时间。pytest-mock: 更方便地使用unittest.mock进行模拟和打桩。3.3 测试数据工厂告别混乱的Setup在setUp方法里手动创建一堆模型实例很快会变得难以维护。使用factory_boy或model_bakery可以优雅地生成测试数据。Factory Boy示例# factories.py import factory from django.contrib.auth.models import User from myapp.models import Order, Product class UserFactory(factory.django.DjangoModelFactory): class Meta: model User username factory.Sequence(lambda n: fuser{n}) email factory.LazyAttribute(lambda obj: f{obj.username}example.com) class ProductFactory(factory.django.DjangoModelFactory): class Meta: model Product name factory.Faker(word) price factory.Faker(pydecimal, left_digits3, right_digits2, positiveTrue) class OrderFactory(factory.django.DjangoModelFactory): class Meta: model Order user factory.SubFactory(UserFactory) product factory.SubFactory(ProductFactory) quantity 1 # 在测试中使用 def test_order_total_price(): order OrderFactory(quantity3) product_price order.product.price assert order.total_price product_price * 3Factory Boy可以处理复杂的关系并使用Faker库生成逼真的假数据让测试更健壮。4. 编写高质量的Django测试有了工具我们来深入每一层测试的编写技巧和避坑指南。4.1 模型测试夯实数据基础模型测试应该聚焦于业务逻辑。假设我们有一个博客应用# models.py from django.db import models from django.utils import timezone class Post(models.Model): title models.CharField(max_length200) content models.TextField() published_date models.DateTimeField(nullTrue, blankTrue) is_published models.BooleanField(defaultFalse) def publish(self): 发布文章的业务逻辑 if not self.is_published: self.is_published True self.published_date timezone.now() self.save() def is_recent(self): 判断是否为近期文章3天内 if not self.published_date: return False return (timezone.now() - self.published_date).days 3对应的测试# tests/test_models.py import pytest from django.utils import timezone from datetime import timedelta from myapp.models import Post pytest.mark.django_db class TestPostModel: def test_publish_method(self): 测试发布文章的逻辑 post Post.objects.create(title草稿, content..., is_publishedFalse) assert post.published_date is None post.publish() post.refresh_from_db() # 必须刷新从数据库重新加载 assert post.is_published is True assert post.published_date is not None # 确保发布时间是调用publish时的时间而不是其他时间 assert post.published_date timezone.now() def test_publish_idempotent(self): 多次调用publish不应该重复更新时间幂等性 post Post.objects.create(title测试, content..., is_publishedTrue) original_date post.published_date post.publish() # 已经是发布状态再次调用 post.refresh_from_db() assert post.published_date original_date # 日期不应改变 def test_is_recent(self): 测试近期文章判断逻辑 post Post.objects.create(title文章, content..., is_publishedTrue) # 未发布的文章不是近期文章 post.is_published False post.published_date None assert post.is_recent() is False # 刚刚发布的文章是近期文章 post.is_published True post.published_date timezone.now() assert post.is_recent() is True # 4天前发布的文章不是近期文章 post.published_date timezone.now() - timedelta(days4) assert post.is_recent() is False实操心得模型测试中一定要记得在调用修改数据库的方法后使用refresh_from_db()。否则你内存中的对象状态可能不是最新的导致断言失败。这是新手常踩的坑。4.2 视图与API测试模拟请求与验证响应Django提供了django.test.Client来模拟浏览器请求。对于API测试DRF提供了APIClient用法类似但更便捷。测试一个需要登录的视图# tests/test_views.py import pytest from django.urls import reverse from django.contrib.auth.models import User pytest.mark.django_db class TestDashboardView: def test_dashboard_requires_login(self, client): 未登录用户访问仪表板应被重定向到登录页 url reverse(dashboard) response client.get(url) # 状态码应该是302重定向或者403禁止访问取决于你的配置 assert response.status_code in [302, 403] if response.status_code 302: # 检查重定向目标是否是登录页 login_url reverse(login) assert login_url in response.url def test_dashboard_accessible_for_logged_in_user(self, client): 已登录用户可以访问仪表板 user User.objects.create_user(usernametestuser, password12345) client.force_login(user) # 关键模拟用户登录 url reverse(dashboard) response client.get(url) assert response.status_code 200 assert 欢迎 in response.content.decode() # 检查响应内容测试DRF API端点# tests/test_api.py import pytest from rest_framework.test import APIClient from rest_framework import status from myapp.models import Product pytest.mark.django_db class TestProductAPI: def test_list_products(self): 测试产品列表API Product.objects.create(name产品A, price10) Product.objects.create(name产品B, price20) client APIClient() response client.get(/api/products/) # 或使用reverse(api-product-list) assert response.status_code status.HTTP_200_OK assert len(response.data) 2 assert response.data[0][name] 产品A def test_create_product_requires_auth(self): 测试创建产品需要认证 client APIClient() data {name: 新产品, price: 30} response client.post(/api/products/, data, formatjson) # 未认证应返回401或403 assert response.status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN] def test_create_product_with_auth(self): 认证用户可创建产品 user User.objects.create_user(usernameadmin, passwordpass) client APIClient() client.force_authenticate(useruser) # DRF的认证方式 data {name: 新产品, price: 30} response client.post(/api/products/, data, formatjson) assert response.status_code status.HTTP_201_CREATED assert Product.objects.filter(name新产品).exists()注意事项测试API时特别注意formatjson参数它确保数据以JSON格式发送并设置正确的Content-Type头。对于文件上传等场景需要使用multipart格式。4.3 表单与序列化器测试把好数据入口关表单和序列化器是数据进入系统的第一道关卡必须严格测试。# tests/test_forms.py import pytest from myapp.forms import ContactForm class TestContactForm: def test_valid_form(self): 测试有效数据 form_data {name: 张三, email: zhangsanexample.com, message: 你好} form ContactForm(dataform_data) assert form.is_valid() is True # 可以进一步检查清理后的数据 assert form.cleaned_data[name] 张三 def test_invalid_email(self): 测试无效邮箱 form_data {name: 张三, email: not-an-email, message: ...} form ContactForm(dataform_data) assert form.is_valid() is False # 检查错误信息是否在email字段上 assert email in form.errors # 可以检查具体的错误信息 assert Enter a valid email address. in str(form.errors[email]) def test_message_too_short(self): 测试自定义验证逻辑消息太短 form_data {name: 张, email: ab.com, message: Hi} form ContactForm(dataform_data) assert form.is_valid() is False assert message in form.errors assert 消息太短 in str(form.errors[message]) # 假设有自定义验证对于DRF序列化器测试逻辑类似但更关注序列化对象-字典和反序列化字典-对象两个过程。4.4 使用Mock隔离外部依赖单元测试的核心是“隔离”。当你的代码调用外部服务如发送邮件、调用第三方API、上传文件到云存储时不应该在测试中真正执行这些操作。unittest.mock模块是你的好帮手。假设有一个视图在用户注册后发送欢迎邮件# views.py from django.core.mail import send_mail def register_user(request): # ... 注册逻辑 user User.objects.create(...) # 发送欢迎邮件 send_mail( 欢迎加入我们, 感谢您注册..., fromexample.com, [user.email], fail_silentlyFalse, ) return HttpResponse(注册成功)测试这个视图时我们不应该真的发邮件# tests/test_views.py from unittest.mock import patch import pytest pytest.mark.django_db def test_register_user_sends_email(): 测试用户注册时会发送邮件 from django.core import mail with patch(myapp.views.send_mail) as mock_send_mail: # 配置mock让它什么都不做但记录被调用的情况 mock_send_mail.return_value 1 # send_mail返回发送成功的邮件数 client APIClient() data {username: newuser, email: newexample.com, password: secure123} response client.post(/api/register/, data, formatjson) assert response.status_code 201 # 断言send_mail被调用了一次 assert mock_send_mail.called is True # 甚至可以断言调用时的参数 call_args mock_send_mail.call_args assert call_args[0][0] 欢迎加入我们 # 第一个参数是主题 assert newexample.com in call_args[0][3] # 第四个参数是收件人列表避坑技巧patch的目标字符串必须是“导入路径”。即在你测试的代码中send_mail是从哪里导入的就patch哪里。这里是myapp.views.send_mail。如果视图里写的是from django.core.mail import send_mail然后直接调用send_mail(...)那么patch路径就是myapp.views.send_mail。如果视图里是import django.core.mail然后django.core.mail.send_mail(...)那就要patchmyapp.views.django.core.mail.send_mail。理解这个“导入路径”概念是正确使用Mock的关键。5. 集成与端到端测试模拟真实用户旅程当各个单元都测试通过后我们需要确保它们组合在一起也能正常工作。这就是集成测试。更进一步的端到端测试模拟真实用户在浏览器中的完整操作。5.1 使用Django TestClient进行集成测试Django的Client不仅可以测试单个视图还可以模拟一系列连续请求形成一个“用户会话”。pytest.mark.django_db def test_user_login_and_access_profile(): 集成测试用户登录后可以访问个人资料并更新信息 client APIClient() user User.objects.create_user(usernametest, passwordpass123, emailtestexample.com) # 1. 登录 login_success client.login(usernametest, passwordpass123) assert login_success is True # 2. 访问个人资料页 (假设这个视图需要登录) profile_response client.get(reverse(profile)) assert profile_response.status_code 200 # 检查页面是否包含用户信息 content profile_response.content.decode() assert testexample.com in content # 3. 提交更新表单 update_data {email: newemailexample.com} update_response client.post(reverse(profile_update), update_data) # 期望是重定向回资料页 assert update_response.status_code 302 # 4. 验证数据是否更新 user.refresh_from_db() assert user.email newemailexample.com5.2 引入Playwright进行浏览器自动化测试对于高度依赖JavaScript交互的单页应用Django的Client就力不从心了。此时需要真正的浏览器自动化工具。Playwright是一个现代、强大且速度快的选择它支持无头模式非常适合CI/CD。首先安装pip install pytest-playwright playwright install chromium # 安装浏览器驱动编写一个端到端测试# tests/e2e/test_user_journey.py import pytest from django.contrib.auth.models import User pytest.mark.e2e # 可以用一个自定义标记来区分慢速的E2E测试 class TestUserRegistrationE2E: pytest.fixture(autouseTrue) def setup(self, django_db_setup, django_db_blocker): 每个测试前清理用户表确保隔离 with django_db_blocker.unblock(): User.objects.all().delete() def test_complete_registration_flow(self, page, live_server): 测试完整的用户注册流程 1. 访问首页 2. 点击注册 3. 填写表单 4. 提交 5. 验证注册成功并跳转 # 1. 导航到首页 page.goto(f{live_server.url}/) # 2. 点击注册链接 (假设链接文本是“注册”) page.click(text注册) # 3. 等待注册页面加载并填写表单 page.wait_for_selector(h1:has-text(用户注册)) # 等待标题 page.fill(input[nameusername], e2e_user) page.fill(input[nameemail], e2eexample.com) page.fill(input[namepassword1], VerySecurePass123!) page.fill(input[namepassword2], VerySecurePass123!) # 4. 点击提交按钮 page.click(button:has-text(提交注册)) # 5. 验证成功消息和跳转 # 假设成功后会显示一个提示并跳转到仪表板 page.wait_for_selector(.alert-success:has-text(注册成功)) # 断言当前URL是仪表板页面 assert page.url f{live_server.url}/dashboard/ # 6. (可选) 验证数据库里确实创建了用户 # 注意由于测试数据库在事务中这里直接查询可能不行通常E2E测试不直接断言数据库状态。 # 而是通过页面上显示的用户名来断言。 page.wait_for_selector(nav:has-text(e2e_user))重要提示端到端测试运行慢、脆弱前端微小的HTML/CSS改动可能导致选择器失效。因此要遵循以下原则少而精只为最关键的用户流程编写E2E测试。使用稳定的选择器优先使用># conftest.py 或 pytest.ini def pytest_configure(config): config.addinivalue_line( markers, slow: marks tests as slow (deselect with -m \not slow\) ) config.addinivalue_line( markers, e2e: marks tests as end-to-end browser tests ) # 在测试文件中标记 pytest.mark.slow def test_complex_calculation(): ... # 运行命令 # 只运行快速测试 pytest -m not slow and not e2e # 只运行E2E测试 pytest -m e2e6.2 集成到CI/CD流水线以GitHub Actions为例一个基本的.github/workflows/test.yml配置如下name: Django Tests on: [push, pull_request] 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 steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install system dependencies (if needed for psycopg2) run: | sudo apt-get update sudo apt-get install -y libpq-dev gcc - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-test.txt # 单独存放测试依赖 - name: Run migrations env: DATABASE_URL: postgres://postgres:postgreslocalhost:5432/postgres DJANGO_SETTINGS_MODULE: myproject.settings.test run: | python manage.py migrate - name: Run linting (e.g., flake8, black --check) run: | black --check . flake8 . - name: Run unit and integration tests with coverage env: DATABASE_URL: postgres://postgres:postgreslocalhost:5432/postgres DJANGO_SETTINGS_MODULE: myproject.settings.test run: | pytest -v --covmyapp --cov-reportxml --cov-reportterm-missing -m not e2e - name: Upload coverage to Codecov uses: codecov/codecov-actionv3 with: file: ./coverage.xml flags: unittests # 可以添加一个单独的job来运行缓慢的E2E测试 e2e: needs: test # 依赖test job先成功 runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python # ... 类似步骤 - name: Install Playwright browsers run: | playwright install chromium - name: Run E2E tests env: DJANGO_SETTINGS_MODULE: myproject.settings.test run: | pytest -v -m e2e这个配置做了几件关键事启动一个PostgreSQL服务容器模拟生产数据库环境。安装依赖运行数据库迁移。运行代码风格检查。运行除E2E外的所有测试并生成覆盖率报告。将覆盖率报告上传到Codecov等平台。可选在单元测试通过后运行E2E测试。6.3 测试覆盖率数字背后的真相测试覆盖率是一个有用的指标但它不是目标。100%的覆盖率不代表代码没bug。要关注的是关键路径和复杂逻辑的覆盖。使用pytest-cov生成报告pytest --covmyapp --cov-reporthtml --cov-reportterm-missing打开生成的htmlcov/index.html你可以清晰地看到哪些行被覆盖了哪些没有。重点检查条件分支if/else是否都被覆盖异常处理代码try/except是否被触发测试复杂的循环和算法逻辑。不要为了追求高覆盖率而写无意义的测试。覆盖率的目的是发现未被测试的代码而不是一个需要刷分的KPI。7. 常见问题与排查技巧实录即使按照最佳实践测试中还是会遇到各种奇怪的问题。这里记录一些我反复遇到的“坑”和解决方法。7.1 数据库问题问题1TransactionManagementError- “不能在一个原子块内执行查询除非设置atomic False”原因Django的测试用例默认包裹在事务中但某些操作如测试MySQL的原始SQL或使用某些第三方库可能尝试创建自己的事务导致冲突。解决最简单的办法是让测试类继承自django.test.TestCase它处理了事务而不是unittest.TestCase。对于pytest使用pytest.mark.django_db装饰器。如果必须使用transaction.atomic()确保在测试中正确管理事务边界或者使用TestCase.captureOnCommitCallbacks来测试事务提交后的回调。问题2测试间数据污染现象测试A创建的数据影响了测试B的结果。原因没有正确隔离。Django的TestCase和pytest的pytest.mark.django_db默认会为每个测试用例回滚数据库但如果你手动管理事务或使用了TransactionTestCase就可能出现污染。解决坚持使用TestCase和pytest.mark.django_db。在setUp和tearDown方法中或使用pytest的fixture显式地清理数据。Factory Boy的工厂类通常不负责清理。使用pytest的fixture并设置scopefunction默认确保每个测试函数获得全新的数据。7.2 静态文件与媒体文件问题测试中找不到静态文件CSS, JS导致页面渲染不全或404。原因Django的开发服务器会自动处理静态文件但测试环境默认不提供静态文件服务。解决在测试设置中使用django.contrib.staticfiles.testing.StaticLiveServerTestCase用于LiveServer测试或者在测试用例中手动使用django.test.override_settings来配置静态文件查找器。from django.test import TestCase, override_settings from django.contrib.staticfiles.testing import StaticLiveServerTestCase # 方法1针对整个测试类 override_settings(DEBUGTrue) # DEBUGTrue时静态文件视图才工作 class MyViewTest(TestCase): ... # 方法2使用StaticLiveServerTestCase进行需要静态文件的端到端测试 class MySeleniumTest(StaticLiveServerTestCase): ...更常见的做法是对于不依赖静态文件正确加载的视图测试直接忽略静态文件404错误。对于需要完整渲染的测试如用Playwright则使用LiveServerTestCase并确保DEBUGTrue。7.3 时间相关测试问题测试涉及timezone.now()或日期比较结果不稳定。原因代码执行需要时间两次调用timezone.now()可能得到不同的值。解决使用Mock来固定时间。from unittest.mock import patch from django.utils import timezone import datetime def test_something_with_time(): fixed_now timezone.datetime(2023, 10, 27, 10, 0, 0, tzinfotimezone.utc) with patch(django.utils.timezone.now, return_valuefixed_now): # 在这个代码块内所有timezone.now()调用都会返回fixed_now result my_function_that_uses_now() assert result expected_value_based_on_fixed_time对于模型中的auto_now_add或auto_now字段在测试中创建对象后可以通过refresh_from_db()获取数据库存储的实际时间进行断言或者使用freezegun这样的第三方库来冻结整个测试的时间。7.4 测试性能优化当测试套件越来越庞大时运行时间会成为一个痛点。使用内存数据库如前所述在测试设置中使用sqlite3的:memory:数据库。并行运行测试使用pytest-xdist插件。pytest -n auto # 自动检测CPU核心数并行运行重用测试数据库pytest-django默认会为每个测试会话创建并销毁一次测试数据库。可以通过--reuse-db参数来尝试重用上一次的数据库但要注意这可能导致测试间污染需谨慎使用。选择性运行测试只运行上次失败的测试(pytest --lf)或只运行与修改文件相关的测试需要pytest-picked等插件。避免不必要的序列化与反序列化在API测试中如果只关心状态码可以避免解析大量的JSON响应体。7.5 测试心态与习惯最后分享几点比技术更重要的心得测试驱动开发尝试在写实现代码之前先写测试TDD。这能强迫你从接口和使用者角度思考设计出更清晰、更模块化的代码。一开始可能不习惯但坚持下来对代码质量提升巨大。测试不是负担是自由有了完善的测试套件你才能有信心进行重构、升级依赖、优化性能。没有测试任何修改都伴随着恐惧。从最重要的部分开始不要试图一开始就给整个项目补全测试。从最核心、最复杂、最容易出错的业务逻辑开始。每次修复一个bug就为它写一个测试防止它再次出现回归测试。测试也要重构和业务代码一样测试代码也会变得混乱。定期回顾测试删除重复代码使用fixture和工厂函数提高可维护性。失败是好事测试失败意味着它发现了问题。把测试失败看作是一次成功的“预警”而不是麻烦。