unittest 测试框架的使用

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 1. unittest 框架解析2. 批量执行测试脚本1)构建测试套件addTest() 方法makeSuite() 方法TestLoader() 方法2)用例的执行顺序3)忽略测试用例的执行3. unittest 断言4. HTML 报告生成5. 异常捕获与错误截图6. 数据驱动1)测试多个不同数据2)测试某个文件中的多组数据txt 文件或者 csv 文件JSON 文件

1. unittest 框架解析

unittest 是 python 的单元测试框架,主要有以下作用:


提供测试用例的组织和执行: unittest 框架可以将成百上千条测试用例组织在一起进行执行

提供丰富的比较方法: 再用例执行完成之后都需要将实际结果和预期结果进行比较(断言),从而断定用例是否通过,unittest 中提供了丰富的断言方法

提供丰富的日志: 当测试用例执行失败会抛出清晰的失败原因,当所有的用例执行完成后会提供丰富的执行结果,执行时间、失败用例数、成功用例数等

unittest 中有四个很重要的概念: test fixture、test case、test suite、test runner


test fixture


对一个测试用例环境的搭建和销毁,就是一个 fixture,通过重写 setUp() 方法和 tearDown() 方法来实现


setUp() 方法可以进行测试环境的搭建,比如获取浏览器的驱动、设置测试 URL、连接数据库等操作


tearDown() 方法及逆行环境的销毁,可以关闭浏览器、关闭数据库等


test case


一个 test case 就是一个测试用例,即一个完整的测试流程,包括 setUp 方法、tearDown 方法以及完成测试过程的代码


test suite


测试套件,test suite 用来将多个测试用例组装在一起


test runner


在 unittest 框架中,通过 textTestRunner 类下的 run() 方法来执行测试用例或者测试套件


下面是一个使用了 unittest 框架的简单测试脚本


脚本中的类必须继承 unittest.TestCase 类,之后这个类就是一个 TestCase

每一个 TestCase 中都必须含有 setUp 方法和 tearDown 方法

执行测试代码的方法必须以 “test_” 开头

unittest 中提供了 main 方法来执行本测试用例


from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
class bing(unittest.TestCase):
    def setUp(self):
        print("------setUp------")
        self.driver = webdriver.Chrome()
        self.url = "https://cn.bing.com/"
        self.driver.maximize_window()
        time.sleep(3)
    def tearDown(self):
        print("------tearDown------")
        self.driver.quit()
    def test_search(self):
        driver = self.driver
        url = self.url
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys("python")
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
        time.sleep(3)
    def test_closeImg(self):
        driver = self.driver
        url = self.url
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("id_sc").click()
        time.sleep(3)
        a = driver.find_element_by_xpath("//*[@id='qs_iotd_ctrl']/div/div[3]/div")
        ActionChains(driver).move_to_element(a).perform()
        time.sleep(3)
        a.click()
        time.sleep(3)
if __name__ == "__main__":
    unittest.main()

2. 批量执行测试脚本

1)构建测试套件

将多个测试用例组织起来形成一个 test suite 测试套件,就可以一次性执行多个测试用例

假设有如下两个测试用例:

testBing.py


from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
class bing(unittest.TestCase):
    def setUp(self):
        print("------setUp------")
        self.driver = webdriver.Chrome()
        self.url = "https://cn.bing.com/"
        self.driver.maximize_window()
        time.sleep(3)
    def tearDown(self):
        print("------tearDown------")
        self.driver.quit()
    def test_search(self):
        driver = self.driver
        url = self.url
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys("python")
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
        time.sleep(3)
    def test_closeImg(self):
        driver = self.driver
        url = self.url
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("id_sc").click()
        time.sleep(3)
        a = driver.find_element_by_xpath("//*[@id='qs_iotd_ctrl']/div/div[3]/div")
        ActionChains(driver).move_to_element(a).perform()
        time.sleep(3)
        a.click()
        time.sleep(3)
if __name__ == "__main__":
    unittest.main()

testBaidu.py

