Detailed operation of python3+requests interface automated test example

Some time ago, due to the transformation of the company's testing direction, the original web page functional testing was transformed into interface testing. Before, most of them were done manually, using postman and jmeter for interface testing. Later, someone in the team talked about the migration of the original web automated testing framework. The automation framework for driving the interface uses the Java language, but for me, who is learning Java but also learning Python, I feel that Python is simpler than Java, so I decided to write the interface automation testing framework for Python myself. Because I have just learned python. This automation framework has been basically completed, so I will make some summaries to facilitate future review. There are many imperfections and I have encountered many problems. I hope the experts can give me some advice. Now let me carry out the main content of today. (Beginner’s road to success, hahaha~~)

1. First, let’s sort out our ideas.

What is the normal interface testing process?

Is the reaction in your mind like this:

Determine the tool for testing the interface—> Configure the required interface parameters—> Conduct the test—> Check the test results (some require database assistance)—> Generate a test report (html report)

Then, we will build our framework step by step according to this process. In this process, we need to separate business and data so that we can be flexible and achieve the purpose of writing the framework. As long as you do it well, you will definitely succeed. This is what I said to myself at the beginning.

Next, let's divide the structure.

My structure is like this, you can refer to it below:

​​​​​​ common: store some common methods

  result: the folder generated during the execution process, which stores the results of each test

  testCase: used to store specific test cases

  testFile: stores files used during the test process, including uploaded files, test cases and database sql statements

  caselist: txt file, configure the case name for each execution

                                       config: Configure some constants, such as database-related information, interface-related information, etc.

                                        readConfig: used to read the contents of the config configuration file

                                        runAll: used to execute case

 Now that the overall structure has been divided, it is time to fill in the entire framework step by step. First, let's take a look at the two files config.ini and readConfig.py. Starting from them, I personally think it is easier to proceed.

Let’s take a look at what the contents of the file look like:

[DATABASE]
host = 50.23.190.57
username = xxxxxx
password = ******
port = 3306
database = databasename
 
[HTTP]
# 接口的url
baseurl = http://xx.xxxx.xx 
port = 8080
timeout = 1.0
 
[EMAIL]
mail_host = smtp.163.com
mail_user = [email protected]
mail_pass = *********
mail_port = 25
sender = [email protected]
receiver = [email protected]/[email protected]
subject = python
content = "All interface test has been complited\nplease read the report file about the detile of result in the attachment."
testuser = Someone
on_off = 1

I believe everyone knows such a configuration file. Yes, we can put all the things that are unchanged here. Haha, how about it, not bad.

Now, we have a fixed "warehouse". To save things that we usually don’t move, then, how do we take it out for my use? At this time, the readConfig.py file was born, and it successfully helped us solve this problem. Let us take a look at its true appearance.

import os
import codecs
import configparser
 
proDir = os.path.split(os.path.realpath(__file__))[0]
configPath = os.path.join(proDir, "config.ini")
 
class ReadConfig:
    def __init__(self):
        fd = open(configPath)
        data = fd.read()
 
        #  remove BOM
        if data[:3] == codecs.BOM_UTF8:
            data = data[3:]
            file = codecs.open(configPath, "w")
            file.write(data)
            file.close()
        fd.close()
 
        self.cf = configparser.ConfigParser()
        self.cf.read(configPath)
 
    def get_email(self, name):
        value = self.cf.get("EMAIL", name)
        return value
 
    def get_http(self, name):
        value = self.cf.get("HTTP", name)
        return value
 
    def get_db(self, name):
        value = self.cf.get("DATABASE", name)
        return value

How about it? It looks very simple. The method we defined is to get the corresponding value according to the name. Isn’t it so easy? ! Of course, here we only use the get method, and there are other methods such as set. Interested students can explore it by themselves, or you can read the blog post about reading configuration files, which we will not repeat here.

Without further ado, let’s first take a look at what common has.

