Django---测试驱动开发(TDD)

本文介绍了Django中测试驱动开发(TDD)的概念,包括单元测试和功能测试。单元测试关注函数或方法的正确性,而功能测试则从用户角度验证应用功能。TDD流程涉及先编写功能测试,然后通过单元测试编写代码。Django的TestCase类支持这两种测试。示例中展示了如何为待办事项应用编写测试,包括测试用例和功能测试脚本。最后,文章提供了一些TDD相关的学习资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本概念

单元测试

单元测试包括对最小代码单元的测试,这通常是函数或方法。单元测试主要是由单元/方法/功能的开发人员完成的,因为他们了解功能的核心。

其目的在于检查每个程序单元能否正确实现详细设计说明中的模块功能、性能、接口和设计约束等要求,发现各模块内部可能存在的各种错误。

单元测试确保每个代码单元在隔离状态下都能正常工作。

它的一个限制是某些功能不能通过单元测试进行测试。即使在所有单元测试成功完成之后,也不能保证产品的正确运行。相同的功能可以用于系统的少数部分,而单元测试只用于一种使用。

一般来说,单元测试的规则之一是不测试常量,单元测试要测试的是逻辑、流程控制和配置。

功能测试

它是一种黑匣子测试,测试将在产品的功能方面进行,而无需查看代码。功能测试主要由专用的软件测试人员完成。它将包括正、负和BVA技术,使用联合国标准化数据测试产品的特定功能。功能测试比单元测试以改进的方式进行测试覆盖。它使用应用程序GUI进行测试,因此更容易确定接口的具体部分负责什么,而不是确定代码负责什么功能。

通过功能测试,你就像一个真正的用户一样,通过与应用交互来测试应用程序。 所以功能测试是集成测试。

对比

单元测试和功能测试之间的界线有时不那么清晰。不过,二者之间有个基本区别:功能测试站在用户的角度从外部测试应用,单元测试则站在程序员的角度从内部测试应用。

功能测试的作用是帮助你开发具有所需功能的应用,还能保证你不会无意中破坏这些功能。单元测试的作用是帮助你编写简洁无错的代码。

单元测试由功能测试驱动,而且更接近于真正的代码。编写单元测试时,按照程序员的方式思考。

单元测试/编写代码循环

在这里插入图片描述

TDD同时使用这两种类型测试应用,采用的工作流程大致如下:

  1. 先写功能测试,从用户的角度描述应用的新功能。

  2. 功能测试失败后,想办法编写代码让它通过(或者说至少让当前失败的测试通过)。此时,使用一个或多个单元测试定义希望代码实现的效果,保证为应用中的每一行代码(至少)编写一个单元测试。

  3. 单元测试失败后,编写最少量的应用代码,刚好让单元测试通过。有时,要在第2步和第3步之间多次往复,直到我们觉得功能测试有一点进展为止。

  4. 然后,再次运行功能测试,看能否通过,或者有没有进展。这一步可能促使我们编写一些新的单元测试和代码等。

在这整个过程中,功能测试站在高层驱动开发,而单元测试则从低层驱动我们做些什么。

TDD

测试驱动开发(TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名

测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。

TDD的优点之一是,永远不会忘记接下该做什么——重新运行测试就知道要做的事

Django 案例

使用TDD理念开发一个lists应用——记录待办事项。让用户输入一些待办事项,并且用户下次访问应用时这些事项还在即可。

Django中的TestCase是标准版unittest.TestCase的增强版,添加了一些Django专用的功能。

层级目录
在这里插入图片描述
模板

<html>
    <head>
        <title>To-Do lists</title>
    </head>
    <body>
        <h1>Your To-Do list</h1>
        <form method="POST">
            <input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
            {% csrf_token %}
        </form>

        <table id="id_list_table">
            {% for item in items %}
                <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
            {% endfor %}
        </table>
    </body>
</html>

lists应用代码

# models.py
from django.db import models


class Item(models.Model):
    text = models.TextField(default='')


# views.py
from django.shortcuts import render, redirect
from lists.models import Item


def home_page(request):
    if request.method == 'POST':
        Item.objects.create(text=request.POST['item_text'])
        return redirect('/')
    items = Item.objects.all()
    return render(request, 'home.html', {'items': items})

# urls.py
from django.contrib import admin
from django.urls import path, re_path
from lists import views

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r'^$', views.home_page, name='home')
]

单元测试

# lists/tests.py
from django.test import TestCase
from django.urls import resolve
from django.http import HttpRequest
from lists.views import home_page
from django.template.loader import render_to_string
from lists.models import Item


class SmokeTest(TestCase):
    def test_bad_maths(self):
        self.assertEquals(1 + 1, 2)


