背景

背景之前写过了,在这儿:

目标

  1. 登录 TesterHome
  2. 发表一个新话题,并保存为草稿

备注

周末把 Selenium 的文档都过了一遍,说实话也不知道明天起床后,还记得住多少,所以写下来加强印象。

正文

学了几个感觉比较重要的概念:

  1. Wait
  2. Actions
  3. Domain specific language (DSL)
  4. Page object models (POM)

1、2 点没啥好说的,要用的时候查 api 就行了。
第 3、4 点我觉得挺关键的,后面可能要反复琢磨,贴下链接:

意思是 Selenium 不推荐我第一篇中那种写法,那样很难维护,推荐按下列原则进行开发:

  1. DomainDrivenDesign: Express your tests in the language of the end-user of the app. (根据用户的行为定义你的用例)
  2. PageObjects: A simple abstraction of the UI of your web app. (把软件的 UI 抽象成一个页面)
  3. LoadableComponent: Modeling PageObjects as components. (把 PageObject 当作一个组件)
  4. BotStyleTests: Using a command-based approach to automating tests, rather than the object-based approach that PageObjects encourage. (建立一些通用的底层操作方法)

我瞎翻译的,大家看原文和原文里的代码,应该会有自己的理解。
然后我根据这些原则改了下代码,如下。

代码

贴代码前,先说一下组织架构:

+ BaseClass
+---+ TesterHomeSignInPage
+---+ TestHomeHomePage
+---+ TesterHomeTopicPage

base_class.py

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


class BaseClass:
    def __init__(self, driver: webdriver):
        self.driver = driver
        # self.driver = webdriver.Chrome()

    def open_page(self, url):
        self.driver.get(url)

    def get_element(self, locator):
        wait = WebDriverWait(self.driver, timeout=3)
        return wait.until(EC.visibility_of_element_located(locator))

    def click_element(self, locator):
        self.get_element(locator).click()

    def input_element(self, locator, text):
        self.get_element(locator).send_keys(text)

    def get_text(self, locator):
        return self.get_element(locator).text

testerhome_login_page.py

from selenium import webdriver
from selenium.webdriver.common.by import By
from pages.base_class import BaseClass
from pages.testerhome_home_page import TestHomeHomePage
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager


class TesterHomeSignInPage(BaseClass):
    # url
    _URL = "http://testerhome.com/"

    # locators
    _pre_login_btn_loc = (By.CSS_SELECTOR, "a.btn.btn-primary.btn-jumbotron.btn-lg")
    _account_input_loc = (By.ID, "user_login")
    _password_input_loc = (By.ID, "user_password")
    _login_btn_loc = (By.CSS_SELECTOR, "input.btn.btn-primary.btn-lg.btn-block")

    ''' actions '''

    def pre_login(self):
        self.open_page(self._URL)
        self.click_element(self._pre_login_btn_loc)
        return self

    def type_username(self, username: str):
        self.input_element(self._account_input_loc, username)
        return self

    def type_password(self, password: str):
        self.input_element(self._password_input_loc, password)
        return self

    def submit_login(self):
        self.click_element(self._login_btn_loc)
        return TestHomeHomePage(self.driver)

    ''' behaviors '''

    def login_as(self, username: str, password: str):
        self.pre_login()\
            .type_username(username) \
            .type_password(password)
        return self.submit_login()

testerhome_home_page.py

from selenium.webdriver.common.by import By
from pages.base_class import BaseClass
from pages.testhome_topic_page import TesterHomeTopicPage


class TestHomeHomePage(BaseClass):
    # locators
    _user_navbar_loc = (By.ID, "navbar-user-menu")
    _menubar_loc = (By.ID, "navbar-new-menu")
    _post_btn_loc = (By.XPATH, "//a[@href='/topics/new']")

    ''' actions '''

    def exist(self):
        return self.get_element(self._user_navbar_loc).is_displayed()

    def post_new_topic(self):
        self.click_element(self._menubar_loc)
        self.click_element(self._post_btn_loc)
        return self

    ''' behaviors '''

    def post_topic(self):
        self.post_new_topic()
        return TesterHomeTopicPage(self.driver)

