[Unittest] Core elements of automated testing framework

[Software Testing Interview Crash Course] How to force yourself to finish the software testing eight-part essay tutorial in one week. After finishing the interview, you will be stable. You can also be a high-paying software testing engineer (automated testing)

1. What is the Unittest framework?
Python comes with a unit testing framework

2. Why use UnitTest framework?

>Batch execution use cases

>Provide rich assertion knowledge

>Can generate reports

3. Core elements:

1). TestCase (test case)

2). TestSuite (test suite)

3). TestRunner (test execution, execution of TestUite test suite)

4). TestLoader (execute test cases in batches - search for modules starting with specified letters in the specified folder) [Recommended]

5). Fixture (two fixed functions, one used during initialization and one used at the end))

Next, we will expand the core elements to understand the unittest framework:

First, let’s introduce the use case rules of unittest:

1. The test file must import the package: import unittest

2. The test class must inherit unittest.TestCase

3. The test method must start with test_

1. TestCase (test case)

1. It is a code file. The real use case code is written in the code file (the prints inside are all simulation test cases)

# 1、导包
# 2、自定义测试类
# 3、在测试类中书写测试方法 采用print 简单书写测试方法
# 4、执行用例
 
import unittest
 
# 2、自定义测试类,需要继承unittest模块中的TestCase类即可
class TestDemo(unittest.TestCase):
    # 书写测试方法,测试用例代码,书写要求,测试方法必须test_ 开头
    def test_method1(self):
        print('测试方法1-1')
 
    def test_method2(self):
        print('测试方法1-2')
 
# 4、执行测试用例
# 4.1 光标放在类后面执行所有的测试用例
# 4.2 光标放在方法后面执行当前的方法测试用例

Note: test_ defined by def is a test case. The test case will only be executed when if __name__ == '___mian___' is executed. Other ordinary functions will not be executed and will be called and executed through self.

2. TestSuite (test suite) and TestRunner (test execution)

1. TestSuite (test suite): used to assemble, package, and manage multiple TestCase (test case) files

2. TestRunner (test execution): used to execute TestSuite (test suite)

Code: First, multiple test case files must be prepared to implement TestSuite and TestRunner. The following code has prepared two test case files unittest_Demo2 and unittest_Demo1.

# 1、导包
# 2、实例化(创建对象)套件对象
# 3、使用套件对象添加用例方法
# 4、实例化对象运行
# 5、使用运行对象去执行套件对象
 
import unittest
 
from unittest_Demo2 import TestDemo
from unittest_Demo1 import Demo
 
suite = unittest.TestSuite()
 
# 将⼀个测试类中的所有⽅法进⾏添加
# 套件对象.addTest(unittest.makeSuite(测试类名))
suite.addTest(unittest.makeSuite(TestDemo))
suite.addTest(unittest.makeSuite(Demo))
 
# 4、实例化运行对象
runner = unittest.TextTestRunner();
# 5、使用运行对象去执行套件对象
# 运⾏对象.run(套件对象)
runner.run(suite)

3. TestLoader (test loading)

illustrate:

Add qualified test methods to the test suite
2. Search for the methods starting from test under the module file starting with the specified letter in the specified directory file, and add these methods to the test suite, and finally return the test suite

3. Same as the Testsuite function, it is a supplement to other functions and is used to assemble test cases.

Generally, test cases are written in the Case folder. When there are too many test cases, you can consider TestLoader.

写法:
1. suite = unittest.TestLoader().discover("指定搜索的目录文件","指定字母开头模块文件")
2. suite = unittest.defaultTestLoader.discover("指定搜索的目录文件","指定字母开头模块文件") 【推荐】
注意:
  如果使用写法1,TestLoader()必须有括号。
# 1. 导包
# 2. 实例化测试加载对象并添加用例 ---> 得到的是 suite 对象
# 3. 实例化 运行对象
# 4. 运行对象执行套件对象
 
import unittest
 
