The most complete and detailed Selenium+Pytest automated testing framework in practice in 2023

 

Preface to the selection #  
selenium automation + pytest test framework

This chapter you need

Certain python basics - at least understand classes and objects, encapsulation inheritance

Certain selenium basics - I won’t talk about selenium in this article. If you don’t know how, you can go to the selenium Chinese translation website by yourself.

Introduction to testing framework#
What are the advantages of testing framework:

The code reuse rate is high. If you don’t use the framework, the code will be very redundant. You
can assemble some advanced functions such as logs, reports, emails, etc.
Improve the maintainability of data such as elements. When elements change, you only need to update the configuration file
and use the update Flexible PageObject design pattern
The overall directory of the test framework Directory
/File Description Is it a python package
common? This package stores common general classes, such as reading configuration files. Yes
config Configuration file directory Yes
logs Log directory    
page For selenium The in-depth encapsulation is
page_element, the page element storage directory    
page_object, the page object POM design pattern. My understanding of this comes from the blog of Bitter Leaf. It is TestCase. It is a set of
all test cases. It is
utils tool class. It is
script script file    
conftest.py pytest glue file.    
pytest.ini pytest configuration file
 

  Such a simple framework structure is clear.

Now that we know the above, let’s get started!

In the project, we first build each directory according to the above framework guidelines.

Note: For python packages, you need to add a __init__.pyfile to identify this directory as a python package.

First manage time #

First of all, because many of our modules will use strings such as timestamps or dates, we first encapsulate the time into a module separately.

Then let other modules call it. Create a new module in utilsthe directorytimes.py

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> time
<span style="color:#7171bf">import</span> datetime
<span style="color:#7171bf">from</span> functools <span style="color:#7171bf">import</span> wraps
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">timestamp</span>():
    <span style="color:#98c379">"""时间戳"""</span>
    <span style="color:#7171bf">return</span> time.time()
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">dt_strftime</span>(fmt=<span style="color:#98c379">"%Y%m"</span>):
    <span style="color:#98c379">"""
    datetime格式化时间
    :param fmt "%Y%m%d %H%M%S
    """</span>
    <span style="color:#7171bf">return</span> datetime.datetime.now().strftime(fmt)
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">sleep</span>(seconds=<span style="color:#d19a66">1.0</span>):
    <span style="color:#98c379">"""
    睡眠时间
    """</span>
    time.sleep(seconds)
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">running_time</span>(func):
    <span style="color:#98c379">"""函数运行时间"""</span>
 
<span style="color:#61aeee">    @wraps(func)</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">wrapper</span>(*args, **kwargs):
        start = timestamp()
        res = func(*args, **kwargs)
        <span style="color:#7171bf">print</span>(<span style="color:#98c379">"校验元素done!用时%.3f秒!"</span> % (timestamp() - start))
        <span style="color:#7171bf">return</span> res
 
    <span style="color:#7171bf">return</span> wrapper
 
 
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
    <span style="color:#7171bf">print</span>(dt_strftime(<span style="color:#98c379">"%Y%m%d%H%M%S"</span>))
 
</code></span></span>

Adding configuration files #Configuration
files are always an essential part of a project!

Concentrate fixed information in fixed files

There should be a file in the conf.py#
project to manage the entire directory. I also set this file in this python project.

Create a conf.py file in the project config directory, and all directory configuration information is written in this file.

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> os
<span style="color:#7171bf">from</span> selenium.webdriver.common.by <span style="color:#7171bf">import</span> By
<span style="color:#7171bf">from</span> utils.times <span style="color:#7171bf">import</span> dt_strftime
 
 
<span style="color:#7171bf">class</span> <span style="color:#61aeee">ConfigManager</span>(<span style="color:#61aeee">object</span>):
    <span style="color:#5c6370"><em># 项目目录</em></span>
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
    <span style="color:#5c6370"><em># 页面元素目录</em></span>
    ELEMENT_PATH = os.path.join(BASE_DIR, <span style="color:#98c379">'page_element'</span>)
 
    <span style="color:#5c6370"><em># 报告文件</em></span>
    REPORT_FILE = os.path.join(BASE_DIR, <span style="color:#98c379">'report.html'</span>)
 
    <span style="color:#5c6370"><em># 元素定位的类型</em></span>
    LOCATE_MODE = {
        <span style="color:#98c379">'css'</span>: By.CSS_SELECTOR,
        <span style="color:#98c379">'xpath'</span>: By.XPATH,
        <span style="color:#98c379">'name'</span>: By.NAME,
        <span style="color:#98c379">'id'</span>: By.ID,
        <span style="color:#98c379">'class'</span>: By.CLASS_NAME
    }
 
    <span style="color:#5c6370"><em># 邮件信息</em></span>
    EMAIL_INFO = {
        <span style="color:#98c379">'username'</span>: <span style="color:#98c379">'[email protected]'</span>,  <span style="color:#5c6370"><em># 切换成你自己的地址</em></span>
        <span style="color:#98c379">'password'</span>: <span style="color:#98c379">'QQ邮箱授权码'</span>,
        <span style="color:#98c379">'smtp_host'</span>: <span style="color:#98c379">'smtp.qq.com'</span>,
        <span style="color:#98c379">'smtp_port'</span>: <span style="color:#d19a66">465</span>
    }
 
    <span style="color:#5c6370"><em># 收件人</em></span>
    ADDRESSEE = [
        <span style="color:#98c379">'[email protected]'</span>,
    ]
 