from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.action_chains import ActionChains
class baidu(unittest.TestCase):
    def setUp(self):
        print("------setUp------")
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.maximize_window()
        time.sleep(3)
    def tearDown(self):
        print("------tearDown------")
        self.driver.quit()
    def test_search(self):
        url = self.url
        driver = self.driver
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("kw").send_keys("python")
        time.sleep(3)
        driver.find_element_by_id("su").click()
        time.sleep(3)
    def test_hao_search(self):
        url = self.url
        driver = self.driver
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_link_text("hao123").click()
        time.sleep(3)
    def test_baiduTranslation(self):
        url = self.url
        driver = self.driver
        driver.get(url)
        time.sleep(3)
        a = driver.find_element_by_link_text("更多")
        ActionChains(driver).move_to_element(a).perform()
        time.sleep(3)
        driver.find_element_by_xpath("//*[@id='s-top-more']/div[1]/a[1]").click()
        time.sleep(3)
if __name__ == '__main__':
    unittest.main()

unittest 中提供了多种方法来构建测试套件

addTest() 方法

TestSuite 类的 addTest 方法可以把不同的测试类中的测试方法组装带测试套件中,但是 addTest 方法一次只能把一个类中的一个方法添加到测试套件中


import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
    # addTest
    suite = unittest.TestSuite()
    suite.addTest(testBing.bing("test_closeImg"))
    suite.addTest(testBing.bing("test_search"))
    suite.addTest(testBaidu.baidu("test_search"))
    suite.addTest(testBaidu.baidu("test_hao_search"))
    return suite
# 执行测试套件
if __name__ == '__main__':
    suite = createSuite()
    # verbosity 设置日志级别,2最高,0最低
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

makeSuite() 方法

makSuite 方法配合 addTest 方法可以实现一次将某个测试类中的所有测试方法添加到测试套件

只需要在 makeSuite 方法中传入测试类名即可

import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
    # makeSuite
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(testBing.bing))
    suite.addTest(unittest.makeSuite(testBaidu.baidu))
    return suite
# 执行测试套件
if __name__ == '__main__':
    suite = createSuite()
    # verbosity 设置日志级别,2最高,0最低
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

TestLoader() 方法

TestLoader 方法的作用与 makeSuite 方法一样,都是将某个测试类中的所有测试方法添加到测试套件中


不同的是 TestLoader 不需要配合 addTest 使用,直接使用 unittest.TestLoader().loadTestsFromTestCase() 方法即可,


import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
    # TestLoader
    suite1 = unittest.TestLoader().loadTestsFromTestCase(testBing.bing)
    suite2 = unittest.TestLoader().loadTestsFromTestCase(testBaidu.baidu)
    suite = unittest.TestSuite([suite1, suite2])
    return suite
# 执行测试套件
if __name__ == '__main__':
    suite = createSuite()
    # verbosity 设置日志级别,2最高,0最低
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

discover() 的应用


discover 方法可以将某个目录下的所有符合标准的脚本文件中的所有测试方法添加到测试套件中


使用 unittest.defaultTestLoader.discover() 方法,第一个参数填入某个目录的绝对路径,第二个参数填入标准文件名,testB*.py 则表示以 testB 开头的 python 文件,第三个参数表示测试模块的顶层目录,一版设置为 None 即可

import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
   # discover 的应用
    discover = unittest.defaultTestLoader.discover("D:\\JAVA\\Python\\project\\src_selenium\\src_unittest", pattern="testB*.py", top_level_dir=None)
    print(discover)
    return discover
# 执行测试套件
if __name__ == '__main__':
    suite = createSuite()
    # verbosity 设置日志级别,2最高,0最低
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

2)用例的执行顺序

unittest 框架在执行多个测试方法时,会根据类名或者方法名的 ASCII 码顺序进行执行,即 0 ~ 9,A ~ Z,a ~ z

就测试方法来说,顺序是根据 “ test_ ” 后的单词进行排序


3)忽略测试用例的执行

如果在某一次测试中不需要执行某一个测试方法,就需要把这个测试方法忽略不执行,使用 @unittest.skip() 注解就可以完成,写入的参数会在控制台打印出


@unittest.skip("test_hao_search 被忽略")
def test_hao_search(self):
    url = self.url
    driver = self.driver
    driver.get(url)
    time.sleep(3)
    driver.find_element_by_link_text("hao123").click()
    time.sleep(3)

101.png


3. unittest 断言

对于每一个单独的测试用例来说,必然会有预期结果和实际结果,通过比对预期结果和实际结果就可以判断测试用例是否通过


反映到代码中就是断言,断言通过则会继续执行下面的代码,否则对应的测试方法就会停止或者生成错误信息,但不会影响其他测试方法的执行


