1. 程式人生 > >單元測試框架Uinttest一文詳解

單元測試框架Uinttest一文詳解

一談及unittest,大家都知道,unittest是Python中自帶的單元測試框架,它裡面封裝好了一些校驗返回的結果方法和一些用例執行前的初始化操作。unittest單元測試框架不僅可以適用於單元測試,還可以適用web自動化測試用例的開發與執行,該測試框架可組織執行測試用例,並且提供了豐富的斷言方法,判斷測試用例是否通過,最終生成測試結果。

在聊unittest時,需要先明白,最基礎的四個概念:TestCase,TestSuite,TestRunner,TestFixture,看如下靜態圖:

unittest執行流程

先編寫好TestCase,然後由TestLoader載入TestCase到TestSuite,其次由TextTestRunner來執行TestSuite,執行的結果儲存在TextTestResult中,我們通過命令列或者unittest.main()執行時,main會呼叫TextTestRunner中的run來執行,或者我們可以直接通過TextTestRunner來執行用例。

unittest模組的各個屬性說明

unittest.TestCase:TestCase類,所有測試用例類繼承的基本類。

class BaiduTest(unittest.TestCase):

unittest.main():使用它可以方便的將一個單元測試模組變為可直接執行的測試指令碼,main()方法使用TestLoader類來搜尋所有包含在該模組中以“test”命名開頭的測試方法,並自動執行他們。執行方法的預設順序是:根據ASCII碼的順序載入測試用例,數字與字母的順序為:0-9,A-Z,a-z。所以以A開頭的測試用例方法會優先執行,以a開頭會後執行。

unittest.TestSuite():unittest框架的TestSuite()類是用來建立測試套件的。

unittest.TextTextRunner():unittest框架的TextTextRunner()類,通過該類下面的run()方法來執行suite所組裝的測試用例,入參為suite測試套件。

unittest.defaultTestLoader(): defaultTestLoader()類,通過該類下面的discover()方法可自動更具測試目錄start_dir匹配查詢測試用例檔案(test*.py),並將查詢到的測試用例組裝到測試套件,因此可以直接通過run()方法執行discover。用法如下:

discover=unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')

unittest.skip():裝飾器,當執行用例時,有些用例可能不想執行等,可用裝飾器暫時遮蔽該條測試用例。一種常見的用法就是比如說想除錯某一個測試用例,想先遮蔽其他用例就可以用裝飾器遮蔽。

@unittest.skip(reason): skip(reason)裝飾器:無條件跳過裝飾的測試,並說明跳過測試的原因。

@unittest.skipIf(reason):skipIf(condition,reason)裝飾器:條件為真時,跳過裝飾的測試,並說明跳過測試的原因。

@unittest.skipUnless(reason):skipUnless(condition,reason)裝飾器:條件為假時,跳過裝飾的測試,並說明跳過測試的原因。

@unittest.expectedFailure():expectedFailure()測試標記為失敗。

setUp():setUp()方法用於每個測試用例執行前的初始化工作。如測試用例中需要訪問資料庫,可以在setUp中建立資料庫連線並進行初始化。如測試用例需要登入web,可以先例項化瀏覽器。

tearDown():tearDown()方法用於每個測試用例執行之後的善後工作。如關閉資料庫連線、關閉瀏覽器。

setUpClass():setUpClass()方法用於所有測試用例前的設定工作。

tearDownClass():tearDownClass()方法用於所有測試用例執行後的清理工作。

addTest():addTest()方法是將測試用例新增到測試套件中。

run():run()方法是執行測試套件的測試用例,入參為suite測試套件。

assert*():在執行測試用例的過程中,最終用例是否執行通過,是通過判斷測試得到的實際結果和預期結果是否相等決定的。

斷言方式如下:

assertEqual(a,b,[msg='測試失敗時列印的資訊']):斷言a和b是否相等,相等則測試用例通過。

assertNotEqual(a,b,[msg='測試失敗時列印的資訊']):斷言a和b是否相等,不相等則測試用例通過。

assertTrue(x,[msg='測試失敗時列印的資訊']):斷言x是否True,是True則測試用例通過。

assertFalse(x,[msg='測試失敗時列印的資訊']):斷言x是否False,是False則測試用例通過。

assertIs(a,b,[msg='測試失敗時列印的資訊']):斷言a是否是b,是則測試用例通過。

assertNotIs(a,b,[msg='測試失敗時列印的資訊']):斷言a是否是b,不是則測試用例通過。