# 实例化测试加载对象并添加用例 ---> 得到的是 suite 对象
# unittest.defaultTestLoader.discover('用例所在的路径', '用例的代码文件名')
# 测试路径:相对路径
# 测试文件名:可以使用 * 通配符,可以重复使用
suite = unittest.defaultTestLoader.discover('./Case', 'cs*.py')
runner = unittest.TextTestRunner()
runner.run(suite)
TestSuite与TestLoader区别:
  共同点:都是测试套件
  不同点:实现方式不同
    TestSuite: 要么添加指定的测试类中所有test开头的方法,要么添加指定测试类中指定某个test开头的方法
    TestLoader: 搜索指定目录下指定字母开头的模块文件中以test字母开头的方法并将这些方法添加到测试套件中,最后返回测试套件

4. Fixture (test fixture)

It is a code structure that will be executed automatically under certain circumstances.

4.1 Method level

A structure that is automatically called before and after each test method (use case code) is executed

def setUp(), will be executed before each test method is executed (initialization)

def tearDown(), will be executed (released) after each test method is executed

Features: several test functions, executed several times. SetUp will be executed before each test function is executed, and tearDwon will be executed after execution.

# 初始化
def setUp(self):
    # 每个测试方法执行之前执行的函数
    pass
 
# 释放
def tearDown(self):
    # 每个测试方法执行之后执行的函数
    pass
场景:当你要登录自己的用户名账户的时候,都会输入网址,当你准备不用这个页面了,都会关闭当前页面;
  1、输入网址 (方法级别)
  2、关闭当前页面 (方法级别)

4.2 Class level

A structure that will be automatically called before and after all methods in each test class are executed (once before and after execution in the entire class)

def setUpClass(), before all methods in the class

def tearDownClass(), after all methods in the class

Features: Run setUpClass once before the test class runs, and run tearDownClass once after the class runs.

Note: Class methods must be decorated with @classmethod

  @classmethod
    def setUpClass(cls):
        print('-----------1.打开浏览器')
 
    @classmethod
    def tearDownClass(cls):
        print('------------5、关闭浏览器')

Case template: a combination of class level and method level implementation

[External link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly (img-GBxQV2uP-1647245316010) (C:/Users/15277/AppData/Roaming/Typora/typora-user-images/ image-20220303153824329.png)]

提示:
  无论使用函数级别还是类级别,最后常用场景为:
    初始化:
      1. 获取浏览器实例化对象
      2. 最大化浏览器
      3. 隐式等待
    结束:
      关闭浏览器驱动对象

5. Assertion☆

1. What is an assertion:

Let the program replace manual work and automatically judge whether the expected results are consistent with the actual results.

The result of the assertion:

1), True, the use case passes

2), False, the code throws an exception and the use case fails.

3). To use assertions in unittest, you need to pass the self.assert method

2. Why should we assert:

Automation scripts are executed unattended, and assertions are needed to determine whether the execution of the automation script passes.

Note: Automation scripts do not write assertions, which is equivalent to not executing the test.

3. Commonly used assertions:

self.assertEqual(ex1, ex2) # 判断ex1 是否和ex2 相等
self.assertIn(ex1, ex2) #  ex2是否包含 ex1   注意:所谓的包含不能跳字符
self.assertTrue(ex) #  判断ex是否为True
 
重点讲前两个assertEqual 和 assertIn
方法:
assertEqual:self.assertEqual(预期结果,实际结果) 判断的是预期是否相等实际
assertIn:self.assertIn(预期结果,实际结果) 判断的是预期是否包含实际中
assertIn('admin', 'admin') # 包含
assertIn('admin', 'adminnnnnnnn') # 包含
assertIn('admin', 'aaaaaadmin') # 包含
assertIn('admin', 'aaaaaadminnnnnnn') # 包含
assertIn('admin', 'addddddmin') # 不是包含
# Login 函数我已经封装好了,这里直接导包调用就可以了。
 
import unittest
 
from login import Login
 
class TestLogin(unittest.TestCase):
    """正确的用户名和密码: admin, 123456, 登录成功"""
 
    def test_success(self):
        self.assertEqual('登录成功', Login('admin', '123456'))
 
    def test_username_error(self):
        """错误的用户名: root, 123456, 登录失败"""
        self.assertEqual('登录失败', Login('root', '123456'))
 
    def test_password_error(self):
        """错误的密码: admin, 123123, 登录失败"""
        self.assertEqual('登录失败', Login('admin', '123123'))
 
    def test_error(self):
        """错误的用户名和错误的密码: aaa, 123123, 登录失败"""
        # self.assertEqual('登录失败',Login('登陆失败','123123'))
        self.assertIn('失败', Login('登录失败', '123123'))