unittest 中提供了丰富的断言方法


msg 参数为断言未通过时的提示语,可以自定义,也可以不写此参数


1683889185096.png1683889185096.png1683889199702.png

1683889199702.png

断言用法小示例:

def test_search(self):
    driver = self.driver
    url = self.url
    driver.get(url)
    time.sleep(3)
    driver.find_element_by_id("sb_form_q").send_keys("python")
    time.sleep(3)
    driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
    time.sleep(3)
    print(driver.title)
    # 验证 “打开的网页 title 不是 ‘python - 搜索’” 是否通过
    self.assertNotEqual("python - 搜索", driver.title, msg="未打开页面")\

未通过就会在控制台给出错误信息

102.png

4. HTML 报告生成

脚本执行完成之后,还需要生成一个测试报告,这里就可以使用 HTMLTestRunner.py 来生成 HTML 形式的测试报告


HTMLTestRunner.py 文件,下载地址: http://tungwaiyip.info/software/HTMLTestRunner.html


下载后将其放入 python 安装目录的 Lib 目录下


HTMLTestRunner 支持python2.7。python3可以参见http://blog.51cto.com/hzqldjb/1590802来进行修改。


import HTMLTestRunner
import os.path
import sys
import time
import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
    # makeSuite
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(testBing.bing))
    suite.addTest(unittest.makeSuite(testBaidu.baidu))
    return suite
if __name__ == '__main__':
    # 创建一个存放 HTML 报告的文件夹
    curPath = sys.path[0]  # 获取当前文件的路径
    if not os.path.exists(curPath + "/result"):
        # 判断是否存在此文件夹,不存在则创建一个
        os.makedirs(curPath + "/result")
    # 用当前时间作为 HTML 报告的文件名
    now = time.strftime("%Y-%m-%d-%H%M%S", time.localtime(time.time()))  # 获取当前时间
    # 拼接报告地址
    fileName = curPath + "/result/" + now + "report.html"
    # 出报告
    with open(fileName, "wb") as f:
        runner = HTMLTestRunner.HTMLTestRunner(f, title="测试报告", description="用例执行情况", verbosity=2)
        suite = createSuite()
        runner.run(suite)

在测试套件的主函数中添加上述代码,就可以生成一个 HTML 报告并保存到指定位置

报告打开之后如下所示:


103.png


标红的就是未通过的用例,点击 fail 还会显示详细的错误信息


5. 异常捕获与错误截图

在用例执行时如果可以将错误现场自动截图,那么就会给我们定位错误带来方便


编写一个函数,函数的主要功能就是截图并且保存到指定位置,此函数不以 “test_” 开头,即不让 unittest 自动执行该函数,将异常捕获之后,只在需 expect 中调用即可


使用 webdriver 下的 get_screenshot_as_file() 方法进行截图并保存

def test_search(self):
    driver = self.driver
    url = self.url
    driver.get(url)
    time.sleep(3)
    driver.find_element_by_id("sb_form_q").send_keys("python")
    time.sleep(3)
    driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
    time.sleep(3)
    try:
        self.assertNotEqual("python - 搜索", driver.title)
    except:
        self.saveScreenshot(driver, "search.png")
def saveScreenshot(self, driver, filename):
    # 创建文件夹保存截图
    if not os.path.exists("./img"):
        os.makedirs("./img")
    # 使用时间作为文件名
    now = time.strftime("%Y-%m-%d-%H%M%S", time.localtime(time.time()))
    driver.get_screenshot_as_file("./img/" + now + "-" + filename)
    time.sleep(1)

常被捕获之后就不会在控制台显示了,我们处理异常的方式是截图错误现场并保存起来

104.png


6. 数据驱动

前面我们所有的数据和用例都是写在一起的,但是如果想在同一个用例中测试多个不同数据,按照之前的方法就需要编写多个用例,但其实可以使用 ddt 数据驱动来完成,ddt 可以使一个用例测试多个数据


unittest 没有自带的数据驱动,所以我们需要使用 pip 另外下载 ddt 数据驱动


ddt 中常用的注解:


@ddt: 修饰测试类

@data: 修饰测试方法,参数是测试数据

@file_data: 修饰测试方法,参数是 JSON 文件名,以文件中的数据作为测试数据

@unpack: 当传递的数据是元组或者列表是,使用此注解修饰测试方法,ddt 就会自动将数据映射到参数上


