├── .gitignore ├── LICENSE ├── README.md ├── demo ├── custom_test_loader.py ├── discover_used.py ├── unittest_assert │ ├── __init__.py │ ├── test_assert_equal.py │ └── test_assert_other.py ├── unittest_base │ ├── __init__.py │ ├── calculator.py │ ├── test_calculator.py │ └── test_improper_use.py ├── unittest_data_driver │ ├── __init__.py │ ├── data │ │ ├── test_data_dict_dict.json │ │ └── test_data_dict_dict.yaml │ ├── file_data │ │ ├── file_data_csv.csv │ │ ├── file_data_dict.json │ │ ├── file_data_dict.yaml │ │ └── file_data_excel.xlsx │ ├── test_case │ │ ├── config.py │ │ └── test_ddt_file_demo.py │ ├── test_ddt_demo.py │ ├── test_ddt_file_demo.py │ └── test_parameterized_demo.py ├── unittest_extend │ ├── django_test.py │ ├── flask_test.py │ ├── nose2_test.py │ ├── seldom_test.py │ └── testify_test.py ├── unittest_fixture │ ├── test_fixture_class.py │ ├── test_fixture_method.py │ └── test_fixture_module.py ├── unittest_other │ ├── test_02_loader.py │ ├── test_example.py │ └── test_main.py ├── unittest_skip │ ├── test_defined_skip.py │ ├── test_skip.py │ └── test_unittest_skip.py └── unittest_sub │ ├── __init__.py │ └── test_subtest.py ├── drawios └── history.drawio ├── images ├── histroy.png ├── html_report.png ├── json_report.png ├── test_fixture.jpg └── unittest.png ├── plugin ├── data_driver │ ├── __init__.py │ ├── conversion.py │ ├── param.py │ └── param_demo.py ├── htmlrunner │ ├── result.py │ ├── runner.py │ └── template │ │ └── report.html ├── jsonrunner │ ├── result.py │ └── runner.py ├── label_plugin │ ├── LabelTestRunner.py │ └── __init__.py ├── reports │ ├── result.html │ └── result.json ├── test_htmlrunner.py ├── test_jsonrunner.py ├── test_label.py └── test_param.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | *.log 4 | report 5 | build 6 | dist 7 | seldom.egg-info 8 | log/ 9 | .history -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn-unittest-class 2 | 3 | 📺[B站视频](https://www.bilibili.com/video/BV1JM4m167oR/?spm_id_from=333.999.0.0&vd_source=0c31d5ad24bbabdbefec07429cf847dc),正在同步更新中~~! 4 | 5 | ## 前言 6 | 7 | 我们在学习自动化测试的时候,核心就是学习三个技术: 8 | 9 | * 编程语言 10 | * 单元测试框架 ⭐︎ 11 | * 测试库 12 | 13 | `unittest` 作为Python标准库中的单元测试框架,仍然可以满足我们的绝大部分单元测试相关工作,虽然,`pytest` 正在变得更加流行。 `unittest`仍未过时,或者到了要被完全抛弃的地步,很多时候我们觉得`unittest` 不是太好用,一方面是因为对它不是足够了解,另一方面它的生态(第三方扩展插件)比较糟糕。 14 | 15 | ### unittest 优势 16 | 17 | `unittest` 仍然是非常优秀的单元测试框架,以下是他的优势和特点。 18 | 19 | * `标准库集成`:unittest 是 Python 标准库的一部分,安装 Python 时默认可用,不需要额外安装。 20 | 21 | * `面向对象的设计`:通过继承 `unittest.TestCase` 组织测试代码,结构清晰,便于扩展和维护。 22 | 23 | * `丰富的断言方法`:提供了多种断言方法,方便进行各种类型的测试检查。 24 | 25 | 本课程希望深入和全面的介绍 `unittest`的使用,以及教你如何开发 `unittest` 扩展插件,来满足单元测试/自动化测试相关工作。 26 | 27 | 28 | * 课程大纲 29 | 30 | ![](/images/unittest.png) 31 | 32 | ## unittest历史 33 | 34 | * unitest 发展轨迹: 35 | 36 | ![](./images/histroy.png) 37 | 38 | ### PyUnit 39 | 40 | PyUnit是最早的Python单元测试框架,其灵感来源于JUnit(Java中的一个单元测试框架)。 由Steve Purcell开发,并成为Python社区的一个非官方标准。PyUnit 不再作为一个独立的框架存在。它的功能和设计思想已经完全融入了 unittest 模块。 41 | 42 | ### unittest 43 | 44 | unittest是Python标准库中的单元测试框架,实际上是对PyUnit的标准化和集成。在`Python 2.1` 中首次引入,作为标准库的一部分,以后Python的每个版本都内置了unittest。 它提供了丰富的测试功能,包括测试用例(TestCase)、测试套件(TestSuite)、测试加载器(TestLoader)、测试运行器(TestRunner)和各种断言方法。 45 | 46 | __主要变化和改进__ 47 | 48 | * 组织结构:unittest模块的结构更加清晰,便于扩展和使用。 49 | 50 | * 改进的断言方法:unittest引入了更多的断言方法,如assertEqual、assertTrue等,方便测试编写。 51 | 52 | * 测试发现:支持自动发现测试用例的机制,使得测试组织更加灵活。 53 | 54 | * 测试夹具(Fixture):支持setUp和tearDown方法,用于在测试前后进行初始化和清理工作。 55 | 56 | ### unittest2 57 | 58 | unittest2是unittest的增强版,主要用于提供在较旧版本Python中引入的unittest功能。其目标是为还没有升级到新版本Python的用户提供最新的unittest功能。在`Python 2.7` 和`Python 3.2`中,unittest模块进行了重大改进和增强,这些改进也被包含在unittest2中。 59 | 60 | __主要变化和改进__ 61 | 62 | * 改进的测试发现机制:更加智能和灵活的测试用例发现功能。 63 | 64 | * 更强大的断言方法:添加了更多断言方法,比如assertIs、assertIsNone、assertIn等。 65 | 66 | * 基于类的setUpClass和tearDownClass方法:用于在整个测试类开始前和结束后执行一些初始化和清理工作。 67 | 68 | * 基于方法的setUpModule和tearDownModule方法:用于在整个测试模块开始前和结束后执行一些初始化和清理工作。 69 | 70 | * 测试跳过和预期失败:引入了@unittest.skip装饰器和相关功能,用于标记跳过的测试和预期会失败的测试。 71 | 72 | ### 总结 73 | 74 | * PyUnit 是早期的单元测试框架,启发了后来的unittest。 75 | 76 | * unittest 是Python标准库中的单元测试框架,基于PyUnit,提供了更丰富和标准化的测试功能。 77 | 78 | * unittest2 是unittest的增强版,主要为了提供在较旧版本Python中引入新功能的支持,但随着Python 2.7和Python 79 | 3.2之后的版本逐渐成为主流,unittest2的使用也逐渐减少。 80 | 81 | 因此,PyUnit、unittest和unittest2基本上是同一个框架的不同演进阶段,而unittest成为了现在Python标准库中的正式单元测试框架。 82 | 83 | 84 | ## 基于unittest的库和框架 85 | 86 | ### django TestCase 87 | 88 | Django 提供的 TestCase 类继承自 unittest.TestCase,并进行了扩展以支持 Django 应用的测试。 89 | 90 | ```python 91 | from django.test import TestCase 92 | 93 | 94 | class IndexPageTest(TestCase): 95 | """ 96 | 测试index登录首页 97 | """ 98 | 99 | def test_index_page_renders_index_template(self): 100 | """ 101 | 断言是否用给定的index.html模版响应 102 | :return: 103 | """ 104 | response = self.client.get('/index/') 105 | self.assertEqual(response.status_code, 200) 106 | self.assertTemplateUsed(response, 'index.html') 107 | ``` 108 | 109 | * 优势:内置对 Django ORM、视图和模板的支持,可以方便地测试 Django 应用的各个方面。 110 | 111 | ### Flask-Testing 112 | 113 | ```python 114 | from urllib import request 115 | from flask import Flask 116 | from flask_testing import LiveServerTestCase 117 | 118 | 119 | class MyTest(LiveServerTestCase): 120 | 121 | def create_app(self): 122 | app = Flask(__name__) 123 | app.config['TESTING'] = True 124 | # Default port is 5000 125 | app.config['LIVESERVER_PORT'] = 8943 126 | # Default timeout is 5 seconds 127 | app.config['LIVESERVER_TIMEOUT'] = 10 128 | return app 129 | 130 | def test_server_is_up_and_running(self): 131 | response = request.urlopen(self.get_server_url()) 132 | self.assertEqual(response.code, 200) 133 | 134 | ``` 135 | 136 | * 优势:提供了对 Flask 应用的全面测试支持,包括视图测试和上下文管理。 137 | 138 | ### Testify 139 | 140 | Testify 是由 Yelp 开发的一个测试框架,旨在替代 unittest,提供更强大的功能。 141 | 142 | ```python 143 | from testify import * 144 | 145 | 146 | class AdditionTestCase(TestCase): 147 | 148 | @class_setup 149 | def init_the_variable(self): 150 | self.variable = 0 151 | 152 | @setup 153 | def increment_the_variable(self): 154 | self.variable += 1 155 | 156 | def test_the_variable(self): 157 | assert_equal(self.variable, 1) 158 | 159 | @suite('disabled', reason='ticket #123, not equal to 2 places') 160 | def test_broken(self): 161 | # raises 'AssertionError: 1 !~= 1.01' 162 | assert_almost_equal(1, 1.01, threshold=2) 163 | 164 | @teardown 165 | def decrement_the_variable(self): 166 | self.variable -= 1 167 | 168 | @class_teardown 169 | def get_rid_of_the_variable(self): 170 | self.variable = None 171 | 172 | 173 | if __name__ == "__main__": 174 | run() 175 | ``` 176 | 177 | * 优势:提供了更直观的测试 API 和强大的测试功能,如分布式测试和并行执行。 178 | 179 | ### nose2 180 | 181 | nose2 是 nose 的继任者,旨在提供一个扩展性强、易于使用的测试框架。 nose2将unittest扩展,使测试更加方便。 182 | 183 | ```python 184 | import unittest 185 | from nose2.tools import params 186 | 187 | 188 | class TestStrings(unittest.TestCase): 189 | def test_upper(self): 190 | self.assertEqual("spam".upper(), "SPAM") 191 | 192 | 193 | @params("Sir Bedevere", "Miss Islington", "Duck") 194 | def test_is_knight(value): 195 | assert value.startswith('Sir') 196 | ``` 197 | 198 | * 优势:提供了自动化发现和运行测试用例的功能,支持丰富的插件和扩展。 199 | 200 | * 运行 201 | 202 | ```shell 203 | > nose2 -v 204 | ``` 205 | 206 | ### Seldom 207 | 208 | seldom 是基于unittest 的自动化测试框架。 209 | 210 | ````python 211 | import seldom 212 | 213 | 214 | class YouTest(seldom.TestCase): 215 | 216 | def test_case(self): 217 | """a simple test case """ 218 | self.assertEqual(1 + 1, 2) 219 | 220 | 221 | if __name__ == '__main__': 222 | seldom.main() 223 | ```` 224 | 225 | * 优势:seldom是一个全功能测试框架,支持 Web/App/API测试等。 226 | 227 | ## unittest 基础 228 | 229 | 👉 [unittest — Unit testing framework](https://docs.python.org/3/library/unittest.html) 230 | 231 | 在unittest文档中有四个重要的概念: Test Case、Test Suite、Test Runner和Test Fixture。只有理解了这几个概念,才能理解单元测试的基本特征。 232 | 233 | * Test Case 234 | 235 | Test Case是最小的测试单元,用于检查特定输入集合的特定返回值。unittest提供了`TestCase`基类,我们创建的测试类需要继承该基类,它可以用来创建新的测试用例。 236 | 237 | * Test Suite 238 | 239 | 测试套件是测试用例、测试套件或两者的集合,用于组装一组要运行的测试。unittest提供了`TestSuite`类来创建测试套件。 240 | 241 | * Test Runner 242 | 243 | Test Runner是一个组件,用于协调测试的执行并向用户提供结果。Test Runner可以使用图形界面、文本界面或返回特殊值来展示执行测试的结果。unittest提供了`TextTestRunner`类运行测试用例。 244 | 245 | * Test Fixture 246 | 247 | Test Fixture代表执行一个或多个测试所需的环境准备,以及关联的清理动作。例如,创建临时或代理数据库、目录,或启动服务器进程。unittest中提供了`setUp()`/`tearDown()`、`setUpClass()`/`tearDownClass()` 248 | 等方法来完成这些操作。 249 | 250 | ### 第一个测试用例 251 | 252 | * 计算器 253 | 254 | 实现一个`Calculator` 计算器类,`add()` 方法用于参数的加法计算。 255 | 256 | ```python 257 | # calculator.py 258 | 259 | class Calculator: 260 | """ 261 | 计算器 262 | """ 263 | 264 | def __init__(self, *args): 265 | self.args = args 266 | 267 | def add(self): 268 | """ 269 | 加法运算 270 | """ 271 | return sum(self.args) 272 | ``` 273 | 274 | * 测试计算器 275 | 276 | 通过`unittest` 编写`Calculator`类的测试用例。 277 | 278 | ```python 279 | # test_calculator.py 280 | import unittest 281 | 282 | from calculator import Calculator 283 | 284 | 285 | # 1. 测试类 TestCalculator 必须继承 unittest.TestCase 286 | class TestCalculator(unittest.TestCase): 287 | 288 | def setUp(self): 289 | """ 290 | 用例前置动作:启动浏览器、连接数据库,准备的数据 等。 291 | """ 292 | print("test start") 293 | 294 | def tearDown(self): 295 | """ 296 | 用例后置动作:关闭浏览器,关闭数据库,删除/还原数据 等。 297 | """ 298 | print("test end") 299 | 300 | # 2.测试用例必须以 test 开头 301 | def test_add_one(self): 302 | c = Calculator(2) 303 | result = c.add() 304 | self.assertEqual(result, 2) 305 | 306 | def test_add_two(self): 307 | c = Calculator(3, 5) 308 | result = c.add() 309 | self.assertEqual(result, 8) 310 | 311 | def test_add_three(self): 312 | c = Calculator(3, 5) 313 | result = c.add() 314 | self.assertEqual(result, 8) 315 | 316 | 317 | if __name__ == '__main__': 318 | # 创建测试套件 319 | suit = unittest.TestSuite() 320 | suit.addTest(TestCalculator("test_add_one")) 321 | suit.addTest(TestCalculator("test_add_two")) 322 | suit.addTest(TestCalculator("test_add_three")) 323 | 324 | # 创建测试运行器 325 | runner = unittest.TextTestRunner() 326 | runner.run(suit) 327 | ``` 328 | 329 | * 运行测试 330 | 331 | ```bash 332 | python test_calculator.py 333 | 334 | test start 335 | test end 336 | .test start 337 | test end 338 | .test start 339 | test end 340 | . 341 | ---------------------------------------------------------------------- 342 | Ran 3 tests in 0.001s 343 | 344 | OK 345 | ``` 346 | 347 | ### 错误的用例设计 348 | 349 | 一些新人在使用 unittest 设计用例是往往会犯一些低级的错误。 350 | 351 | ❌ 错误的方法。 352 | 353 | ```py 354 | import unittest 355 | 356 | 357 | class TestImproperUse(unittest.TestCase): 358 | """ 359 | 用例错误的设计 360 | """ 361 | 362 | def test_login(self, username, password): 363 | """ 364 | 1. 给用例加参数 365 | """ 366 | print("this is login case") 367 | 368 | def test_case_1(self): 369 | print("this is test case 1") 370 | 371 | def test_case_2(self): 372 | """ 373 | 2. 在一条用例里面调用另一条用例。 374 | """ 375 | self.test_case_1() 376 | print("this is test case 2") 377 | 378 | 379 | if __name__ == '__main__': 380 | unittest.main() 381 | ``` 382 | 383 | __1. 给用例加参数。__ 384 | 385 | 用例在执行的时候会报验证的错误,默认用例执行的时候是通过加载器去加载用例的。所以,加载器不知道`test_login()` 所需要的参数,以及如何穿参数。 386 | 387 | __2. 在一条用例里面调用另一条用例。__ 388 | 389 | 运行的时候,代码上没有问题。但这样设计是错误。每个用例都是独立的个体,不应该被另一条用例调用。如果两条用例都用到了相同的操作,应该把相同的操作封装成一个功能的方法,然后分别被两条用例调用。 390 | 391 | ✔️ 正确的方法 392 | 393 | ```python 394 | import unittest 395 | 396 | 397 | class CorrectUsage(unittest.TestCase): 398 | 399 | def login(self, username, password): 400 | """封装的登录""" 401 | print("this is login method") 402 | 403 | def test_case_1(self): 404 | self.login("admin", "admin123") 405 | print("this is test case 1") 406 | 407 | def test_case_2(self): 408 | self.login("guest", "guest123") 409 | print("this is test case 2") 410 | 411 | ``` 412 | 413 | ### 命令行工具 414 | 415 | #### unitest 命令使用 416 | 417 | unittest模块可以从命令行中使用,来运行来自模块、类甚至单个测试方法的测试。 418 | 419 | ```bash 420 | python -m unittest test_file1 test_file2 421 | python -m unittest test_file.TestClass 422 | python -m unittest test_file.TestClass.test_method 423 | ``` 424 | 425 | > `python -m` 以脚本形式运行库模块. 426 | 427 | 可以传入包含任何组合模块名称和完全限定的类或方法名称的列表。 428 | 429 | 测试模块也可以通过文件路径指定: 430 | 431 | ```bash 432 | python -m unittest tests/test_something.py 433 | ``` 434 | 435 | 这样可以让你使用shell文件名自动补全来指定测试模块。指定的文件仍然必须可以作为一个模块进行导入。路径会被转换成模块名,去掉`.py`并将路径分隔符转换为`.`。 436 | 437 | 通过传入`-v` 选项 来运行更详细的测试: 438 | 439 | ```bash 440 | python -m unittest -v test_file 441 | ``` 442 | 443 | ❗ __使用 `python -m unittest` 不允许只指定目录名,不然检测不到任何用例。__ 444 | 445 | 目录结构: 446 | 447 | ``` 448 | tests/ 449 | ├── __init__.py 450 | ├── test_file1.py 451 | └── test_file2.py 452 | ``` 453 | 454 | 指定目录测试: 455 | 456 | ```bash 457 | python -m unittest tests 458 | 459 | ---------------------------------------------------------------------- 460 | Ran 0 tests in 0.000s 461 | 462 | NO TESTS RAN 463 | ``` 464 | 465 | 466 | 当不带参数执行时,会启用`Test Discovery`(后面会介绍 `discovery()` 方法): 467 | 468 | ```bash 469 | python -m unittest 470 | ``` 471 | 472 | #### unitest 命令选项 473 | 474 | 获取所有命令行选项的列表: 475 | 476 | ```bash 477 | python -m unittest -h 478 | 479 | usage: python.exe -m unittest [-h] [-v] [-q] [--locals] [-f] [-c] [-b] [-k TESTNAMEPATTERNS] [tests ...] 480 | 481 | positional arguments: 482 | tests a list of any number of test modules, classes and test methods. 483 | 484 | options: 485 | -h, --help show this help message and exit 486 | -v, --verbose Verbose output 487 | -q, --quiet Quiet output 488 | --locals Show local variables in tracebacks 489 | -f, --failfast Stop on first fail or error 490 | -c, --catch Catch Ctrl-C and display results so far 491 | -b, --buffer Buffer stdout and stderr during tests 492 | -k TESTNAMEPATTERNS Only run tests which match the given substring 493 | 494 | Examples: 495 | python.exe -m unittest test_module - run tests from test_module 496 | python.exe -m unittest module.TestClass - run tests from module.TestClass 497 | python.exe -m unittest module.Class.test_method - run specified test method 498 | python.exe -m unittest path/to/test_file.py - run tests from test_file.py 499 | 500 | usage: python.exe -m unittest discover [-h] [-v] [-q] [--locals] [-f] [-c] [-b] [-k TESTNAMEPATTERNS] [-s START] 501 | [-p PATTERN] [-t TOP] 502 | 503 | options: 504 | -h, --help show this help message and exit 505 | -v, --verbose Verbose output 506 | -q, --quiet Quiet output 507 | --locals Show local variables in tracebacks 508 | -f, --failfast Stop on first fail or error 509 | -c, --catch Catch Ctrl-C and display results so far 510 | -b, --buffer Buffer stdout and stderr during tests 511 | -k TESTNAMEPATTERNS Only run tests which match the given substring 512 | -s START, --start-directory START 513 | Directory to start discovery ('.' default) 514 | -p PATTERN, --pattern PATTERN 515 | Pattern to match tests ('test*.py' default) 516 | -t TOP, --top-level-directory TOP 517 | Top level directory of project (defaults to start directory) 518 | 519 | For test discovery all test modules must be importable from the top level directory of the project. 520 | ``` 521 | 522 | __重要选项说明__ 523 | 524 | * `-b` / `--buffer` : 测试运行期间,标准输出和标准错误流会被缓冲。通过测试时的输出会被丢弃,测试失败或出错时,输出会被正常回显,并添加到失败消息中。 525 | 526 | ```bash 527 | python -m unittest -b 528 | ``` 529 | 530 | * `-c` / `--catch` : 测试运行期间按`Control + C` 会等待当前测试结束,然后报告到目前为止的所有结果。第二次按`Control + C` 531 | 会引发正常的 KeyboardInterrupt 异常。 532 | 533 | ```bash 534 | python -m unittest -c 535 | ``` 536 | 537 | * `-f`/ `--failfast`:在出现第一个错误或失败时停止测试运行。 538 | 539 | ```bash 540 | python -m unittest -f 541 | ``` 542 | 543 | * `-k`:只运行与模式或子字符串匹配的测试方法和类。可以多次使用此选项,这样所有与给定模式中的任何一个匹配的测试用例都会被包括进来。 544 | 545 | 例如,`-k foo` 会匹配`foo_tests.SomeTest.test_something`,`bar_tests.SomeTest.test_foo`,但不会匹配`bar_tests.FooTest.test_something`。 546 | 547 | ```bash 548 | python -m unittest -k cal 549 | ``` 550 | 551 | * `--durations N` 显示N个最慢的测试用例(N=0表示全部)(python 3.12 新增)。 552 | 553 | ```bash 554 | python -m unittest --durations 2 555 | ``` 556 | 557 | * `-v`,`--verbose` : 详细的输出。 558 | 559 | ```bash 560 | python -m unittest -v 561 | ``` 562 | 563 | __测试发现__ 564 | 565 | Unittest支持简单的测试查找。为了与测试查找兼容,所有的测试文件必须是从项目的顶级目录中导入的模块或包(这意味着它们的文件名必须是有效的标识符)。 566 | 567 | 测试发现在`TestLoader.discover()`中实现,但也可以从命令行使用。基本的命令行用法是: 568 | 569 | 发现子命令有以下选项: 570 | 571 | * `-s`,`--start-directory`: 开始查找的目录(默认为`.`) 572 | 573 | * `-p`,`--pattern` 匹配测试文件的模式(默认为`test*.py`) 574 | 575 | * `-t`,`--top-level-directory`: 项目的顶层目录(默认为开始目录) 576 | 577 | `-s`,`-p`和`-t`选项可以按照这个顺序作为位置参数传入。以下两个命令行是等效的: 578 | 579 | ```bash 580 | python -m unittest discover -s project_directory -p "*_test.py" 581 | python -m unittest discover project_directory "*_test.py" 582 | ``` 583 | 584 | 除了作为路径外,还可以传入包名作为开始目录,例如`myproject.subpackage.test`。提供的包名将被导入,其在文件系统上的位置将被用作开始目录。 585 | 586 | ### 断言方法 587 | 588 | > unittest的断言方法非常丰富,除了简单的 相等、包含,还有 类型、异常、警告、日志的断言。 589 | 590 | TestCase类提供了几种断言方法来检查并报告失败。以下表格列出了最常用的方法(更多断言方法请参见下表): 591 | 592 | | Method | Checks that | New in | 593 | |-----------------------------|----------------------|--------| 594 | | `assertEqual(a, b)` | a == b | | 595 | | `assertNotEqual(a, b)` | a != b | | 596 | | `assertTrue(x)` | bool(x) is True | | 597 | | `assertFalse(x)` | bool(x) is False | | 598 | | `assertIs(a, b)` | a is b | 3.1 | 599 | | `assertIsNot(a, b)` | a is not b | 3.1 | 600 | | `assertIsNone(x)` | x is None | 3.1 | 601 | | `assertIsNotNone(x)` | x is not None | 3.1 | 602 | | `assertIn(a, b)` | a in b | 3.1 | 603 | | `assertNotIn(a, b)` | a not in b | 3.1 | 604 | | `assertIsInstance(a, b)` | isinstance(a, b) | 3.2 | 605 | | `assertNotIsInstance(a, b)` | not isinstance(a, b) | 3.2 | 606 | 607 | 还可以使用以下方法检查异常、警告和日志消息的生成: 608 | 609 | | Method | Checks that | New in | 610 | |-------------------------------------------------|----------------------------------------------------------------------|--------| 611 | | `assertRaises(exc, fun, *args, **kwds)` | `fun(*args, **kwds)` raises `exc` | | 612 | | `assertRaisesRegex(exc, r, fun, *args, **kwds)` | `fun(*args, **kwds)` raises `exc` and the message matches regex `r` | 3.1 | 613 | | `assertWarns(warn, fun, *args, **kwds)` | `fun(*args, **kwds)` raises `warn` | 3.2 | 614 | | `assertWarnsRegex(warn, r, fun, *args, **kwds)` | `fun(*args, **kwds)` raises `warn` and the message matches regex `r` | 3.2 | 615 | | `assertLogs(logger, level)` | The `with` block logs on `logger` with minimum `level` | 3.4 | 616 | | `assertNoLogs(logger, level)` | The `with` block does not log on `logger` with minimum `level` | 3.10 | 617 | 618 | 还有其他方法可以用来执行更具体的检查,比如: 619 | 620 | | Method | Checks | New in | 621 | |------------------------------|-----------------------------------------------------------------------------------|--------| 622 | | `assertAlmostEqual(a, b)` | `round(a - b, 7) == 0` | | 623 | | `assertNotAlmostEqual(a, b)` | `round(a - b, 7) != 0` | | 624 | | `assertGreater(a, b)` | `a > b` | 3.1 | 625 | | `assertGreaterEqual(a, b)` | `a >= b` | 3.1 | 626 | | `assertLess(a, b)` | `a < b` | 3.1 | 627 | | `assertLessEqual(a, b)` | `a <= b` | 3.1 | 628 | | `assertRegex(s, r)` | `r.search(s)` | 3.1 | 629 | | `assertNotRegex(s, r)` | `not r.search(s)` | 3.2 | 630 | | `assertCountEqual(a, b)` | `a` and `b` have the same elements in the same number, regardless of their order. | 3.2 | 631 | 632 | __assertEqual()__ 633 | 634 | `assertEqual()` 自动使用的特定类型方法列表如下表所示。请注意,通常不需要直接调用这些方法。 635 | 636 | | Method | Checks | New in | 637 | |-------------------------------|---------------------|--------| 638 | | `assertMultiLineEqual(a, b)` | strings | 3.1 | 639 | | `assertSequenceEqual(a, b)` | sequences | 3.1 | 640 | | `assertListEqual(a, b)` | lists | 3.1 | 641 | | `assertTupleEqual(a, b)` | tuples | 3.1 | 642 | | `assertSetEqual(a, b)` | sets or frozensets | 3.1 | 643 | | `assertDictEqual(a, b)` | dicts | 3.1 | 644 | 645 | ### Fixture 646 | 647 | Fixtures的概念前面有过简单的介绍,我们可以形象地把它看作夹心饼干外层的两片饼干,这两片饼干就是setUp/tearDown,中间的奶油就是测试用例。 648 | 649 | ![](/images/test_fixture.jpg) 650 | 651 | 类和模块级别的固定装置是在`TestSuite`中实现的。当测试套件遇到来自新类的测试时,会调用上一个类的`tearDownClass()`(如果有的话),然后调用新类的`setUpClass()`。 652 | 653 | 类似地,如果一个测试来自前一个测试的不同模块,则会运行前一个模块的`tearDownModule`,然后运行新模块的`setUpModule`。 654 | 655 | __setUp and tearDown__ 656 | 657 | * `setUp` 658 | 659 | 准备测试装置的方法。这个方法会在调用测试方法之前立即被调用;除了AssertionError或SkipTest之外,此方法引发的任何异常都将被视为错误而不是测试失败。默认实现什么也不做。 660 | 661 | * `tearDwon` 662 | 663 | 测试方法被调用并记录结果后立即调用的方法。即使测试方法引发异常,也会调用此方法,因此子类中的实现可能需要特别小心地检查内部状态。此方法将仅在`setUp()`成功时调用,而不管测试方法的结果如何。默认实现不执行任何操作。 664 | 665 | ```python 666 | import unittest 667 | 668 | 669 | class Test(unittest.TestCase): 670 | 671 | def setUp(self) -> None: 672 | print("before") 673 | 674 | def test_case(self): 675 | print("this is case") 676 | 677 | def tearDown(self) -> None: 678 | print("after") 679 | 680 | 681 | if __name__ == '__main__': 682 | unittest.main() 683 | ``` 684 | 685 | __setUpClass and tearDownClass__ 686 | 687 | 这些必须被实现为类方法。 688 | 689 | ```python 690 | import unittest 691 | 692 | 693 | class SomeWork(): 694 | 695 | def init_env(self): 696 | print("初始化环境") 697 | 698 | def clear_env(self): 699 | print("清理环境配置") 700 | 701 | 702 | class Test(unittest.TestCase): 703 | some_work = None 704 | 705 | @classmethod 706 | def setUpClass(cls): 707 | cls.some_work = SomeWork() 708 | cls.some_work.init_env() 709 | 710 | def test_case(self): 711 | print("this is case") 712 | 713 | @classmethod 714 | def tearDownClass(cls): 715 | cls.some_work.clear_env() 716 | 717 | 718 | if __name__ == '__main__': 719 | unittest.main() 720 | ``` 721 | 722 | __setUpModule and tearDownModule__ 723 | 724 | 这些应该作为函数来实现。 725 | 726 | ```python 727 | import unittest 728 | 729 | 730 | def setUpModule(): 731 | print("all module case before") 732 | 733 | 734 | def tearDownModule(): 735 | print("all module case after") 736 | 737 | 738 | class Test(unittest.TestCase): 739 | 740 | def test_case(self): 741 | print("this is case") 742 | 743 | 744 | if __name__ == '__main__': 745 | unittest.main() 746 | ``` 747 | 748 | ### 跳过测试和预期失败 749 | 750 | #### 基本使用 751 | 752 | unittest支持跳过单个测试方法甚至整个测试类。此外,它还支持将测试标记为“预期失败”,即一个有问题并将失败的测试,但不应计入TestResult的失败。 753 | 754 | * `unittest.skip(reason)` 755 | 756 | 无条件地跳过装饰的测试,需要说明跳过测试的原因。 757 | 758 | * `unittest.skipIf(condition, reason)` 759 | 760 | 如果条件为真,则跳过装饰的测试。 761 | 762 | * `unittest.skipUnless(condition, reason)` 763 | 764 | 当条件为真时,执行装饰的测试。 765 | 766 | * `unittest.expectedFailure()` 767 | 768 | 不管执行结果是否失败,都将测试标记为失败。 769 | 770 | __基本的跳过示例__ 771 | 772 | ```python 773 | import sys 774 | import unittest 775 | 776 | __version__ = 0 777 | 778 | 779 | def element_is_exists() -> bool: 780 | """ 781 | element is exists 782 | :return: 783 | """ 784 | return False 785 | 786 | 787 | class MyTestCase(unittest.TestCase): 788 | 789 | @unittest.skip("demonstrating skipping") 790 | def test_nothing(self): 791 | self.fail("shouldn't happen") 792 | 793 | @unittest.skipIf(__version__ < 3, "not supported in this library version") 794 | def test_format(self): 795 | # Tests that work for only a certain version of the library. 796 | pass 797 | 798 | @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") 799 | def test_windows_support(self): 800 | # windows specific testing code 801 | pass 802 | 803 | def test_maybe_skipped(self): 804 | if element_is_exists(): 805 | self.skipTest("external resource not available") 806 | # test code that depends on the external resource 807 | pass 808 | ``` 809 | 810 | __跳过测试类__ 811 | 812 | ```python 813 | import unittest 814 | 815 | 816 | @unittest.skip("showing class skipping") 817 | class MySkippedTestCase(unittest.TestCase): 818 | def test_not_run(self): 819 | pass 820 | ``` 821 | 822 | __预期用例失败__ 823 | 824 | ```python 825 | import unittest 826 | 827 | 828 | class ExpectedFailureTestCase(unittest.TestCase): 829 | @unittest.expectedFailure 830 | def test_fail(self): 831 | self.assertEqual(1, 0, "broken") 832 | ``` 833 | 834 | 跳过的测试不会运行`setUp()`或`tearDown()`。跳过的类不会运行`setUpClass()`或`tearDownClass()`。跳过的模块不会运行`setUpModule()`或`tearDownModule()`。 835 | 836 | #### 自己封装skip 837 | 838 | 制作自己的跳过装饰器很容易,只需制作一个在希望跳过测试时调用skip()的装饰器即可。这个装饰器会跳过测试,除非传递的对象具有特定属性。 839 | 840 | ```python 841 | import unittest 842 | 843 | 844 | def skipUnlessHasattr(obj, attr): 845 | """ 846 | 自定义 skipUnlessHasattr 装饰器 847 | :param obj: 848 | :param attr: 849 | :return: 850 | """ 851 | if hasattr(obj, attr): 852 | return lambda func: func 853 | return unittest.skip("{!r} doesn't have {!r}".format(obj, attr)) 854 | 855 | 856 | # 857 | class ExampleClass: 858 | """ 859 | 示例类,包含一些属性 860 | """ 861 | 862 | def __init__(self): 863 | self.some_attribute = 'value' 864 | 865 | 866 | class MyTestCase(unittest.TestCase): 867 | 868 | @skipUnlessHasattr(ExampleClass(), 'some_attribute') 869 | def test_with_attribute(self): 870 | self.assertTrue(True) 871 | 872 | @skipUnlessHasattr(ExampleClass(), 'missing_attribute') 873 | def test_without_attribute(self): 874 | self.assertTrue(True) 875 | 876 | 877 | # 运行测试 878 | if __name__ == '__main__': 879 | unittest.main() 880 | ``` 881 | 882 | ### 测试发现 discover 883 | 884 | `discover()`是unittest非常重要的一个方法,用于实现测试发现。 885 | 886 | ```python 887 | import unittest 888 | 889 | unittest.defaultTestLoader.discover(start_dir, pattern='test*.py', top_level_dir=None) 890 | ``` 891 | 892 | __参数说明__ 893 | 894 | * `test_dir`: 指定测试目录。 895 | * `pattern`: 指定查找用例的规则。 896 | * `top_level_dir`: 指定项目顶层目录。 897 | 898 | 从指定的起始目录递归查找所有测试模块,返回一个包含它们的TestSuite对象。只会加载与模式匹配的测试文件(使用类似shell的模式匹配)。只有可导入的模块名称(即有效的Python标识符)才会被加载。 899 | 900 | 所有测试模块必须从项目的顶层可导入。如果起始目录不是顶层目录,则必须单独指定`top_level_dir`。 901 | 902 | 如果导入模块失败,例如由于语法错误,则这将被记录为单个错误,发现将继续。如果导入失败是因为引发了SkipTest,则将其记录为跳过而不是错误。 903 | 904 | 如果发现一个包(包含一个名为__init__.py的文件的目录),将检查该包是否有load_tests函数。如果存在,则将调用package.load_tests( 905 | loader, tests, pattern)。测试发现会确保在调用期间只检查一次包是否包含测试,即使load_tests函数本身调用loader.discover。 906 | 907 | 如果`load_tests`存在,则发现不会递归进入该包,`load_tests`负责加载包中的所有测试。 908 | 909 | 模式故意不存储为loader属性,以便包可以继续自行发现。 910 | 911 | top_level_dir被内部存储,并用作对discover()的任何嵌套调用的默认值。也就是说,如果一个包的load_tests调用loader.discover() 912 | ,则不需要传递此参数。 913 | 914 | start_dir可以是一个点分隔的模块名称,也可以是一个目录。 915 | 916 | #### top_level_dir参数说明 917 | 918 | 假设你的项目结构如下: 919 | 920 | ``` 921 | my_project/ 922 | ├── top_level_dir/ 923 | │ ├── __init__.py 924 | │ ├── main_module.py 925 | │ └── tests/ 926 | │ ├── __init__.py 927 | │ ├── test_module1.py 928 | │ └── test_module2.py 929 | └── run_tests.py 930 | ``` 931 | 932 | 如果你在 `run_tests.py` 中运行测试,并且 `run_tests.py` 位于 `my_project` 目录下,则需要指定 `top_level_dir`,因为 `start_dir`并不是项目的顶层目录。 933 | 934 | ```python 935 | import unittest 936 | 937 | if __name__ == '__main__': 938 | # 指定测试开始的目录为 `top_level_dir/tests` 939 | start_dir = 'top_level_dir/tests' 940 | # 指定顶层目录为 `top_level_dir` 941 | top_level_dir = 'top_level_dir' 942 | 943 | suite = unittest.defaultTestLoader.discover( 944 | start_dir=start_dir, 945 | pattern='test*.py', 946 | top_level_dir=top_level_dir 947 | ) 948 | 949 | runner = unittest.TextTestRunner() 950 | runner.run(suite) 951 | ``` 952 | 953 | 在这个例子中: 954 | 955 | `start_dir` 是 `top_level_dir/tests`,它不是项目的顶层目录。因此,我们必须指定 `top_level_dir` 为 `top_level_dir`。这样,unittest 会正确地将 `top_level_dir` 作为项目的顶层目录,然后在 `top_level_dir/tests` 目录中查找匹配 pattern='test*.py' 的测试模块。 956 | 957 | 当你不指定 `top_level_dir` 时,unittest 会尝试从 `start_dir` 的父目录开始导入模块。如果 `start_dir`不是项目的顶层目录,就会导致模块导入错误。例如,它可能会找不到 `top_level_dir.tests.test_module1`,因为它没有在正确的路径下查找。 958 | 959 | 960 | #### 自定义测试加载器 961 | 962 | `discover()` 只运行指定一个目录(start_dir)和匹配规则(pattern)执行用例,如果需要同时运行不同目录下的不同用例。我们可以自定义一个运行加载器。查找多个目录下面的用例放到一个测试套件中。 963 | 964 | ```py 965 | import os 966 | 967 | import unittest 968 | 969 | 970 | def custom_test_loader(start_dir: str, sub_dir_list: dict): 971 | """ 972 | 自定义测试加载器 973 | :param start_dir: 开始查找用例的目录, 974 | :param sub_dir_list: 包含的子目录 975 | :return: 976 | """ 977 | test_suite = unittest.TestSuite() 978 | 979 | for subdir, pattern in sub_dir_list.items(): 980 | sub_dir_path = os.path.join(start_dir, subdir) 981 | if os.path.isdir(sub_dir_path) is True: 982 | # 使用unittest.defaultTestLoader来发现并加载指定子目录下的测试 983 | sub_suite = unittest.defaultTestLoader.discover( 984 | start_dir=sub_dir_path, 985 | pattern=pattern, 986 | top_level_dir=start_dir) 987 | test_suite.addTest(sub_suite) 988 | 989 | return test_suite 990 | 991 | 992 | if __name__ == '__main__': 993 | # 假设你的测试目录结构从当前目录开始 994 | start_dir = os.path.dirname(os.path.abspath(__file__)) 995 | # 定义你想要包含的子目录列表 996 | run_sub_dir = { 997 | "unittest_assert": "*_equal.py", 998 | "unittest_base": "test_*.py" 999 | } 1000 | suite = custom_test_loader(start_dir, run_sub_dir) 1001 | runner = unittest.TextTestRunner() 1002 | runner.run(suite) 1003 | ``` 1004 | 1005 | 其中,`start_dir`用于指定顶层目录;`run_sub_dir`用于定义要运行的子目录,以及文件的匹配规则,这样就可以比较灵活的配置多个目录的用例执行了。 1006 | 1007 | 1008 | ### 子测试 subtests 1009 | 1010 | 在你的测试中如果用例有很小的差异时,比如一些参数不同,unittest允许在测试方法的主体内使用`subTest()`上下文管理器来区分它们。 1011 | 1012 | 例如下面的测试: 1013 | 1014 | ```python 1015 | import unittest 1016 | 1017 | 1018 | def calculate_discounted_price(original_price, discount_rate): 1019 | """ 1020 | 计算打折后的价格。 1021 | :param original_price: 原价 1022 | :param discount_rate: 折扣率(0-1之间的浮点数) 1023 | :return: 折后价格 1024 | """ 1025 | return original_price * (1 - discount_rate) 1026 | 1027 | 1028 | class TestDiscountCalculator(unittest.TestCase): 1029 | 1030 | def test_calculate_discounted_price(self): 1031 | # 定义一系列测试用例 1032 | test_cases = [ 1033 | (100, 0.1, 90.0), # 10% 的折扣 1034 | (200, 0.2, 160.0), # 20% 的折扣 1035 | (300, 0.3, 220.0), # 30% 的折扣 210.0 error 1036 | (400, 0.4, 240.0), # 40% 的折扣 1037 | (500, 0.5, 250.0), # 50% 的折扣 1038 | ] 1039 | 1040 | # 使用 subTest 对每个测试用例进行迭代 1041 | for original_price, discount_rate, expected_price in test_cases: 1042 | print(f"原价:{original_price}, 折扣率:{discount_rate}, 现价:{expected_price}") 1043 | with self.subTest( 1044 | op=original_price, 1045 | dr=discount_rate, 1046 | ep=expected_price 1047 | ): 1048 | calculated_price = calculate_discounted_price(original_price, discount_rate) 1049 | self.assertAlmostEqual(calculated_price, expected_price, 1050 | msg=f"对于原价{original_price}和折扣率{discount_rate},计算结果有误。") 1051 | 1052 | 1053 | if __name__ == '__main__': 1054 | unittest.main() 1055 | ``` 1056 | 1057 | 执行结果: 1058 | 1059 | ```shell 1060 | > python test_sub_test.py 1061 | 1062 | python .\test_sub_test.py 1063 | 原价:100, 折扣率:0.1, 现价:90.0 1064 | 原价:200, 折扣率:0.2, 现价:160.0 1065 | 原价:300, 折扣率:0.3, 现价:220.0 1066 | F原价:400, 折扣率:0.4, 现价:240.0 1067 | 原价:500, 折扣率:0.5, 现价:250.0 1068 | 1069 | ====================================================================== 1070 | FAIL: test_calculate_discounted_price (__main__.TestDiscountCalculator.test_calculate_discounted_price) (original_price=300, discount_rate=0.3, expected_price=220.0) 1071 | ---------------------------------------------------------------------- 1072 | Traceback (most recent call last): 1073 | File "D:\github\Learn-unittest-class\demo\unittest_sub\test_sub_test.py", line 35, in test_calculate_discounted_price 1074 | self.assertAlmostEqual(calculated_price, expected_price, 1075 | AssertionError: 210.0 != 220.0 within 7 places (10.0 difference) : 对于原价300和折扣率0.3,计算结果有误。 1076 | 1077 | ---------------------------------------------------------------------- 1078 | Ran 1 test in 0.001s 1079 | 1080 | FAILED (failures=1) 1081 | ``` 1082 | 1083 | 如果不使用子测试,执行将在第一次失败后停止。 1084 | 1085 | 1086 | ### 类&方法使用 1087 | 1088 | #### main() 方法 1089 | 1090 | 每次都调用`TestSuite` 和 `TestRunner`去组装和运行测试会比较麻烦, `main()` 是为了方便测试当前文件中的测试用例, 1091 | 1092 | ```python 1093 | import unittest 1094 | 1095 | 1096 | class TestLogin(unittest.TestCase): 1097 | 1098 | def test_login_fail(self): 1099 | ... 1100 | 1101 | def test_login_success(self): 1102 | ... 1103 | 1104 | 1105 | class TestRegister(unittest.TestCase): 1106 | 1107 | def test_register_fail(self): 1108 | ... 1109 | 1110 | def test_register_success(self): 1111 | ... 1112 | 1113 | 1114 | if __name__ == '__main__': 1115 | unittest.main() 1116 | ``` 1117 | 1118 | 只要用例编写遵循 unittest 规范,`main()`即可查找用例。 1119 | 1120 | 1121 | __main方法入参__ 1122 | 1123 | ```python 1124 | import unittest 1125 | 1126 | unittest.main(module='__main__', defaultTest=None, argv=None, testRunner=None, testLoader=unittest.defaultTestLoader, exit=True, verbosity=1, failfast=None, catchbreak=None, buffer=None, warnings=None) 1127 | ``` 1128 | 1129 | __主要参数说明__ 1130 | 1131 | * testRunner: 指定运行器,默认 `unittest.TextTestRunner`。 1132 | * module :指定测试模块,默认`__main__` 即当前文件,也可以指定当前同目录下的其他文件。 1133 | * verbosity: 日志级别,想看更详细的日志可以设置为 `2`。 1134 | * failfast: 更快的使用例失败,设置为`True` 出现第一条失败的用例停止执行。 1135 | * buffer: 测试运行期间,标准输出和标准错误流会被缓冲。 1136 | 1137 | 1138 | #### 测试加载 TestLoader 1139 | 1140 | TestLoader类用于从类和模块创建测试套件。通常情况下,不需要创建这个类的实例;unittest模块提供了一个实例,可以作为`unittest.defaultTestLoader`共享。然而,使用子类或实例允许定制一些可配置属性。 1141 | 1142 | TestLoader提供了一些加载用例的方法,用于查找用例。 1143 | 1144 | * `loadTestsFromTestCase()` 1145 | 1146 | 这个方法用于从给定的测试用例类加载测试。它会为该类创建一个实例,并收集所有以 test_ 开头的方法作为测试用例。 1147 | 1148 | * `loadTestsFromName()` 1149 | 1150 | 这个方法用于根据给定的测试用例或测试套件的全名(包括模块名)来加载测试。如果名字指的是一个测试用例类,则会创建该类的一个实例作为测试用例;如果名字指的是一个测试方法,则会加载那个方法作为单独的测试。 1151 | 1152 | * `loadTestsFromNames()` 1153 | 1154 | 此方法接受一个名字列表,每个名字可以是测试用例类、方法或模块的名字,并根据这些名字加载测试。它比 loadTestsFromName() 更灵活,可以一次性加载多个测试。 1155 | 1156 | * `loadTestsFromModule()` 1157 | 1158 | 这个方法直接从给定的模块加载所有测试用例。它会自动查找该模块中所有继承自`unittest.TestCase`的类,并收集它们的测试方法。 1159 | 1160 | 1161 | __使用示例__ 1162 | 1163 | 假设有一个测试模块 `test_example.py`,其中包含一个测试类 `ExampleTest` 和一个测试方法 `test_case_1` 和 `test_case_1`。 1164 | 1165 | ```python 1166 | # test_example.py 1167 | import unittest 1168 | 1169 | 1170 | class ExampleTest(unittest.TestCase): 1171 | 1172 | def test_case_1(self): 1173 | self.assertEqual(1 + 1, 2) 1174 | 1175 | def test_case_2(self): 1176 | self.assertEqual(2 + 2, 4) 1177 | ... 1178 | ``` 1179 | 1180 | 不同加载器的用法: 1181 | 1182 | ```python 1183 | import unittest 1184 | 1185 | import test_example 1186 | from test_example import ExampleTest 1187 | 1188 | if __name__ == '__main__': 1189 | test_loader = unittest.TestLoader() 1190 | # 加载测试类 1191 | suite = test_loader.loadTestsFromTestCase(ExampleTest) 1192 | 1193 | # 加载测试 1194 | suite = test_loader.loadTestsFromName("test_example.ExampleTest.test_case_1") 1195 | 1196 | # 加载多个测试 1197 | suite = test_loader.loadTestsFromNames([ 1198 | "test_example.ExampleTest.test_case_1", 1199 | "test_example.ExampleTest.test_case_2" 1200 | ]) 1201 | 1202 | # 加载测试模块 1203 | suite = test_loader.loadTestsFromModule(test_example) 1204 | 1205 | # 测试运行器 1206 | runner = unittest.TextTestRunner() 1207 | runner.run(suite) 1208 | ``` 1209 | 1210 | ### 常见问题 1211 | 1212 | 1213 | #### 用例执行顺序 1214 | 1215 | 测试用例的执行顺序涉及多个层级: 1216 | 1217 | 多个测试目录 > 多个测试文件 > 多个测试类 > 多个测试方法(测试用例)。 1218 | 1219 | unittest提供的`main()`方法和`discover()`方法是按照什么顺序查找测试用例的呢? 1220 | 1221 | 1222 | 我们先运行一个例子,再解释unittest的执行策略。 1223 | 1224 | ```py 1225 | import unittest 1226 | 1227 | class TestBdd(unittest.TestCase): 1228 | 1229 | def setUp(self): 1230 | print("test TestBdd:") 1231 | 1232 | def test_ccc(self): 1233 | print("test ccc") 1234 | 1235 | def test_aaa(self): 1236 | print("test aaa") 1237 | 1238 | 1239 | class TestAdd(unittest.TestCase): 1240 | 1241 | def setUp(self): 1242 | print("test TestAdd:") 1243 | 1244 | def test_bbb(self): 1245 | print("test bbb") 1246 | 1247 | 1248 | if __name__ == '__main__': 1249 | unittest.main() 1250 | ``` 1251 | 1252 | 执行结果如下。 1253 | 1254 | ```bash 1255 | test TestAdd: 1256 | test bbb 1257 | .test TestBdd: 1258 | test aaa 1259 | .test TestBdd: 1260 | test ccc 1261 | . 1262 | ---------------------------------------------------------------------- 1263 | Ran 3 tests in 0.000s 1264 | ``` 1265 | 1266 | 无论执行多少次,结果都是一样的。通过上面的结果,相信你已经找到main()方法执行测试用例的规律了。 1267 | 1268 | 因为unittest默认根据ASCII码的顺序加载测试用例的(__数字与字母的顺序为`0~9`,`A~Z`,`a~z`__),所以`TestAdd`类会优先于`TestBdd`类被执行,`test_aaa()`方法会优先于`test_ccc()`方法被执行,也就是说,它并不是按照测试用例的创建顺序从上到下执行的。 1269 | 1270 | `discover()`方法和`main()`方法的执行顺序是一样的。对于测试目录与测试文件来说,上面的规律同样适用。`test_aaa.py`文件会优先于`test_bbb.py`文件被执行。所以,如果想让某个测试文件先执行,可以在命名上加以控制。 1271 | 1272 | 测试套件`TestSuite`类,通过`addTest()`方法可以控制用例的执行顺序。 1273 | 1274 | #### 多级目录无法识别用例 1275 | 1276 | 当测试用例的数量达到一定量级时,就要考虑目录划分,比如规划如下测试目录。 1277 | 1278 | ``` 1279 | test_project 1280 | ├──/test_case/ 1281 | │ ├── test_bbb/ 1282 | │ │ ├── test_ccc/ 1283 | │ │ │ └── test_c.py 1284 | │ │ └── test_b.py 1285 | │ ├── test_ddd/ 1286 | │ │ └── test_d.py 1287 | │ └── test_a.py 1288 | └─ run_tests.py 1289 | ``` 1290 | 1291 | 对于上面的目录结构,如果将`discover()`方法中的`start_dir`参数定义为`./test_case`目录,那么只能加载`test_a.py`文件中的测试用例。 1292 | 1293 | 如何让unittest查找test_case/下子目录中的测试文件呢?方法很简单,就是在每个子目录下放一个`__init__.py`文件。`__init__.py`文件的作用是将一个目录标记成一个标准的Python模块。 1294 | 1295 | 1296 | ## unittest扩展开发 1297 | 1298 | ### 数据驱动 1299 | 1300 | 数据驱动时非常重要的功能,尤其时做 UI自动化和 接口自动化的时候,可以极大的节省的用例的编写。unittest 有第三方的测试数据驱动测试库。 1301 | 1302 | #### Parameterized 1303 | 1304 | `Parameterized`是Python的一个参数化库,同时支持unittest、Nose和pytest单元测试框架。 1305 | 1306 | GitHub地址:https://github.com/wolever/parameterized 1307 | 1308 | Parameterized支持pip安装。 1309 | 1310 | ``` 1311 | > pip install parameterized 1312 | ``` 1313 | 1314 | 这里将通过Parameterized实现unittest参数化。 1315 | 1316 | ```python 1317 | import math 1318 | import unittest 1319 | from parameterized import parameterized 1320 | from parameterized import parameterized_class 1321 | 1322 | 1323 | @parameterized_class(('a', 'b', 'expected_sum', 'expected_product'), [ 1324 | (1, 2, 3, 2), 1325 | (5, 5, 10, 25) 1326 | ]) 1327 | class TestMathClass(unittest.TestCase): 1328 | def test_add(self): 1329 | self.assertEqual(self.a + self.b, self.expected_sum) 1330 | 1331 | def test_multiply(self): 1332 | self.assertEqual(self.a * self.b, self.expected_product) 1333 | 1334 | 1335 | class TestMathUnitTest(unittest.TestCase): 1336 | @parameterized.expand([ 1337 | ("negative", -1.5, -2.0), 1338 | ("integer", 1, 1.0), 1339 | ("large fraction", 1.6, 1), 1340 | ]) 1341 | def test_floor(self, name, input, expected): 1342 | self.assertEqual(math.floor(input), expected) 1343 | ``` 1344 | 1345 | 这里的主要改动在测试用例部分。 1346 | 1347 | 首先,导入`Parameterized`库下面的`parameterized`类。 1348 | 1349 | 其次,通过`@parameterized.expand()`来装饰测试用例`test_floor()`。 1350 | 1351 | 在`@parameterized.expand()`中,每个元组都可以被认为是一条测试用例。元组中的数据为该条测试用例变化的值。在测试用例中,通过参数来取每个元组中的数据。 1352 | 1353 | 最后,使用unittest的`main()`方法,设置`verbosity`参数为2,输出更详细的执行日志。运行上面的测试用例,结果如下。 1354 | 1355 | ```shell 1356 | > python test_parameterized_demo.py 1357 | 1358 | test_add (__main__.TestMathClass_0.test_add) ... ok 1359 | test_multiply (__main__.TestMathClass_0.test_multiply) ... ok 1360 | test_add (__main__.TestMathClass_1.test_add) ... ok 1361 | test_multiply (__main__.TestMathClass_1.test_multiply) ... ok 1362 | test_floor_0_negative (__main__.TestMathUnitTest.test_floor_0_negative) ... ok 1363 | test_floor_1_integer (__main__.TestMathUnitTest.test_floor_1_integer) ... ok 1364 | test_floor_2_large_fraction (__main__.TestMathUnitTest.test_floor_2_large_fraction) ... ok 1365 | 1366 | ---------------------------------------------------------------------- 1367 | Ran 7 tests in 0.001s 1368 | ``` 1369 | 1370 | 通过测试结果可以看到,因为是根据`@parameterized.expand()`中元组的个数来统计测试用例数的,所以产生了3条测试用例。 1371 | 1372 | `test_floor`为定义的测试用例的名称。参数化会自动加上`0`、`1`和`2`来区分每条测试用例。 1373 | 1374 | 1375 | #### ddt 1376 | 1377 | > Data-Driven Tests for Python Unittest 1378 | 1379 | Python unittest单元测试框架数据驱动测试。 1380 | 1381 | DDT(数据驱动测试)允许您通过使用不同的测试数据来执行一个测试用例,使其看起来像是多个测试用例。 1382 | 1383 | github: https://github.com/datadriventests/ddt 1384 | 1385 | DDT支持pip安装。 1386 | 1387 | ```shell 1388 | > pip install ddt 1389 | > pip install pyyaml # 需要用到yaml文件 1390 | ``` 1391 | 1392 | __使用示例__ 1393 | 1394 | 1395 | DDT由一个类装饰器DDT(用于TestCase子类)和两个方法装饰器(用于想要相乘的测试)组成: 1396 | 1397 | * `data`: 包含与您想要提供给测试的值一样多的参数。 1398 | * `file_data`: 将从JSON或YAML文件加载测试数据。 1399 | 1400 | 一般来说,数据中的每个值都将作为单个参数传递给您的测试方法。如果这些值是元组等,您将需要在测试中解包它们。另外,还可以使用一个额外的装饰器`unpack`,它将自动将元组和列表解包为多个参数,将字典解包为多个关键字参数。请参考下面的例子。 1401 | 1402 | * 目录结构 1403 | 1404 | ``` 1405 | ├───unittest_data_driver 1406 | │ └───data/ 1407 | | | ├───test_data_dict_dict.json 1408 | | | └───test_data_dict_dict.yaml 1409 | ├───test_ddt_file_data.py 1410 | ├───test_ddt_data.py 1411 | ``` 1412 | 1413 | * 数据驱动 1414 | 1415 | ```python 1416 | test_ddt_data.py 1417 | import unittest 1418 | from ddt import ddt, data, unpack 1419 | 1420 | 1421 | @ddt 1422 | class TestBaidu(unittest.TestCase): 1423 | 1424 | @data(["case1", "selenium"], ["case2", "ddt"], ["case3", "python"]) 1425 | @unpack 1426 | def test_list_data(self, case, search_key): 1427 | print("第一组测试用例:", case) 1428 | 1429 | @data(("case1", "selenium"), ("case2", "ddt"), ("case3", "python")) 1430 | @unpack 1431 | def test_tuple_data(self, case, search_key): 1432 | print("第二组测试用例:", case) 1433 | 1434 | @data({"search_key": "selenium"}, 1435 | {"search_key": "ddt"}, 1436 | {"search_key": "python"}) 1437 | @unpack 1438 | def test_dict_data(self, search_key): 1439 | print("第三组测试用例:", search_key) 1440 | 1441 | 1442 | if __name__ == '__main__': 1443 | unittest.main(verbosity=2) 1444 | ``` 1445 | 1446 | 执行结果: 1447 | 1448 | ```bash 1449 | > python test_ddt_demo.py 1450 | 1451 | test_dict_data_1 (__main__.TestBaidu.test_dict_data_1) ... 第三组测试用例: selenium 1452 | ok 1453 | test_dict_data_2 (__main__.TestBaidu.test_dict_data_2) ... 第三组测试用例: ddt 1454 | ok 1455 | test_dict_data_3 (__main__.TestBaidu.test_dict_data_3) ... 第三组测试用例: python 1456 | ok 1457 | test_list_data_1___case1____selenium__ (__main__.TestBaidu.test_list_data_1___case1____selenium__) ... 第一组测试用例: case1 1458 | ok 1459 | test_list_data_2___case2____ddt__ (__main__.TestBaidu.test_list_data_2___case2____ddt__) ... 第一组测试用例: case2 1460 | ok 1461 | test_list_data_3___case3____python__ (__main__.TestBaidu.test_list_data_3___case3____python__) ... 第一组测试用例: case3 1462 | ok 1463 | test_tuple_data_1___case1____selenium__ (__main__.TestBaidu.test_tuple_data_1___case1____selenium__) ... 第二组测试用例: case1 1464 | ok 1465 | test_tuple_data_2___case2____ddt__ (__main__.TestBaidu.test_tuple_data_2___case2____ddt__) ... 第二组测试用例: case2 1466 | ok 1467 | test_tuple_data_3___case3____python__ (__main__.TestBaidu.test_tuple_data_3___case3____python__) ... 第二组测试用例: case3 1468 | ok 1469 | 1470 | ---------------------------------------------------------------------- 1471 | Ran 9 tests in 0.003s 1472 | 1473 | OK 1474 | ``` 1475 | 1476 | 首先,DDT也会给数据驱动的用例加上编号:`1`、`2`、`3`;其次,用例的参数也会作为用例名称的一部分。 1477 | 1478 | * 文件数据驱动 1479 | 1480 | ```python 1481 | # test_ddt_file_data.py 1482 | import unittest 1483 | from ddt import ddt, file_data 1484 | 1485 | @ddt 1486 | class TestBaidu(unittest.TestCase): 1487 | 1488 | @file_data('data/test_data_dict_dict.json') 1489 | def test_file_data_json_dict_dict(self, start, end, value): 1490 | self.assertLess(start, end) 1491 | self.assertLess(value, end) 1492 | self.assertGreater(value, start) 1493 | 1494 | @file_data('data/test_data_dict_dict.yaml') 1495 | def test_file_data_yaml_dict_dict(self, start, end, value): 1496 | self.assertLess(start, end) 1497 | self.assertLess(value, end) 1498 | self.assertGreater(value, start) 1499 | 1500 | 1501 | if __name__ == '__main__': 1502 | unittest.main(verbosity=2) 1503 | ``` 1504 | 1505 | * 依赖的数据文件。 1506 | 1507 | `data/test_data_dict_dict.json` 1508 | 1509 | ```json 1510 | { 1511 | "positive_integer_range": { 1512 | "start": 0, 1513 | "end": 2, 1514 | "value": 1 1515 | }, 1516 | "negative_integer_range": { 1517 | "start": -2, 1518 | "end": 0, 1519 | "value": -1 1520 | }, 1521 | "positive_real_range": { 1522 | "start": 0.0, 1523 | "end": 1.0, 1524 | "value": 0.5 1525 | }, 1526 | "negative_real_range": { 1527 | "start": -1.0, 1528 | "end": 0.0, 1529 | "value": -0.5 1530 | } 1531 | } 1532 | ``` 1533 | 1534 | `data/test_data_dict_dict.yaml` 1535 | 1536 | ```yaml 1537 | positive_integer_range: 1538 | start: 0 1539 | end: 2 1540 | value: 1 1541 | 1542 | negative_integer_range: 1543 | start: -2 1544 | end: 0 1545 | value: -1 1546 | 1547 | positive_real_range: 1548 | start: 0.0 1549 | end: 1.0 1550 | value: 0.5 1551 | 1552 | negative_real_range: 1553 | start: -1.0 1554 | end: 0.0 1555 | value: -0.5 1556 | ``` 1557 | 1558 | #### 设计数据驱动 1559 | 1560 | `Parameterized` 和 `ddt` 对于unittest都不够完美。 1561 | 1562 | * `Parameterized`:首先,不支持数据文件,其次,` @parameterized.expand()`的写法明显是个二等公民。 1563 | 1564 | * `ddt`:使用太多的装饰器了,例如,使用数据驱动要用到 `@ddt`、`@data`、`@unpack`等装饰器;此外,不支持 `csv/excel` 等数据文件。 1565 | 1566 | 1567 | __@data装饰器__ 1568 | 1569 | 将`Parameterized` 的`@parameterized.expand()` 的代码剥离出来,进行修改和重命名为 `@data()`。[查看代码](./plugin/data_driver/) 1570 | 1571 | 1572 | * **使用示例** 1573 | 1574 | ```py 1575 | # test_param.py 1576 | import unittest 1577 | from data_driver.param import data 1578 | 1579 | 1580 | class DataTest(unittest.TestCase): 1581 | 1582 | @data([ 1583 | ("case1", "hello"), 1584 | ("case2", "hi"), 1585 | ("case3", "你好"), 1586 | ]) 1587 | def test_data_tuple(self, name, keyword): 1588 | """ 1589 | tuple数据 1590 | """ 1591 | print("tuple->", name, keyword) 1592 | 1593 | @data([ 1594 | ["case1", "hello"], 1595 | ["case2", "hi"], 1596 | ["case3", "你好"], 1597 | ]) 1598 | def test_data_list(self, name, keyword): 1599 | """ 1600 | list数据 1601 | """ 1602 | print("list->", name, keyword) 1603 | 1604 | @data([ 1605 | {"scene": "case1", "keyword": "hello"}, 1606 | {"scene": "case2", "keyword": "hi"}, 1607 | {"scene": "case3", "keyword": "你好"}, 1608 | ]) 1609 | def test_data_dict(self, name, keyword): 1610 | """ 1611 | dict数据 1612 | """ 1613 | print("dict->", name, keyword) 1614 | 1615 | 1616 | if __name__ == '__main__': 1617 | unittest.main() 1618 | ``` 1619 | 1620 | 通过简单的设计可以支持`tuple`、`list`、`dict` 三种格式的数据。 1621 | 1622 | * **运行结果** 1623 | 1624 | ```bash 1625 | > python test_param.py 1626 | 1627 | dict-> case1 hello 1628 | .dict-> case2 hi 1629 | .dict-> case3 你好 1630 | .list-> case1 hello 1631 | .list-> case2 hi 1632 | .list-> case3 你好 1633 | .tuple-> case1 hello 1634 | .tuple-> case2 hi 1635 | .tuple-> case3 你好 1636 | . 1637 | ---------------------------------------------------------------------- 1638 | Ran 9 tests in 0.002s 1639 | 1640 | OK 1641 | ``` 1642 | 1643 | __@file_data装饰器__ 1644 | 1645 | 在实现 `@data()`的前提下,我们进一步实现 `@file_data()`装饰,支持`json/yaml/csv/excel`四种数据格式。[查看代码](./plugin/data_driver/) 1646 | 1647 | * **使用示例** 1648 | 1649 | ```py 1650 | # test_param.py 1651 | import unittest 1652 | from data_driver.param import file_data 1653 | 1654 | 1655 | 1656 | class FileDataTest(unittest.TestCase): 1657 | 1658 | @file_data("file_data_dict.json") 1659 | def test_file_data_json(self, start, end, value): 1660 | """ 1661 | json数据文件 1662 | """ 1663 | print("json file->", start, end, value) 1664 | 1665 | @file_data("file_data_dict.yaml") 1666 | def test_file_data_yaml(self, start, end, value): 1667 | """ 1668 | yaml数据文件 1669 | """ 1670 | print("yaml file->", start, end, value) 1671 | 1672 | @file_data("file_data_csv.csv", line=2) 1673 | def test_file_data_csv(self, firstname, lastname): 1674 | """ 1675 | csv数据文件 1676 | """ 1677 | print("csv file->", firstname, lastname) 1678 | 1679 | @file_data(file="file_data_excel.xlsx", sheet="Sheet1", line=2) 1680 | def test_file_data_excel(self, firstname, lastname): 1681 | """ 1682 | excel数据文件 1683 | """ 1684 | print("excel file->", firstname, lastname) 1685 | 1686 | 1687 | if __name__ == '__main__': 1688 | unittest.main() 1689 | ``` 1690 | 1691 | * **说明:** 1692 | - 数据支持`用例文件`向上查找三层,所以,测试文件可以放到任意目录,`file_data()` 都能找到,不需要指定数据文件目录。 1693 | - 根据数据文件类型不同,相应的参数也会有差异。比如,excel文件需要指定 sheet标签页,line从第几行开始读取。 1694 | 1695 | 1696 | * **运行结果** 1697 | 1698 | ```bash 1699 | > python test_param.py 1700 | 1701 | .csv file-> Forest Hobbs 1702 | .csv file-> Ferdinand Lozano 1703 | .excel file-> Marshall Conrad 1704 | .excel file-> Ruthie Pitts 1705 | .json file-> 0 2 1 1706 | .json file-> -2 0 -1 1707 | .json file-> 0.0 1.0 0.5 1708 | .json file-> -1.0 0.0 -0.5 1709 | .yaml file-> 0 2 1 1710 | .yaml file-> -2 0 -1 1711 | .yaml file-> 0 1 0.5 1712 | .yaml file-> -1 0 -0.5 1713 | . 1714 | ---------------------------------------------------------------------- 1715 | Ran 12 tests in 0.004s 1716 | 1717 | OK 1718 | ``` 1719 | 1720 | 1721 | ### 测试报告 1722 | 1723 | unittest 不支持生成测试报告,`tungwaiyip` 基于unittest设计了`HTMLTestRunner`用于生成HTML报告,由于年代久远,风格老旧,且仅仅支持python2,于是,各种基于`HTMLTestRunner`的魔改营运而生。 1724 | 1725 | Github: https://github.com/tungwaiyip/HTMLTestRunner 1726 | 1727 | Github: https://github.com/SeldomQA/XTestRunner (更具现代风格的HTML测试报告) 1728 | 1729 | 当然我们要实现基于unittest的测试报告并不复杂,核心是重写unittest的`TextTestRunner`即可。 1730 | 1731 | #### JSON测试报告 1732 | 1733 | 我实现了 `jsonrunner` 运行器用于生成JSON格式的测试报告。[查看代码](./plugin/jsonrunner/) 1734 | 1735 | * **使用示例** 1736 | 1737 | ```py 1738 | # test_jsonrunner.py 1739 | import unittest 1740 | from jsonrunner.runner import JSONTestRunner 1741 | 1742 | 1743 | class TestDemo(unittest.TestCase): 1744 | """Test Demo class""" 1745 | 1746 | def test_pass(self): 1747 | """pass case""" 1748 | self.assertEqual(5, 5) 1749 | 1750 | def test_fail(self): 1751 | """fail case""" 1752 | self.assertEqual(5, 6) 1753 | 1754 | def test_error(self): 1755 | """error case""" 1756 | self.assertEqual(a, 6) 1757 | 1758 | @unittest.skip("skip case") 1759 | def test_skip(self): 1760 | """skip case""" 1761 | ... 1762 | 1763 | 1764 | if __name__ == '__main__': 1765 | suit = unittest.TestSuite() 1766 | suit.addTests([ 1767 | TestDemo("test_pass"), 1768 | TestDemo("test_skip"), 1769 | TestDemo("test_fail"), 1770 | TestDemo("test_error") 1771 | ]) 1772 | 1773 | runner = JSONTestRunner(output="./reports/result.json") 1774 | runner.run(suit) 1775 | ``` 1776 | 1777 | * **说明:** 1778 | - 调用jsonrunner中的`JSONTestRunner`运行器,`output`参数指写输出的文件名。然后,通过`run()`方法传入运行的测试套件。 1779 | 1780 | * **运行结果** 1781 | 1782 | ```bash 1783 | > python test_jsonrunner.py 1784 | 1785 | Time Elapsed: 0:00:00.001987 1786 | .1SFE 1787 | ``` 1788 | 1789 | * **生成JSON结果** 1790 | 1791 | ![](./images/json_report.png) 1792 | 1793 | 1794 | #### HTML测试报告 1795 | 1796 | 我实现了 `htmlrunner` 运行器用于生成JSON格式的测试报告。[查看代码](./plugin/htmlrunner/) 1797 | 1798 | * **使用示例** 1799 | 1800 | ```py 1801 | # test_htmlrunner.py 1802 | import unittest 1803 | 1804 | from htmlrunner.runner import HTMLTestRunner 1805 | 1806 | 1807 | class TestDemo(unittest.TestCase): 1808 | """Test Demo class""" 1809 | 1810 | def test_pass(self): 1811 | """pass case""" 1812 | self.assertEqual(5, 5) 1813 | 1814 | def test_fail(self): 1815 | """fail case""" 1816 | self.assertEqual(5, 6) 1817 | 1818 | def test_error(self): 1819 | """error case""" 1820 | self.assertEqual(a, 6) 1821 | 1822 | @unittest.skip("skip case") 1823 | def test_skip(self): 1824 | """skip case""" 1825 | ... 1826 | 1827 | 1828 | if __name__ == '__main__': 1829 | suit = unittest.TestSuite() 1830 | suit.addTests([ 1831 | TestDemo("test_pass"), 1832 | TestDemo("test_skip"), 1833 | TestDemo("test_fail"), 1834 | TestDemo("test_error") 1835 | ]) 1836 | 1837 | runner = HTMLTestRunner(output="./reports/result.html") 1838 | runner.run(suit) 1839 | ``` 1840 | 1841 | * **说明:** 1842 | - 调用htmlrunner中的`HTMLTestRunner`运行器,`output`参数指写输出的文件名。然后,通过`run()`方法传入运行的测试套件。 1843 | 1844 | * **运行结果** 1845 | 1846 | ```bash 1847 | > python test_htmlrunner.py 1848 | 1849 | Time Elapsed: 0:00:00.001987 1850 | .1SFE 1851 | ``` 1852 | 1853 | * **生成HTML结果** 1854 | 1855 | ![](./images/html_report.png) 1856 | 1857 | 1858 | ### 黑白名单支持 1859 | 1860 | unittest本身不支持给用例打标签,并基于标签运行&跳过测试用例,不过,我们仍然可以通过重写unittest的`TextTestRunner` 实现这个功能。 1861 | 1862 | `label_plugin`实现了标签的黑白名单功能。[查看代码](./plugin/label_plugin/) 1863 | 1864 | * **使用示例** 1865 | 1866 | ```py 1867 | # test_label.py 1868 | import unittest 1869 | from label_plugin import label 1870 | from label_plugin.LabelTestRunner import LabelTestRunner 1871 | 1872 | 1873 | class LabelTest(unittest.TestCase): 1874 | 1875 | @label("base") 1876 | def test_label_base(self): 1877 | self.assertEqual(1 + 1, 2) 1878 | 1879 | @label("slow") 1880 | def test_label_slow(self): 1881 | self.assertEqual(1, 2) 1882 | 1883 | def test_no_label(self): 1884 | self.assertEqual(2 + 3, 5) 1885 | 1886 | 1887 | if __name__ == '__main__': 1888 | suit = unittest.TestSuite() 1889 | suit.addTests([ 1890 | LabelTest("test_label_base"), 1891 | LabelTest("test_label_slow"), 1892 | LabelTest("test_no_label"), 1893 | ]) 1894 | runner = LabelTestRunner( 1895 | whitelist=["base"], # 设置白名单 1896 | # blacklist=["slow"], # 设置黑名单 1897 | ) 1898 | runner.run(suit) 1899 | 1900 | ``` 1901 | 1902 | * **说明:** 1903 | - 如果只运行标签为`base`的用例,设置白名单(whitelist)。 1904 | - 如果只想屏蔽标签为`slow`的用例,设置黑名单(blacklist)。 1905 | 1906 | * **运行结果** 1907 | 1908 | ```bash 1909 | > python test_label.py 1910 | .ss 1911 | ---------------------------------------------------------------------- 1912 | Ran 3 tests in 0.000s 1913 | 1914 | OK (skipped=2) 1915 | ``` 1916 | 1917 | > 注意:不执行的用例会被**跳过**,并记录到用例的运行总数中。 1918 | 1919 | -------------------------------------------------------------------------------- /demo/custom_test_loader.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import unittest 4 | 5 | 6 | def custom_test_loader(start_dir: str, sub_dir_list: dict): 7 | """ 8 | 自定义测试加载器 9 | :param start_dir: 开始查找用例的目录, 10 | :param sub_dir_list: 包含的子目录 11 | :return: 12 | """ 13 | test_suite = unittest.TestSuite() 14 | 15 | for subdir, pattern in sub_dir_list.items(): 16 | sub_dir_path = os.path.join(start_dir, subdir) 17 | if os.path.isdir(sub_dir_path) is True: 18 | # 使用unittest.defaultTestLoader来发现并加载指定子目录下的测试 19 | sub_suite = unittest.defaultTestLoader.discover( 20 | start_dir=sub_dir_path, 21 | pattern=pattern, 22 | top_level_dir=start_dir) 23 | test_suite.addTest(sub_suite) 24 | 25 | return test_suite 26 | 27 | 28 | if __name__ == '__main__': 29 | # 假设你的测试目录结构从当前目录开始 30 | start_dir = os.path.dirname(os.path.abspath(__file__)) 31 | # 定义你想要包含的子目录列表 32 | run_sub_dir = { 33 | "unittest_assert": "*_equal.py", 34 | "unittest_base": "test_*.py" 35 | } 36 | suite = custom_test_loader(start_dir, run_sub_dir) 37 | runner = unittest.TextTestRunner() 38 | runner.run(suite) 39 | -------------------------------------------------------------------------------- /demo/discover_used.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | # from unittest_base.test_calculator import TestCalculator 4 | # 5 | # suit = unittest.TestSuite() 6 | # suit.addTests([ 7 | # TestCalculator("test_add_one"), 8 | # TestCalculator("test_add_two"), 9 | # TestCalculator("test_add_three") 10 | # ]) 11 | 12 | 13 | if __name__ == '__main__': 14 | # 指定测试开始的目录为 `top_level_dir/tests` 15 | start_dir = 'top_level_dir/tests' 16 | # 指定顶层目录为 `top_level_dir` 17 | top_level_dir = 'top_level_dir' 18 | 19 | suite = unittest.defaultTestLoader.discover( 20 | start_dir="unittest_base", 21 | pattern='test_cal*.py', 22 | top_level_dir="??" 23 | ) 24 | 25 | runner = unittest.TextTestRunner() 26 | runner.run(suite) 27 | -------------------------------------------------------------------------------- /demo/unittest_assert/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/demo/unittest_assert/__init__.py -------------------------------------------------------------------------------- /demo/unittest_assert/test_assert_equal.py: -------------------------------------------------------------------------------- 1 | """ 2 | assert 基本使用 3 | """ 4 | import unittest 5 | 6 | 7 | def pprint(msg) -> str: 8 | print(msg) 9 | return msg 10 | 11 | 12 | class MyNumber: 13 | def __init__(self, value): 14 | self.value = value 15 | 16 | def __repr__(self): 17 | return f"MyNumber({self.value})" 18 | 19 | 20 | class TestBaseAssert(unittest.TestCase): 21 | # ... 22 | 23 | def test_base_assert(self): 24 | """ 25 | 基础断言方法 26 | :return: 27 | """ 28 | num = 100 29 | num2 = 80 30 | 31 | self.assertEqual(1 + 2, 2) 32 | self.assertNotEqual(1 + 2, 1) 33 | 34 | ret = True 35 | self.assertTrue(ret) 36 | self.assertFalse(ret) 37 | 38 | self.assertIs(num < num2, ret) 39 | self.assertIsNot(num < num2, ret) 40 | 41 | r = pprint("hello") 42 | self.assertIsNone(r) 43 | self.assertIsNotNone(r) 44 | 45 | result = "欢迎, lisi" 46 | username = "zhangsan" 47 | self.assertIn(username, result) 48 | self.assertNotIn(username, result) 49 | 50 | usernames = ["zhangsan", "lisi"] 51 | self.assertIsInstance(usernames, list) 52 | self.assertNotIsInstance(usernames, str) 53 | 54 | self.assertDictEqual({"key": 11}, {"key": 22}) 55 | self.assertEqual({"key": 11}, {"key": 22}) 56 | 57 | def test_isinstance_my_number(self): 58 | # 创建一个MyNumber的实例 59 | my_num = MyNumber(42) 60 | you_num = 43 61 | # 使用assertIsInstance()进行断言 62 | self.assertIsInstance(my_num, MyNumber) 63 | self.assertNotIsInstance(you_num, MyNumber) 64 | 65 | def test_eq(self): 66 | str1 = """hello 67 | world""" 68 | str2 = """hello world""" 69 | self.assertMultiLineEqual(str1, str2) 70 | self.assertEqual(str1, str2) 71 | 72 | 73 | if __name__ == '__main__': 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /demo/unittest_assert/test_assert_other.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def add_floats(a: float, b: float): 5 | return a + b 6 | 7 | 8 | class MyTestClass: 9 | def __init__(self, value): 10 | self.value = value 11 | 12 | def __str__(self): 13 | return f"MyTestClass({self.value})" 14 | 15 | # 测试数据 16 | 17 | 18 | data1 = MyTestClass(10) 19 | data2 = MyTestClass(5) 20 | list1 = [1, 2, 3, 4] 21 | list2 = [1, 2, 3, 4, 5, 6] # 注意这里有一个字符串'4' 22 | 23 | 24 | class TestAssertions(unittest.TestCase): 25 | 26 | def test_add_floats(self): 27 | # 两个浮点数相加 28 | result = add_floats(0.1, 0.2) 29 | 30 | # 由于浮点数的精度问题,直接比较可能不等 31 | # 使用assertAlmostEqual来允许微小的差异 32 | self.assertAlmostEqual(result, 0.4, delta=0.0001) 33 | 34 | # 另一个例子,这次我们故意制造一个较大的误差 35 | bad_result = 0.3001 36 | with self.assertRaises(AssertionError): 37 | # 这个断言会失败,因为bad_result和0.3的差值大于delta 38 | self.assertAlmostEqual(bad_result, 0.3, delta=0.0001) 39 | 40 | def test_assertGreater(self): 41 | # 检查 data1.value 是否大于 data2.value 42 | self.assertGreater(data1.value, data2.value) 43 | 44 | def test_assertLess(self): 45 | # 检查 data2.value 是否小于 data1.value 46 | self.assertLess(data2.value, data1.value) 47 | 48 | def test_assertRegex(self): 49 | # 假设我们有一个字符串,并想检查它是否匹配某个正则表达式 50 | test_string = "This is a test string 123" 51 | # 使用正则表达式匹配包含数字的模式 52 | self.assertRegex(test_string, r'\d+') 53 | 54 | def test_assertCountEqual(self): 55 | # 检查两个列表是否包含相同的元素(不考虑顺序) 56 | # 注意 list2 中有一个字符串'4',但整数值4仍然被认为是相等的(因为它们是相等的值) 57 | self.assertCountEqual(list1, list2) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /demo/unittest_base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/demo/unittest_base/__init__.py -------------------------------------------------------------------------------- /demo/unittest_base/calculator.py: -------------------------------------------------------------------------------- 1 | class Calculator: 2 | """ 3 | 计算器 4 | """ 5 | 6 | def __init__(self, *args): 7 | self.args = args 8 | 9 | def add(self): 10 | """ 11 | 加法运算 12 | """ 13 | return sum(self.args) 14 | -------------------------------------------------------------------------------- /demo/unittest_base/test_calculator.py: -------------------------------------------------------------------------------- 1 | """ 2 | 认识unittest测试用例: 3 | * test fixture 4 | * test case 5 | * test suite 6 | * test runner 7 | """ 8 | import time 9 | import unittest 10 | 11 | from .calculator import Calculator 12 | 13 | 14 | class TestCalculator(unittest.TestCase): 15 | """ 16 | # 测试类:**. 创建测试类 必须 继承 unittest.TestCase 17 | """ 18 | 19 | def setUp(self) -> None: 20 | """ 21 | text fixture 22 | 用例前置动作:启动浏览器、连接数据库,准备的数据, 23 | """ 24 | print("test start") 25 | 26 | def tearDown(self) -> None: 27 | """ 28 | text fixture 29 | 用例后置动作:关闭浏览器,关闭数据库,删除/还原数据 30 | """ 31 | print("test end") 32 | 33 | def login(self, username, password): 34 | """封装用户登录模块""" 35 | print("this is method") 36 | 37 | def test_case(self): 38 | """ 39 | 1. 必须以 test 开头. 40 | 2.每个用例都是独立的个体,不能有入参,不能被调用。 41 | """ 42 | print("this is test case") 43 | 44 | def test_user_login(self): 45 | """测试用户登录""" 46 | self.login("admin", "admin123") 47 | 48 | def test_aa_fail(self): 49 | self.assertAlmostEqual(2 + 2, 3) 50 | 51 | def test_add_one(self): 52 | self.login("aaa", "bbb") 53 | c = Calculator(2) 54 | result = c.add() 55 | time.sleep(1) 56 | self.assertEqual(result, 2) 57 | 58 | def test_add_two(self): 59 | c = Calculator(3, 5) 60 | result = c.add() 61 | time.sleep(2) 62 | self.assertEqual(result, 8) 63 | 64 | def test_add_three(self): 65 | c = Calculator(3, 5, 2) 66 | result = c.add() 67 | time.sleep(3) 68 | self.assertEqual(result, 10) 69 | 70 | 71 | if __name__ == '__main__': 72 | # 创建测试套件 test suite 73 | suit = unittest.TestSuite() 74 | # suit.addTest(TestCalculator("test_add_one")) 75 | # suit.addTest(TestCalculator("test_add_two")) 76 | # suit.addTest(TestCalculator("test_add_three")) 77 | suit.addTests([ 78 | TestCalculator("test_add_one"), 79 | TestCalculator("test_add_two"), 80 | TestCalculator("test_add_three") 81 | ]) 82 | # 创建测试运行器 test runner 83 | runner = unittest.TextTestRunner() 84 | runner.run(suit) 85 | -------------------------------------------------------------------------------- /demo/unittest_base/test_improper_use.py: -------------------------------------------------------------------------------- 1 | """ 2 | unittest 错误用法 3 | """ 4 | import unittest 5 | 6 | 7 | class TestImproperUse(unittest.TestCase): 8 | """ 9 | 用例错误的设计 10 | """ 11 | 12 | def test_login(self, username, password): 13 | """ 14 | 1. 给用例加参数 15 | """ 16 | print("this is login case") 17 | 18 | def test_case_1(self): 19 | print("this is test case 1") 20 | 21 | def test_case_2(self): 22 | """ 23 | 2. 在一条用例里面调用另一条用例。 24 | """ 25 | self.test_case_1() 26 | print("this is test case 2") 27 | 28 | 29 | class CorrectUsage(unittest.TestCase): 30 | 31 | def login(self, username, password): 32 | """封装的登录""" 33 | print("this is login method") 34 | 35 | def test_case_1(self): 36 | self.login("admin", "admin123") 37 | print("this is test case 1") 38 | 39 | def test_case_2(self): 40 | self.login("guest", "guest123") 41 | print("this is test case 2") 42 | 43 | 44 | if __name__ == '__main__': 45 | unittest.main() 46 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/demo/unittest_data_driver/__init__.py -------------------------------------------------------------------------------- /demo/unittest_data_driver/data/test_data_dict_dict.json: -------------------------------------------------------------------------------- 1 | { 2 | "positive_integer_range": { 3 | "start": 0, 4 | "end": 2, 5 | "value": 1 6 | }, 7 | "negative_integer_range": { 8 | "start": -2, 9 | "end": 0, 10 | "value": -1 11 | }, 12 | "positive_real_range": { 13 | "start": 0.0, 14 | "end": 1.0, 15 | "value": 0.5 16 | }, 17 | "negative_real_range": { 18 | "start": -1.0, 19 | "end": 0.0, 20 | "value": -0.5 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/data/test_data_dict_dict.yaml: -------------------------------------------------------------------------------- 1 | positive_integer_range: 2 | start: 0 3 | end: 2 4 | value: 1 5 | 6 | negative_integer_range: 7 | start: -2 8 | end: 0 9 | value: -1 10 | 11 | positive_real_range: 12 | start: 0.0 13 | end: 1.0 14 | value: 0.5 15 | 16 | negative_real_range: 17 | start: -1.0 18 | end: 0.0 19 | value: -0.5 -------------------------------------------------------------------------------- /demo/unittest_data_driver/file_data/file_data_csv.csv: -------------------------------------------------------------------------------- 1 | firstname,lastname 2 | Forest,Hobbs 3 | Ferdinand,Lozano 4 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/file_data/file_data_dict.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "start": 0, 4 | "end": 2, 5 | "value": 1 6 | }, 7 | { 8 | "start": -2, 9 | "end": 0, 10 | "value": -1 11 | }, 12 | { 13 | "start": 0.0, 14 | "end": 1.0, 15 | "value": 0.5 16 | }, 17 | { 18 | "start": -1.0, 19 | "end": 0.0, 20 | "value": -0.5 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/file_data/file_data_dict.yaml: -------------------------------------------------------------------------------- 1 | - start: 0 2 | end: 2 3 | value: 1 4 | - start: -2 5 | end: 0 6 | value: -1 7 | - start: 0 8 | end: 1 9 | value: 0.5 10 | - start: -1 11 | end: 0 12 | value: -0.5 13 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/file_data/file_data_excel.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/demo/unittest_data_driver/file_data/file_data_excel.xlsx -------------------------------------------------------------------------------- /demo/unittest_data_driver/test_case/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | DATA_DIR = os.path.join(BASE_DIR, "data") 5 | 6 | 7 | # 8 | # json_path = os.path.join(DATA_DIR, "test_data_dict_dict.json") 9 | # yaml_path = os.path.join(DATA_DIR, "test_data_dict_dict.yaml") 10 | # 11 | 12 | def data_file(file_name: str) -> str: 13 | file_path = os.path.join(DATA_DIR, file_name) 14 | return file_path 15 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/test_case/test_ddt_file_demo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ddt import ddt, file_data 4 | 5 | from config import data_file 6 | 7 | 8 | @ddt 9 | class TestFile(unittest.TestCase): 10 | 11 | @file_data(data_file("test_data_dict_dict.json")) 12 | def test_file_data_json_dict_dict(self, start, end, value): 13 | self.assertLess(start, end) 14 | self.assertLess(value, end) 15 | self.assertGreater(value, start) 16 | 17 | @file_data(data_file("test_data_dict_dict.yaml")) 18 | def test_file_data_yaml_dict_dict(self, start, end, value): 19 | self.assertLess(start, end) 20 | self.assertLess(value, end) 21 | self.assertGreater(value, start) 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main(verbosity=2) 26 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/test_ddt_demo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ddt import ddt, data, unpack 4 | 5 | 6 | @ddt 7 | class TestData(unittest.TestCase): 8 | 9 | @data(["case1", "selenium"], ["case2", "ddt"], ["case3", "python"]) 10 | @unpack 11 | def test_list_data(self, case, search_key): 12 | print("第一组测试用例:", case) 13 | 14 | @data(("case1", "selenium"), ("case2", "ddt"), ("case3", "python")) 15 | @unpack 16 | def test_tuple_data(self, case, search_key): 17 | print("第二组测试用例:", case) 18 | 19 | @data({"search_key": "selenium", "case_name": "case1"}, 20 | {"search_key": "ddt", "case_name": "case2"}, 21 | {"search_key": "python", "case_name": "case3"}) 22 | @unpack 23 | def test_dict_data(self, case_name, search_key): 24 | print("第三组测试用例:", case_name) 25 | 26 | 27 | if __name__ == '__main__': 28 | unittest.main(verbosity=2) 29 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/test_ddt_file_demo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ddt import ddt, file_data 4 | 5 | 6 | @ddt 7 | class TestFile(unittest.TestCase): 8 | 9 | @file_data('data/test_data_dict_dict.json') 10 | def test_file_data_json_dict_dict(self, start, end, value): 11 | self.assertLess(start, end) 12 | self.assertLess(value, end) 13 | self.assertGreater(value, start) 14 | 15 | @file_data('data/test_data_dict_dict.yaml') 16 | def test_file_data_yaml_dict_dict(self, start, end, value): 17 | self.assertLess(start, end) 18 | self.assertLess(value, end) 19 | self.assertGreater(value, start) 20 | 21 | 22 | if __name__ == '__main__': 23 | unittest.main(verbosity=2) 24 | -------------------------------------------------------------------------------- /demo/unittest_data_driver/test_parameterized_demo.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from parameterized import parameterized 5 | 6 | 7 | def calculate_discounted_price(original_price, discount_rate): 8 | """ 9 | 计算打折后的价格。 10 | :param original_price: 原价 11 | :param discount_rate: 折扣率(0-1之间的浮点数) 12 | :return: 折后价格 13 | """ 14 | return original_price * (1 - discount_rate) 15 | 16 | 17 | from parameterized import parameterized_class 18 | 19 | 20 | @parameterized_class(('a', 'b', 'expected_sum', 'expected_product'), [ 21 | (1, 2, 3, 2), 22 | (5, 5, 10, 25) 23 | ]) 24 | class TestMathClass(unittest.TestCase): 25 | 26 | def test_add(self): 27 | print("param-->", self.a, self.b, self.expected_sum) 28 | self.assertEqual(self.a + self.b, self.expected_sum) 29 | 30 | def test_multiply(self): 31 | print("param-->", self.a, self.b, self.expected_sum) 32 | self.assertEqual(self.a * self.b, self.expected_product) 33 | 34 | 35 | @unittest.skip("跳过") 36 | class TestMathUnitTest(unittest.TestCase): 37 | 38 | @parameterized.expand([ 39 | ("negative", -1.5, -2.0), 40 | ("integer", 1, 1.0), 41 | ("large fraction", 1.6, 1), 42 | ]) 43 | def test_floor(self, name, input, expected): 44 | self.assertEqual(math.floor(input), expected) 45 | 46 | @parameterized.expand([ 47 | (100, 0.1, 90.0), # 10% 的折扣 48 | (200, 0.2, 160.0), # 20% 的折扣 49 | (300, 0.3, 220.0), # 30% 的折扣 210.0 error 50 | (400, 0.4, 240.0), # 40% 的折扣 51 | (500, 0.5, 250.0), # 50% 的折扣 52 | (600, 0.6, 240.0), # 60% 的折扣 53 | ]) 54 | def test_calculate_discounted_price(self, original_price, discount_rate, expected_price): 55 | """测试折扣率""" 56 | calculated_price = calculate_discounted_price(original_price, discount_rate) 57 | # 断言失败了不会影响后续的数据的遍历。 58 | self.assertAlmostEqual(calculated_price, expected_price, 59 | msg=f"对于原价{original_price}和折扣率{discount_rate},计算结果有误。") 60 | 61 | 62 | if __name__ == '__main__': 63 | unittest.main(verbosity=2) 64 | -------------------------------------------------------------------------------- /demo/unittest_extend/django_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | django test 3 | """ 4 | from django.test import TestCase 5 | 6 | 7 | class IndexPageTest(TestCase): 8 | """ 9 | 测试index登录首页 10 | """ 11 | 12 | def test_index_page_renders_index_template(self): 13 | """ 14 | 断言是否用给定的index.html模版响应 15 | :return: 16 | """ 17 | response = self.client.get('/index/') 18 | self.assertEqual(response.status_code, 200) 19 | self.assertTemplateUsed(response, 'index.html') 20 | -------------------------------------------------------------------------------- /demo/unittest_extend/flask_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask Testing 3 | """ 4 | from urllib import request 5 | 6 | from flask import Flask 7 | from flask_testing import LiveServerTestCase 8 | 9 | 10 | class MyTest(LiveServerTestCase): 11 | 12 | def create_app(self): 13 | app = Flask(__name__) 14 | app.config['TESTING'] = True 15 | # Default port is 5000 16 | app.config['LIVESERVER_PORT'] = 8943 17 | # Default timeout is 5 seconds 18 | app.config['LIVESERVER_TIMEOUT'] = 10 19 | return app 20 | 21 | def test_server_is_up_and_running(self): 22 | response = request.urlopen(self.get_server_url()) 23 | self.assertEqual(response.code, 200) 24 | -------------------------------------------------------------------------------- /demo/unittest_extend/nose2_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | nose2 testing 3 | """ 4 | import unittest 5 | 6 | from nose2.tools import params 7 | 8 | 9 | class TestStrings(unittest.TestCase): 10 | def test_upper(self): 11 | self.assertEqual("spam".upper(), "SPAM") 12 | 13 | 14 | @params("Sir Bedevere", "Miss Islington", "Duck") 15 | def test_is_knight(value): 16 | assert value.startswith('Sir') 17 | -------------------------------------------------------------------------------- /demo/unittest_extend/seldom_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | seldom testing 3 | """ 4 | import seldom 5 | 6 | 7 | class YouTest(seldom.TestCase): 8 | 9 | def test_case(self): 10 | """a simple test case """ 11 | self.assertEqual(1 + 1, 2) 12 | 13 | 14 | if __name__ == '__main__': 15 | seldom.main() 16 | -------------------------------------------------------------------------------- /demo/unittest_extend/testify_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | testify testing 3 | """ 4 | from testify import * 5 | 6 | 7 | class AdditionTestCase(TestCase): 8 | 9 | @class_setup 10 | def init_the_variable(self): 11 | self.variable = 0 12 | 13 | @setup 14 | def increment_the_variable(self): 15 | self.variable += 1 16 | 17 | def test_the_variable(self): 18 | assert_equal(self.variable, 1) 19 | 20 | @suite('disabled', reason='ticket #123, not equal to 2 places') 21 | def test_broken(self): 22 | # raises 'AssertionError: 1 !~= 1.01' 23 | assert_almost_equal(1, 1.01, threshold=2) 24 | 25 | @teardown 26 | def decrement_the_variable(self): 27 | self.variable -= 1 28 | 29 | @class_teardown 30 | def get_rid_of_the_variable(self): 31 | self.variable = None 32 | 33 | 34 | if __name__ == "__main__": 35 | run() 36 | -------------------------------------------------------------------------------- /demo/unittest_fixture/test_fixture_class.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class SomeWork(): 5 | 6 | def init_env(self): 7 | print("初始化环境") 8 | 9 | def clear_env(self): 10 | print("清理环境配置") 11 | 12 | 13 | class Test(unittest.TestCase): 14 | some_work = None 15 | 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.some_work = SomeWork() 19 | cls.some_work.init_env() 20 | 21 | def test_case(self): 22 | print("this is case") 23 | 24 | def test_case2(self): 25 | print("this is case2") 26 | 27 | @classmethod 28 | def tearDownClass(cls): 29 | cls.some_work.clear_env() 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /demo/unittest_fixture/test_fixture_method.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class Test(unittest.TestCase): 5 | 6 | def user_login(self): 7 | # 1. 输入用户名 8 | # 2. 输入密码 9 | # 3. 点击 登录 10 | print("执行登录的动作") 11 | 12 | def setUp(self) -> None: 13 | self.user_login() 14 | print("before") 15 | 16 | def test_case_send_mail(self): 17 | # 写邮件 _> 发送 18 | print("写邮件 _> 发送") 19 | 20 | def test_case_del_mail(self): 21 | # 查看邮件 _> 删除 22 | print("查看邮件 _> 删除") 23 | 24 | def test_case_read_mail(self): 25 | # 打开邮件 _> 查看 26 | print("打开邮件 _> 查看") 27 | 28 | def tearDown(self) -> None: 29 | print("after") 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /demo/unittest_fixture/test_fixture_module.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def setUpModule(): 5 | print("all module case before") 6 | 7 | 8 | def tearDownModule(): 9 | print("all module case after") 10 | 11 | 12 | class Test(unittest.TestCase): 13 | 14 | def test_case(self): 15 | print("this is Test class case") 16 | 17 | 18 | class Test2(unittest.TestCase): 19 | 20 | def test_case(self): 21 | print("this is Test2 class case") 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /demo/unittest_other/test_02_loader.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import test_example 4 | 5 | # from test_example import ExampleTest 6 | 7 | if __name__ == '__main__': 8 | # 1.用于测试所有的用例, 更适合测试项目里面 9 | # suit = unittest.defaultTestLoader.discover() 10 | 11 | # 2. 用于用例级别,更适合单个用例的执行 12 | # suite = unittest.TestSuite() 13 | # suite.addTests([ 14 | # ExampleTest("test_case_2"), 15 | # ExampleTest("test_case_1"), 16 | # ]) 17 | 18 | # 3. 当前文件下面所有用例的执行 main() 19 | 20 | test_loader = unittest.TestLoader() 21 | # 加载测试类 22 | # suite = test_loader.loadTestsFromTestCase(ExampleTest) 23 | 24 | # 加载测试用例 **不能指定目录 25 | # suite = test_loader.loadTestsFromName("test_example.ExampleTest.test_case_1") 26 | 27 | # 加载多个测试 28 | # suite = test_loader.loadTestsFromNames([ 29 | # "test_example.ExampleTest.test_case_1", 30 | # "test_example.ExampleTest2.test_case_4" 31 | # ]) 32 | 33 | # 加载测试模块 模块(module) = 文件 34 | suite = test_loader.loadTestsFromModule(test_example) 35 | 36 | # 测试运行器 37 | runner = unittest.TextTestRunner() 38 | runner.run(suite) 39 | -------------------------------------------------------------------------------- /demo/unittest_other/test_example.py: -------------------------------------------------------------------------------- 1 | # test_example.py 2 | import unittest 3 | 4 | 5 | # 目录>文件>类>方法:名称的字符,按照 0~9 a~z顺序。 6 | # ** 用例之间不要有依赖。避免 7 | # 通过命令控制执行:test_01_login.py test_02_create.py ... test_09_logout.py 8 | # 需求:就是按照从上到下的顺序? 需要自己去修改。 9 | 10 | class ExampleTest3(unittest.TestCase): 11 | def test_case_2(self): 12 | print("this is test_case_2") 13 | self.assertEqual(2 + 2, 4) 14 | 15 | def test_case_1(self): 16 | print("this is test_case_1") 17 | self.assertEqual(1 + 1, 2) 18 | 19 | 20 | class ExampleTest2(unittest.TestCase): 21 | def test_case_3(self): 22 | print("this is test_case_3") 23 | self.assertEqual(1 + 1, 2) 24 | 25 | def test_case_4(self): 26 | print("this is test_case_4") 27 | self.assertEqual(2 + 2, 5) 28 | -------------------------------------------------------------------------------- /demo/unittest_other/test_main.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestLogin(unittest.TestCase): 5 | 6 | def test_login_fail(self): 7 | ... 8 | 9 | def test_login_success(self): 10 | ... 11 | 12 | 13 | class TestRegister(unittest.TestCase): 14 | 15 | def test_register_fail(self): 16 | self.assertEqual(1, 3) 17 | 18 | def test_register_success(self): 19 | ... 20 | 21 | 22 | if __name__ == '__main__': 23 | unittest.main( 24 | failfast=True, 25 | testRunner=unittest.TextTestRunner, 26 | module="test_example", 27 | verbosity=2, 28 | buffer=True 29 | ) 30 | -------------------------------------------------------------------------------- /demo/unittest_skip/test_defined_skip.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def skipUnlessHasattr(obj, attr): 5 | """ 6 | 自定义 skipUnlessHasattr 装饰器 7 | :param obj: 8 | :param attr: 9 | :return: 10 | """ 11 | if hasattr(obj, attr): 12 | return lambda func: func 13 | return unittest.skip("{!r} doesn't have {!r}".format(obj, attr)) 14 | 15 | 16 | def skipTestBaseLogin(username, password): 17 | # 登录的逻辑 18 | is_login = False 19 | if username == "admin" and password == "admin123": 20 | is_login = True 21 | 22 | if is_login: 23 | return lambda func: func 24 | 25 | return unittest.skip("登录失败") 26 | 27 | 28 | class ExampleClass: 29 | """ 30 | 示例类,包含一些属性 31 | """ 32 | 33 | def __init__(self): 34 | self.some_attribute = 'value' 35 | 36 | 37 | ec = ExampleClass() 38 | 39 | 40 | class MyTestCase(unittest.TestCase): 41 | 42 | @skipTestBaseLogin("admin", "admin456") 43 | def test_login_after(self): 44 | print("登录之后") 45 | 46 | @skipUnlessHasattr(ec, 'some_attribute') 47 | def test_with_attribute(self): 48 | self.assertTrue(True) 49 | 50 | @skipUnlessHasattr(ec, 'missing_attribute') 51 | def test_without_attribute(self): 52 | self.assertTrue(True) 53 | 54 | 55 | # 运行测试 56 | if __name__ == '__main__': 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /demo/unittest_skip/test_skip.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def user_login_no_fail() -> bool: 5 | # .... 6 | return True # 真失败 7 | 8 | 9 | def user_login() -> bool: 10 | # .... 11 | return False # 成功 12 | 13 | 14 | class Config: 15 | IS_LOGIN_SUCCESS = False 16 | 17 | 18 | class MyTest(unittest.TestCase): 19 | 20 | @unittest.skip("直接跳过测试") 21 | def test_skip(self): 22 | print("test aaa") 23 | 24 | @unittest.skipIf(user_login_no_fail() is False, "登录没有失败") 25 | def test_skip_if(self): 26 | print('登录失败之后,注册账号') 27 | 28 | def test_aa_login(self): 29 | # 测试执行登录 30 | print("执行登录") 31 | Config.IS_LOGIN_SUCCESS = True 32 | print(Config.IS_LOGIN_SUCCESS) 33 | 34 | @unittest.skipUnless(user_login() is True, "当条件为真时执行测试") 35 | def test_zz_skip_unless(self): 36 | print('登录之后,执行', Config.IS_LOGIN_SUCCESS) 37 | 38 | @unittest.expectedFailure # 失败是在预期之内,所以,失败了不会抛错误 39 | def test_expected_failure(self): 40 | print('test ddd') 41 | self.assertEqual(2, 3) 42 | 43 | 44 | @unittest.skip("整个测试类跳过") 45 | class MySkippedTestCase(unittest.TestCase): 46 | 47 | def test_not_run(self): 48 | print('test eee') 49 | 50 | 51 | if __name__ == '__main__': 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /demo/unittest_skip/test_unittest_skip.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | __version__ = 0 5 | 6 | 7 | def element_is_exists() -> bool: 8 | """ 9 | element is exists 10 | :return: 11 | """ 12 | return False 13 | 14 | 15 | class MyTestCase(unittest.TestCase): 16 | 17 | @unittest.skip("demonstrating skipping") 18 | def test_nothing(self): 19 | self.fail("shouldn't happen") 20 | 21 | @unittest.skipIf(__version__ < 3, "not supported in this library version") 22 | def test_format(self): 23 | # Tests that work for only a certain version of the library. 24 | pass 25 | 26 | @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") 27 | def test_windows_support(self): 28 | # windows specific testing code 29 | pass 30 | 31 | def test_maybe_skipped(self): 32 | if element_is_exists(): 33 | self.skipTest("external resource not available") 34 | # test code that depends on the external resource 35 | pass 36 | -------------------------------------------------------------------------------- /demo/unittest_sub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/demo/unittest_sub/__init__.py -------------------------------------------------------------------------------- /demo/unittest_sub/test_subtest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def calculate_discounted_price(original_price, discount_rate): 5 | """ 6 | 计算打折后的价格。 7 | :param original_price: 原价 8 | :param discount_rate: 折扣率(0-1之间的浮点数) 9 | :return: 折后价格 10 | """ 11 | return original_price * (1 - discount_rate) 12 | 13 | 14 | class TestDiscountCalculator(unittest.TestCase): 15 | 16 | # def test_case_1(self): 17 | # case_data = (100, 0.1, 90.0) 18 | # calculated_price = calculate_discounted_price(case_data[0], case_data[1]) 19 | # print("result", calculated_price) 20 | # self.assertAlmostEqual(calculated_price, case_data[2], 21 | # msg=f"对于原价{case_data[0]}和折扣率{case_data[2]},计算结果有误。") 22 | # 23 | # def test_case_2(self): 24 | # case_data = (200, 0.2, 160.0) 25 | # calculated_price = calculate_discounted_price(case_data[0], case_data[1]) 26 | # print("result", calculated_price) 27 | # self.assertAlmostEqual(calculated_price, case_data[2], 28 | # msg=f"对于原价{case_data[0]}和折扣率{case_data[2]},计算结果有误。") 29 | # 30 | # def test_case_3(self): 31 | # case_data = (300, 0.3, 220.0) 32 | # calculated_price = calculate_discounted_price(case_data[0], case_data[1]) 33 | # print("result", calculated_price) 34 | # self.assertAlmostEqual(calculated_price, case_data[2], 35 | # msg=f"对于原价{case_data[0]}和折扣率{case_data[2]},计算结果有误。") 36 | # 37 | # def test_case_4(self): 38 | # case_data = (400, 0.4, 240.0) 39 | # calculated_price = calculate_discounted_price(case_data[0], case_data[1]) 40 | # print("result", calculated_price) 41 | # self.assertAlmostEqual(calculated_price, case_data[2], 42 | # msg=f"对于原价{case_data[0]}和折扣率{case_data[2]},计算结果有误。") 43 | # 44 | # def test_case_5(self): 45 | # case_data = (500, 0.5, 250.0) 46 | # calculated_price = calculate_discounted_price(case_data[0], case_data[1]) 47 | # print("result", calculated_price) 48 | # self.assertAlmostEqual(calculated_price, case_data[2], 49 | # msg=f"对于原价{case_data[0]}和折扣率{case_data[2]},计算结果有误。") 50 | 51 | def test_calculate_discounted_price(self): 52 | # 定义一系列测试用例 53 | test_cases = [ 54 | (100, 0.1, 90.0), # 10% 的折扣 55 | (200, 0.2, 160.0), # 20% 的折扣 56 | (300, 0.3, 220.0), # 30% 的折扣 210.0 error 57 | (400, 0.4, 240.0), # 40% 的折扣 58 | (500, 0.5, 250.0), # 50% 的折扣 59 | ] 60 | # for case in test_cases: 61 | # print("case", case) 62 | # calculated_price = calculate_discounted_price(case[0], case[1]) 63 | # print("result", calculated_price) 64 | # self.assertAlmostEqual(calculated_price, case[2], 65 | # msg=f"对于原价{case[0]}和折扣率{case[2]},计算结果有误。") 66 | 67 | # 使用 subTest 对每个测试用例进行迭代 68 | for original_price, discount_rate, expected_price in test_cases: 69 | print(f"原价:{original_price}, 折扣率:{discount_rate}, 现价:{expected_price}") 70 | with self.subTest( 71 | op=original_price, 72 | dr=discount_rate, 73 | ep=expected_price 74 | ): 75 | calculated_price = calculate_discounted_price(original_price, discount_rate) 76 | # 断言失败了不会影响后续的数据的遍历。 77 | self.assertAlmostEqual(calculated_price, expected_price, 78 | msg=f"对于原价{original_price}和折扣率{discount_rate},计算结果有误。") 79 | 80 | 81 | if __name__ == '__main__': 82 | unittest.main() 83 | -------------------------------------------------------------------------------- /drawios/history.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /images/histroy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/images/histroy.png -------------------------------------------------------------------------------- /images/html_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/images/html_report.png -------------------------------------------------------------------------------- /images/json_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/images/json_report.png -------------------------------------------------------------------------------- /images/test_fixture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/images/test_fixture.jpg -------------------------------------------------------------------------------- /images/unittest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/images/unittest.png -------------------------------------------------------------------------------- /plugin/data_driver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoTestClass/Learn-unittest-class/7b960a08ee91b80aec754b1fd7998c94b3ceeb50/plugin/data_driver/__init__.py -------------------------------------------------------------------------------- /plugin/data_driver/conversion.py: -------------------------------------------------------------------------------- 1 | """ 2 | Data type conversion of different files 3 | """ 4 | import codecs 5 | import csv 6 | import json 7 | from itertools import islice 8 | 9 | import yaml 10 | from openpyxl import load_workbook 11 | 12 | 13 | def check_data(list_data: list) -> list: 14 | """ 15 | Checking test data format. 16 | :param list_data: 17 | :return: 18 | """ 19 | if isinstance(list_data, list) is False: 20 | raise TypeError("The data format is not `list`.") 21 | if len(list_data) == 0: 22 | raise ValueError("The data format cannot be `[]`.") 23 | if isinstance(list_data[0], dict): 24 | test_data = [] 25 | for data in list_data: 26 | line = [] 27 | for d in data.values(): 28 | line.append(d) 29 | test_data.append(line) 30 | return test_data 31 | 32 | return list_data 33 | 34 | 35 | def csv_to_list(file: str = None, line: int = 1, end_line: int = None) -> list: 36 | """ 37 | Convert CSV file data to list 38 | :param file: Path to file 39 | :param line: Start line of read data 40 | :param end_line: End line of read data 41 | :return: list data 42 | 43 | Usage: 44 | csv_to_list("data.csv", line=1) 45 | """ 46 | if file is None: 47 | raise FileExistsError("Please specify the CSV file to convert.") 48 | 49 | table_data = [] 50 | with codecs.open(file, 'r', encoding='utf_8_sig') as csv_file: 51 | csv_data = csv.reader(csv_file) 52 | for i in islice(csv_data, line - 1, end_line): 53 | table_data.append(i) 54 | 55 | return table_data 56 | 57 | 58 | def excel_to_list(file: str = None, sheet: str = "Sheet1", line: int = 1, end_line: int = None) -> list: 59 | """ 60 | Convert Excel file data to list 61 | :param file: Path to file 62 | :param sheet: Excel sheet, default name is Sheet1 63 | :param line: Start line of read data 64 | :param end_line: Start line of read data 65 | :return: list data 66 | 67 | Usage: 68 | excel_to_list("data.xlsx", sheet="Sheet1", line=1) 69 | """ 70 | if file is None: 71 | raise FileExistsError("Please specify the Excel file to convert.") 72 | 73 | excel_table = load_workbook(file) 74 | sheet = excel_table[sheet] 75 | if end_line is None: 76 | end_line = sheet.max_row 77 | 78 | table_data = [] 79 | for i in sheet.iter_rows(line, end_line): 80 | line_data = [] 81 | for field in i: 82 | line_data.append(field.value) 83 | table_data.append(line_data) 84 | 85 | return table_data 86 | 87 | 88 | def json_to_list(file: str = None, key: str = None) -> list: 89 | """ 90 | Convert JSON file data to list 91 | :param file: Path to file 92 | :param key: Specifies the key for the dictionary 93 | :return: list data 94 | 95 | Usage: 96 | json_to_list("data.yaml", key="login") 97 | """ 98 | if file is None: 99 | raise FileExistsError("Please specify the JSON file to convert.") 100 | 101 | if key is None: 102 | with open(file, "r", encoding="utf-8") as json_file: 103 | data = json.load(json_file) 104 | list_data = check_data(data) 105 | else: 106 | with open(file, "r", encoding="utf-8") as json_file: 107 | try: 108 | data = json.load(json_file)[key] 109 | list_data = check_data(data) 110 | except KeyError as exc: 111 | raise ValueError(f"Check the test data, no '{key}'.") from exc 112 | 113 | return list_data 114 | 115 | 116 | def yaml_to_list(file: str = None, key: str = None) -> list: 117 | """ 118 | Convert YAML file data to list 119 | :param file: Path to file 120 | :param key: Specifies the key for the dictionary 121 | :return: list data 122 | 123 | Usage: 124 | yaml_to_list("data.yaml", key="login") 125 | """ 126 | if file is None: 127 | raise FileExistsError("Please specify the YAML file to convert.") 128 | 129 | if key is None: 130 | with open(file, "r", encoding="utf-8") as yaml_file: 131 | data = yaml.load(yaml_file, Loader=yaml.FullLoader) 132 | list_data = check_data(data) 133 | else: 134 | with open(file, "r", encoding="utf-8") as yaml_file: 135 | try: 136 | data = yaml.load(yaml_file, Loader=yaml.FullLoader)[key] 137 | list_data = check_data(data) 138 | except KeyError as exc: 139 | raise ValueError(f"Check the YAML test data, no '{key}'") from exc 140 | 141 | return list_data 142 | -------------------------------------------------------------------------------- /plugin/data_driver/param.py: -------------------------------------------------------------------------------- 1 | """ 2 | 引用 parameterized 库 3 | """ 4 | import inspect 5 | import os 6 | import warnings 7 | from functools import wraps 8 | 9 | from parameterized.parameterized import default_doc_func 10 | from parameterized.parameterized import default_name_func 11 | from parameterized.parameterized import delete_patches_if_need 12 | from parameterized.parameterized import parameterized 13 | from parameterized.parameterized import reapply_patches_if_need 14 | from parameterized.parameterized import skip_on_empty_helper 15 | 16 | try: 17 | from conversion import check_data 18 | from conversion import csv_to_list 19 | from conversion import excel_to_list 20 | from conversion import yaml_to_list 21 | from conversion import json_to_list 22 | except ModuleNotFoundError: 23 | from .conversion import check_data 24 | from .conversion import csv_to_list 25 | from .conversion import excel_to_list 26 | from .conversion import yaml_to_list 27 | from .conversion import json_to_list 28 | 29 | 30 | def _search_file_path(file_name: str, file_dir: str) -> str: 31 | """ 32 | search file path 33 | :param file_name: 34 | :param file_dir: 35 | """ 36 | if os.path.isfile(file_name) is True: 37 | file_path = file_name 38 | 39 | find_dir = os.path.dirname(os.path.dirname(file_dir)) 40 | 41 | file_path = None 42 | for root, _, files in os.walk(find_dir, topdown=False): 43 | for file in files: 44 | if file == file_name: 45 | file_path = os.path.join(root, file_name) 46 | break 47 | else: 48 | continue 49 | break 50 | 51 | if file_path is None: 52 | return "" 53 | 54 | return file_path 55 | 56 | 57 | def file_data(file: str, line: int = 1, sheet: str = "Sheet1", key: str = None, end_line: int = None): 58 | """ 59 | Support file parameterization. 60 | 61 | :param file: file name 62 | :param line: Start line number of an Excel/CSV file 63 | :param end_line: End line number of an Excel/CSV file 64 | :param sheet: Excel sheet name 65 | :param key: Key name of an YAML/JSON file 66 | 67 | Usage: 68 | d.json 69 | ```json 70 | { 71 | "login": [ 72 | ["admin", "admin123"], 73 | ["guest", "guest123"] 74 | ] 75 | } 76 | ``` 77 | >> @file_data(file="d.json", key="login") 78 | ... def test_case(self, username, password): 79 | ... print(username) 80 | ... print(password) 81 | """ 82 | if file is None: 83 | raise FileExistsError("File name does not exist.") 84 | 85 | stack_t = inspect.stack() 86 | ins = inspect.getframeinfo(stack_t[1][0]) 87 | file_dir = os.path.dirname(os.path.abspath(ins.filename)) 88 | 89 | file_path = _search_file_path(file, file_dir) 90 | if file_path == "": 91 | raise FileExistsError(f"No '{file}' data file found.") 92 | 93 | suffix = file.split(".")[-1] 94 | if suffix == "csv": 95 | data_list = csv_to_list(file_path, line=line, end_line=end_line) 96 | elif suffix == "xlsx": 97 | data_list = excel_to_list(file_path, sheet=sheet, line=line, end_line=end_line) 98 | elif suffix == "json": 99 | data_list = json_to_list(file_path, key=key) 100 | elif suffix == "yaml": 101 | data_list = yaml_to_list(file_path, key=key) 102 | else: 103 | raise FileExistsError(f"Your file is not supported: {file}") 104 | 105 | return data(data_list) 106 | 107 | 108 | def data(input, name_func=None, doc_func=None, skip_on_empty=False, 109 | namespace=None, **legacy): 110 | """ A "brute force" method of parameterizing test cases. Creates new 111 | test cases and injects them into the namespace that the wrapped 112 | function is being defined in. Useful for parameterizing tests in 113 | subclasses of 'UnitTest', where Nose test generators don't work. 114 | 115 | :param input: An iterable of values to pass to the test function. 116 | :param name_func: A function that takes a single argument (the 117 | value from the input iterable) and returns a string to use as 118 | the name of the test case. If not provided, the name of the 119 | test case will be the name of the test function with the 120 | parameter value appended. 121 | :param doc_func: A function that takes a single argument (the 122 | value from the input iterable) and returns a string to use as 123 | the docstring of the test case. If not provided, the docstring 124 | of the test case will be the docstring of the test function. 125 | :param skip_on_empty: If True, the test will be skipped if the 126 | input iterable is empty. If False, a ValueError will be raised 127 | if the input iterable is empty. 128 | :param namespace: The namespace (dict-like) to inject the test cases 129 | into. If not provided, the namespace of the test function will 130 | be used. 131 | 132 | >>> @parameterized.expand([("foo", 1, 2)]) 133 | ... def test_add1(name, input, expected): 134 | ... actual = add1(input) 135 | ... assert_equal(actual, expected) 136 | ... 137 | >>> locals() 138 | ... test_add1_foo_0: ... 139 | >>> 140 | """ 141 | 142 | input = check_data(input) 143 | 144 | if "testcase_func_name" in legacy: 145 | warnings.warn("testcase_func_name= is deprecated; use name_func=", 146 | DeprecationWarning, stacklevel=2) 147 | if not name_func: 148 | name_func = legacy["testcase_func_name"] 149 | 150 | if "testcase_func_doc" in legacy: 151 | warnings.warn("testcase_func_doc= is deprecated; use doc_func=", 152 | DeprecationWarning, stacklevel=2) 153 | if not doc_func: 154 | doc_func = legacy["testcase_func_doc"] 155 | 156 | doc_func = doc_func or default_doc_func 157 | name_func = name_func or default_name_func 158 | 159 | def parameterized_expand_wrapper(f, instance=None): 160 | frame_locals = namespace 161 | if frame_locals is None: 162 | frame_locals = inspect.currentframe().f_back.f_locals 163 | 164 | parameters = parameterized.input_as_callable(input)() 165 | 166 | if not parameters: 167 | if not skip_on_empty: 168 | raise ValueError( 169 | "Parameters iterable is empty (hint: use " 170 | "`parameterized.expand([], skip_on_empty=True)` to skip " 171 | "this test when the input is empty)" 172 | ) 173 | return wraps(f)(skip_on_empty_helper) 174 | 175 | digits = len(str(len(parameters) - 1)) 176 | for num, p in enumerate(parameters): 177 | name = name_func(f, "{num:0>{digits}}".format(digits=digits, num=num), p) 178 | # If the original function has patches applied by 'mock.patch', 179 | # re-construct all patches on the just former decoration layer 180 | # of param_as_standalone_func so as not to share 181 | # patch objects between new functions 182 | nf = reapply_patches_if_need(f) 183 | frame_locals[name] = parameterized.param_as_standalone_func(p, nf, name) 184 | frame_locals[name].__doc__ = doc_func(f, num, p) 185 | 186 | # Delete original patches to prevent new function from evaluating 187 | # original patching object as well as re-constructed patches. 188 | delete_patches_if_need(f) 189 | 190 | f.__test__ = False 191 | 192 | return parameterized_expand_wrapper 193 | -------------------------------------------------------------------------------- /plugin/data_driver/param_demo.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | import warnings 4 | from functools import wraps 5 | 6 | import openpyxl 7 | from parameterized.parameterized import default_doc_func 8 | from parameterized.parameterized import default_name_func 9 | from parameterized.parameterized import delete_patches_if_need 10 | from parameterized.parameterized import parameterized 11 | from parameterized.parameterized import reapply_patches_if_need 12 | from parameterized.parameterized import skip_on_empty_helper 13 | 14 | from .conversion import check_data 15 | 16 | 17 | def read_json_file(file_path: str) -> list: 18 | """ 19 | 读取JSON文件 20 | :param file_path: 21 | :return: 22 | """ 23 | with open(file_path, encoding="utf8") as f: 24 | data = json.load(f) 25 | print(data) 26 | 27 | return data 28 | 29 | 30 | def read_excel_file(file_path: str, sheet_name=None, start_line: int = 1) -> list: 31 | """ 32 | 读取excel文件 33 | :param file_path: 34 | :param sheet_name: 35 | :param start_line: 36 | :return: 37 | """ 38 | wb = openpyxl.load_workbook(file_path) 39 | if sheet_name is None: 40 | sheet_name = "Sheet1" 41 | 42 | try: 43 | # 获取指定的sheet标签页 44 | sheet = wb[sheet_name] 45 | 46 | # 读取数据sheet_name 47 | data_list = [] 48 | for row in sheet.iter_rows(min_row=start_line, values_only=True): 49 | line = [] 50 | for cell in row: 51 | line.append(cell) 52 | data_list.append(line) 53 | except KeyError: 54 | raise ValueError(f"Sheet '{sheet_name}' not found in the Excel file.") 55 | 56 | # 关闭Excel文件 57 | wb.close() 58 | 59 | return data_list 60 | 61 | 62 | def file_data(file_path: str, sheet_name=None, start_line: int = 1): 63 | file_type = file_path.split(".")[-1] 64 | 65 | if file_type == "json": 66 | file_data = read_json_file(file_path) 67 | 68 | elif file_type == "xlsx": 69 | file_data = read_excel_file(file_path) 70 | 71 | else: 72 | raise TypeError("不支持文件类型") 73 | 74 | return data(file_data) 75 | 76 | 77 | def data(input, name_func=None, doc_func=None, skip_on_empty=False, 78 | namespace=None, **legacy): 79 | """ A "brute force" method of parameterizing test cases. Creates new 80 | test cases and injects them into the namespace that the wrapped 81 | function is being defined in. Useful for parameterizing tests in 82 | subclasses of 'UnitTest', where Nose test generators don't work. 83 | 84 | :param input: An iterable of values to pass to the test function. 85 | :param name_func: A function that takes a single argument (the 86 | value from the input iterable) and returns a string to use as 87 | the name of the test case. If not provided, the name of the 88 | test case will be the name of the test function with the 89 | parameter value appended. 90 | :param doc_func: A function that takes a single argument (the 91 | value from the input iterable) and returns a string to use as 92 | the docstring of the test case. If not provided, the docstring 93 | of the test case will be the docstring of the test function. 94 | :param skip_on_empty: If True, the test will be skipped if the 95 | input iterable is empty. If False, a ValueError will be raised 96 | if the input iterable is empty. 97 | :param namespace: The namespace (dict-like) to inject the test cases 98 | into. If not provided, the namespace of the test function will 99 | be used. 100 | 101 | >>> @parameterized.expand([("foo", 1, 2)]) 102 | ... def test_add1(name, input, expected): 103 | ... actual = add1(input) 104 | ... assert_equal(actual, expected) 105 | ... 106 | >>> locals() 107 | ... test_add1_foo_0: ... 108 | >>> 109 | """ 110 | 111 | input = check_data(input) 112 | 113 | if "testcase_func_name" in legacy: 114 | warnings.warn("testcase_func_name= is deprecated; use name_func=", 115 | DeprecationWarning, stacklevel=2) 116 | if not name_func: 117 | name_func = legacy["testcase_func_name"] 118 | 119 | if "testcase_func_doc" in legacy: 120 | warnings.warn("testcase_func_doc= is deprecated; use doc_func=", 121 | DeprecationWarning, stacklevel=2) 122 | if not doc_func: 123 | doc_func = legacy["testcase_func_doc"] 124 | 125 | doc_func = doc_func or default_doc_func 126 | name_func = name_func or default_name_func 127 | 128 | def parameterized_expand_wrapper(f, instance=None): 129 | frame_locals = namespace 130 | if frame_locals is None: 131 | frame_locals = inspect.currentframe().f_back.f_locals 132 | 133 | parameters = parameterized.input_as_callable(input)() 134 | 135 | if not parameters: 136 | if not skip_on_empty: 137 | raise ValueError( 138 | "Parameters iterable is empty (hint: use " 139 | "`parameterized.expand([], skip_on_empty=True)` to skip " 140 | "this test when the input is empty)" 141 | ) 142 | return wraps(f)(skip_on_empty_helper) 143 | 144 | digits = len(str(len(parameters) - 1)) 145 | for num, p in enumerate(parameters): 146 | name = name_func(f, "{num:0>{digits}}".format(digits=digits, num=num), p) 147 | # If the original function has patches applied by 'mock.patch', 148 | # re-construct all patches on the just former decoration layer 149 | # of param_as_standalone_func so as not to share 150 | # patch objects between new functions 151 | nf = reapply_patches_if_need(f) 152 | frame_locals[name] = parameterized.param_as_standalone_func(p, nf, name) 153 | frame_locals[name].__doc__ = doc_func(f, num, p) 154 | 155 | # Delete original patches to prevent new function from evaluating 156 | # original patching object as well as re-constructed patches. 157 | delete_patches_if_need(f) 158 | 159 | f.__test__ = False 160 | 161 | return parameterized_expand_wrapper 162 | -------------------------------------------------------------------------------- /plugin/htmlrunner/result.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import TestResult 3 | 4 | 5 | class _TestResult(TestResult): 6 | """ 7 | 继承 unittest.TestResult 类,重写通过/失败/错误/跳过等方法 8 | """ 9 | 10 | def __init__(self, verbosity=1): 11 | TestResult.__init__(self) 12 | self.success_count = 0 13 | self.failure_count = 0 14 | self.error_count = 0 15 | self.skip_count = 0 16 | self.verbosity = verbosity 17 | self.result = [] 18 | 19 | def startTest(self, test): 20 | """ 21 | 开始测试 22 | :param test: 23 | :return: 24 | """ 25 | TestResult.startTest(self, test) 26 | 27 | def addSuccess(self, test): 28 | """ 29 | 记录成功的用例 30 | :param test: 31 | :return: 32 | """ 33 | self.success_count += 1 34 | TestResult.addSuccess(self, test) 35 | self.result.append((0, test, '')) 36 | if self.verbosity > 1: 37 | sys.stderr.write('ok') 38 | sys.stderr.write(str(test)) 39 | sys.stderr.write('\n') 40 | else: 41 | sys.stderr.write('.' + str(self.success_count)) 42 | 43 | def addFailure(self, test, err): 44 | """ 45 | 记录失败的用例 46 | :param test: 47 | :param err: 48 | :return: 49 | """ 50 | self.failure_count += 1 51 | TestResult.addFailure(self, test, err) 52 | _, _exc_str = self.failures[-1] 53 | self.result.append((1, test, _exc_str)) 54 | if self.verbosity > 1: 55 | sys.stderr.write('F') 56 | sys.stderr.write(str(test)) 57 | sys.stderr.write('\n') 58 | else: 59 | sys.stderr.write('F') 60 | 61 | def addError(self, test, err): 62 | """ 63 | 记录错误的用例 64 | :param test: 65 | :param err: 66 | :return: 67 | """ 68 | self.error_count += 1 69 | TestResult.addError(self, test, err) 70 | _, _exc_str = self.errors[-1] 71 | self.result.append((2, test, _exc_str)) 72 | if self.verbosity > 1: 73 | sys.stderr.write('E') 74 | sys.stderr.write(str(test)) 75 | sys.stderr.write('\n') 76 | else: 77 | sys.stderr.write('E') 78 | 79 | def addSkip(self, test, reason): 80 | """ 81 | 记录跳过的用例 82 | :param test: 83 | :param reason: 84 | :return: 85 | """ 86 | self.skip_count += 1 87 | TestResult.addSkip(self, test, reason) 88 | self.result.append((3, test, reason)) 89 | if self.verbosity > 1: 90 | sys.stderr.write('S') 91 | sys.stderr.write(str(test)) 92 | sys.stderr.write('\n') 93 | else: 94 | sys.stderr.write('S') 95 | -------------------------------------------------------------------------------- /plugin/htmlrunner/runner.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from jinja2 import Environment, FileSystemLoader, select_autoescape 5 | 6 | from htmlrunner.result import _TestResult 7 | 8 | # template模板目录 9 | PATH_DIR = os.path.dirname(os.path.abspath(__file__)) 10 | TEMP_DIR = os.path.join(PATH_DIR, "template") 11 | 12 | # 加载 template 目录 13 | env = Environment( 14 | loader=FileSystemLoader(TEMP_DIR), 15 | autoescape=select_autoescape()) 16 | 17 | # 指定 table.html 文件 18 | template = env.get_template("report.html") 19 | 20 | # 定义用例类型 21 | case_type = { 22 | 0: "passed", 23 | 1: "failure", 24 | 2: "errors", 25 | 3: "skipped" 26 | } 27 | 28 | 29 | class HTMLTestRunner: 30 | """ 31 | 运行测试:生成HTML格式的测试结果 32 | """ 33 | 34 | def __init__(self, output, verbosity=1, report_name: str = "unittest单元测试报告"): 35 | self.output = output 36 | self.verbosity = verbosity 37 | self.report_name = report_name 38 | self.start_time = datetime.datetime.now() 39 | 40 | def run(self, test): 41 | """ 42 | 运行测试 43 | """ 44 | result = _TestResult(self.verbosity) 45 | test(result) 46 | case_info = self.test_result(result) 47 | 48 | # 测试结果转HTML 49 | stop_time = datetime.datetime.now() 50 | self.result_to_html(case_info, stop_time) 51 | 52 | print(f"Time Elapsed: {stop_time - self.start_time}") 53 | return result 54 | 55 | def test_result(self, result): 56 | """ 57 | 解析测试结果 58 | """ 59 | class_list = [] 60 | sorted_result = self.sort_result(result.result) 61 | for cid, (cls, cls_results) in enumerate(sorted_result): 62 | # 统计类下面用例数据 63 | passed = failure = errors = skipped = 0 64 | for n, t, e in cls_results: 65 | if n == 0: 66 | passed += 1 67 | elif n == 1: 68 | failure += 1 69 | elif n == 2: 70 | errors += 1 71 | else: 72 | skipped += 1 73 | 74 | # 格式化类的描述信息 75 | if cls.__module__ == "__main__": 76 | name = cls.__name__ 77 | else: 78 | name = "%s.%s" % (cls.__module__, cls.__name__) 79 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 80 | desc = doc and '%s' % doc or name 81 | 82 | cases = [] 83 | for tid, (n, t, e) in enumerate(cls_results): 84 | case_info = self.generate_case_data(cid, tid, n, t, e) 85 | cases.append(case_info) 86 | 87 | class_list.append({ 88 | "desc": desc, 89 | "count": passed + failure + errors + skipped, 90 | "pass": passed, 91 | "fail": failure, 92 | "error": errors, 93 | "skipped": skipped, 94 | "cases": cases 95 | }) 96 | 97 | return class_list 98 | 99 | @staticmethod 100 | def sort_result(result_list): 101 | """ 102 | unittest运行用例没有特定的顺序, 103 | 这里将测试用例按照测试类分组 104 | """ 105 | rmap = {} 106 | classes = [] 107 | for n, t, e in result_list: 108 | cls = t.__class__ 109 | if not cls in rmap: 110 | rmap[cls] = [] 111 | classes.append(cls) 112 | rmap[cls].append((n, t, e)) 113 | r = [(cls, rmap[cls]) for cls in classes] 114 | return r 115 | 116 | @staticmethod 117 | def generate_case_data(cid, tid, n, t, e): 118 | """ 119 | 生成测试用例数据 120 | """ 121 | tid = (n == 0 and "p" or "f") + f"t{cid + 1}.{tid + 1}" 122 | name = t.id().split('.')[-1] 123 | doc = t.shortDescription() or "" 124 | 125 | case = { 126 | "number": tid, 127 | "name": name, 128 | "doc": doc, 129 | "result": case_type.get(n), 130 | "error": e 131 | } 132 | 133 | return case 134 | 135 | def result_to_html(self, result, stop_time): 136 | """ 137 | 测试结果转HTML 138 | """ 139 | run_time = str(stop_time - self.start_time) 140 | tmp = template.render(class_list=result, 141 | report_name=self.report_name, 142 | run_time=run_time) 143 | 144 | # 保存HTML结果 145 | with open(self.output, "w", encoding="utf-8") as f: 146 | f.write(tmp) 147 | -------------------------------------------------------------------------------- /plugin/htmlrunner/template/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Start - unittest report template 6 | 7 | 8 | 12 | 17 | 22 | 23 | 28 |
29 |
30 |
31 |
38 |
41 |
42 | 75 | 76 |

{{ report_name }}

77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |

89 | Test Result 92 |

93 |
94 |
95 |
96 |
102 | {% for class in class_list %} 103 |
104 | 107 | 108 | 109 | 112 | 115 | 118 | 121 | 124 | 127 | 130 | 131 | 132 | 133 | {% for case in class.cases %} 134 | 135 | 140 | 145 | 150 | 155 | 160 | 161 | {% endfor %} 162 | 163 |
110 | {{ class.name }} 111 | 113 | {{ class.desc }} 114 | 116 | {{ class.count }} 117 | 119 | {{ class.pass }} 120 | 122 | {{ class.fail }} 123 | 125 | {{ class.error }} 126 | 128 | {{ class.skipped }} 129 |
136 | {{ case.number }} 139 | 141 | {{ case.name }} 144 | 146 | {{ case.doc }} 149 | 151 | {{ case.result }} 154 | 156 | {{ case.error }} 159 |
164 |
165 | {% endfor %} 166 |
167 |
168 |
169 |

运行时间:{{ run_time }}

170 |
171 |
172 |
173 |
174 |
175 |
176 | 209 |
210 |
211 |
212 | 213 | 214 | -------------------------------------------------------------------------------- /plugin/jsonrunner/result.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import TestResult 3 | 4 | 5 | class _TestResult(TestResult): 6 | """ 7 | 继承 unittest.TestResult 类,重写通过/失败/错误/跳过等方法 8 | """ 9 | 10 | def __init__(self, verbosity=1): 11 | TestResult.__init__(self) 12 | self.success_count = 0 13 | self.failure_count = 0 14 | self.error_count = 0 15 | self.skip_count = 0 16 | self.verbosity = verbosity 17 | self.result = [] 18 | 19 | def startTest(self, test): 20 | """ 21 | 开始测试 22 | :param test: 23 | :return: 24 | """ 25 | TestResult.startTest(self, test) 26 | 27 | def addSuccess(self, test): 28 | """ 29 | 记录成功的用例 30 | :param test: 31 | :return: 32 | """ 33 | self.success_count += 1 34 | TestResult.addSuccess(self, test) 35 | self.result.append((0, test, '')) 36 | if self.verbosity > 1: 37 | sys.stderr.write('ok') 38 | sys.stderr.write(str(test)) 39 | sys.stderr.write('\n') 40 | else: 41 | sys.stderr.write('.' + str(self.success_count)) 42 | 43 | def addFailure(self, test, err): 44 | """ 45 | 记录失败的用例 46 | :param test: 47 | :param err: 48 | :return: 49 | """ 50 | self.failure_count += 1 51 | TestResult.addFailure(self, test, err) 52 | _, _exc_str = self.failures[-1] 53 | self.result.append((1, test, _exc_str)) 54 | if self.verbosity > 1: 55 | sys.stderr.write('F') 56 | sys.stderr.write(str(test)) 57 | sys.stderr.write('\n') 58 | else: 59 | sys.stderr.write('F') 60 | 61 | def addError(self, test, err): 62 | """ 63 | 记录错误的用例 64 | :param test: 65 | :param err: 66 | :return: 67 | """ 68 | self.error_count += 1 69 | TestResult.addError(self, test, err) 70 | _, _exc_str = self.errors[-1] 71 | self.result.append((2, test, _exc_str)) 72 | if self.verbosity > 1: 73 | sys.stderr.write('E') 74 | sys.stderr.write(str(test)) 75 | sys.stderr.write('\n') 76 | else: 77 | sys.stderr.write('E') 78 | 79 | def addSkip(self, test, reason): 80 | """ 81 | 记录跳过的用例 82 | :param test: 83 | :param reason: 84 | :return: 85 | """ 86 | self.skip_count += 1 87 | TestResult.addSkip(self, test, reason) 88 | self.result.append((3, test, reason)) 89 | if self.verbosity > 1: 90 | sys.stderr.write('S') 91 | sys.stderr.write(str(test)) 92 | sys.stderr.write('\n') 93 | else: 94 | sys.stderr.write('S') 95 | -------------------------------------------------------------------------------- /plugin/jsonrunner/runner.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | from jsonrunner.result import _TestResult 5 | 6 | # 定义用例类型 7 | case_type = { 8 | 0: "passed", 9 | 1: "failure", 10 | 2: "errors", 11 | 3: "skipped" 12 | } 13 | 14 | 15 | class JSONTestRunner: 16 | """ 17 | 运行测试:生成JSON格式的测试结果 18 | """ 19 | 20 | def __init__(self, output, verbosity=1): 21 | self.output = output 22 | self.verbosity = verbosity 23 | self.start_time = datetime.datetime.now() 24 | 25 | def run(self, test): 26 | """ 27 | 运行测试 28 | """ 29 | result = _TestResult(self.verbosity) 30 | test(result) 31 | case_info = self.test_result(result) 32 | 33 | # 测试结果转JSON 34 | self.result_to_json(case_info) 35 | 36 | stop_time = datetime.datetime.now() 37 | print(f"Time Elapsed: {stop_time - self.start_time}") 38 | return result 39 | 40 | def test_result(self, result): 41 | """ 42 | 解析测试结果 43 | """ 44 | class_list = [] 45 | sorted_result = self.sort_result(result.result) 46 | for cid, (cls, cls_results) in enumerate(sorted_result): 47 | # 统计类下面用例数据 48 | passed = failure = errors = skipped = 0 49 | for n, t, e in cls_results: 50 | if n == 0: 51 | passed += 1 52 | elif n == 1: 53 | failure += 1 54 | elif n == 2: 55 | errors += 1 56 | else: 57 | skipped += 1 58 | 59 | # 格式化类的描述信息 60 | if cls.__module__ == "__main__": 61 | name = cls.__name__ 62 | else: 63 | name = "%s.%s" % (cls.__module__, cls.__name__) 64 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 65 | desc = doc and '%s' % doc or name 66 | 67 | cases = [] 68 | for tid, (n, t, e) in enumerate(cls_results): 69 | case_info = self.generate_case_data(cid, tid, n, t, e) 70 | cases.append(case_info) 71 | 72 | class_list.append({ 73 | "desc": desc, 74 | "count": passed + failure + errors + skipped, 75 | "pass": passed, 76 | "fail": failure, 77 | "error": errors, 78 | "skipped": skipped, 79 | "cases": cases 80 | }) 81 | 82 | return class_list 83 | 84 | @staticmethod 85 | def sort_result(result_list): 86 | """ 87 | unittest运行用例没有特定的顺序, 88 | 这里将测试用例按照测试类分组 89 | """ 90 | rmap = {} 91 | classes = [] 92 | for n, t, e in result_list: 93 | cls = t.__class__ 94 | if not cls in rmap: 95 | rmap[cls] = [] 96 | classes.append(cls) 97 | rmap[cls].append((n, t, e)) 98 | r = [(cls, rmap[cls]) for cls in classes] 99 | return r 100 | 101 | @staticmethod 102 | def generate_case_data(cid, tid, n, t, e): 103 | """ 104 | 生成测试用例数据 105 | """ 106 | tid = (n == 0 and "p" or "f") + f"t{cid + 1}.{tid + 1}" 107 | name = t.id().split('.')[-1] 108 | doc = t.shortDescription() or "" 109 | 110 | case = { 111 | "number": tid, 112 | "name": name, 113 | "doc": doc, 114 | "result": case_type.get(n), 115 | "error": e 116 | } 117 | 118 | return case 119 | 120 | def result_to_json(self, result): 121 | """ 122 | 测试结果转JSON 123 | """ 124 | with open(self.output, "w", encoding="utf-8") as f: 125 | json.dump(result, f, ensure_ascii=False, indent=4) 126 | -------------------------------------------------------------------------------- /plugin/label_plugin/LabelTestRunner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run tests in debug mode 3 | """ 4 | import functools 5 | import unittest 6 | 7 | 8 | class LabelTestRunner(unittest.TextTestRunner): 9 | """Label test runner""" 10 | 11 | def __init__(self, *args, **kwargs): 12 | """ 13 | Append blacklist & whitelist attributes to TestRunner instance 14 | """ 15 | self.whitelist = set(kwargs.pop('whitelist', [])) 16 | self.blacklist = set(kwargs.pop('blacklist', [])) 17 | 18 | super(LabelTestRunner, self).__init__(*args, **kwargs) 19 | 20 | @classmethod 21 | def test_iter(cls, suite): 22 | """ 23 | Iterate through test suites, and yield individual tests 24 | """ 25 | for test in suite: 26 | if isinstance(test, unittest.TestSuite): 27 | for t in cls.test_iter(test): 28 | yield t 29 | else: 30 | yield test 31 | 32 | def run(self, testlist): 33 | """ 34 | Run the given test case or test suite. 35 | """ 36 | # Change given testlist into a TestSuite 37 | suite = unittest.TestSuite() 38 | 39 | # Add each test in testlist, apply skip mechanism if necessary 40 | for test in self.test_iter(testlist): 41 | # Determine if test should be skipped 42 | skip = bool(self.whitelist) 43 | test_method = getattr(test, test._testMethodName) 44 | test_labels = getattr(test, '_labels', set()) | getattr(test_method, '_labels', set()) 45 | if test_labels & self.whitelist: 46 | skip = False 47 | if test_labels & self.blacklist: 48 | skip = True 49 | 50 | if skip: 51 | # Test should be skipped. 52 | # replace original method with function "skip" 53 | # Create a "skip test" wrapper for the actual test method 54 | @functools.wraps(test_method) 55 | def skip_wrapper(*args, **kwargs): 56 | raise unittest.SkipTest('label exclusion') 57 | 58 | skip_wrapper.__unittest_skip__ = True 59 | if len(self.whitelist) >= 1: 60 | skip_wrapper.__unittest_skip_why__ = f'label whitelist {self.whitelist}' 61 | if len(self.blacklist) >= 1: 62 | skip_wrapper.__unittest_skip_why__ = f'label blacklist {self.blacklist}' 63 | setattr(test, test._testMethodName, skip_wrapper) 64 | 65 | suite.addTest(test) 66 | 67 | # Resume normal TextTestRunner function with the new test suite 68 | super(LabelTestRunner, self).run(suite) 69 | -------------------------------------------------------------------------------- /plugin/label_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | def label(*labels): 2 | """ 3 | Test case classification label 4 | 5 | Usage: 6 | class MyTest(unittest.TestCase): 7 | 8 | @label('quick') 9 | def test_foo(self): 10 | pass 11 | """ 12 | 13 | def inner(cls): 14 | # append labels to class 15 | cls._labels = set(labels) | getattr(cls, '_labels', set()) 16 | return cls 17 | 18 | return inner 19 | -------------------------------------------------------------------------------- /plugin/reports/result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Start - unittest report template 6 | 7 | 8 | 12 | 17 | 22 | 23 | 28 |
29 |
30 |
31 |
38 |
41 |
42 | 75 | 76 |

unittest演示测试报告

77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |

89 | Test Result 92 |

93 |
94 |
95 |
96 |
102 | 103 |
104 | 107 | 108 | 109 | 112 | 115 | 118 | 121 | 124 | 127 | 130 | 131 | 132 | 133 | 134 | 135 | 140 | 145 | 150 | 155 | 160 | 161 | 162 | 163 | 168 | 173 | 178 | 183 | 188 | 189 | 190 | 191 | 196 | 201 | 206 | 211 | 220 | 221 | 222 | 223 | 228 | 233 | 238 | 243 | 253 | 254 | 255 | 256 |
110 | 111 | 113 | Test Demo class 114 | 116 | 4 117 | 119 | 1 120 | 122 | 1 123 | 125 | 1 126 | 128 | 1 129 |
136 | pt1.1 139 | 141 | test_pass 144 | 146 | pass case 149 | 151 | passed 154 | 156 | 159 |
164 | ft1.2 167 | 169 | test_skip 172 | 174 | skip case 177 | 179 | skipped 182 | 184 | skip case 187 |
192 | ft1.3 195 | 197 | test_fail 200 | 202 | fail case 205 | 207 | failure 210 | 212 | Traceback (most recent call last): 214 | File "D:\github\AutoTestClass\Learn-unittest-class\plugin\test_htmlrunner.py", line 15, in test_fail 215 | self.assertEqual(5, 6) 216 | AssertionError: 5 != 6 217 | 219 |
224 | ft1.4 227 | 229 | test_error 232 | 234 | error case 237 | 239 | errors 242 | 244 | Traceback (most recent call last): 246 | File "D:\github\AutoTestClass\Learn-unittest-class\plugin\test_htmlrunner.py", line 19, in test_error 247 | self.assertEqual(a, 6) 248 | ^ 249 | NameError: name 'a' is not defined 250 | 252 |
257 |
258 | 259 |
260 |
261 |
262 |

运行时间:0:00:00.001009

263 |
264 |
265 |
266 |
267 |
268 |
269 | 302 |
303 |
304 |
305 | 306 | -------------------------------------------------------------------------------- /plugin/reports/result.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "desc": "Test Demo class", 4 | "count": 4, 5 | "pass": 1, 6 | "fail": 1, 7 | "error": 1, 8 | "skipped": 1, 9 | "cases": [ 10 | { 11 | "number": "pt1.1", 12 | "name": "test_pass", 13 | "doc": "pass case", 14 | "result": "passed", 15 | "error": "" 16 | }, 17 | { 18 | "number": "ft1.2", 19 | "name": "test_skip", 20 | "doc": "skip case", 21 | "result": "skipped", 22 | "error": "skip case" 23 | }, 24 | { 25 | "number": "ft1.3", 26 | "name": "test_fail", 27 | "doc": "fail case", 28 | "result": "failure", 29 | "error": "Traceback (most recent call last):\n File \"D:\\github\\AutoTestClass\\Learn-unittest-class\\plugin\\test_jsonrunner.py\", line 15, in test_fail\n self.assertEqual(5, 6)\nAssertionError: 5 != 6\n" 30 | }, 31 | { 32 | "number": "ft1.4", 33 | "name": "test_error", 34 | "doc": "error case", 35 | "result": "errors", 36 | "error": "Traceback (most recent call last):\n File \"D:\\github\\AutoTestClass\\Learn-unittest-class\\plugin\\test_jsonrunner.py\", line 19, in test_error\n self.assertEqual(a, 6)\n ^\nNameError: name 'a' is not defined\n" 37 | } 38 | ] 39 | } 40 | ] -------------------------------------------------------------------------------- /plugin/test_htmlrunner.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from htmlrunner.runner import HTMLTestRunner 4 | 5 | 6 | class TestDemo(unittest.TestCase): 7 | """Test Demo class""" 8 | 9 | def test_pass(self): 10 | """pass case""" 11 | self.assertEqual(5, 5) 12 | 13 | def test_fail(self): 14 | """fail case""" 15 | self.assertEqual(5, 6) 16 | 17 | def test_error(self): 18 | """error case""" 19 | self.assertEqual(a, 6) 20 | 21 | @unittest.skip("skip case") 22 | def test_skip(self): 23 | """skip case""" 24 | ... 25 | 26 | 27 | if __name__ == '__main__': 28 | suit = unittest.TestSuite() 29 | suit.addTests([ 30 | TestDemo("test_pass"), 31 | TestDemo("test_skip"), 32 | TestDemo("test_fail"), 33 | TestDemo("test_error") 34 | ]) 35 | 36 | runner = HTMLTestRunner(output="./reports/result.html", report_name="unittest演示测试报告") 37 | runner.run(suit) 38 | -------------------------------------------------------------------------------- /plugin/test_jsonrunner.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from jsonrunner.runner import JSONTestRunner 4 | 5 | 6 | class TestDemo(unittest.TestCase): 7 | """Test Demo class""" 8 | 9 | def test_pass(self): 10 | """pass case""" 11 | self.assertEqual(5, 5) 12 | 13 | def test_fail(self): 14 | """fail case""" 15 | self.assertEqual(5, 6) 16 | 17 | def test_error(self): 18 | """error case""" 19 | self.assertEqual(a, 6) 20 | 21 | @unittest.skip("skip case") 22 | def test_skip(self): 23 | """skip case""" 24 | ... 25 | 26 | 27 | if __name__ == '__main__': 28 | suit = unittest.TestSuite() 29 | suit.addTests([ 30 | TestDemo("test_pass"), 31 | TestDemo("test_skip"), 32 | TestDemo("test_fail"), 33 | TestDemo("test_error") 34 | ]) 35 | 36 | runner = JSONTestRunner(output="./reports/result.json", verbosity=2) 37 | runner.run(suit) 38 | -------------------------------------------------------------------------------- /plugin/test_label.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from label_plugin import label 4 | from label_plugin.LabelTestRunner import LabelTestRunner 5 | 6 | 7 | class LabelTest(unittest.TestCase): 8 | 9 | @label("base") 10 | def test_label_base(self): 11 | self.assertEqual(1 + 1, 2) 12 | 13 | @label("slow") 14 | def test_label_slow(self): 15 | self.assertEqual(1, 2) 16 | 17 | def test_no_label(self): 18 | self.assertEqual(2 + 3, 5) 19 | 20 | 21 | if __name__ == '__main__': 22 | suit = unittest.TestSuite() 23 | suit.addTests([ 24 | LabelTest("test_label_base"), 25 | LabelTest("test_label_slow"), 26 | LabelTest("test_no_label"), 27 | ]) 28 | runner = LabelTestRunner( 29 | whitelist=["base"], # 设置白名单 30 | # blacklist=["slow"], # 设置黑名单 31 | ) 32 | runner.run(suit) 33 | -------------------------------------------------------------------------------- /plugin/test_param.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data_driver.param import data, file_data 4 | 5 | 6 | class DataTest(unittest.TestCase): 7 | 8 | @data([ 9 | ("case1", "hello"), 10 | ("case2", "hi"), 11 | ("case3", "你好"), 12 | ]) 13 | def test_data_tuple(self, name, keyword): 14 | """ 15 | tuple数据 16 | """ 17 | print("tuple->", name, keyword) 18 | 19 | @data([ 20 | ["case1", "hello"], 21 | ["case2", "hi"], 22 | ["case3", "你好"], 23 | ]) 24 | def test_data_list(self, name, keyword): 25 | """ 26 | list数据 27 | """ 28 | print("list->", name, keyword) 29 | 30 | @data([ 31 | {"scene": "case1", "keyword": "hello"}, 32 | {"scene": "case2", "keyword": "hi"}, 33 | {"scene": "case3", "keyword": "你好"}, 34 | ]) 35 | def test_data_dict(self, name, keyword): 36 | """ 37 | dict数据 38 | """ 39 | print("dict->", name, keyword) 40 | 41 | 42 | class FileDataTest(unittest.TestCase): 43 | 44 | @file_data("file_data_dict.json") 45 | def test_file_data_json(self, start, end, value): 46 | """ 47 | json数据文件 48 | """ 49 | print("json file->", start, end, value) 50 | 51 | @file_data("file_data_dict.yaml") 52 | def test_file_data_yaml(self, start, end, value): 53 | """ 54 | yaml数据文件 55 | """ 56 | print("yaml file->", start, end, value) 57 | 58 | @file_data("file_data_csv.csv", line=2) 59 | def test_file_data_csv(self, firstname, lastname): 60 | """ 61 | csv数据文件 62 | """ 63 | print("csv file->", firstname, lastname) 64 | 65 | @file_data(file="file_data_excel.xlsx", sheet="Sheet1", line=2) 66 | def test_file_data_excel(self, firstname, lastname): 67 | """ 68 | excel数据文件 69 | """ 70 | print("excel file->", firstname, lastname) 71 | 72 | 73 | if __name__ == '__main__': 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==5.0.6 2 | Flask-Testing==0.8.1 3 | testify==0.11.3 4 | seldom==3.7.0 --------------------------------------------------------------------------------