6. Skip

For some unfinished test functions and test classes that do not meet the test conditions and do not want to be executed, you can use skip

"""
使用方法,装饰器完成
代码书写在 TestCase 文件
"""
# 直接将测试函数标记成跳过
@unittest.skip('跳过条件')
# 根据条件判断测试函数是否跳过 , 判断条件成立, 跳过
@unittest.skipIf(判断条件,'跳过原因')
import unittest
 
version = 20
 
class TestDemo1(unittest.TestCase):
    
    @unittest.skip('直接跳过')
    def test_method1(self):
        print('测试用例1-1')
 
    @unittest.skipIf(version > 19, '版本大于19,测试跳过')
    def test_method2(self):
        print('测试用例1-2')

result

7. Data-driven (unittest ddt)☆

ddt:data-driver tests

Data-driven: Data drives the execution of the entire test case, that is, the test data determines the test results.

The problems that data-driven solutions solve are:

1) Separate code and data to avoid code redundancy

2) Do not write repeated code logic;

You need to install the ddt package in the python interpreter to use it:

 

To check whether it is installed, enter pip list in cmd and name it. If there is ddt, it means the installation is successful.

grammar:

1. To use data driver, add the modifier @ddt before the class.

Note: print is used in the method. For convenience, test cases are simulated, mainly to learn data-driven. In practice, the code of the test case is written in the method.

import unittest
from ddt import ddt, data
 
@ddt  
class TestDemo(unittest.TestCase):
    # 单一参数
    @data('17611110000', '17611112222')
    def test_1(self, phone):
        print('测试一电话号码:', phone)
        
if __name__ == '__main__':
    unittest.main()
else:
    pass

1), use ddt with selenium

"""
unittest + selenium
"""
import unittest
from time import sleep
 
from ddt import ddt, data
from selenium import webdriver
 
@ddt
class TestBaidu(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = webdriver.Chrome()
        self.driver.get('https://www.sogou.com/')
 
    def tearDown(self) -> None:
        sleep(3)
        self.driver.quit()
  
    # 单一参数
    @data('易烊千玺', '王嘉尔')
    def test_01(self, name):
        self.driver.find_element_by_id('query').send_keys(name)
        self.driver.find_element_by_id('stb').click()
 
if __name__ == '__main__':
    unittest.main()

self: equivalent to this in java, a reference to the current object, self.driver defines the driver variable.

2. In practice, it is impossible to pass a single parameter, and multiple parameters will be used:

注意事项:
1)、多个数据传参的时候@data里面是要用列表形式
2)、会用到 @unpack 装饰器 进行拆包,把对应的内容传入对应的参数;
import unittest
from ddt import ddt, data, unpack
 
@ddt
class TestDemo(unittest.TestCase):
    # 多参数数据驱动
    @data(['admin', '123456'])
    # unpack 是进行拆包,不然会把列表里面的数据全部传到username这个一个参数,我们要实现列表中的两个数据分别传入对应的变量中
    @unpack
    def test_2(self, username, password):
        print('测试二:', username, password)
 
 
if __name__ == '__main__':
    unittest.main()
else:
    pass

However, the above steps are all about data in the code. If you want to test data such as n mobile phone numbers, it will be very troublesome to write them all in the @data decorator, which leads to the separation of code and data in the data driver.

3. Put the data into a text file and read the data from the file, such as JSON, excel, xml, txt and other format files. The json file type is demonstrated here.

json file processing, this link introduces the basic operations of json files and Python files

(1), driven by json file

[
  {
    "username": "admin",
    "password": "123456"
  },
  {
    "username": "normal",
    "password": "45678"
  }
]
(2) Read the json file in the test code
import json
import unittest
from ddt import ddt, data, unpack
 
# 用json多个参数读取
def reads_phone():
    with open('user.json', encoding='utf-8') as f:
        result = json.load(f)  # 列表
        return result
    