Now that we have completed the configuration file and reading the configuration file, and have seen the content in common, we can write the common methods in common. Where to start? Today, let’s turn over the cards of “Log.py”, because it is relatively independent, we deal with it alone, and we also want to lay a good foundation for it to serve us in the future.

Here, I want to say a few more words to you. For this log file, I have enabled a separate thread for it. This will make it more convenient for us to write logs during the entire running process. You can also know the name by looking at it. , here are all the operations we have on the output logs, mainly the stipulation of the output format, the definition of the output level and other output definitions, etc. In short, anything you want to do with the log can be put here. Let's take a look at the code. Nothing is more direct and effective than this.

import logging
from datetime import datetime
import threading

 First, we need to introduce the required modules as above before we can proceed with the next operation.

class Log:
    def __init__(self):
        global logPath, resultPath, proDir
        proDir = readConfig.proDir
        resultPath = os.path.join(proDir, "result")
        # create result file if it doesn't exist
        if not os.path.exists(resultPath):
            os.mkdir(resultPath)
        # defined test result file name by localtime
        logPath = os.path.join(resultPath, str(datetime.now().strftime("%Y%m%d%H%M%S")))
        # create test result file if it doesn't exist
        if not os.path.exists(logPath):
            os.mkdir(logPath)
        # defined logger
        self.logger = logging.getLogger()
        # defined log level
        self.logger.setLevel(logging.INFO)
 
        # defined handler
        handler = logging.FileHandler(os.path.join(logPath, "output.log"))
        # defined formatter
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        # defined formatter
        handler.setFormatter(formatter)
        # add handler
        self.logger.addHandler(handler)

, Now, we have created the above Log class, and in the __init__ initialization method, we have performed log-related initialization operations. The specific operation content and comments have been written very clearly (the English is a bit poor, but everyone can understand it, hehe...). In this way, the basic format of the log has been defined. As for other methods, it is up to everyone to use it. Okay, after all, everyone’s needs are different, so we will only write down common shared methods. The next step is to put it into a thread. Please see the following code:

class MyLog:
    log = None
    mutex = threading.Lock()
 
    def __init__(self):
        pass
 
    @staticmethod
    def get_log():
 
        if MyLog.log is None:
            MyLog.mutex.acquire()
            MyLog.log = Log()
            MyLog.mutex.release()
 
        return MyLog.log

Doesn’t it seem as complicated as you think? Hahaha, it’s that simple. Python is much simpler than Java. This is why I chose it. Although I have just learned it, there are still many things I don’t understand. . About the learning of threads in python. I hope everyone will make progress with me. Okay, now the content of the log is over. Don’t you feel great? In fact, don’t be afraid at any time, and believe that “nothing is difficult in the world, only those who are willing to do it”.

Next, we continue to build. What we need to do this time is the content of configHttp.py. That's right, we're starting to configure the interface file! (I finally wrote the interface, aren’t you very happy~)

The following is the main part of the interface file, let us take a look.

import requests
import readConfig as readConfig
from common.Log import MyLog as Log
 
localReadConfig = readConfig.ReadConfig()
 
class ConfigHttp:
    def __init__(self):
        global host, port, timeout
        host = localReadConfig.get_http("baseurl")
        port = localReadConfig.get_http("port")
        timeout = localReadConfig.get_http("timeout")
        self.log = Log.get_log()
        self.logger = self.log.get_logger()
        self.headers = {}
        self.params = {}
        self.data = {}
        self.url = None
        self.files = {}
 
    def set_url(self, url):
        self.url = host + url
 
    def set_headers(self, header):
        self.headers = header
 
    def set_params(self, param):
        self.params = param
 
    def set_data(self, data):
        self.data = data
 
    def set_files(self, file):
        self.files = file
 
    # defined http get method
    def get(self):
        try:
            response = requests.get(self.url, params=self.params, headers=self.headers, timeout=float(timeout))
            # response.raise_for_status()
            return response
        except TimeoutError:
            self.logger.error("Time out!")
            return None
 
    # defined http post method
    def post(self):
        try:
            response = requests.post(self.url, headers=self.headers, data=self.data, files=self.files, timeout=float(timeout))
            # response.raise_for_status()
            return response
        except TimeoutError:
            self.logger.error("Time out!")
            return None