<span style="color:#61aeee">    @property</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">log_file</span>(self):
        <span style="color:#98c379">"""日志目录"""</span>
        log_dir = os.path.join(self.BASE_DIR, <span style="color:#98c379">'logs'</span>)
        <span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> os.path.exists(log_dir):
            os.makedirs(log_dir)
        <span style="color:#7171bf">return</span> os.path.join(log_dir, <span style="color:#98c379">'{}.log'</span>.<span style="color:#7171bf">format</span>(dt_strftime()))
 
<span style="color:#61aeee">    @property</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">ini_file</span>(self):
        <span style="color:#98c379">"""配置文件"""</span>
        ini_file = os.path.join(self.BASE_DIR, <span style="color:#98c379">'config'</span>, <span style="color:#98c379">'config.ini'</span>)
        <span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> os.path.exists(ini_file):
            <span style="color:#7171bf">raise</span> FileNotFoundError(<span style="color:#98c379">"配置文件%s不存在!"</span> % ini_file)
        <span style="color:#7171bf">return</span> ini_file
 
 
cm = ConfigManager()
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
    <span style="color:#7171bf">print</span>(cm.BASE_DIR)
</code></span></span>

Note: QQ email authorization code: click to view the generation tutorial

This conf file imitates the setting style of Django's settings.py file, but there are some differences.

In this file we can set our own directories and view our current directory.

The convention is followed: constant names are all in uppercase, and function names are in lowercase. It looks overall beautiful.

config.ini#
Create a new config.ini file in the project config directory, and temporarily put the URL we need to test in it.

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-ini"><span style="color:#e06c75">[HOST]</span>
<span style="color:#d19a66">HOST</span> = https://www.baidu.com
</code></span></span>

Read configuration file #

The configuration file is created. Next we need to read the configuration file to use the information inside.

We commoncreate a new readconfig.pyfile in the directory

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> configparser
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
 
HOST = <span style="color:#98c379">'HOST'</span>
 
 
<span style="color:#7171bf">class</span> <span style="color:#61aeee">ReadConfig</span>(<span style="color:#61aeee">object</span>):
    <span style="color:#98c379">"""配置文件"""</span>
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self):
        self.config = configparser.RawConfigParser()  <span style="color:#5c6370"><em># 当有%的符号时请使用Raw读取</em></span>
        self.config.read(cm.ini_file, encoding=<span style="color:#98c379">'utf-8'</span>)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">_get</span>(self, section, option):
        <span style="color:#98c379">"""获取"""</span>
        <span style="color:#7171bf">return</span> self.config.get(section, option)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">_set</span>(self, section, option, value):
        <span style="color:#98c379">"""更新"""</span>
        self.config.<span style="color:#7171bf">set</span>(section, option, value)
        <span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(cm.ini_file, <span style="color:#98c379">'w'</span>) <span style="color:#7171bf">as</span> f:
            self.config.write(f)
 
<span style="color:#61aeee">    @property</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">url</span>(self):
        <span style="color:#7171bf">return</span> self._get(HOST, HOST)
 
 
ini = ReadConfig()
 
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
    <span style="color:#7171bf">print</span>(ini.url)
</code></span></span>

You can see that we used Python's built-in configparser module to read the config.ini file.

For extracting the url value, I used the high-level syntax @property attribute value, which is simpler to write.

Record operation log #Log
, everyone should be familiar with this term, which is to record the actions in the code.

Create a new logger.py file in the utils directory.

This file is used by us to record some operating steps during the automated testing process.
 

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> logging
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
 
 
<span style="color:#7171bf">class</span> <span style="color:#61aeee">Log</span>:
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self):
        self.logger = logging.getLogger()
        <span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> self.logger.handlers:
            self.logger.setLevel(logging.DEBUG)
 
            <span style="color:#5c6370"><em># 创建一个handle写入文件</em></span>
            fh = logging.FileHandler(cm.log_file, encoding=<span style="color:#98c379">'utf-8'</span>)
            fh.setLevel(logging.INFO)
 
            <span style="color:#5c6370"><em># 创建一个handle输出到控制台</em></span>
            ch = logging.StreamHandler()
            ch.setLevel(logging.INFO)
 
            <span style="color:#5c6370"><em># 定义输出的格式</em></span>
            formatter = logging.Formatter(self.fmt)
            fh.setFormatter(formatter)
            ch.setFormatter(formatter)
 
            <span style="color:#5c6370"><em># 添加到handle</em></span>
            self.logger.addHandler(fh)
            self.logger.addHandler(ch)
 