@ddt
class TestDemo(unittest.TestCase):
    # 多参数数据驱动
    @data(*reads_phone())
    # unpack 是进行拆包,不然会把列表里面的数据全部传到username这个一个参数,我们要实现列表中的两个数据分别传入对应的变量中
    @unpack
    def test_2(self, username, password):
        print('测试二:', username, password)
 
 
if __name__ == '__main__':
    unittest.main()
else:
    pass
注意事项:
1、with open里面默认是 ”r“ 
2、@data 里面的 * 含义是实现每个json对象单个传入方法执行,不然会吧json文件里面所用数据全部传入 
  > * 是元祖;
  > ** 是字典;
3、参数不能传错,要对应

Results of the:

(3), txt file driver

One line represents a group:

admin,123456
normal,456789
 
import unittest
def read():
    lis = []
    with open('readtext.txt', 'r', encoding='utf-8') as f:
        for line in f.readlines():
            # lis.append(line) #  ['admin,123456\n', 'normal,456789\n']
            # lis.append(line.strip('\n'))  ['admin,123456', 'normal,456789'] 两个字符串
            lis.append(line.strip('\n').split(','))  # [['admin', '123456'], ['normal', '456789']]
    return lis
 
class TestDome(unittest.TestCase):
    def test_01(self):
        li = read()
        print(li)
 
 
if __name__ == '__main__':
    unittest.main()
"""
split():一个字符串里面用某个字符分割,返回列表
strip():去掉两边的字符或者字符串,默认删除空白符(包括'\n', '\r',  '\t',  ' ')
"""
(3), csv file driver
供应商名称,联系人,移动电话
英业达,张三,13261231234
阿里巴巴,李四,13261231231
日立公司,王五,13261231233

Writing method one:

"""
编写 csvv.py脚本读取csv中的测试数据
"""
import csv
class ReadCsv():
    def read_csv(self):
        lis = []
        # 用csv的API的reader方法!!!!
        data = csv.reader(open('testdata.csv', 'r'))  #!!!!
        next(data, None)
        for line in data:
            lis.append(line)
            # lis.append(line[0])  # 二维数组可以省略行,列不可以省略
            # lis.append(line[1])
 
        return lis
 
# 实例化类
readCsv = ReadCsv()
# 打印类中的方法
print(readCsv.read_csv())

Writing method two: Recommended

def csvTest():
    li = []
    with open('user.csv', 'r', encoding='utf-8') as f:
        filename = csv.reader(f)
        next(filename, None)
        for r in filename:
            li.append(r)
        return li

 (4), yaml file driver

-
  username: admin9
  password: 123456
-
  username: normal
  password: 789456

Corresponding json file

[
  {
    "username": "admin9",
    "password": 123456
  },
  {
    "username": "normal",
    "password": 7894
  }
]

Writing method:

"""
使用yaml数据驱动
"""
 
import unittest
from time import sleep
 