assertIsNone(x,[msg='測試失敗時列印的資訊']):斷言x是否None,是None則測試用例通過。

assertIsNotNone(x,[msg='測試失敗時列印的資訊']):斷言x是否None,不是None則測試用例通過。

assertIn(a,b,[msg='測試失敗時列印的資訊']):斷言a是否在b中,在b中則測試用例通過。

assertNotIn(a,b,[msg='測試失敗時列印的資訊']):斷言a是否在b中,不在b中則測試用例通過。

assertIsInstance(a,b,[msg='測試失敗時列印的資訊']):斷言a是是b的一個例項,是則測試用例通過。

assertNotIsInstance(a,b,[msg='測試失敗時列印的資訊']):斷言a是是b的一個例項,不是則測試用例通過。

說了這麼多的屬性,接下來用一段程式碼舉例:

unittest基本程式碼示例:

import unittest
from unittestbasic1.unittest_operation import *

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_basic.py
@time: 2019-09-26 23:38
@desc:
'''

class TestMathFunc(unittest.TestCase):

    # TestCase基類方法,所有case執行之前自動執行
    @classmethod
    def setUpClass(cls):
        print("*************這裡是所有測試用例前的準備工作*************")

    # TestCase基類方法,所有case執行之後自動執行
    @classmethod
    def tearDownClass(cls):
        print("*************這裡是所有測試用例後的清理工作*************")

    # TestCase基類方法,每次執行case前自動執行
    def setUp(self):
        print("-------------這裡是一個測試用例前的準備工作-------------")

    # TestCase基類方法,每次執行case後自動執行
    def tearDown(self):
        print("-------------這裡是一個測試用例後的清理工作-------------")

    @unittest.skip("我想臨時跳過這個測試用例.")
    def test_add(self):
        self.assertEqual(3, add(1, 2))
        self.assertNotEqual(3, add(2, 2))  # 測試業務方法add

    def test_minus(self):
        self.skipTest('跳過這個測試用例')
        self.assertEqual(1, minus(3, 2))  # 測試業務方法minus

    def test_multi(self):
        self.assertEqual(6, multi(2, 3))  # 測試業務方法multi

    def test_divide(self):
        self.assertEqual(2, divide(6, 3))  # 測試業務方法divide
        self.assertEqual(2.5, divide(5, 2))


if __name__ == '__main__':
    unittest.main(verbosity=2)

基本函式如下:

def add(a, b):
    return a+b


def minus(a, b):
    return a-b


def multi(a, b):
    return a*b


def divide(a, b):
    return a/b

執行程式碼,結果如下所示:

 

理論加實踐,結合起來看,應該更容易明白些,切莫做紙上談兵。

新增用例執行方式

弄明白了unittest的基本屬性,接下來分享下新增用例執行的不同方式

方式一:使用addTests單個新增

用addTests方法單個新增用例,程式碼示例如下:

import unittest
from unittestbasic2.unittest_operation2 import TestMathFunc

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite1.py
@time: 2019-09-26 23:38
@desc:
'''

if __name__ == '__main__':
    suite = unittest.TestSuite()
    # 單個新增測試用例
    suite.addTest(TestMathFunc("test_multi"))
    suite.addTest(TestMathFunc("test_divide"))
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

方式二:測試用例新增到用例集中

從方法一種看,單個新增用例比較麻煩,該方法可以將用例新增到一個用例集中,再通過suite統一執行,程式碼示例如下:

import unittest
from unittestbasic2.unittest_operation2 import TestMathFunc

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite2.py
@time: 2019-09-26 23:38
@desc:
'''

if __name__ == '__main__':
    suite = unittest.TestSuite()
    # 將測試用例新增到一個用例集中
    tests = [TestMathFunc("test_add"), TestMathFunc("test_minus")]
    suite.addTests(tests)  # 將測試用例列表新增到測試組中
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

方式三:TestLoader方法載入用例

使用TestLoader方法載入用例,就是無法對case進行排序,執行順序是隨機的。實現方法有如下三種,程式碼示例如下:

import unittest
from unittestbasic2.unittest_operation2 import TestMathFunc

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite3.py
@time: 2019-09-26 23:44
@desc:
'''

