自动化工具 数据驱动测试框架从入门到放弃 (一)

竹杖子 · 2017年02月14日 · 最后由 ting 回复于 2017年03月21日 · 3075 次阅读

手把手教你搭建数据驱动测试框架(一)


前言


在自动化测试框架中,数据驱动的意思指定的是测试用例或者说测试套件是由外部数据集合来驱动的框架。这里说的数据集可以是任何类型的数据文件比如 xls,xlsx,csv 等等。它的核心的思想就是数据和测试代码分离,及时当测试数据发生大量变化的情况下测试代码(或者说测试用例)可以保持不变。数据集合里有 1 条数据和 100 条甚至更多,是不会影响到测试用例的执行。如果想使用一组不同的数据来执行的相同的操作没那么你就可以选择数据驱动。

比如,在这么一种场景下,例如你需要用多个不同的账号和密码来登陆某个邮箱,来验证哪些是有效的值,哪些是错误的值,或者哪些值可以导致出错,这只是一个简单的例子。另外一个简单的例子就是网络电话的测试,当我们模拟拨号、挂断、回拨等等操作时我们希望验证每个按钮是否能正常工作并能得到正确的结果。

本篇文章是基于 Selenium WebDriver 的数据驱动测试框架,如果大家跟着一步一步搭建下来,最终的框架将最终实现下面的这些功能:

  • 可以在测试套件列表里设置指定的测试套件是否执行
  • 可以在测试用例列表里设置指定的测试用例是否执行
  • 可以在测试数据表里设置某行测试数据是否被读取执行
  • 一个测试用例可以由多个测试数据驱动运行
  • 可以打印测试结果报告,显示指定数据行执行之后的结果
  • 可以打印整体测试结果报告,显示哪些用例执行通过,哪些执行失败、哪些跳过没执行
  • 报告可以是 testng 的形式或者是 XSLT

接下来的过程中不会讲太多理论的东西,更注重于实践的步骤,我将一步一步的介绍如何搭建这个框架,演示总个过程。

在开始之前还是要啰嗦一下,大家要做自动化测试,开发能力是必须的。所以下面所有过程都是假定一个前提,那就是你必须熟悉 java,eclipse,TestNg,ant,POI 等等工具,如果还不清楚的同学可以先熟悉一下这

搭建


创建工作空间

按照下面步骤在创建 项目工作空间

  • 在 F 盘下创建文件夹 backup
  • 在 backup 文件夹下创建 Training 文件夹

创建完毕就有这样的目录结构 F:\backup\Training。然后打开 eclipse,选择上面创建的文件夹为工作空间,如下图所示:

### 创建项目 ####
创建一个名为 WDDF 的项目,我们将使用这个项目来搭建我们的测试框架,创建完毕,包结构如图所示:

创建项目的目录结构

创建项目目录结构也就是在项目下创建需要的包和文件夹结构。包结构合理就很容易对项目的资源进行管理而不胡乱。在 “WDDF” 项目下创建如下包:

  1. com.stta.ExcelFiles: --存放.xsl 文件
  2. com.stta.Logging: --存放.log 文件
  3. com.stta.property: --存放.property 文件
  4. com.stta.SuiteOne: --存放测试套件一相关文件
  5. com.stta.SuiteTwo: --存放测试套件二相关文件
  6. com.stta.TestSuiteBase: --存放基本类文件
  7. com.stta.utility: --存放工具类文件
  8. com.stta.xslt: --存放 testng-results.xsl 文件

同时创建文件夹:
1.JarFiles: --存放所有需要的相关的 jar 文件

到此,项目结构就如下图所示:

针对不同的文件类型创建分离的包的是由好处的,这样子可以在我们查找、修改文件的时候很便利,例如你要修稿.xls 文件的数据,那么你几可以直接在 com.stta.ExcelFiles 包里面找,同样的你想查看执行的日志,就可以直接在 com.stta.Logging 里面查看,非常直观。

好了,到此为止,可以说我们项目的基本初始化工作基本完成。后续,在需要的时候,我们还会添加新的的包和文件到项目中去。

下载需要的 Jar 包

现在项目结构都创建好,接下来要做的就是下载所有用到的 Jar 包。这里我会罗列所有需要下载 jar 包。并一个个下载下来保存到 JarFiles 文件夹下。