from selenium import webdriver
from ddt import ddt, data, unpack, file_data
 
 
@ddt
class YamlTest(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = webdriver.Chrome()
        self.driver.get('file:///D:/%E6%A1%8C%E9%9D%A2/page/%E6%B3%A8%E5%86%8CA.html')
        self.driver.maximize_window()
 
    def tearDown(self) -> None:
        driver = self.driver
        sleep(3)
        driver.quit()
 
    # file_data 传入多个参数的时候,@unpack 的解包不起作用
    @unittest.skip
    @file_data('../user.yaml')
    @unpack
    def test_yaml01(self, username, password):
        driver = self.driver
        driver.find_element_by_id('userA').send_keys(username)
        driver.find_element_by_id('passwordA').send_keys(password)
 
    # 注意:传的参数名称要与yaml文件对应
    # 在yaml数据中文件中采用对象(键值对)的方式来定义数据内容
    @file_data('../user1.yaml')
    def test_yaml02(self, username, password):
        driver = self.driver
        driver.find_element_by_id('userA').send_keys(username)
        driver.find_element_by_id('passwordA').send_keys(password)
 
 
if __name__ == '__main__':
    unittest.main()

Note: file_date decorator can directly read yaml and json files

(4), Excel file driver

When creating an excel table, you need to exit pychram and create an excel table in the root directory to save, otherwise an error will be reported.

def read_excel():
    xlsx = openpyxl.load_workbook("../excel.xlsx")
    sheet1 = xlsx['Sheet1']
    print(sheet1.max_row)  # 行
    print(sheet1.max_column)  # 列
    print('=======================================================')
    allList = []
    for row in range(2, sheet1.max_row + 1):
        rowlist = []
        for column in range(1, sheet1.max_column + 1):
            rowlist.append(sheet1.cell(row, column).value)
        allList.append(rowlist)
    return allList

Use excel to log in to csdn operation

"""
测试excel数据驱动
"""
 
import unittest
from time import sleep
 
import openpyxl as openpyxl
from ddt import ddt, data, unpack
from selenium import webdriver
 
 
# 读取excel表中的数据,使用xlrd,openpyxl
def read_excel():
    xlsx = openpyxl.load_workbook("../excel.xlsx")
    sheet1 = xlsx['Sheet1']
    print(sheet1.max_row)  # 行
    print(sheet1.max_column)  # 列
    print('=======================================================')
    allList = []
    for row in range(2, sheet1.max_row + 1):
        rowlist = []
        for column in range(1, sheet1.max_column + 1):
            rowlist.append(sheet1.cell(row, column).value)
        allList.append(rowlist)
    return allList
 
 
@ddt
class ExcelText(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = webdriver.Chrome()
        self.driver.get('https://passport.csdn.net/login?code=applets')
        self.driver.maximize_window()
 
    def tearDown(self) -> None:
        driver = self.driver
        sleep(3)
        driver.quit()
 
    @data(*read_excel())
    @unpack
    def test_excel01(self, flag, username, password):
        print(flag, username, password)
        driver = self.driver
        sleep(2)
        driver.find_element_by_xpath('/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[1]/span[4]').click()
        driver.find_element_by_xpath('/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[1]/div/input').send_keys(username)
        driver.find_element_by_xpath('/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[2]/div/input').send_keys(password)
        driver.find_element_by_xpath('/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[4]/button').click()
 
if __name__ == '__main__':
    unittest.main()

10. Screenshot operation

It is impossible for a use case to be successful every time it is run, and there will definitely be times when it is not successful. If we can capture errors and save error screenshots, this will be a great feature and will also make it easier for us to locate errors.

Screenshot method:driver.get_screenshot_as_file

"""
捕捉异常截图测试
"""
import os.path
import time
import unittest
from time import sleep
 
from selenium import webdriver
 
 
class ScreeshotTest(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = webdriver.Chrome()
        self.driver.get('https://www.sogou.com/')
        self.driver.maximize_window()
 
    def tearDown(self) -> None:
        sleep(3)
        driver = self.driver
        driver.quit()
 
    def test_01(self):
        driver = self.driver
        driver.find_element_by_id('query').send_keys("易烊千玺")
        driver.find_element_by_id('stb').click()
        sleep(3)
        print(driver.title)
        try:
            self.assertEqual(driver.title, u"搜狗一下你就知道", msg="不相等")
        except:
            self.saveScreenShot(driver, "shot.png")
        sleep(5)
 
    def saveScreenShot(self, driver, filename):
        if not os.path.exists("./imge"):
            os.makedirs("./imge")
 
        # 格式十分重要,小写大写敏感 %Y%m%d-%H%M%S
        now = time.strftime("%Y%m%d-%H%M%S", time.localtime(time.time()))
        driver.get_screenshot_as_file("./imge/" + now + "-" + filename)
        sleep(3)
 
 
if __name__ == '__main__':
    unittest.main()

11. Test report

There are two types of test reports:

1. Built-in test report 2. Generate third-party test report

9.1 Bring your own test report

Only when the code of TestCase is run separately will a test report be generated.

 

 

10.2 Generate third-party test reports

Here you need a third-party test running class module, and then place it in the directory of the code

 Just like these two modules, put them in the code directory

步骤:
 1. 获取第三方的 测试运行类模块 , 将其放在代码的目录中
 2. 导包 unittest
 3. 使用 套件对象, 加载对象 去添加用例方法
 4. 实例化 第三方的运行对象 并运行 套件对象
   HTMLTestRunner()

copy

Writing method one:

import unittest
 
from HTMLTestRunner import HTMLTestRunner
 
suite = unittest.defaultTestLoader.discover('.', 'Uni*.py')
file = 'report1.html'
with open(file, 'wb') as f:
    runner = HTMLTestRunner(f, 2, '测试报告', 'python3.10')  # 运行对象
    # 运行对象执行套件, 要写在 with 的缩进中
    runner.run(suite)

Writing method two:

"""
生成测试报告
"""
import os.path
import sys
import time
import unittest
from time import sleep
 
from HTMLTestRunner import HTMLTestRunner
 
def createsuite():
    discovers = unittest.defaultTestLoader.discover("./cases", pattern="cs*.py")
    print(discovers)
    return discovers
 
 
if __name__ == '__main__':
    # 当前路径
    # sys.path 是一个路径的集合
    curpath = sys.path[0]
    print(sys.path)
    print(sys.path[0])
 
    # 当前路径文件resultreport不存在时,就创建一个
    if not os.path.exists(curpath+'/resultreport'):
        os.makedirs(curpath+'/resultreport')
 
    # 2、解决重名问题
    now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))
    print(time.time())
    print(time.localtime(time.time()))
    # 文件名是 路径 加上 文件的名称
    filename = curpath+'/resultreport/'+now+'resultreport.html'
    # 打开文件html,是用wb以写的方式打开
    with open(filename, 'wb') as f:
        runner = HTMLTestRunner(f, 2, u"测试报告", u"测试用例情况")
        suite = createsuite()
        runner.run(suite)

The current path here can also ./be used represent! ! !

"""
生成测试报告
"""
import os.path
import sys
import time
import unittest
from time import sleep
from HTMLTestRunner import HTMLTestRunner
 
def createsuite():
    discovers = unittest.defaultTestLoader.discover("./cases", pattern="cs*.py")
    print(discovers)
    return discovers
 
 
if __name__ == '__main__':
    # 当前路径文件resultreport不存在时,就创建一个
    if not os.path.exists('./resultreport'):
        os.makedirs('./resultreport')
 
    # 2、解决重名问题
    # 格式十分重要 %Y-%m-%d-%H %M %S
    now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))
    print(time.time())
    print(time.localtime(time.time()))
    # 文件名是 路径 加上 文件的名称
    filename = './resultreport/'+now+'resultreport.html'
    # 打开文件html,是用wb以写的方式打开
    with open(filename, 'wb') as f:
        runner = HTMLTestRunner(f, 2, u"测试报告", u"测试用例情况")
        suite = createsuite()
        runner.run(suite)

