基本概念
单元测试
单元测试包括对最小代码单元的测试,这通常是函数或方法。单元测试主要是由单元/方法/功能的开发人员完成的,因为他们了解功能的核心。
其目的在于检查每个程序单元能否正确实现详细设计说明中的模块功能、性能、接口和设计约束等要求,发现各模块内部可能存在的各种错误。
单元测试确保每个代码单元在隔离状态下都能正常工作。
它的一个限制是某些功能不能通过单元测试进行测试。即使在所有单元测试成功完成之后,也不能保证产品的正确运行。相同的功能可以用于系统的少数部分,而单元测试只用于一种使用。
一般来说,单元测试的规则之一是不测试常量,单元测试要测试的是逻辑、流程控制和配置。
功能测试
它是一种黑匣子测试,测试将在产品的功能方面进行,而无需查看代码。功能测试主要由专用的软件测试人员完成。它将包括正、负和BVA技术,使用联合国标准化数据测试产品的特定功能。功能测试比单元测试以改进的方式进行测试覆盖。它使用应用程序GUI进行测试,因此更容易确定接口的具体部分负责什么,而不是确定代码负责什么功能。
通过功能测试,你就像一个真正的用户一样,通过与应用交互来测试应用程序。 所以功能测试是集成测试。
对比
单元测试和功能测试之间的界线有时不那么清晰。不过,二者之间有个基本区别:功能测试站在用户的角度从外部测试应用,单元测试则站在程序员的角度从内部测试应用。
功能测试的作用是帮助你开发具有所需功能的应用,还能保证你不会无意中破坏这些功能。单元测试的作用是帮助你编写简洁无错的代码。
单元测试由功能测试驱动,而且更接近于真正的代码。编写单元测试时,按照程序员的方式思考。
单元测试/编写代码循环
TDD同时使用这两种类型测试应用,采用的工作流程大致如下:
-
先写功能测试,从用户的角度描述应用的新功能。
-
功能测试失败后,想办法编写代码让它通过(或者说至少让当前失败的测试通过)。此时,使用一个或多个单元测试定义希望代码实现的效果,保证为应用中的每一行代码(至少)编写一个单元测试。
-
单元测试失败后,编写最少量的应用代码,刚好让单元测试通过。有时,要在第2步和第3步之间多次往复,直到我们觉得功能测试有一点进展为止。
-
然后,再次运行功能测试,看能否通过,或者有没有进展。这一步可能促使我们编写一些新的单元测试和代码等。
在这整个过程中,功能测试站在高层驱动开发,而单元测试则从低层驱动我们做些什么。
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 # 自动运行所有测试文件,也可以指定文件