<span style="color:#61aeee">    @property</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">fmt</span>(self):
        <span style="color:#7171bf">return</span> <span style="color:#98c379">'%(levelname)s\t%(asctime)s\t[%(filename)s:%(lineno)d]\t%(message)s'</span>
 
 
log = Log().logger
 
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
    log.info(<span style="color:#98c379">'hello world'</span>)
</code></span></span>

Run the file in the terminal and see the command line print out:

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-shell">INFO 2020-12-01 16:00:05,467 [logger.py: 38] hello world
</code></span></span>
Then the log file for the current month was generated in the project logs directory.

Briefly understand the POM model#
Since we will talk about elements below, let’s first understand the POM model.

Page Object mode has the following advantages.

This view comes from "Selenium Automated Testing - Based on Python Language"

Abstracting the object can minimize the impact of developers modifying the page code on the test. Therefore, you only need to
adjust the page object and have no impact on the test;
you can reuse part of the test code in multiple test cases;
test The code becomes more readable, flexible and maintainable
Page Object pattern diagram

basepage - the base class of selenium, which encapsulates selenium methods
pageelements - page elements, extract page elements separately and put them into a file
searchpage - page object class, integrate selenium methods and page elements
testcase - Use pytest to write test cases for the integrated searchpage.
From the above figure, we can see that through the POM model idea, we put:

Selenium Method
Page Element
Page Object
Test
Case The above four code bodies are split. Although it will increase the code when there are few use cases, it is of great significance when there are many use cases. The amount of code will be obvious when the use cases increase. decrease. Our code maintenance has become more intuitive and obvious, the code readability has become much better than the factory mode, and the code reuse rate has also been greatly improved.

Simply learn element positioning #In
my daily work, I have seen many students who right-click Copy Xpath to copy elements directly in the browser. The element expression obtained in this way is often not stable enough to be run in webdriver. Some minor changes in the front end will cause a NoSuchElementException error when the element cannot be located.

Therefore, in actual work and study, we should strengthen our element positioning capabilities and use relatively stable positioning syntax such as xpath and CSS selector as much as possible. Because the syntax of CSS selector is blunt and difficult to understand, it is not friendly to novices, and it lacks some positioning syntax compared to xpath. So we choose xpath for our element positioning syntax.

xpath#

Syntax rules
The introduction to xpath in the novice tutorial is a language for finding information in XML documents.

 

Positioning tool

Advantages of chromath
: This is a test positioning plug-in for Chrome browser, similar to firepath. I tried it and the overall feeling is very good. The friendliness towards noobs is very good.
Disadvantages: FQ is required to install this plug-in.
Katalon recording tool
The recorded script will also have information about positioning elements.
Write it yourself - I recommend this
advantage: This is the method I recommend, because when you are proficient to a certain level, what you write will be more intuitive and concise, and it will run quickly When problems occur during automated testing, they can be quickly located.
Disadvantages: It requires a certain accumulation of xpath and CSS selector syntax, and it is not easy to get started.
Manage page elements#
The test address selected for this tutorial is Baidu homepage, so the corresponding elements are also on Baidu homepage.

There is a directory page_element in the project framework design that is specially used to store files for positioning elements.

Through comparison of various configuration files, I chose the YAML file format here. It's easy to read and interactive.

We create a new search.yaml file in page_element.

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-yaml"><span style="color:#98c379">搜索框:</span> <span style="color:#98c379">"id==kw"</span>
<span style="color:#98c379">候选:</span> <span style="color:#98c379">"css==.bdsug-overflow"</span>
<span style="color:#98c379">搜索候选:</span> <span style="color:#98c379">"css==#form div li"</span>
<span style="color:#98c379">搜索按钮:</span> <span style="color:#98c379">"id==su"</span>
</code></span></span>

The element positioning file has been created, now we need to read this file.

commonCreate files in the directory readelement.py.

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> os
<span style="color:#7171bf">import</span> yaml
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
 
 
<span style="color:#7171bf">class</span> <span style="color:#61aeee">Element</span>(<span style="color:#61aeee">object</span>):
    <span style="color:#98c379">"""获取元素"""</span>
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self, name):
        self.file_name = <span style="color:#98c379">'%s.yaml'</span> % name
        self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)
        <span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> os.path.exists(self.element_path):
            <span style="color:#7171bf">raise</span> FileNotFoundError(<span style="color:#98c379">"%s 文件不存在!"</span> % self.element_path)
        <span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(self.element_path, encoding=<span style="color:#98c379">'utf-8'</span>) <span style="color:#7171bf">as</span> f:
            self.data = yaml.safe_load(f)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">__getitem__</span>(self, item):
        <span style="color:#98c379">"""获取属性"""</span>
        data = self.data.get(item)
        <span style="color:#7171bf">if</span> data:
            name, value = data.split(<span style="color:#98c379">'=='</span>)
            <span style="color:#7171bf">return</span> name, value
        <span style="color:#7171bf">raise</span> ArithmeticError(<span style="color:#98c379">"{}中不存在关键字:{}"</span>.<span style="color:#7171bf">format</span>(self.file_name, item))
 
 
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
    search = Element(<span style="color:#98c379">'search'</span>)
    <span style="color:#7171bf">print</span>(search[<span style="color:#98c379">'搜索框'</span>])