Notice:

Instantiate third-party running objects. The initialization of HTMLTestRunner() has a variety of customizable settings.

 HTMLTestRunner()
 1、stream=sys.stdout, 必填,测试报告的文件对象(open ), 注意点,要使用 wb 打开
 2、verbosity=1, 可选, 报告的详细程度,默认 1 简略, 2 详细
 3、title=None, 可选, 测试报告的标题
 4、description=None 可选, 描述信息, Python 的版本, pycharm 版本

 The final result is generated

The unittest framework is basically based on this knowledge. There are a lot of things to remember in it, so you need to type more code to form a memory...

 The following are supporting learning materials. For those who are doing [software testing], it should be the most comprehensive and complete preparation warehouse. This warehouse has also accompanied me through the most difficult journey. I hope it can also help you!

Software testing interview applet

A software test question bank that has been used by millions of people! ! ! Who is who knows! ! ! The most comprehensive interview test mini program on the Internet, you can use your mobile phone to answer questions, take the subway, bus, and roll it up!

Covers the following interview question sections:

1. Basic theory of software testing, 2. web, app, interface function testing, 3. network, 4. database, 5. linux

6. Web, app, interface automation, 7. Performance testing, 8. Programming basics, 9. HR interview questions, 10. Open test questions, 11. Security testing, 12. Computer basics

 

How to obtain documents:

This document should be the most comprehensive and complete preparation warehouse for friends who want to engage in [software testing]. This warehouse has also accompanied me through the most difficult journey. I hope it can also help you!

All of the above can be shared. You only need to search the vx official account: Programmer Hugo to get it for free.

Guess you like

Origin blog.csdn.net/2301_79535544/article/details/133182569