Let’s pick out the key points here. First of all, you can see that the editor used the requests that come with python to conduct the interface test this time. I believe that interested friends have already seen that the python+requests model is very useful and it has been packaged for us. The method of testing the interface is very convenient to use. Here, let me take the two methods of get and post. (These two methods are the most commonly used in daily life. For other methods, you can imitate and expand by yourself)

The most commonly seen in get method
        interface testing are the get method and the post method. Among them, the get method is used to obtain the interface test. To put it bluntly, that is to say, using the get interface will not change the background data, and the get method After passing the parameters, the format of the URL is as follows: http://interface address?key1=value1&key2=value2, does it look familiar~ (Anyway, it looks familiar to me~\(≧▽≦)/~ La la la), then how do we use it, please continue reading.

For the get method provided by requests, there are several commonly used parameters:

url: Obviously, it is the address url of the interface.

headers: customized request headers (headers), for example: content-type = application/x-www-form-urlencoded

params: used to pass the parameters used in the test interface. Here we use the dictionary form (key: value) in python to pass the parameters.

timeout: Set the maximum time for interface connection (a timeout error will be thrown if this time is exceeded)

Now, we already know what each parameter means. All that is left is to fill in the values. Is it a mechanical application? Haha, this is how I learned it mechanically~

Give a chestnut:

url='http://api.shein.com/v2/member/logout'
header={'content-type': application/x-www-form-urlencoded}
param={'user_id': 123456,'email' : [email protected]}
timeout=0.5
requests.get (url, headers=header, params=param, timeout=timeout)
The post method
        is similar to the get method. Just set the corresponding parameters. Let’s give a chestnut directly and enter the code directly:

url='http://api.shein.com/v2/member/login'
header={'content-type': application/x-www-form-urlencoded}
data={'email': [email protected] ,'password': 123456}
timeout=0.5 How about
requests.post (url, headers=header, data=data, timeout=timeout) , is it also very simple?
Here we need to explain that we no longer use params to pass the parameters in the post method, but instead use data to pass them. Hahaha, finally finished, let’s explore (explain) the return value of the interface.

Still only talking about commonly used operations that return values.

text: Get the text format of the interface return value

json(): Get the json() format of the interface return value

status_code: Return status code (success: 200)

headers: Return the complete request header information (headers['name']: Return the specified headers content)

encoding: Returns the character encoding format

url: Returns the complete url address of the interface

The above are the commonly used methods, and everyone can choose them by themselves.

Regarding throwing exceptions on failed requests, we can use "raise_for_status()" to complete, then when an error occurs in our request, an exception will be thrown. Here, I would like to remind my friends that if your interface has an incorrect address, there will be a corresponding error message (sometimes it needs to be tested). At this time, you must not use this method to throw an error, because python itself If you throw the error when linking the interface, you will not be able to test the expected content later. And the program will crash directly here, counting as errors. (Don’t ask me how I know this, because I discovered it during testing)

alright. We have finished talking about the interface file. Do you feel that success is not far away? Well, if you have already seen this, then congratulations, there is still a long way to go~ Hahaha, it is so willful. (After all, in order to make it easier for all novices who are similar to me to understand, the editor has also used all the ancient power in my body)

Slowly take a long breath and continue with the following content. . .

Quick, I want to learn (read) and practice (read) the content in common.py.

import os
from xlrd import open_workbook
from xml.etree import ElementTree as ElementTree
from common.Log import MyLog as Log
 
localConfigHttp = configHttp.ConfigHttp()
log = Log.get_log()
logger = log.get_logger()
 