####Apache POI API####

Apache POI API 是用来从.xls 文件中写入或者读取数据用的,所以必须下载 Apache POI API 和它依赖相关的 jar 包,并设置它门在 build path 中,我们才能使用它。
Apache POI API 可以在它的官网直接下载。进去后点击 “poi-bin-3.10-FINAL-20140208.tar.gz”。下载完毕后解压,解压目录下和子目录里面的下列文件拷贝到 “WDDF"项目的 JarFiles 文件夹下:

  1. poi-3.10-FINAL-20140208.jar
  2. poi-ooxml-3.10-FINAL-20140208.jar
  3. poi-ooxml-schemas-3.10-FINAL-20140208.jar
  4. xmlbeans-2.3.0.jar (在子目录 ooxml-lib 下)
  5. dom4j-1.6.1.jar (在子目录 ooxml-lib 下)

    注:上述 jar 包在以后可能会变,因为版本会一直更新嘛。用最新的就是了,下面提到的 jar 包情况和此处一样

Apache log4j

Apache log4j 是用来记录测试执行期间产生的日志的,我们也需要把它加到 build path 中。从 Apache log4j主页下载 Apache log4j jar 文件,并拷贝到 “WDDF"项目的 JarFiles 文件夹下:

  1. log4j-1.2.17.jar

Selenium WebDriver

如果你使用过 Selenium WebDriver,估计就知道使用 selenium webdriver 需要下载那些 jar 包了。从 Selenium WebDriver主页下载 Selenium WebDriver 的 zip 包,解压,并把"selenium-x.xx.x"目录和它的子目录 “libs” 下的所有 jar 文件拷贝到 “WDDF"项目的 JarFiles 文件夹下。

与生成 XSLT 报告相关的 jar

为了能够产生交互式的测试报告,我们还需要一些工具的支持。到这个页面下载一个 zip 包。解压,并把"testng-xslt-1.1.2-master" -> "lib" 下的下列文件拷贝到 “WDDF"项目的 JarFiles 文件夹下:

  1. saxon-8.7.jar
  2. SaxonLiaison.jar

拷贝 “testng-xslt-1.1.2-master\src\main\resources"目录下的 testng-results.xsl 文件到包 com.stta.xslt 下面。

  1. testng-results.xsl

还要在这里下载 testng-xslt-maven-plugin-test-0.0.jar,并拷贝到 JarFiles 文件夹下。后续如果需要,我们再下载其他 jar 包。

到此为止,我们的 JarFile 目录和 com.stta.xslt 包看起来如下图所示:

配置环境

本框架中,我们使用 Apache POI API 来从.xls 中读取数据,Apache log4j 来记录测试执行过程中产生的日志,而 xslt 报告用来产生交互式 HTML 报告。下载完相关的 jar 包后,我们就需要把他们加到 项目的 build path 中。

  • 选择邮件点击项目 WDDF,选择 "Build Path" -> "Configure Build Path"
  • 点击 Libraries->Add external JARs
  • 选择上述下载的所有 jar 包,点添加->OK

这样所有 jar 就添加到 build path 中了。查看一下 eclipse,会多了一个 “Referenced Libraries”,Referenced Libraries 下包含所有所需 的 jar 包:

创建类文件

#### 创建基类 ####

com.stta.TestSuiteBase 下创建 SuiteBase.java,这个类用于总个框架测试套件的基类

1.SuiteBase.java

com.stta.SuiteOne 这个包用来存放测试套件一的相关的类,创建如下类:

  1. SuiteOneBase.java ---这个类用于测试套件一的基类
  2. SuiteOneCaseOne.java ---这个为测试套件一的用例一
  3. SuiteOneCaseOne.java ---这个为测试套件二的用例二

com.stta.SuiteTwo 这个包用来存放测试套件二相关的类,创建如下类:

  1. SuiteTwoBase.java
  2. SuiteTwoCaseOne.java
  3. SuiteTwoCaseTwo.java

com.stta.utility 包作为测试框架的基本工具包,创建如下两个类:

  1. Read_XLS.java ---读取.xls 文件相关更能的类
  2. SuiteUtility.java ---工具类