1)测试多个不同数据

一个参数的多个不同值


使用 @ddt 参数修饰测试类,@data 注解修饰测试方法并传入参数的不同值,在方法的参数列表中添加一个参数作为本方法的测试参数

from ddt import ddt, data, unpack, file_data
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
@ddt
class bing(unittest.TestCase):
    def setUp(self):
        print("------setUp------")
        self.driver = webdriver.Chrome()
        self.url = "https://cn.bing.com/"
        self.driver.maximize_window()
        time.sleep(3)
    def tearDown(self):
        print("------tearDown------")
        self.driver.quit()
    # 分别测试 bing 搜索 python、java、rust
    @data("python", "java", "rust")
    def test_search(self, value):
        driver = self.driver
        url = self.url
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys(value)
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
        time.sleep(3)
if __name__ == "__main__":
    unittest.main()

多个参数的多个不同值


修饰的注解不变,变得只是 @data 注解中的数据,可以使用列表表示一组测试数据


需要注意的是:当有多个参数时,需要使用 @unpack 注解修饰测试方法以映射多个参数


如下形式表示两个参数的多组数据


@data([3, 2], [4, 3], [5, 3])

from selenium import webdriver
import time
import unittest
from ddt import data, ddt, file_data, unpack
@ddt
class baidu(unittest.TestCase):
    def setUp(self):
        print("------setUp------")
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.maximize_window()
        time.sleep(3)
    def tearDown(self):
        print("------tearDown------")
        self.driver.quit()
    @data(["python", "python_百度搜索"], ["java", "python_百度搜索"], ["rust", "python_百度搜索"])
    @unpack
    def test_search(self, value, expect_value):
        url = self.url
        driver = self.driver
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("kw").send_keys(value)
        time.sleep(3)
        driver.find_element_by_id("su").click()
        time.sleep(3)
        self.assertEqual(expect_value, driver.title, msg="网页未打开")
if __name__ == '__main__':
    unittest.main()

2)测试某个文件中的多组数据

txt 文件或者 csv 文件

同样使用使用 @data() 注解修饰测试方法,但是参数填入的是解析 txt/csv 文件的方法

需要注意的是:文件的开头一行必须是 data,后面每一行为一组数据,这是固定格式,如下所示



105.png


import csv
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.action_chains import ActionChains
from ddt import data, ddt, file_data, unpack
def get_txt(file_name):
    tmp_data = []
    with open("./data/" + file_name, "r") as f:
        readers = csv.reader(f, delimiter=",", quotechar="|")
        next(readers, None)
        for row in readers:
            rows = []
            for i in row:
                rows.append(i)
            tmp_data.append(rows)
        print(tmp_data)
        return tmp_data
@ddt
class baidu(unittest.TestCase):
    def setUp(self):
        print("------setUp------")
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.maximize_window()
        time.sleep(3)
    def tearDown(self):
        print("------tearDown------")
        self.driver.quit()
    @data(*get_txt("test_Baidu.txt"))
    @unpack
    def test_search(self, value, expect_value):
        url = self.url
        driver = self.driver
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("kw").send_keys(value)
        time.sleep(3)
        driver.find_element_by_id("su").click()
        time.sleep(3)
        self.assertEqual(expect_value, driver.title, msg="网页未打开")
if __name__ == '__main__':
    unittest.main(verbosity=2)

JSON 文件

使用 @file_data(json 文件名) 修饰测试方法即可调用 json 文件中的数据


import os.path
from ddt import ddt, data, unpack, file_data
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
@ddt
class bing(unittest.TestCase):
    def setUp(self):
        print("------setUp------")
        self.driver = webdriver.Chrome()
        self.url = "https://cn.bing.com/"
        self.driver.maximize_window()
        time.sleep(3)
    def tearDown(self):
        print("------tearDown------")
        self.driver.quit()
    # @data("python", "java", "rust")
    @file_data("./data/test_Bing.json")
    def test_search(self, value):
        driver = self.driver
        url = self.url
        driver.get(url)
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys(value)
        time.sleep(3)
        driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
        time.sleep(3)