# 从excel文件中读取测试用例
def get_xls(xls_name, sheet_name):
    cls = []
    # get xls file's path
    xlsPath = os.path.join(proDir, "testFile", xls_name)
    # open xls file
    file = open_workbook(xlsPath)
    # get sheet by name
    sheet = file.sheet_by_name(sheet_name)
    # get one sheet's rows
    nrows = sheet.nrows
    for i in range(nrows):
        if sheet.row_values(i)[0] != u'case_name':
            cls.append(sheet.row_values(i))
    return cls
 
# 从xml文件中读取sql语句
database = {}
def set_xml():
    if len(database) == 0:
        sql_path = os.path.join(proDir, "testFile", "SQL.xml")
        tree = ElementTree.parse(sql_path)
        for db in tree.findall("database"):
            db_name = db.get("name")
            # print(db_name)
            table = {}
            for tb in db.getchildren():
                table_name = tb.get("name")
                # print(table_name)
                sql = {}
                for data in tb.getchildren():
                    sql_id = data.get("id")
                    # print(sql_id)
                    sql[sql_id] = data.text
                table[table_name] = sql
            database[db_name] = table
 
def get_xml_dict(database_name, table_name):
    set_xml()
    database_dict = database.get(database_name).get(table_name)
    return database_dict
 
def get_sql(database_name, table_name, sql_id):
    db = get_xml_dict(database_name, table_name)
    sql = db.get(sql_id)
    return sql

The above are the two main contents of our common, what? Don’t know what it is yet? Let me tell you.

We use xml.etree.Element to operate the xml file, and then use our custom method to obtain different (want) (want) values ​​based on passing different parameters.
Use xlrd to operate excel files. Note that we use excel files to manage test cases.
Does it sound a bit confusing? The editor was also confused when I first learned it. It’s easy to understand after reading the document.

excel file:

xml file:

As for the specific method, I will not explain it bit by bit. I always feel that everyone understands it (the editor just learned it, so I hope you can understand). It’s just that I personally need to record it in detail so that it can be easily reviewed in the future.

Next, let’s take a look at the database and send emails (you can also leave this part out if needed)

Let’s look at our old friend “database” first.

The editor is using the MySQL database this time, so let's take it as an example.

import pymysql
import readConfig as readConfig
from common.Log import MyLog as Log
 
localReadConfig = readConfig.ReadConfig()
 
class MyDB:
    global host, username, password, port, database, config
    host = localReadConfig.get_db("host")
    username = localReadConfig.get_db("username")
    password = localReadConfig.get_db("password")
    port = localReadConfig.get_db("port")
    database = localReadConfig.get_db("database")
    config = {
        'host': str(host),
        'user': username,
        'passwd': password,
        'port': int(port),
        'db': database
    }
 
    def __init__(self):
        self.log = Log.get_log()
        self.logger = self.log.get_logger()
        self.db = None
        self.cursor = None
 
    def connectDB(self):
        try:
            # connect to DB
            self.db = pymysql.connect(**config)
            # create cursor
            self.cursor = self.db.cursor()
            print("Connect DB successfully!")
        except ConnectionError as ex:
            self.logger.error(str(ex))
 
    def executeSQL(self, sql, params):
        self.connectDB()
        # executing sql
        self.cursor.execute(sql, params)
        # executing by committing to DB
        self.db.commit()
        return self.cursor
 
    def get_all(self, cursor):
        value = cursor.fetchall()
        return value
 
    def get_one(self, cursor):
        value = cursor.fetchone()
        return value
 
    def closeDB(self):
        self.db.close()
        print("Database closed!")

This is the complete database file. Because the editor's requirements are not very complicated for database operations, these basically meet the requirements. Attention, before that, friends, please install pymysql first! Install pymysql! Install pymysql! (Important things are said three times). The installation method is very simple. Since the editor uses pip to manage the python package installation, you only need to enter the pip folder under the python installation path and execute the following command:

pip install pymysql
hahaha, so we can use python to connect to the database~ (Applause, celebrate)