创建完毕,目录结构如图所示:

创建 xls 文件

为了简单起见,这次搭建的测试框架只支持.xls 文件,通过检索.xls 文件,测试框架可以获取要测试套件名称、是否执行的标志,测试用例的名称以及是否执行用例的标志位,而且等测试执行完毕,可以把测试结果.xls 文件的末尾。现在先创建如三个.xls 文件:

  1. TestSuiteList.xls
  2. SuiteOne.xls
  3. SuiteTwo.xls

创建完,com.stta.ExcelFiles 看起来像下面的样子:

下面讲下这三个.xls 文件怎么创建:

  • TestSuiteList.xls TestSuiteList.xls 文件只有第一个 sheet 有数据,把第一个 sheet 命名为 SuitesList,SuitesListl 里面有三列分别是 SuiteName、SuiteToRun、Skipped/Executed 如图:

  • SuiteOne.xls 与 SuiteTwo.xls

这两文件里面的表格是一致的,就放在一起说明,这两个.xls 文件有三个 sheet,第一个是 TestCasesList,测试用例列表,指明本测试套有几个测试用例。第二和第三个 sheet 名称分别是 SuiteOneCaseOne 和 SuiteOneCaseTwo,分别是测试用例一个测试用例二的测试数据。TestCasesList 如下图所示,表格也是有三列,分别表示测试用例名称、是否执行、和执行结果 三个。

SuiteOneCaseOne 和 SuiteOneCaseTwo 的表格结构如下图所示:


记住一个原则,如下图所示,即 eclipse 里从测试类的名称、TestCasesList 表的测试用例名称和测试用例数据的 sheet 名称这个三个要保持一致。后续增加任何的测试用例都要遵循这个原则。

开始写代码

前面把要用掉的类文件先创建起来了,需要的用到的文件也都准备好了,可以开始写代码了。首先先写框架的工具类来读取数据,因为这些工具类在其他类中频繁用到先写。

  • Read_XLS.java 代码如下

package com.stta.utility;

import java.io.FileInputStream;
import java.io.FileOutputStream;
//import java.io.IOException;

import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;



public class Read_XLS { 
    public  String filelocation;
    public  FileInputStream ipstr = null;
    public  FileOutputStream opstr =null;
    private HSSFWorkbook wb = null;
    private HSSFSheet ws = null;    

    public Read_XLS(String filelocation) {      
        this.filelocation=filelocation;
        try {
            ipstr = new FileInputStream(filelocation);
            wb = new HSSFWorkbook(ipstr);
            ws = wb.getSheetAt(0);
            ipstr.close();
        } catch (Exception e) {         
            e.printStackTrace();
        } 

    }

    //检索 .xls 文件 sheets的行数.
    public int retrieveNoOfRows(String wsName){     
        int sheetIndex=wb.getSheetIndex(wsName);
        if(sheetIndex==-1)
            return 0;
        else{
            ws = wb.getSheetAt(sheetIndex);
            int rowCount=ws.getLastRowNum()+1;      
            return rowCount;        
        }
    }

    //检索.xls文件sheets的列数
    public int retrieveNoOfCols(String wsName){
        int sheetIndex=wb.getSheetIndex(wsName);
        if(sheetIndex==-1)
            return 0;
        else{
            ws = wb.getSheetAt(sheetIndex);
            int colCount=ws.getRow(0).getLastCellNum();         
            return colCount;
        }
    }

    //读取测试套件和测试用例的SuiteToRun and CaseToRun标志
    public String retrieveToRunFlag(String wsName, String colName, String rowName){

        int sheetIndex=wb.getSheetIndex(wsName);
        if(sheetIndex==-1)
            return null;
        else{
            int rowNum = retrieveNoOfRows(wsName);
            int colNum = retrieveNoOfCols(wsName);
            int colNumber=-1;
            int rowNumber=-1;           

            HSSFRow Suiterow = ws.getRow(0);                

            for(int i=0; i<colNum; i++){
                if(Suiterow.getCell(i).getStringCellValue().equals(colName.trim())){
                    colNumber=i;                    
                }                   
            }

            if(colNumber==-1){
                return "";              
            }


            for(int j=0; j<rowNum; j++){
                HSSFRow Suitecol = ws.getRow(j);                
                if(Suitecol.getCell(0).getStringCellValue().equals(rowName.trim())){
                    rowNumber=j;    
                }                   
            }

            if(rowNumber==-1){
                return "";              
            }

            HSSFRow row = ws.getRow(rowNumber);
            HSSFCell cell = row.getCell(colNumber);
            if(cell==null){
                return "";
            }
            String value = cellToString(cell);
            return value;           
        }           
    }