</code></span></span>

The special method __getitem__ is used to call any attribute and read the value in yaml.

In this way, we realize the storage and calling of positioned elements.

But there is still a question, how can we ensure that every element we write does not go wrong? Human errors are inevitable, but we can run a review of the file through the code. Not all problems can currently be discovered.

So we write a file, create the inspect.py file in the script file directory, and inspect all element yaml files.
 

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> os
<span style="color:#7171bf">import</span> yaml
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">from</span> utils.times <span style="color:#7171bf">import</span> running_time
 
 
<span style="color:#61aeee">@running_time</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">inspect_element</span>():
    <span style="color:#98c379">"""检查所有的元素是否正确
    只能做一个简单的检查
    """</span>
    <span style="color:#7171bf">for</span> files <span style="color:#7171bf">in</span> os.listdir(cm.ELEMENT_PATH):
        _path = os.path.join(cm.ELEMENT_PATH, files)
        <span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(_path, encoding=<span style="color:#98c379">'utf-8'</span>) <span style="color:#7171bf">as</span> f:
            data = yaml.safe_load(f)
        <span style="color:#7171bf">for</span> k <span style="color:#7171bf">in</span> data.values():
            <span style="color:#7171bf">try</span>:
                pattern, value = k.split(<span style="color:#98c379">'=='</span>)
            <span style="color:#7171bf">except</span> ValueError:
                <span style="color:#7171bf">raise</span> Exception(<span style="color:#98c379">"元素表达式中没有`==`"</span>)
            <span style="color:#7171bf">if</span> pattern <span style="color:#7171bf">not</span> <span style="color:#7171bf">in</span> cm.LOCATE_MODE:
                <span style="color:#7171bf">raise</span> Exception(<span style="color:#98c379">'%s中元素【%s】没有指定类型'</span> % (_path, k))
            <span style="color:#7171bf">elif</span> pattern == <span style="color:#98c379">'xpath'</span>:
                <span style="color:#7171bf">assert</span> <span style="color:#98c379">'//'</span> <span style="color:#7171bf">in</span> value,\
                    <span style="color:#98c379">'%s中元素【%s】xpath类型与值不配'</span> % (_path, k)
            <span style="color:#7171bf">elif</span> pattern == <span style="color:#98c379">'css'</span>:
                <span style="color:#7171bf">assert</span> <span style="color:#98c379">'//'</span> <span style="color:#7171bf">not</span> <span style="color:#7171bf">in</span> value, \
                    <span style="color:#98c379">'%s中元素【%s]css类型与值不配'</span> % (_path, k)
            <span style="color:#7171bf">else</span>:
                <span style="color:#7171bf">assert</span> value, <span style="color:#98c379">'%s中元素【%s】类型与值不匹配'</span> % (_path, k)
 
 
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
    inspect_element()
</code></span></span>

Execute this file:

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">Verify element done! It took <span style="color:#d19a66">0.002</span> seconds!
</code></span></span>
As you can see, within a short period of time, we reviewed the filled-in YAML file.

Now the basic components we need have been roughly completed.

Next we will carry out the most important step, encapsulating selenium.

Encapsulate Selenium base class #In
factory mode we write like this:

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> time
<span style="color:#7171bf">from</span> selenium <span style="color:#7171bf">import</span> webdriver
 
 
driver = webdriver.Chrome()
driver.get(<span style="color:#98c379">'https://www.baidu.com'</span>)
driver.find_element_by_xpath(<span style="color:#98c379">"//input[@id='kw']"</span>).send_keys(<span style="color:#98c379">'selenium'</span>)
driver.find_element_by_xpath(<span style="color:#98c379">"//input[@id='su']"</span>).click()
time.sleep(<span style="color:#d19a66">5</span>)
driver.quit()
</code></span></span>

Very straightforward, simple, and clear.

Create the driver object, open the Baidu web page, search for selenium, click search, then stay for 5 seconds to view the results, and finally close the browser.

So why do we encapsulate selenium's methods? First of all, our above-mentioned relatively primitive method is basically not suitable for daily UI automated testing, because the actual operation of the UI interface is far more complicated. It may be due to network reasons or control reasons that our elements have not been displayed yet. Click or enter. Therefore, we need to encapsulate the selenium method and build a stable method through built-in explicit wait or certain conditional statements. Moreover, encapsulating selenium methods is beneficial to daily code maintenance.