Friends, have you noticed that we do not have specific variable values ​​in the entire file? Why? That's right, because we wrote the config.ini file earlier, all database configuration information is in this file. Isn't it very convenient? Even if you change the database in the future, you only need to modify the contents of the config.ini file. , combined with the previous test case management (excel file), the storage of sql statements (xml file), and what we are going to talk about next, businessCommon.py and the folder where specific cases are stored, then we have separated data and business Hahahaha, think about how much work it will take to modify the content of test cases and SQL statements in the future. You no longer need to modify every case. You only need to modify a few fixed files. Will you be happy immediately? (Well, if you want to laugh, just laugh loudly)

Returning to the configDB.py file above, the content is very simple. I believe everyone can understand it. It is to connect to the database, execute SQL, obtain the results, and finally close the database. There is nothing different.

It’s time to talk about emails. Have you ever encountered this problem: after each test, you need to give the developer a test report. Well, for a lazy person like me, I don’t want to always find others to develop, so I thought that after each test, we can let the program send an email to the developer to tell them that the test is over. , and send the test report as an attachment to the developer's mailbox via email. Wouldn't it be great!

Therefore, configEmail.py came into being. Dangdangdang...please see:

import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from datetime import datetime
import threading
import readConfig as readConfig
from common.Log import MyLog
import zipfile
import glob
 
localReadConfig = readConfig.ReadConfig()
 
class Email:
    def __init__(self):
        global host, user, password, port, sender, title, content
        host = localReadConfig.get_email("mail_host")
        user = localReadConfig.get_email("mail_user")
        password = localReadConfig.get_email("mail_pass")
        port = localReadConfig.get_email("mail_port")
        sender = localReadConfig.get_email("sender")
        title = localReadConfig.get_email("subject")
        content = localReadConfig.get_email("content")
        self.value = localReadConfig.get_email("receiver")
        self.receiver = []
        # get receiver list
        for n in str(self.value).split("/"):
            self.receiver.append(n)
        # defined email subject
        date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.subject = title + " " + date
        self.log = MyLog.get_log()
        self.logger = self.log.get_logger()
        self.msg = MIMEMultipart('mixed')
 
    def config_header(self):
        self.msg['subject'] = self.subject
        self.msg['from'] = sender
        self.msg['to'] = ";".join(self.receiver)
 
    def config_content(self):
        content_plain = MIMEText(content, 'plain', 'utf-8')
        self.msg.attach(content_plain)
 
    def config_file(self):
        # if the file content is not null, then config the email file
        if self.check_file():
 
            reportpath = self.log.get_result_path()
            zippath = os.path.join(readConfig.proDir, "result", "test.zip")
            # zip file
            files = glob.glob(reportpath + '\*')
            f = zipfile.ZipFile(zippath, 'w', zipfile.ZIP_DEFLATED)
            for file in files:
                f.write(file)
            f.close()
 
            reportfile = open(zippath, 'rb').read()
            filehtml = MIMEText(reportfile, 'base64', 'utf-8')
            filehtml['Content-Type'] = 'application/octet-stream'
            filehtml['Content-Disposition'] = 'attachment; filename="test.zip"'
            self.msg.attach(filehtml)
 
    def check_file(self):
        reportpath = self.log.get_report_path()
        if os.path.isfile(reportpath) and not os.stat(reportpath) == 0:
            return True
        else:
            return False
 
    def send_email(self):
        self.config_header()
        self.config_content()
        self.config_file()
        try:
            smtp = smtplib.SMTP()
            smtp.connect(host)
            smtp.login(user, password)
            smtp.sendmail(sender, self.receiver, self.msg.as_string())
            smtp.quit()
            self.logger.info("The test report has send to developer by email.")
        except Exception as ex:
            self.logger.error(str(ex))
 
class MyEmail:
    email = None
    mutex = threading.Lock()
 
    def __init__(self):
        pass
 
    @staticmethod
    def get_email():
 
        if MyEmail.email is None:
            MyEmail.mutex.acquire()
            MyEmail.email = Email()
            MyEmail.mutex.release()
        return MyEmail.email
 
 