    //读取测试数据的DataToRun标志.
    public String[] retrieveToRunFlagTestData(String wsName, String colName){

        int sheetIndex=wb.getSheetIndex(wsName);
        if(sheetIndex==-1)
            return null;
        else{
            int rowNum = retrieveNoOfRows(wsName);
            int colNum = retrieveNoOfCols(wsName);
            int colNumber=-1;


            HSSFRow Suiterow = ws.getRow(0);                
            String data[] = new String[rowNum-1];
            for(int i=0; i<colNum; i++){
                if(Suiterow.getCell(i).getStringCellValue().equals(colName.trim())){
                    colNumber=i;                    
                }                   
            }

            if(colNumber==-1){
                return null;                
            }

            for(int j=0; j<rowNum-1; j++){
                HSSFRow Row = ws.getRow(j+1);
                if(Row==null){
                    data[j] = "";
                }
                else{
                    HSSFCell cell = Row.getCell(colNumber);
                    if(cell==null){
                        data[j] = "";
                    }
                    else{
                        String value = cellToString(cell);
                        data[j] = value;    
                    }   
                }
            }

            return data;            
        }           
    }

    //从测试用例数据sheets读取测试数据.
    public Object[][] retrieveTestData(String wsName){
        int sheetIndex=wb.getSheetIndex(wsName);
        if(sheetIndex==-1)
            return null;
        else{
                int rowNum = retrieveNoOfRows(wsName);
                int colNum = retrieveNoOfCols(wsName);

                Object data[][] = new Object[rowNum-1][colNum-2];

                for (int i=0; i<rowNum-1; i++){
                    HSSFRow row = ws.getRow(i+1);
                    for(int j=0; j< colNum-2; j++){                 
                        if(row==null){
                            data[i][j] = "";
                        }
                        else{
                            HSSFCell cell = row.getCell(j); 

                            if(cell==null){
                                data[i][j] = "";                            
                            }
                            else{
                                cell.setCellType(Cell.CELL_TYPE_STRING);
                                String value = cellToString(cell);
                                data[i][j] = value;                     
                            }
                        }
                    }               
                }           
                return data;        
        }

    }       


    public static String cellToString(HSSFCell cell){
        int type;
        Object result;
        type = cell.getCellType();          
        switch (type){
            case 0 :
                result = cell.getNumericCellValue();
                break;

            case 1 : 
                result = cell.getStringCellValue();
                break;

            default :
                throw new RuntimeException("Unsupportd cell.");         
        }
        return result.toString();
    }