class HomePageTest(TestCase):
    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')
        self.assertEquals(found.func, home_page)

    # def test_home_page_returns_correct_html(self):
    # request = HttpRequest()
    # response = home_page(request)
    # html = response.content.decode('utf-8')
    # expected_html = render_to_string('home.html')
    # self.assertEqual(html, expected_html)

    # self.assertTrue(html.startswith('<html>'))
    # self.assertIn('<title>To-Do lists</title>', html)
    # self.assertTrue(html.strip().endswith('</html>'))

    # response = self.client.get('/')
    # html = response.content.decode('utf-8')
    # expected_html = render_to_string('home.html')
    # self.assertEqual(html, expected_html)
    # self.assertTemplateUsed(response, 'home.html')

    def test_uses_home_template(self):
        response = self.client.get('/')
        self.assertTemplateUsed(response, 'home.html')

    def test_displays_all_list_items(self):
        Item.objects.create(text='itemey 1')
        Item.objects.create(text='itemey 2')

        response = self.client.get('/')

        self.assertIn('itemey 1', response.content.decode())
        self.assertIn('itemey 2', response.content.decode())

    def test_can_save_a_POST_request(self):
        item_text = 'A new list item'
        self.client.post('/', data={'item_text': item_text})

        # 检查是否把一个新Item对象存入数据库
        self.assertEqual(Item.objects.count(), 1)
        new_item = Item.objects.first()
        # 检查文本是否正确
        self.assertEqual(new_item.text, item_text)

    def test_redirects_after_POST(self):
        response = self.client.post('/', data={'item_text': 'A new list item'})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/')

    def test_only_saves_items_when_necessary(self):
        self.client.get('/')
        self.assertEqual(Item.objects.count(), 0)


class ItemModelTest(TestCase):

    def test_saving_and_retrieving_items(self):
        first_item = Item()
        first_item_text = 'The first (ever) list item'
        first_item.text = first_item_text
        first_item.save()

        second_item = Item()
        second_item_text = 'Item the second'
        second_item.text = second_item_text
        second_item.save()

        saved_items = Item.objects.all()
        self.assertEqual(saved_items.count(), 2)

        first_saved_item = saved_items[0]
        second_saved_item = saved_items[1]
        self.assertEqual(first_saved_item.text, first_item_text)
        self.assertEqual(second_saved_item.text, second_item_text)

功能测试

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# function_test\tests.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
import unittest

# LiveServerTestCase会自动创建一个测试数据库(跟单元测试一样),并启动一个开发服务器,让功能测试在其中运行。
from django.test import LiveServerTestCase


class NewVisitorTest(LiveServerTestCase):
    def setUp(self) -> None:
        self.browser = webdriver.Chrome()

    def tearDown(self) -> None:
        pass
        self.browser.quit()

    def check_for_row_in_list_table(self, row_text):
        """辅助方法:检查row.text"""
        table = self.browser.find_element_by_id('id_list_table')
        rows = table.find_elements_by_tag_name('tr')
        self.assertIn(row_text, [row.text for row in rows])

    def test_can_start_a_list_and_retrieve_it_later(self):
        # 查看首页
        # self.browser.get('http://localhost:8000')
        self.browser.get(self.live_server_url)

        # 网页标题和头部都包含'To-Do'
        self.assertIn('To-Do', self.browser.title)
        header_text = self.browser.find_element_by_tag_name('h1').text
        self.assertIn('To-Do', header_text)

        # 代办事项
        inputbox = self.browser.find_element_by_id('id_new_item')
        self.assertEqual(inputbox.get_attribute('placeholder'), 'Enter a to-do item')

        inputbox.send_keys('Buy peacock feathers')
        # 回车键
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        self.check_for_row_in_list_table('1: Buy peacock feathers')

        # 表格中数据有变化时,页面会自动刷新,需要重新查找元素
        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Use peacock feathers to make a fly')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        self.check_for_row_in_list_table('2: Use peacock feathers to make a fly')
        self.check_for_row_in_list_table('1: Buy peacock feathers')

        self.fail('Finish the test!')


if __name__ == '__main__':
    # 禁止抛出ResourceWarning异常
    unittest.main(warnings='ignore')

命令

python manage.py test # 自动运行所有测试文件,也可以指定文件

阅读资料