if __name__ == "__main__":
    email = MyEmail.get_email()

Here is the complete content of the file, but unfortunately, the editor encountered a problem that has not yet been solved. I raised it here and hope that God can give a solution! Begging!

Problem: Use 163 free email server to send emails, but every time you send an email, it will be bounced by the 163 email server, and the error code thrown is: 554 

The official description is as follows:

However, but... the small demo for sending email written by the editor before integrating email into this framework can send emails normally. This problem bothers me and I still haven't solved it. I hope God can enlighten me.

We are not far from success. Let me briefly explain the HTMLTestRunner.py file. This file was not written by me. I am just its porter. Hahaha, this file was downloaded from the Internet and written by a master. It is used Generate a test report in html format, what? Want to know what generating a test report looks like? Okay, this will satisfy your curiosity:

It looks good. Well, if you are smart, you can also explore this file yourself, modify it, and make it your own style~

Okay, here comes the highlight, our runAll.py. Please watch the protagonist appear.

This is the entry point for our entire framework to run. After the above content is completed, this is the last step. After writing it, our framework is complete. (Applause, scatter flowers~)

import unittest
import HTMLTestRunner
 
    def set_case_list(self):
        fb = open(self.caseListFile)
        for value in fb.readlines():
            data = str(value)
            if data != '' and not data.startswith("#"):
                self.caseList.append(data.replace("\n", ""))
        fb.close()
 
    def set_case_suite(self):
        self.set_case_list()
        test_suite = unittest.TestSuite()
        suite_model = []
 
        for case in self.caseList:
            case_file = os.path.join(readConfig.proDir, "testCase")
            print(case_file)
            case_name = case.split("/")[-1]
            print(case_name+".py")
            discover = unittest.defaultTestLoader.discover(case_file, pattern=case_name + '.py', top_level_dir=None)
            suite_model.append(discover)
 
        if len(suite_model) > 0:
            for suite in suite_model:
                for test_name in suite:
                    test_suite.addTest(test_name)
        else:
            return None
        return test_suite
 
    def run(self):
        try:
            suit = self.set_case_suite()
            if suit is not None:
                logger.info("********TEST START********")
                fp = open(resultPath, 'wb')
                runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='Test Report', description='Test Description')
                runner.run(suit)
            else:
                logger.info("Have no case to test.")
        except Exception as ex:
            logger.error(str(ex))
        finally:
            logger.info("*********TEST END*********")
            # send test report by email
            if int(on_off) == 0:
                self.email.send_email()
            elif int(on_off) == 1:
                logger.info("Doesn't send report email to developer.")
            else:
                logger.info("Unknow state.")

I posted the main parts of runAll above. First, we need to read the case names that need to be executed from the caselist.txt file, then add them to the unittest test set that comes with python, and finally execute the run() function to execute the test. set. There is still a lot to learn about python’s unittest, so I won’t go into details here.

Finally, the entire interface automation framework has been explained. Do you understand it? What? Are there any files in the directory structure that were previously posted? Hehe,,, I believe that without the editor having to say more, everyone probably knows the functions of the remaining folders. Hmm~ After thinking about it for a long time, I decided to just talk briefly. Just look at the picture above, simple and clear:

  The result folder will be generated when the case is executed for the first time, and future test results will be saved in this folder. At the same time, the folder for each test is named after the system time and contains two files, the log file and the test Report.

The testCase folder stores the specific test cases we wrote. The above are just some of the ones I wrote. Note that all case names must start with test. This is because unittest will automatically match all .py files starting with test under the testCase folder when testing.

   Under the testFile folder, place the excel file used to manage test cases during our testing and the xml file of the sql statement used for database query.

The last thing is the caselist.txt file, let me take a look at it:

Anything that is not commented out is the name of the case to be executed. Just write the name of the case you want to execute here.

Huh~ let out a long sigh of relief, the whole process is finally completed.

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!

Guess you like

Origin blog.csdn.net/2301_76643199/article/details/132832560