    //在测试数据和测试用例表里写入测试结果
    public boolean writeResult(String wsName, String colName, int rowNumber, String Result){
        try{
            int sheetIndex=wb.getSheetIndex(wsName);
            if(sheetIndex==-1)
                return false;           
            int colNum = retrieveNoOfCols(wsName);
            int colNumber=-1;


            HSSFRow Suiterow = ws.getRow(0);            
            for(int i=0; i<colNum; i++){                
                if(Suiterow.getCell(i).getStringCellValue().equals(colName.trim())){
                    colNumber=i;                    
                }                   
            }

            if(colNumber==-1){
                return false;               
            }

            HSSFRow Row = ws.getRow(rowNumber);
            HSSFCell cell = Row.getCell(colNumber);
            if (cell == null)
                cell = Row.createCell(colNumber);           

            cell.setCellValue(Result);

            opstr = new FileOutputStream(filelocation);
            wb.write(opstr);
            opstr.close();


        }catch(Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }

    //在测试套件表里写入测试结果.
    public boolean writeResult(String wsName, String colName, String rowName, String Result){
        try{
            int rowNum = retrieveNoOfRows(wsName);
            int rowNumber=-1;
            int sheetIndex=wb.getSheetIndex(wsName);
            if(sheetIndex==-1)
                return false;           
            int colNum = retrieveNoOfCols(wsName);
            int colNumber=-1;


            HSSFRow Suiterow = ws.getRow(0);            
            for(int i=0; i<colNum; i++){                
                if(Suiterow.getCell(i).getStringCellValue().equals(colName.trim())){
                    colNumber=i;                    
                }                   
            }

            if(colNumber==-1){
                return false;               
            }

            for (int i=0; i<rowNum-1; i++){
                HSSFRow row = ws.getRow(i+1);               
                HSSFCell cell = row.getCell(0); 
                cell.setCellType(Cell.CELL_TYPE_STRING);
                String value = cellToString(cell);  
                if(value.equals(rowName)){
                    rowNumber=i+1;
                    break;
                }
            }       

            HSSFRow Row = ws.getRow(rowNumber);
            HSSFCell cell = Row.getCell(colNumber);
            if (cell == null)
                cell = Row.createCell(colNumber);           

            cell.setCellValue(Result);

            opstr = new FileOutputStream(filelocation);
            wb.write(opstr);
            opstr.close();


        }catch(Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

  • SuiteUtility.java 代码:



package com.stta.utility;

public class SuiteUtility { 

public static boolean checkToRunUtility(Read_XLS xls, String sheetName, String ToRun, String testSuite){

    boolean Flag = false;       
    if(xls.retrieveToRunFlag(sheetName,ToRun,testSuite).equalsIgnoreCase("y")){
        Flag = true;
    }
    else{
        Flag = false;
    }
    return Flag;        
}

public static String[] checkToRunUtilityOfData(Read_XLS xls, String sheetName, String ColName){     
    return xls.retrieveToRunFlagTestData(sheetName,ColName);            
}

public static Object[][] GetTestDataUtility(Read_XLS xls, String sheetName){
    return xls.retrieveTestData(sheetName); 
}

public static boolean WriteResultUtility(Read_XLS xls, String sheetName, String ColName, int rowNum, String Result){            
    return xls.writeResult(sheetName, ColName, rowNum, Result);         
}

public static boolean WriteResultUtility(Read_XLS xls, String sheetName, String ColName, String rowName, String Result){            
    return xls.writeResult(sheetName, ColName, rowName, Result);            
}

}

简单数据读取测试

测试框架的.xls 文件读取工具已经准备好,按照前面创建的测试类,我们要创建两个测试套件,每个测试套件有两个测试用例,总共就是 2 个测试套件和 4 个测试用例。对于每个测试套件分别对应两个不同.xls 文件。测试套件、测试用例与.xls 文件之间的映射关系如下所示:

  • com.stta.SuiteOne -> SuiteOne.xls
    • SuiteOneCaseOne.java -> SuiteOneCaseOne Sheet
    • SuiteOneCaseTwo.java -> SuiteOneCaseTwo Sheet
  • com.stta.SuiteTwo -> SuiteTwo.xls
    • SuiteTwoCaseOne.java -> SuiteTwoCaseOne Sheet
    • SuiteTwoCaseTwo.java -> SuiteTwoCaseTwo Sheet

编写测试代码

我们先尝试从 SuiteOne.xls 文件的 SuiteOneCaseOne sheet 里读取数据,并驱动测试用例 SuiteOneCaseOne.java 执行。首先先写 SuiteBase.java。

  • SuiteBase.java

SuiteBase 类中创建一个初始化方法,用来初始化.xls 文件路径


package com.stta.TestSuiteBase;

import java.io.IOException;
import com.stta.utility.Read_XLS;

public class SuiteBase {    

public static Read_XLS TestSuiteListExcel=null;
public static Read_XLS TestCaseListExcelOne=null;
public static Read_XLS TestCaseListExcelTwo=null;

public void init() throws IOException{
    //代码中文件路径根据实际情况填写 
    //使用Read_XLS工具类初始化测试测试套件列表TestSuiteList.xls
    TestSuiteListExcel = new Read_XLS(System.getProperty("user.dir")+"\\src\\com\\stta\\ExcelFiles\\TestSuiteList.xls");
    //使用Read_XLS工具类初始化测试套件一SuiteOne.xls
    TestCaseListExcelOne = new Read_XLS(System.getProperty("user.dir")+"\\src\\com\\stta\\ExcelFiles\\SuiteOne.xls");
    //使用Read_XLS工具类初始化测试套件二SuiteTwo.xls
    TestCaseListExcelTwo = new Read_XLS(System.getProperty("user.dir")+"\\src\\com\\stta\\ExcelFiles\\SuiteTwo.xls");                                                                           
}   
}

  • SuiteOneBase.java

SuiteOneBase 类在这个步骤中我们还不实现任何功能,仅仅继承了 SuiteBase 类


package com.stta.SuiteOne;

import com.stta.TestSuiteBase.SuiteBase;

//SuiteOneBase 类继承  SuiteBase 类.
public class SuiteOneBase extends SuiteBase{    

}

  • SuiteOneCaseOne.java

这是我们的测试类,在这个文件中我们从.xls 文件读取数据并打印出来。这个类 包含了下列三个方法:

  • checkCaseToRun()---使用 testNG 的@BeforeTest标签,它将在所有@Test方法之前被运行,这个方法只要就是调用 SuiteBase 类的 Init() 来初始化.xls 文件路径,保存在变量 FilePath 中。
  • SuiteOneCaseOneTest():这个是测试方法,附带@Test标签,并使用 dataProvider 来获取数据
  • SuiteOneCaseOneData():从.xls 文件读取数据并返回给 SuiteOneCaseOneTest() 方法

package com.stta.SuiteOne;

import java.io.IOException;

import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.stta.utility.Read_XLS;
import com.stta.utility.SuiteUtility;

//SuiteOneCaseOne 类 继承自  SuiteOneBase 类.
public class SuiteOneCaseOne extends SuiteOneBase{
Read_XLS FilePath = null;   
String TestCaseName = null; 

@BeforeTest
public void checkCaseToRun() throws IOException{
    //调用SuiteBase类的init()来初始化.xls文件
    init(); 
    FilePath = TestCaseListExcelOne;
    System.out.println("FilePath Is : "+FilePath);
    TestCaseName = this.getClass().getSimpleName(); 
    System.out.println("TestCaseName Is : "+TestCaseName);
}

//在每个迭代中接收4列的数据.
@Test(dataProvider="SuiteOneCaseOneData")
public void SuiteOneCaseOneTest(String DataCol1,String DataCol2,String DataCol3,String ExpectedResult){
    System.out.println("Value Of DataCol1 = "+DataCol1);
    System.out.println("Value Of DataCol2 = "+DataCol2);
    System.out.println("Value Of DataCol3 = "+DataCol3);
    System.out.println("Value Of ExpectedResult = "+ExpectedResult);        
}   

//data provider回在每个迭代中一个一个返回4列数据
@DataProvider
public Object[][] SuiteOneCaseOneData(){
    //To retrieve data from Data 1 Column,Data 2 Column,Data 3 Column and Expected Result column of SuiteOneCaseOne data Sheet.
    //Last two columns (DataToRun and Pass/Fail/Skip) are Ignored programatically when reading test data.
    return SuiteUtility.GetTestDataUtility(FilePath, TestCaseName);
    }
}

运行数据读取测试

到此为止,我们简单的数据读取测试已经准备完毕,可以试运行一下确保没问题,再继续往下。在 SuiteOneCaseOne.java 文件点击右键选择 Run As -> TestNG 。如果你是按照前述的步骤一步一步来的话,测试运行时没问题的。因为 SuiteOneCaseOne 数据 sheet 里有两行数据,所以@Test将会被执行两次。执行完,就可以在控制台看到如下信息:

如果你看到上述消息,说明到目前为止我们搭建的基于数据驱动的自动化是框架暂时是没有问题的,已经可以成功简单的数据读入测试。

关于数据区驱动自动化测试框架的搭建教程第一部分先到这,后续将继续实现真实的测试用例、融合 testng.xml 来管理测试用例、整合 Selenium WebDriver、ant,添加报告打印等等,逐渐完善成一个功能齐全的框架。大家都可以见到真实的搭建过程。敬请关注!

共收到 2 条回复 时间 点赞

代码用代码块

你自己觉得这个东西好维护吗?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册