软件设计说明书 版本:V1.0 文 档 编 号 保 密 等 级 作 者 最后修改日期 审 核 人 最后审批日期 批 准 人 最后批准日期 修订记录 日期 版本 修订说明 修订人 目 录 1 引言 1 1.1 文档控制 1 1.2 目的 1 1.3 范围 1 1.4 定义、首字母缩写词和缩略语 1 1.5 参考资料 1 1.6 概述 1 2 整体说明 1 2.1 业务背景 1 2.2 功能模型 1 2.3 用例模型概述 1 2.3.1 子系统一 1 2.4 假设和依赖关系 1 3 包1:出入库 2 3.1 冲补帐务-样例1 2 3.1.1 系统用例 2 3.1.2 类关系图 2 3.1.3 类图 3 3.1.4 顺序图 4 3.2 冲补入库单-样例2 4 3.2.1 入库单冲账 4 3.2.2 入库单补账 6 引言 文档控制 目的 范围 定义、首字母缩写词和缩略语 参考资料 概述 整体说明 业务背景 [此小节应说明软件的业务背景,包括组织机构、业务流程等。] 功能模型 [此小节应说明用例包的组织,以用例包的形式来表达软件的功能。] 用例模型概述 子系统一 用例图 [此小节应说明此子系统下的用例图。] 主角列表 [此小节以列表的形式说明此子系统相关的主角。] 用例列表 [此小节以列表的形式说明此子系统相关的用例。] 假设和依赖关系 [本节说明所有重要的技术可行性假设、子系统或构件可用性假设,或者可作为此文档所述软件可行性的基础的其他与项目有关的假设。] 包1:出入库 画出包图、核心业务处理流程、状态转换图来说明核心业务的工作方式。 冲补帐务-样例1 系统用例 类关系图 类图 实体类 边界类 控制类 顺序图 分仓保管帐建帐处理流程 备注: 去掉价位一栏; 收获年限改为“年限“,入库年限改为非必输项; 国别改为必输项; 等级改为非必输项; 增加建帐的修改功能:只能修改等级、入库年限、产地。 冲补入库单-样例2 入库单冲账 前台界面 名称:account_grainin_strike.jsp 界面说明: 序号 界面名称 表 字段 字段含义 备注 1 编号 入库单 RC_RKD_RKDBH 入库单编号 顺序号 入库单 RC_RKD_SXH 顺序号 计划安排表 入库单 RC_RKD_RKJHAPBNM 入库计划安排表内码 需要转化为编号 计划明细 入库单 RC_RKD_JHMXNM 计划明细内码 需要转化为编号 入库通知单编号 入库单 RC_RKD_ZGKRKTZDNM 直管库入库通知单内码 需要转化为编号 客户名称 入库单 RC_RKD_KHNM,RC_RKD_KHMC 客户名称 客户内码 合同号 入库单 RC_RKD_HTNM, RC_RKD_HTH 合同号 合同内码 到库时间 入库登记信息 RC_RKDJXX_DKSJ 到库时间 收获年份 入库单 RC_RKD_NX 年限 等级 入库检验信息 RC_RKJYXX_WLDJNM 物料等级内码 后台服务 功能简介 入库账务服务 包名 com.digitalchina.zcl.stock.account 类名 AccountGraininServer 方法 名称 参数 返回值 描述 入库单补账 前台界面 名称:account_grainin_repair.jsp 界面说明: 序号 界面名称 表 字段 字段含义 备注 1 编号 入库单 RC_RKD_RKDBH 入库单编号 顺序号 入库单 RC_RKD_SXH 顺序号 扣杂 入库检验信息 RC_RKJYXX_KZZ 扣杂质(%) 扣水% 入库检验信息 RC_RKJYXX_KSF 扣水份(%) 备注 入库检验信息 RC_RKJYXX_JYYJBZ 检验意见备注 3 选择类型 入库检验信息 RC_RKJJXX_JJLX 检斤类型 0,称重;1,标准包 毛重 入库检验信息 RC_RKJJXX_MZ 毛重 水分扣量 入库检验信息 RC_RKJJXX_SFKL 水分扣量 杂质扣量 入库检验信息 RC_RKJJXX_ZZKL 杂质扣量 其它扣量 入库检验信息 RC_RKJJXX_QTKL 其它扣量 后台服务 功能简介 入库账务服务 包名 com.digitalchina.zcl.stock.account 类名 AccountGraininServer 方法 名称 参数 返回值 描述 补账 repairAccount 被补入库单内码 入库单补账; 根据被补单据产生一笔单据(RC_RKD): 红单标志为蓝单; 补帐标志为1补帐 补帐单据内码为被补单据内码; 补帐日期为当前日期; 审核标志为未审核; 能否记保管帐为1能; 能否记统计帐为1能; 保管帐记帐标志为未记帐; 统计帐记帐标志为未记帐; 删除标志为未删除; 制单时间为当前时间; 其余要素用户录入;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值