if __name__ == "__main__":
    unittest.main()


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
14天前
|
测试技术 C# 数据库
C# 单元测试框架 NUnit 一分钟浅谈
【10月更文挑战第17天】单元测试是软件开发中重要的质量保证手段,NUnit 是一个广泛使用的 .NET 单元测试框架。本文从基础到进阶介绍了 NUnit 的使用方法,包括安装、基本用法、参数化测试、异步测试等,并探讨了常见问题和易错点,旨在帮助开发者有效利用单元测试提高代码质量和开发效率。
116 64
|
4天前
|
测试技术 Android开发 UED
探索软件测试中的自动化框架选择
【10月更文挑战第29天】 在软件开发的复杂过程中,测试环节扮演着至关重要的角色。本文将深入探讨自动化测试框架的选择,分析不同框架的特点和适用场景,旨在为软件开发团队提供决策支持。通过对比主流自动化测试工具的优势与局限,我们将揭示如何根据项目需求和团队技能来选择最合适的自动化测试解决方案。此外,文章还将讨论自动化测试实施过程中的关键考虑因素,包括成本效益分析、维护难度和扩展性等,确保读者能够全面理解自动化测试框架选择的重要性。
16 1
|
10天前
|
监控 安全 jenkins
探索软件测试的奥秘:自动化测试框架的搭建与实践
【10月更文挑战第24天】在软件开发的海洋里,测试是确保航行安全的灯塔。本文将带领读者揭开软件测试的神秘面纱,深入探讨如何从零开始搭建一个自动化测试框架,并配以代码示例。我们将一起航行在自动化测试的浪潮之上,体验从理论到实践的转变,最终达到提高测试效率和质量的彼岸。
|
13天前
|
Web App开发 敏捷开发 存储
自动化测试框架的设计与实现
【10月更文挑战第20天】在软件开发的快节奏时代,自动化测试成为确保产品质量和提升开发效率的关键工具。本文将介绍如何设计并实现一个高效的自动化测试框架,涵盖从需求分析到框架搭建、脚本编写直至维护优化的全过程。通过实例演示,我们将探索如何利用该框架简化测试流程,提高测试覆盖率和准确性。无论你是测试新手还是资深开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧。
|
2天前
|
机器学习/深度学习 自然语言处理 物联网
探索自动化测试框架的演变与未来趋势
随着软件开发行业的蓬勃发展,软件测试作为保障软件质量的重要环节,其方法和工具也在不断进化。本文将深入探讨自动化测试框架从诞生至今的发展历程,分析当前主流框架的特点和应用场景,并预测未来的发展趋势,为软件开发团队选择合适的自动化测试解决方案提供参考。
|
1月前
|
Web App开发 IDE 测试技术
自动化测试的利器:Selenium 框架深度解析
【10月更文挑战第2天】在软件开发的海洋中,自动化测试犹如一艘救生艇,让质量保证的过程更加高效与精准。本文将深入探索Selenium这一强大的自动化测试框架,从其架构到实际应用,带领读者领略自动化测试的魅力和力量。通过直观的示例和清晰的步骤,我们将一起学习如何利用Selenium来提升软件测试的效率和覆盖率。
|
5天前
|
测试技术 持续交付
探索软件测试中的自动化框架:优势与挑战
【10月更文挑战第28天】 随着软件开发的快速进步,自动化测试已成为确保软件质量的关键步骤。本文将探讨自动化测试框架的优势和面临的挑战,以及如何有效地克服这些挑战。
14 0
|
25天前
|
机器学习/深度学习 并行计算 数据可视化
目标分类笔记(二): 利用PaddleClas的框架来完成多标签分类任务(从数据准备到训练测试部署的完整流程)
这篇文章介绍了如何使用PaddleClas框架完成多标签分类任务,包括数据准备、环境搭建、模型训练、预测、评估等完整流程。
69 0
目标分类笔记(二): 利用PaddleClas的框架来完成多标签分类任务(从数据准备到训练测试部署的完整流程)
|
25天前
|
机器学习/深度学习 数据采集 算法
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)
这篇博客文章介绍了如何使用包含多个网络和多种训练策略的框架来完成多目标分类任务,涵盖了从数据准备到训练、测试和部署的完整流程,并提供了相关代码和配置文件。
42 0
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)
|
25天前
|
测试技术 Python
自动化测试项目学习笔记(三):Unittest加载测试用例的四种方法
本文介绍了使用Python的unittest框架来加载测试用例的四种方法,包括通过测试用例类、模块、路径和逐条加载测试用例。
53 0
自动化测试项目学习笔记(三):Unittest加载测试用例的四种方法