We create the webpage.py file in the page directory.
 

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#98c379">"""
selenium基类
本文件存放了selenium基类的封装方法
"""</span>
<span style="color:#7171bf">from</span> selenium.webdriver.support <span style="color:#7171bf">import</span> expected_conditions <span style="color:#7171bf">as</span> EC
<span style="color:#7171bf">from</span> selenium.webdriver.support.ui <span style="color:#7171bf">import</span> WebDriverWait
<span style="color:#7171bf">from</span> selenium.common.exceptions <span style="color:#7171bf">import</span> TimeoutException
 
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">from</span> utils.times <span style="color:#7171bf">import</span> sleep
<span style="color:#7171bf">from</span> utils.logger <span style="color:#7171bf">import</span> log
 
 
<span style="color:#7171bf">class</span> <span style="color:#61aeee">WebPage</span>(<span style="color:#61aeee">object</span>):
    <span style="color:#98c379">"""selenium基类"""</span>
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self, driver):
        <span style="color:#5c6370"><em># self.driver = webdriver.Chrome()</em></span>
        self.driver = driver
        self.timeout = <span style="color:#d19a66">20</span>
        self.wait = WebDriverWait(self.driver, self.timeout)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">get_url</span>(self, url):
        <span style="color:#98c379">"""打开网址并验证"""</span>
        self.driver.maximize_window()
        self.driver.set_page_load_timeout(<span style="color:#d19a66">60</span>)
        <span style="color:#7171bf">try</span>:
            self.driver.get(url)
            self.driver.implicitly_wait(<span style="color:#d19a66">10</span>)
            log.info(<span style="color:#98c379">"打开网页:%s"</span> % url)
        <span style="color:#7171bf">except</span> TimeoutException:
            <span style="color:#7171bf">raise</span> TimeoutException(<span style="color:#98c379">"打开%s超时请检查网络或网址服务器"</span> % url)
 
<span style="color:#61aeee">    @staticmethod</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">element_locator</span>(func, locator):
        <span style="color:#98c379">"""元素定位器"""</span>
        name, value = locator
        <span style="color:#7171bf">return</span> func(cm.LOCATE_MODE[name], value)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">find_element</span>(self, locator):
        <span style="color:#98c379">"""寻找单个元素"""</span>
        <span style="color:#7171bf">return</span> WebPage.element_locator(<span style="color:#7171bf">lambda</span> *args: self.wait.until(
            EC.presence_of_element_located(args)), locator)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">find_elements</span>(self, locator):
        <span style="color:#98c379">"""查找多个相同的元素"""</span>
        <span style="color:#7171bf">return</span> WebPage.element_locator(<span style="color:#7171bf">lambda</span> *args: self.wait.until(
            EC.presence_of_all_elements_located(args)), locator)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">elements_num</span>(self, locator):
        <span style="color:#98c379">"""获取相同元素的个数"""</span>
        number = <span style="color:#7171bf">len</span>(self.find_elements(locator))
        log.info(<span style="color:#98c379">"相同元素:{}"</span>.<span style="color:#7171bf">format</span>((locator, number)))
        <span style="color:#7171bf">return</span> number
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">input_text</span>(self, locator, txt):
        <span style="color:#98c379">"""输入(输入前先清空)"""</span>
        sleep(<span style="color:#d19a66">0.5</span>)
        ele = self.find_element(locator)
        ele.clear()
        ele.send_keys(txt)
        log.info(<span style="color:#98c379">"输入文本:{}"</span>.<span style="color:#7171bf">format</span>(txt))
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">is_click</span>(self, locator):
        <span style="color:#98c379">"""点击"""</span>
        self.find_element(locator).click()
        sleep()
        log.info(<span style="color:#98c379">"点击元素:{}"</span>.<span style="color:#7171bf">format</span>(locator))
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">element_text</span>(self, locator):
        <span style="color:#98c379">"""获取当前的text"""</span>
        _text = self.find_element(locator).text
        log.info(<span style="color:#98c379">"获取文本:{}"</span>.<span style="color:#7171bf">format</span>(_text))
        <span style="color:#7171bf">return</span> _text
 
<span style="color:#61aeee">    @property</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">get_source</span>(self):
        <span style="color:#98c379">"""获取页面源代码"""</span>
        <span style="color:#7171bf">return</span> self.driver.page_source
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">refresh</span>(self):
        <span style="color:#98c379">"""刷新页面F5"""</span>
        self.driver.refresh()
        self.driver.implicitly_wait(<span style="color:#d19a66">30</span>)
</code></span></span>

In the file, we have made a secondary encapsulation of methods such as explicitly waiting for selenium's click, send_keys, etc. Improved operation success rate.

Okay, we have completed about half of the POM model. Next we enter the page object.

Create page object #Create
a searchpage.py file in the page_object directory.
 

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">from</span> page.webpage <span style="color:#7171bf">import</span> WebPage, sleep
<span style="color:#7171bf">from</span> common.readelement <span style="color:#7171bf">import</span> Element
 
search = Element(<span style="color:#98c379">'search'</span>)
 
 
<span style="color:#7171bf">class</span> <span style="color:#61aeee">SearchPage</span>(<span style="color:#61aeee">WebPage</span>):
    <span style="color:#98c379">"""搜索类"""</span>
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">input_search</span>(self, content):
        <span style="color:#98c379">"""输入搜索"""</span>
        self.input_text(search[<span style="color:#98c379">'搜索框'</span>], txt=content)
        sleep()
 
<span style="color:#61aeee">    @property</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">imagine</span>(self):
        <span style="color:#98c379">"""搜索联想"""</span>
        <span style="color:#7171bf">return</span> [x.text <span style="color:#7171bf">for</span> x <span style="color:#7171bf">in</span> self.find_elements(search[<span style="color:#98c379">'候选'</span>])]
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">click_search</span>(self):
        <span style="color:#98c379">"""点击搜索"""</span>
        self.is_click(search[<span style="color:#98c379">'搜索按钮'</span>])
</code></span></span>

In this file, we have encapsulated the steps of entering search keywords, clicking search, and searching for Lenovo.

and configured annotations.

We should develop the habit of writing comments in daily life, because after a while, without comments, the code will be difficult to read.

Okay, our page object is now complete. Next we start writing test cases. Before we start testing, let’s get familiar with the pytest testing framework.

A brief introduction to Pytest#

Open the official website of the pytest framework. pytest: helps you write better programs — pytest documentation

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em># content of test_sample.py</em></span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">inc</span>(x):
    <span style="color:#7171bf">return</span> x + <span style="color:#d19a66">1</span>
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">test_answer</span>():
    <span style="color:#7171bf">assert</span> inc(<span style="color:#d19a66">3</span>) == <span style="color:#d19a66">5</span>
</code></span></span>

I think the official tutorial is not suitable for introductory reading, and there is no Chinese version.

It is recommended to take a look at Shanghai Youyou’s pytest tutorial .

pytest.ini #

The configuration file in the pytest project can provide global control over the operations during the execution of pytest.

Create a new file in the project root directory pytest.ini.

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-ini"><span style="color:#e06c75">[pytest]</span>
<span style="color:#d19a66">addopts</span> = --html=report.html --self-contained-html
</code></span></span>

addopts specifies other parameter descriptions during execution:
--html=report/report.html --self-contained-html Generate a pytest-html styled report
-s Output the debugging information in our use case
-q Test quietly
-v You can output more detailed execution information of the use case, such as the file where the use case is located and the name of the use case. Writing
test cases #We
will use pytest to write test cases.

Create the test_search.py ​​file in the TestCase directory.
 

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> re
<span style="color:#7171bf">import</span> pytest
<span style="color:#7171bf">from</span> utils.logger <span style="color:#7171bf">import</span> log
<span style="color:#7171bf">from</span> common.readconfig <span style="color:#7171bf">import</span> ini
<span style="color:#7171bf">from</span> page_object.searchpage <span style="color:#7171bf">import</span> SearchPage
 
 
<span style="color:#7171bf">class</span> <span style="color:#61aeee">TestSearch</span>:
<span style="color:#61aeee">    @pytest.fixture(scope=<span style="color:#3388aa">'function'</span>, autouse=<span style="color:#56b6c2">True</span>)</span>
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">open_baidu</span>(self, drivers):
        <span style="color:#98c379">"""打开百度"""</span>
        search = SearchPage(drivers)
        search.get_url(ini.url)
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">test_001</span>(self, drivers):
        <span style="color:#98c379">"""搜索"""</span>
        search = SearchPage(drivers)
        search.input_search(<span style="color:#98c379">"selenium"</span>)
        search.click_search()
        result = re.search(<span style="color:#98c379">r'selenium'</span>, search.get_source)
        log.info(result)
        <span style="color:#7171bf">assert</span> result
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">test_002</span>(self, drivers):
        <span style="color:#98c379">"""测试搜索候选"""</span>
        search = SearchPage(drivers)
        search.input_search(<span style="color:#98c379">"selenium"</span>)
        log.info(<span style="color:#7171bf">list</span>(search.imagine))
        <span style="color:#7171bf">assert</span> <span style="color:#7171bf">all</span>([<span style="color:#98c379">"selenium"</span> <span style="color:#7171bf">in</span> i <span style="color:#7171bf">for</span> i <span style="color:#7171bf">in</span> search.imagine])
 
 
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
    pytest.main([<span style="color:#98c379">'TestCase/test_search.py'</span>])
 
</code></span></span>

We have written it for testing.

pytest.fixture implements the same pre-start and post-cleanup decorators as unittest setup and teardown.

First test case:

We implemented the selenium keyword in Baidu, clicked the search button, and used regular expressions to find the source code of the result page in the search results. If the number returned is greater than 10, we consider it passed.
Second test case:

We implemented, search for selenium, and then assert whether all results in the search candidates have the selenium keyword.
Finally, we write an execution startup statement below.

At this time we should enter execution, but there is still a problem, we have not passed the driver yet.

conftest.py#
We create a new conftest.py file in the project root directory.
 

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> pytest
<span style="color:#7171bf">from</span> py.xml <span style="color:#7171bf">import</span> html
<span style="color:#7171bf">from</span> selenium <span style="color:#7171bf">import</span> webdriver
 
 
driver = <span style="color:#56b6c2">None</span>
 
 
<span style="color:#61aeee">@pytest.fixture(scope=<span style="color:#3388aa">'session'</span>, autouse=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">drivers</span>(request):
    <span style="color:#7171bf">global</span> driver
    <span style="color:#7171bf">if</span> driver <span style="color:#7171bf">is</span> <span style="color:#56b6c2">None</span>:
        driver = webdriver.Chrome()
        driver.maximize_window()
 
    <span style="color:#7171bf">def</span> <span style="color:#61aeee">fn</span>():
        driver.quit()
 
    request.addfinalizer(fn)
    <span style="color:#7171bf">return</span> driver
 
 
<span style="color:#61aeee">@pytest.hookimpl(hookwrapper=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_runtest_makereport</span>(item):
    <span style="color:#98c379">"""
    当测试失败的时候,自动截图,展示到html报告中
    :param item:
    """</span>
    pytest_html = item.config.pluginmanager.getplugin(<span style="color:#98c379">'html'</span>)
    outcome = <span style="color:#7171bf">yield</span>
    report = outcome.get_result()
    report.description = <span style="color:#7171bf">str</span>(item.function.__doc__)
    extra = <span style="color:#7171bf">getattr</span>(report, <span style="color:#98c379">'extra'</span>, [])
 
    <span style="color:#7171bf">if</span> report.when == <span style="color:#98c379">'call'</span> <span style="color:#7171bf">or</span> report.when == <span style="color:#98c379">"setup"</span>:
        xfail = <span style="color:#7171bf">hasattr</span>(report, <span style="color:#98c379">'wasxfail'</span>)
        <span style="color:#7171bf">if</span> (report.skipped <span style="color:#7171bf">and</span> xfail) <span style="color:#7171bf">or</span> (report.failed <span style="color:#7171bf">and</span> <span style="color:#7171bf">not</span> xfail):
            file_name = report.nodeid.replace(<span style="color:#98c379">"::"</span>, <span style="color:#98c379">"_"</span>) + <span style="color:#98c379">".png"</span>
            screen_img = _capture_screenshot()
            <span style="color:#7171bf">if</span> file_name:
                html = <span style="color:#98c379">'<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:1024px;height:768px;" '</span> \
                       <span style="color:#98c379">'onclick="window.open(this.src)" align="right"/></div>'</span> % screen_img
                extra.append(pytest_html.extras.html(html))
        report.extra = extra
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_header</span>(cells):
    cells.insert(<span style="color:#d19a66">1</span>, html.th(<span style="color:#98c379">'用例名称'</span>))
    cells.insert(<span style="color:#d19a66">2</span>, html.th(<span style="color:#98c379">'Test_nodeid'</span>))
    cells.pop(<span style="color:#d19a66">2</span>)
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_row</span>(report, cells):
    cells.insert(<span style="color:#d19a66">1</span>, html.td(report.description))
    cells.insert(<span style="color:#d19a66">2</span>, html.td(report.nodeid))
    cells.pop(<span style="color:#d19a66">2</span>)
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_html</span>(report, data):
    <span style="color:#7171bf">if</span> report.passed:
        <span style="color:#7171bf">del</span> data[:]
        data.append(html.div(<span style="color:#98c379">'通过的用例未捕获日志输出.'</span>, class_=<span style="color:#98c379">'empty log'</span>))
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">_capture_screenshot</span>():
    <span style="color:#98c379">'''
    截图保存为base64
    :return:
    '''</span>
    <span style="color:#7171bf">return</span> driver.get_screenshot_as_base64()
 
</code></span></span>

conftest.py is the glue file of the test framework pytest, which uses the fixture method to encapsulate and pass the driver.

Execution case #We
have written the entire framework and test cases above.

We enter the home directory of the current project and execute the command:

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">pytest
</code></span></span>
命令行输出:

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">Test session starts (platform: win32, Python <span style="color:#d19a66">3.7</span>.<span style="color:#d19a66">7</span>, pytest <span style="color:#d19a66">5.3</span>.<span style="color:#d19a66">2</span>, py<span style="color:#7171bf">test-sugar</span> <span style="color:#d19a66">0.9</span>.<span style="color:#d19a66">2</span>)
cachedir: .pytest_cache
metadata: {<span style="color:#98c379">'Python'</span>: <span style="color:#98c379">'3.7.7'</span>, <span style="color:#98c379">'Platform'</span>: <span style="color:#98c379">'Windows-10-10.0.18362-SP0'</span>, <span style="color:#98c379">'Packages'</span>: {<span style="color:#98c379">'pytest'</span>: <span style="color:#98c379">'5.3.2'</span>, <span style="color:#98c379">'py'</span>: <span style="color:#98c379">'1.8.0'</span>, <span style="color:#98c379">'pluggy'</span>: <span style="color:#98c379">'0.13.1'</span>}, <span style="color:#98c379">'Plugins'</span>: {<span style="color:#98c379">'forked'</span>: <span style="color:#98c379">'1.1.3'</span>, <span style="color:#98c379">'html'</span>: <span style="color:#98c379">'2.0.1'</span>, <span style="color:#98c379">'metadata'</span>: <span style="color:#98c379">'1.8.0'</span>, <span style="color:#98c379">'ordering'</span>: <span style="color:#98c379">'0.6'</span>, <span style="color:#98c379">'rerunfailures'</span>: <span style="color:#98c379">'8.0'</span>, <span style="color:#98c379">'sugar'</span>: <span style="color:#98c379">'0.9.2'</span>, <span style="color:#98c379">'xdist'</span>: <span style="color:#98c379">'1.31.0'</span>}, <span style="color:#98c379">'JAVA_HOME'</span>: <span style="color:#98c379">'D:\\Program Files\\Java\\jdk1.8.0_131'</span>}
rootdir: C:\Users\hoou\PycharmProjects\web<span style="color:#56b6c2">-demotest</span>, inifile: pytest.ini
plugins: forked<span style="color:#56b6c2">-1</span>.<span style="color:#d19a66">1.3</span>, html<span style="color:#56b6c2">-2</span>.<span style="color:#d19a66">0.1</span>, metadata<span style="color:#56b6c2">-1</span>.<span style="color:#d19a66">8.0</span>, ordering<span style="color:#56b6c2">-0</span>.<span style="color:#d19a66">6</span>, rerunfailures<span style="color:#56b6c2">-8</span>.<span style="color:#d19a66">0</span>, sugar<span style="color:#56b6c2">-0</span>.<span style="color:#d19a66">9.2</span>, xdist<span style="color:#56b6c2">-1</span>.<span style="color:#d19a66">31.0</span>
collecting ... 
DevTools listening on ws://<span style="color:#d19a66">127.0</span>.<span style="color:#d19a66">0.1</span>:<span style="color:#d19a66">10351</span>/devtools/browser/<span style="color:#d19a66">78</span>bef34d<span style="color:#56b6c2">-b94c-4087-b724-34fb6b2ef6d1</span>
 
 TestCase\test_search.py::TestSearch.test_001 ✓                                                                                              <span style="color:#d19a66">50</span>% █████     
 
 TestCase\test_search.py::TestSearch.test_002 ✓                                                                                             <span style="color:#d19a66">100</span>% ██████████
<span style="color:#56b6c2">-------------------------------</span> generated html file: file://C:\Users\hoou\PycharmProjects\web<span style="color:#56b6c2">-demotest</span>\report\report.html <span style="color:#56b6c2">--------------------------------</span> 
 
Results (<span style="color:#d19a66">12.90</span>s):
       <span style="color:#d19a66">2</span> passed
</code></span></span>

You can see that the two use cases have been executed successfully.

A report.html file is generated in the report directory of the project.

This is the generated test report file.

Send email #After
the project execution is completed, you need to send it to your own or other people's mailbox to view the results.

We write a module for sending emails.

Create a new send_mail.py file in the utils directory
 

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> zmail
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
 
 
<span style="color:#7171bf">def</span> <span style="color:#61aeee">send_report</span>():
    <span style="color:#98c379">"""发送报告"""</span>
    <span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(cm.REPORT_FILE, encoding=<span style="color:#98c379">'utf-8'</span>) <span style="color:#7171bf">as</span> f:
        content_html = f.read()
    <span style="color:#7171bf">try</span>:
        mail = {
            <span style="color:#98c379">'from'</span>: <span style="color:#98c379">'[email protected]'</span>,
            <span style="color:#98c379">'subject'</span>: <span style="color:#98c379">'最新的测试报告邮件'</span>,
            <span style="color:#98c379">'content_html'</span>: content_html,
            <span style="color:#98c379">'attachments'</span>: [cm.REPORT_FILE, ]
        }
        server = zmail.server(*cm.EMAIL_INFO.values())
        server.send_mail(cm.ADDRESSEE, mail)
        <span style="color:#7171bf">print</span>(<span style="color:#98c379">"测试邮件发送成功!"</span>)
    <span style="color:#7171bf">except</span> Exception <span style="color:#7171bf">as</span> e:
        <span style="color:#7171bf">print</span>(<span style="color:#98c379">"Error: 无法发送邮件,{}!"</span>, <span style="color:#7171bf">format</span>(e))
 
 
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">"__main__"</span>:
    <span style="color:#98c379">'''请先在config/conf.py文件设置QQ邮箱的账号和密码'''</span>
    send_report()
</code></span></span>

Execute this file:

<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-shell">The test email was sent successfully!
</code></span></span>
You can see that the test report email has been sent successfully. Open your mailbox.

Email received successfully.

This demo project has been completed as a whole. Isn’t it very rewarding? You feel a sense of accomplishment the moment you send the email.

Finally, you must have an overall understanding of the pytest+selenium framework, and you have reached another level on the road to automated testing.

Generation of allure test report #I
have already written the allure report in another blog, and it was also modified using the open source project of this article, so I will just put a link here^_^

pytest uses allure test report - wave with the wind - Blog Park

Open source address#
In order to facilitate learning and communication, this sample project has been open sourced in Code Cloud:

web-demotest: pytest+selenium+allure UI+POM automated testing framework

Finally, I would like to thank everyone who has read my article carefully. Looking at the fans and their attention, there is always some courtesy. Although it is not a very valuable thing, if you can use it, you can follow the public account "Programmer Tiger Balm" directly Take away:

Guess you like

Origin blog.csdn.net/m0_61046899/article/details/131481086