if __name__ == '__main__':
    suite = unittest.TestSuite()
    # 第一種方法:傳入'模組名.TestCase名'
    # 用addTests + TestLoader。不過用TestLoader的方法是無法對case進行排序的
    # loadTestsFromName(),傳入'模組名.TestCase名'
    # ①
    # suite.addTests(unittest.TestLoader().loadTestsFromName('unittest_operation2.TestMathFunc'))
    # suite.addTests(unittest.TestLoader().loadTestsFromName('unittest_operation1.TestMathFunc'))
    # 這裡還可以把'模組名.TestCase名'放到一個列表中
    # ②
    # suite.addTests(unittest.TestLoader().loadTestsFromNames(['unittest_operation2.TestMathFunc', 'unittest_operation1.TestMathFunc']))

    # 第二種方法:傳入TestCase 需要匯入對應模組
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

方式四:通過discover()方法

看了前三種實現方式,都比較單一,實際運用中,case數量不僅僅只有一兩個,一個一個去新增的話,那得多麻煩。故而引用discover()方法,匹配指定資料夾下以test開頭的測試用例,程式碼示例如下:

import unittest

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite4.py
@time: 2019-09-26 23:46
@desc:
'''

if __name__ == '__main__':
    path = './testcase'
    all_cases = unittest.defaultTestLoader.discover(path, 'test*.py')
    # 找到某個目錄下所有的以test開頭的Python檔案裡面的測試用例
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(all_cases)

如上所述,就是常見的用例執行方式了,不過,運用最多的還是最後一種方式。

測試報告

在做測試時,自然是要生成測試報告的,一份好的測試報告,可以很直觀的看出當前程式碼構建的系統狀況,也可以幫助自己很快定位問題。

前段時間,在github上搜索到一份帶截圖的測試報告模板,所以引用使用了一番,整體很不錯,自己也略小有調整,將報告模板引用到程式碼中即可使用。

說到測試報告,再使用之前的程式碼,就實現不了,需要引用HTMLTestRunner。我一般使用html格式的報告,還有一種是xml格式的,使用jenkins構建程式碼的話,那就需要生成xml格式的測試報告了。

html格式測試報告

先來看如何生成html格式的測試報告,程式碼示例如下:

import unittest
from utils.HTMLTestRunner_cn import HTMLTestRunner
import time

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite5.py
@time: 2019-09-26 23:58
@desc:
'''

if __name__ == '__main__':
    path = './testcase'
    report_file = '../'
    # 找到某個目錄下所有的以test開頭的Python檔案裡面的測試用例
    all_cases = unittest.defaultTestLoader.discover(path, 'test*.py')

    report_time = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
    report_file_path = report_file + '/report/' + report_time + '-result.html'
    file_result = open(report_file_path, 'wb')
    runner = HTMLTestRunner(stream=file_result, title="簡單運算demo", description="溫一壺清酒", verbosity=2, retry=2,
                           save_last_try=True)
    runner.run(all_cases)

執行後,檢視測試報告,如下所示:

以自己的產品做了個登入測試,生成的測試報告更全面,程式碼示例如下:

from selenium import webdriver
import unittest
import time
from utils.HTMLTestRunner_cn import HTMLTestRunner

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittestlogin.py
@time: 2019-09-22 11:38
@desc:
'''


class case_01(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.driver = webdriver.Chrome()
        cls.driver.maximize_window()

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()

    def add_img(self):
        self.imgs.append(self.driver.get_screenshot_as_base64())
        return True

    def setUp(self):
        # 在是python3.x 中,如果在這裡初始化driver ,因為3.x版本 unittestbasic1 執行機制不同,會導致用力失敗時截圖失敗
        self.driver.get("")
        self.driver.implicitly_wait(30)
        self.imgs = []
        self.addCleanup(self.cleanup)

    def cleanup(self):
        pass

    def test_case1(self):
        """ 正常登入"""
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
        self.add_img()
        self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
        time.sleep(1)
        self.add_img()
        self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
        time.sleep(1)
        self.add_img()
        self.driver.find_element_by_css_selector("button[type='button']").click()
        time.sleep(3)
        self.add_img()
        user_name = self.driver.find_element_by_css_selector(".user-name").text
        self.assertEqual("一壺清酒stage1111", user_name, msg="登入失敗!!")
        self.driver.find_element_by_css_selector(".user-name").click()
        time.sleep(3)
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[2]/div[1]/div[2]/div[5]/div[3]/ul/li[2]/span/i").click()
        print("退出登入")
        time.sleep(5)

    def test_case2(self):
        """ 賬號不存在"""
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
        self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
        time.sleep(1)
        self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
        time.sleep(1)
        self.driver.find_element_by_css_selector("button[type='button']").click()
        time.sleep(1)
        self.add_img()
        print("登入失敗!")

    def test_case3(self):
        """ 密碼錯誤"""
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
        self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
        time.sleep(1)
        self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
        time.sleep(1)
        self.driver.find_element_by_css_selector("button[type='button']").click()
        time.sleep(1)
        self.add_img()
        print("登入失敗!")

    @unittest.skip("臨時跳過這個測試用例")
    def test_case4(self):
        """ 跳過該用例"""
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
        self.driver.find_element_by_css_selector("input[type='text']").text()
        time.sleep(1)
        self.driver.find_element_by_css_selector("input[type='password']").text()
        time.sleep(1)
        self.driver.find_element_by_css_selector("button[type='button']").click()
        time.sleep(3)

    @unittest.skipIf(3 > 2, "判斷為真,此用例不執行")
    def test_case5(self):
        """ skipIf判斷,為真則不執行"""
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
        self.driver.find_element_by_css_selector("input[type='text']").text()
        time.sleep(1)
        self.driver.find_element_by_css_selector("input[type='password']").text()
        time.sleep(1)
        self.driver.find_element_by_css_selector("button[type='button']").click()
        time.sleep(3)

    @unittest.skipUnless(3 < 2, "判斷為假,此用例不執行")
    def test_case6(self):
        """ skipIf判斷,為假則不執行"""
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
        self.driver.find_element_by_css_selector("input[type='text']").text()
        time.sleep(1)
        self.driver.find_element_by_css_selector("input[type='password']").text()
        time.sleep(1)
        self.driver.find_element_by_css_selector("button[type='button']").click()
        time.sleep(3)

    def test_case7(self):
        """ 斷言失敗"""
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[1]/div[1]/div[2]/div[1]/div/div[1]/div/div/div/div/div[3]").click()
        self.driver.find_element_by_css_selector("input[type='text']").send_keys("")
        time.sleep(1)
        self.driver.find_element_by_css_selector("input[type='password']").send_keys("")
        time.sleep(1)
        self.driver.find_element_by_css_selector("button[type='button']").click()
        time.sleep(3)
        user_name = self.driver.find_element_by_css_selector(".user-name").text
        self.assertEqual("一壺清酒stage", user_name, msg="登入失敗!!")
        self.driver.find_element_by_css_selector(".user-name").click()
        time.sleep(3)
        self.driver.find_element_by_xpath(
            "//*[@id='app']/div/div[2]/div[1]/div[2]/div[5]/div[3]/ul/li[2]/span/i").click()
        print("退出登入")


if __name__ == "__main__":
    suites = unittest.TestSuite()
    suites.addTest(case_01("test_case1"))
    suites.addTest(case_01("test_case2"))
    suites.addTest(case_01("test_case3"))
    suites.addTest(case_01("test_case4"))
    suites.addTest(case_01("test_case5"))
    suites.addTest(case_01("test_case6"))
    suites.addTest(case_01("test_case7"))
    report_time = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
    report_file = '../'
    report_file_path = report_file + '/report/' + report_time + '-result.html'
    file_result = open(report_file_path, 'wb')
    # retry重試次數
    runer = HTMLTestRunner(stream=file_result, title="帶截圖的測試報告", description="溫一壺清酒", verbosity=2, retry=0,
                           save_last_try=True)
    runer.run(suites)

檢視測試報告如下所示:

xml格式測試報告

生成xml格式的測試報告,程式碼示例如下:

import unittest
import time
import xmlrunner

'''
@author: wenyihuqingjiu
@project: unittestdemo
@file: unittest_demo_suite6.py
@time: 2019-09-27 00:11
@desc:
'''

if __name__ == '__main__':
    path = './testcase'
    report_file = '../'
    # 找到某個目錄下所有的以test開頭的Python檔案裡面的測試用例
    all_cases = unittest.defaultTestLoader.discover(path, 'test*.py')

    report_time = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
    report_file_path = report_file + '/report/'
    runner = xmlrunner.XMLTestRunner(verbosity=2, output=report_file_path)    # output 報告存放路徑
    runner.run(all_cases)

生成的報告如下所示:

問題

生成的測試報告,傳送到郵箱中,預覽附件時,部分文字是亂碼,樣式按鈕也不可點選,是因為樣式檔案被瀏覽器攔截了嘛?

預覽檔案如下:

 

本文僅代表作者觀點,系作者@溫一壺清酒發表。
歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。
文章出處:http://www.cnblogs.com/hong-fithing/