testerhome_topic_page.py

from selenium.webdriver.common.by import By

from pages.base_class import BaseClass


class TesterHomeTopicPage(BaseClass):
    # locators
    _title_input_loc = (By.ID, "topic_title")
    _category_btn_loc = (By.ID, "node-selector-button")
    _content_input_loc = (By.ID, "topic_body")
    _draft_btn_loc = (By.ID, "save_as_draft")
    _save_btn_loc = (By.CSS_SELECTOR, "input.btn.btn-primary")
    _title_loc = (By.XPATH, "//h1[@class='media-heading']//span[@class='title']")

    # locators inside category
    _selenium_category_loc = (By.XPATH, "//a[@data-id='73']")  # 要研究一下怎么把所有节点都获取到,然后根据title使用,不然我要写几十个locators

    # flags
    _is_draft = True

    ''' actions '''

    def write_title(self, title: str):
        self.input_element(self._title_input_loc, title)
        return self

    def choose_category(self, category: str):
        self.click_element(self._category_btn_loc)  # 要研究一下怎么把所有节点都获取到,然后根据title使用
        self.click_element(self._selenium_category_loc)
        return self

    def write_content(self, content: str):
        self.input_element(self._content_input_loc, content)
        return self

    def save_as_draft(self):
        self.click_element(self._draft_btn_loc)
        return self

    def submit_topic(self):
        self.click_element(self._save_btn_loc)
        return self

    def get_title(self):
        return self.get_text(self._title_loc)

    ''' behaviors '''

    def post_topic(self, title: str, category: str, content: str, _is_draft=True):
        self.write_title(title)\
            .choose_category(category)\
            .write_content(content)\
            .save_as_draft() if _is_draft else self.submit_topic()

测试

if __name__ == '__main__':
    service = ChromeService(executable_path=ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service)

    sign_in_page = TesterHomeSignInPage(driver)

    home_page = sign_in_page.login_as("xxxx@foxmail.com", "xxxx")
    assert home_page.exist()

    topic_page = home_page.post_topic()
    topic_testdata = {
        "title": "记录用 selenium 把部分业务实现自动化测试(二)",
        "category": "Selenium",
        "content": "7月17日晴\n今天好热,我多希望有朝一日能有人对我说:宝,早安。”,而不是”早,保安。“"
    }
    topic_page.post_topic(**topic_testdata)
    assert topic_page.get_title() == topic_testdata["title"]

    driver.quit()

效果图

(提交太多次话题被限制了,不上图了)

发现问题

遇到挺多问题的,有的解决了,有的没解决,都和大家分享一下:

  1. 在写话题的时候,要选"节点",这儿有很多个按钮,假如我要一个个写 locators,比较麻烦,后面再想下怎么拿到全部节点,然后根据 title 去定位。(应该可以用 find_elements)

  2. 我的代码组织,体现不了 TopicPage 是 HomePage 下的一个子页面,后面再想下怎么写,可以让接盘侠一眼看出这两个 Page 的关系。

  3. Selenium 支持在已有页面下 debug,效率会高点,链接如下:Selenium Debug

  4. 写这个 locator 的时候遇到个小坑:

    _selenium_category_loc = (By.XPATH, "//a[@data-id='73']")
    

    原本我是用 By.CSS_SELECTOR 的,但会报 InvalidSelectorException 的错误,查了资料后发现是 css 中的类名不能以数字开头导致的。
    其实我按照文章里的方法也没成功,后面再查为啥了,先改用 XPATH,资料链接也贴下:
    关于 CSS_SELECTOR 的 InvalidSelectorException

计划

  1. 下周回公司后,尝试把手上的需求写成自动化
  2. 去 Github 上看下别人写的 Selenium 是怎样的


↙↙↙阅读原文可查看相关链接,并与作者交流