前言

本课题是天津理工大学软件学院的选修课《软件质量管理》的课题,本人使用Selenium自动化测试工具对12306网站进行黑盒测试。

  • 测试方法:黑盒测试
  • 测试工具:Selenium自动化测试工具
  • 测试模块:登录模块,车票查询模块

黑盒测试简介

黑盒测试通过测试来检测每个功能是否都能正常使用。在测试中,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息。黑盒测试着眼于程序外部结构,不考虑内部逻辑结构,主要针对软件界面和软件功能进行测试。黑盒测试是以用户的角度,从输入数据与输出数据的对应关系出发进行测试的。

Selenium简介

Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。

测试人员使用PyCharm集成开发环境通过Python语言和Selenium客户端库编写自动化程序,让浏览器驱动去驱动浏览器进行相关的自动化测试,达到像人一样在浏览器里操作Web界面的效果。Selenium的工作原理如下图所示。

image-20201109130413472

在上图中,我们可以看到两个核心:Selenium客户端库和浏览器驱动。

Selenium组织提供了多种编程语言的Selenium客户端库,包括Java,Python,JavaScript等,以方便不同编程语言的开发者使用。我们只需要安装好客户端库,调用这些库,就可以发出自动化请求给浏览器。浏览器驱动也是一个独立的程序,是由浏览器厂商提供的,不同的浏览器需要不同的浏览器驱动。比如 Chrome浏览器和火狐浏览器有不同的驱动程序。

总的来说,Selenium自动化测试流程如下:

  • 自动化程序调用Selenium客户端库函数(比如点击按钮元素)
  • 客户端库会发送Selenium命令给浏览器驱动程序
  • 浏览器驱动程序接收到命令后,驱动浏览器去执行命令
  • 浏览器执行命令
  • 浏览器驱动程序获取命令执行的结果,返回给我们自动化程序
  • 自动化程序对返回结果进行处理

测试用例设计

登录模块

登录模块测试用例如下表所示。其中,√ 代表输入正确数据,× 代表没有输入。

注意:滑块需要用户名,密码,验证码都验证成功后才能滑动滑块,因此当用户名,密码,验证码是√时,滑块才能有√。

编号输入数据输入数据输入数据输入数据预期输出
用户名密码验证码滑块
1庞伟杰(注:登录成功后显示登录人名字)
2×请按住滑块拖动到最右边,完成校验。
3××请选择验证码!
4××请输入密码!
5××请输入用户名!
6×××请输入密码!
7×××请输入用户名!
8×××请输入用户名!
9××××请输入用户名!

车票查询模块

车票查询测试用例如下表所示。

注意:出发日默认选择当天,不能清空,因此出发日均不为空。

编号输入数据输入数据输入数据预期输出
出发地目的地出发日
1北京天津2020-10-20查询成功(注:所有查询车次详细信息)
2北京×2020-10-20请输入目的地(注:目的地输入框红框提示)
3×天津2020-10-20请输入出发地(注:出发地输入框红框提示)
4××2020-10-20请输入出发地(注:出发地输入框红框提示)

测试内容实现

登录模块

测试分析

中国铁路12306登录界面如下图所示。首先,12306支持扫码登录和帐号登录,为了简化登录流程,我们选择帐号登录。由下图可知,登录需要12306的帐号,密码,验证码。

image-20201109130720943

输入相关信息并点击登录后,还需要使用滑块来验证,如下图所示。

image-20201109130715047

综合上述分析,中国铁路12306登录需要帐号,密码,验证码以及滑块验证即可登录。

测试实现

首先,我们需要先对Selenium进行初始化,并且由于12306网站会对Selenium自动化测试软件检测。如果是Selenium自动化软件测试,12306网站会进行登录限制。因此,为了避免不必要的麻烦我们需要屏蔽12306网站的检测。

屏蔽12306网站检测的思路是:通过调用JavaScript脚本判断当前浏览器是否使用了webdriver(Selenium自动化测试的驱动),如果使用了会返回true,如下图所示,同时12306网站会对登录进行相关的限制,无法进行进一步的测试。

image-20201109130944087

很明显地,我们只需要把JavaScript脚本返回的值设置为非true即可,相关屏蔽以及初始化代码如下:

# 规避检测
self.driver = webdriver.Chrome(r'D:\SeleniumTest\webdriver\chromedriver.exe')
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  """
})
self.driver.get('https://kyfw.12306.cn/otn/resources/login.html')
self.driver.maximize_window()
time.sleep(2)

设置成功后,我们再次查看返回值,显示undefind如下图所示。

image-20201109131109064

然后,通过网页元素定位方法切换登录方式,输入登录帐号和密码,代码如下:

# 切换登录方式
switch_type_e = self.driver.find_elements_by_class_name('login-hd-account')[0]
switch_type_e.click()
time.sleep(2)
# 输入帐号密码
username_e = self.driver.find_element_by_id('J-userName')
username_e.send_keys(username)
password_e = self.driver.find_element_by_id('J-password')
password_e.send_keys(password)

由网页源代码分析可知,如下图所示,中国铁路12306的验证码以Base64加密的形式展示,因此我们需要把src属性的值获取到,解码成二进制信息写出文件,再调用第三方接口进行识别。

img

对于保存验证码图片,提交第三方接口进行识别以及自动模拟点击,代码如下:

# 保存验证码图片
src = self.driver.find_element_by_id('J-loginImg').get_attribute('src') + ''
image_data = base64.b64decode(src.split(',')[1])
with open('captcha.jpg', 'wb') as f:
    f.write(image_data)
    
# 提交第三方接口识别
url = "http://littlebigluo.qicp.net:47720/"
response = requests.request("POST", url, data={"type": "1"}
, files={'pic_xxfile': open('captcha.jpg', 'rb')})
result = []
for i in re.findall("<B>(.*)</B>", response.text)[0].split(" "):
    result.append(int(i))
print(result)

# 自动模拟点击
img_element = self.driver.find_element_by_id("J-loginImg")
for i in range(len(result)):
try:
    if result[i] <= 4:
        print('1 - 4')
        ActionChains(self.driver).move_to_element_with_offset(img_element, 40 + 72 * (result[i] - 1), 73).click().perform()
    else:
        print('4 - 8')
        ActionChains(self.driver).move_to_element_with_offset(img_element, 40 + 72 * ((result[i] - 4) - 1), 145).click().perform()
except Exception as e:
    print(e)

对于滑块的处理,可以使用Selenium提供的ActionChains进行模拟拖动,可以实现无限重试直到滑动验证成功,代码如下:

# 点击登录
login_e = self.driver.find_element_by_id('J-login')
login_e.click()
time.sleep(3)

# 处理滑动模块
if slide:
    while True:
        try:
            info = self.driver.find_elements_by_class_name('nc-lang-cnt')[0].text
            if info == '请按住滑块,拖动到最右边':
               self.slide()  # 滑
            if info == '哎呀,出错了,点击刷新再来一次':
               # 点击刷新
               self.driver.find_element_by_xpath('//*[@id="J-slide-passcode"]/div/span/a').click()
               time.sleep(0.2)
               self.slide()  # 滑
       except:
           print('ok!')
           break

def slide(self):
    slide_e = self.driver.find_element_by_id('nc_1_n1z')
    action = ActionChains(self.driver)  # 动作链
    action.click_and_hold(slide_e)  # 点击长按指定的标签
    try:
        for i in range(5):
        action.move_by_offset(100, 0).perform()
        time.sleep(0.5)
    except StaleElementReferenceException:
        print('滑块拉完了')
    finally:
        action.release()  # 释放动作链

车票查询模块

测试分析

中国铁路12306选择出发地和选择日期如下图所示。

image-20201109131848861

image-20201109131852255

当我们使用浏览器自带的抓包功能对查询HTTP请求进行捕获时发现,提交的查询信息并不是出发地和目的地的中文,而是它们的车站代码(leftTicketDTO.from_station和leftTicketDTO.to_station参数对应的值),如下图所示。

image-20201109131906523

由上述分析,我们在网页源代码中定位表单name为提交请求的字段的表单元素,定位分析如下图所示。

image-20201109131919688

由上图可知,我们可以得知对于需要中文显示的模块,如出发地和到达地,有两个表单元素:一个是用于中文显示,另一个用于真实提交表单(使用了属性type设置为hidden让表单元素处于隐藏状态)。而对于出发日期模块,仅有一个表单元素,既用于显示,也用于真实提交表单,但拥有一个readOnly只读属性,我们需要去掉以这个属性方便我们使用send_keys()方法输入。因此,在进行自动化输入的时候,需要注意的是要处理的是用于真实提交的表单,而不是仅用作中文显示的表单。

测试实现

由上述分析,首先需要处理隐藏的真实提交表单元素。如果表单元素属性type是hidden,即使获取到了标签也调用send_keys()方法也无法进行赋值。因此,首先要进行的是去掉真实提交表单元素的type属性和去除日期的只读属性,去除type属性的方法是:采用JavaScipt脚本进行处理,然后使用通过Selenium驱动去执行脚本。代码如下:

js = "document.getElementById('fromStation').type = 'text';" \
     "document.getElementById('toStation').type = 'text';" \
     "document.getElementById('train_date').readOnly = false"
self.driver.execute_script(js)  # 执行js代码

我们使用手动测试执行上述JavaScript脚本,测试有效:隐藏的表单均显示出来了,只读的表单也变成可读可写的了,测试结果如下图所示。

image-20201109132040374

但是,对于真实提交的表单信息需要车站代码,我们该如何实现通过中文的车站名来进行查询呢?通过抓取HTTP报文包技术,抓取到了车站代码的字典URL地址,通过访问URL地址,经过处理后的部分字典内容如下所示。

...

@bjb|北京北|VAP|beijingbei|bjb|0

@bjd|北京东|BOP|beijingdong|bjd|1

@bji|北京|BJP|beijing|bj|2

@bjn|北京南|VNP|beijingnan|bjn|3

@bjx|北京西|BXP|beijingxi|bjx|4

...

例如,当我们输入“北京”的时候,需要返回的是“BJP”。通过对字典结构分析,使用正则匹配技术通过中文车站名称查询车站代码,代码如下:

def search_station_code(station_name):
    url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js'
    txt = requests.get(url).text
    # ([\u4e00-\u9fa5]+) 汉字范围    ([A-Z]+) A-Z范围
    stations = re.findall('([\u4e00-\u9fa5]+)\\|([A-Z]+)', txt)  
    for station in stations:
        if station[0] == station_name:
            return station[1]

至此,我们便可以使用Python来进行自动化输入并点击查询,代码如下:

# 出发站点
from_station_e = self.driver.find_element_by_id('fromStation')   
from_station_e.clear()  # 清空默认值
from_station_e.send_keys('' if from_station == ''else search_station_code(from_station))
# 到达站点  
to_station_e = self.driver.find_element_by_id('toStation')       
to_station_e.clear()  # 清空默认值
to_station_e.send_keys('' if to_station == '' else search_station_code(to_station))
# 出发日期
train_date_e = self.driver.find_element_by_id('train_date')      
train_date_e.clear()  # 清空默认值
train_date_e.send_keys('' if date == '' else date)
# 查询
search_e = self.driver.find_element_by_id('query_ticket')        
search_e.click()

对于查询结果,根据源代码分析结果是存在于table标签中的,使用for循环迭代打印出来,代码如下:

train_no_list = self.driver.find_elements_by_xpath('//tbody[@id="queryLeftTable"]//tr[@style="display:none;"]')
from_station_list = self.driver.find_elements_by_xpath('//div[@class="cdz"]//strong[1]')
to_station_list = self.driver.find_elements_by_xpath('//div[@class="cdz"]//strong[2]')
start_time_list = self.driver.find_elements_by_xpath('//strong[@class="start-t"]')
end_time_list = self.driver.find_elements_by_xpath('//strong[@class="color999"]')
cost_time_list = self.driver.find_elements_by_xpath('//div[@class="ls"]')
for i in range(len(train_no_list)):
    print('第' + str(i + 1) + '趟'
  + '\t\t车次:' + train_no_list[i].get_attribute('datatran')
  + '\t\t出发站点:' + from_station_list[i].text
  + '\t\t到达站点:' + to_station_list[i].text
  + '\t\t出发时间:' + start_time_list[i].text
  + '\t\t到达时间:' + end_time_list[i].text

测试结果与分析

登录模块

测试用例编码

class LoginTestCase(unittest.TestCase):
    def setUp(self):
        print("hello")

    def tearDown(self):
        print('bye')

    def test_001(self):
        service = LoginService()
        service.login('17322200692', '*********', True, True)
        time.sleep(3)
        res = service.driver.find_element_by_xpath('//*[@id="js-minHeight"]/div[1]/div[1]/strong').text
        self.assertEqual('庞伟杰', res)

    def test_002(self):
        service = LoginService()
        service.login('17322200692', '*********', True, False)
        res = service.driver.find_element_by_xpath('//*[@id="login_slide_box"]/div[2]/div/div[1]').text
        self.assertEqual('请按住滑块拖动到最右边,完成校验。', res)

    def test_003(self):
        service = LoginService()
        service.login('17322200692', '*********', False, False)
        res = service.driver.find_element_by_xpath('//*[@id="J-login-error"]/span').text
        self.assertEqual('请选择验证码!', res)

    def test_004(self):
        service = LoginService()
        service.login('17322200692', '', True, True)
        res = service.driver.find_element_by_xpath('//*[@id="J-login-error"]/span').text
        self.assertEqual('请输入密码!', res)

    def test_005(self):
        service = LoginService()
        service.login('', '*********', True, False)
        res = service.driver.find_element_by_xpath('//*[@id="J-login-error"]/span').text
        self.assertEqual('请输入用户名!', res)

    def test_006(self):
        service = LoginService()
        service.login('17322200692', '', False, False)
        res = service.driver.find_element_by_xpath('//*[@id="J-login-error"]/span').text
        self.assertEqual('请输入密码!', res)

    def test_007(self):
        service = LoginService()
        service.login('', '*********', False, False)
        res = service.driver.find_element_by_xpath('//*[@id="J-login-error"]/span').text
        self.assertEqual('请输入用户名!', res)

    def test_008(self):
        service = LoginService()
        service.login('', '', True, False)
        res = service.driver.find_element_by_xpath('//*[@id="J-login-error"]/span').text
        self.assertEqual('请输入用户名!', res)

    def test_009(self):
        service = LoginService()
        service.login('', '', False, False)
        res = service.driver.find_element_by_xpath('//*[@id="J-login-error"]/span').text
        self.assertEqual('请输入用户名!', res)

测试套件及HTML测试报告生成编码

class LoginTest(unittest.TestCase):
    def test_suit(self):
        # 创建测试套件
        suit = unittest.TestSuite()
        # 向测试套件中添加测试用例
        case_list = ['test_001', 'test_002', 'test_003', 'test_004',
                     'test_005', 'test_006', 'test_007', 'test_008', 'test_009']
        for case in case_list:
            suit.addTest(LoginTestCase(case))
        # 生成 html格式的测试报告步骤
        with open('login_report.html', 'wb') as f:
            HTMLTestRunner(
                stream=f,  # 设定测试数据写入哪个文件
                title='登录测试',  # 设定测试报告的标题
                description='登录测试',  # 设定测试报告的描述
                verbosity=2
            ).run(suit)

测试结果

HTML测试报告如下图所示。

image-20201109132647626

测试用例测试结果表如下所示。

编号输入数据输入数据输入数据输入数据预期输出实际输出
用户名密码验证码滑块
1庞伟杰(注:登录成功后显示登录人名字)庞伟杰(注:登录成功后显示登录人名字)
2×请按住滑块拖动到最右边,完成校验。请按住滑块拖动到最右边,完成校验。
3××请选择验证码!请选择验证码!
4××请输入密码!请输入密码!
5××请输入用户名!请输入用户名!
6×××请输入密码!请输入密码!
7×××请输入用户名!请输入用户名!
8×××请输入用户名!请输入用户名!
9××××请输入用户名!请输入用户名!

车票查询模块

测试用例编码

class SearchTestCase(unittest.TestCase):
    def setUp(self):
        print("hello")

    def tearDown(self):
        print('bye')

    def test_001(self):
        service = SearchService()
        service.search_ticket('北京', '天津', '2020-10-20')
        time.sleep(1)
        res = service.driver.find_element_by_xpath('//*[@id="queryLeftTable"]').text
        self.assertNotEqual('', res)

    def test_002(self):
        service = SearchService()
        service.search_ticket('北京', '', '2020-10-20')
        time.sleep(1)
        res = service.driver.find_element_by_xpath('//*[@id="toStationText"]').get_attribute('class')
        self.assertEqual('inp-txt error', res)

    def test_003(self):
        service = SearchService()
        service.search_ticket('', '天津', '2020-10-20')
        time.sleep(1)
        res = service.driver.find_element_by_xpath('//*[@id="fromStationText"]').get_attribute('class')
        self.assertEqual('inp-txt error', res)

    def test_004(self):
        service = SearchService()
        service.search_ticket('', '天津', '2020-10-20')
        time.sleep(1)
        res = service.driver.find_element_by_xpath('//*[@id="fromStationText"]').get_attribute('class')
        self.assertEqual('inp-txt error', res)

测试套件及HTML测试报告生成编码

class SearchTest(unittest.TestCase):

    def test_suit(self):
        # 创建测试套件
        suit = unittest.TestSuite()
        # 向测试套件中添加测试用例
        case_list = ['test_001', 'test_002', 'test_003', 'test_004']
        for case in case_list:
            suit.addTest(SearchTestCase(case))
        # 生成 html格式的测试报告步骤
        with open('search_report.html', 'wb') as f:
            HTMLTestRunner(
                stream=f,  # 设定测试数据写入哪个文件
                title='搜索测试',  # 设定测试报告的标题
                description='搜索测试',  # 设定测试报告的描述
                verbosity=2
            ).run(suit)

测试结果

HTML测试报告如下图所示。

image-20201109132955271

测试用例测试结果表如下所示。

编号输入数据输入数据输入数据实际输出实际输出
出发地目的地出发日
1北京天津2020-10-20查询成功(注:所有查询车次详细信息)查询成功(注:所有查询车次详细信息)
2北京×2020-10-20请输入目的地(注:目的地输入框红框提示)请输入目的地(注:目的地输入框红框提示)
3×天津2020-10-20请输入出发地(注:出发地输入框红框提示)请输入出发地(注:出发地输入框红框提示)
4××2020-10-20请输入出发地(注:出发地输入框红框提示)请输入出发地(注:出发地输入框红框提示)
©著作权归作者所有

发表评论

正在加载 Emoji