TestNG框架学习

目录

1.简介

2.简单使用

3.注解说明

4.断言

5.testng.xml

 

6.参数化测试

 1.使用textng.xml传送参数

2.使用@DataProvider传递参数

7.失败用例重跑

8.测试报告优化


1.简介

TestNG是一个开源自动化测试框架,其灵感来自JUnit和NUnit,TestNG还涵盖了整个核心的JUnit4功能,但引入了一些新的功能,使其功能更强大,使用更方便。

优势:支持依赖测试方法,并行测试,负载测试,局部故障;灵活的插件API;支持多线程测试;

2.简单使用

IDEA中默认已经安装了testng,eclipse中需要自行安装

public class FirstTestNg {
    //添加@Test,说明这个方法是一个测试方法
    @Test
    public static void testBaidu() {
        System.out.println("hello TestNG");
    }
}

在需要进行测试的方法前加@Test注解即可,@Test”,就相当于告诉TestNG, 这个是一个测试方法,只有添加了这个注解,才会被认为是一个测试用例,才会被执行

直接运行即可

//    TestNG的执行用例的方式:先是把每个测试类文件下找到添加了@Test注释的方法,然后把这些测试方法添加到一个测试套件
     (Test Suite)
//    然后去执行这个Test Suite

运行结果如下:

 

3.注解说明

TestNG支持多种注解,可以进行各种组合

                      注解                                                                   描述
@BeforeSuite 在该套件的所有测试都运行在注释的方法之前,仅运行一次
@AfterSuite 在该套件的所有测试都运行在注释方法之后,仅运行一次
@BeforeClass 在调用当前类的第一个测试方法之前运行,注释方法仅运行一次
@AfterClass 在调用当前类的第一个测试方法之后运行,注释方法仅运行一次
@BeforeTest 注释的方法将在属于test标签内的类的所有测试方法运行之前运行
@AfterTest 注释的方法将在属于test标签内的类的所有测试方法运行之后运行
@BeforeGroups 配置方法将在之前运行组列表。 此方法保证在调用属于这些组中的任何一个的第一个测试方法之前不久运行
@AfterGroups 此配置方法将在之后运行组列表。该方法保证在调用属于任何这些组的最后一个测试方法之后不久运行
@BeforeMethod 注释方法将在每个测试方法之前运行
@AfterMethod 注释方法将在每个测试方法之后运行
@DataProvider 标记一种方法来提供测试方法的数据。 注释方法必须返回一个Object [] [],其中每个Object []可以被分配给测试方法的参数列表。 要从该DataProvider接收数据的@Test方法需要使用与此注释名称相等的dataProvider名称
@Factory 将一个方法标记为工厂,返回TestNG将被用作测试类的对象。 该方法必须返回Object []
@Listeners 定义测试类上的侦听器
@Parameters 描述如何将参数传递给@Test方法
@Test 将类或方法标记为测试的一部分,此标记若放在类上,则该类所有公共方法都将被作为测试方法
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterGroups;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeGroups;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

public class NewTest {

  @Test(groups="group1")
  public void test1() {
	  System.out.println("test1 from group1");
	  Assert.assertTrue(true);
  }
  
  @Test(groups="group1")
  public void test11() {
	  System.out.println("test11 from group1");
	  Assert.assertTrue(true);
  }
  
  @Test(groups="group2")
  public void test2() 
  {
	  System.out.println("test2 from group2");
	  Assert.assertTrue(true);
  }
  
  @BeforeTest
  public void beforeTest() 
  {
	  System.out.println("beforeTest");
  }
  
  @AfterTest
  public void afterTest() 
  {
	  System.out.println("afterTest");
  }
  
  @BeforeClass
  public void beforeClass() 
  {
	  System.out.println("beforeClass");
  }
  
  @AfterClass
  public void afterClass() 
  {
	  System.out.println("afterClass");
  }
  
  @BeforeSuite
  public void beforeSuite() 
  {
	  System.out.println("beforeSuite");
  }
  
  @AfterSuite
  public void afterSuite() 
  {
	  System.out.println("afterSuite");
  }
  
  //只对group1有效,即test1和test11
  @BeforeGroups(groups="group1")
  public void beforeGroups() 
  {
	  System.out.println("beforeGroups");
  }
  
  //只对group1有效,即test1和test11
  @AfterGroups(groups="group1")
  public void afterGroups() 
  {
	  System.out.println("afterGroups");
  }
  
  @BeforeMethod
  public void beforeMethod() 
  {
	  System.out.println("beforeMethod");
  }
  
  @AfterMethod
  public void afterMethod() 
  {
	  System.out.println("afterMethod");
  }
}

执行顺序:

beforeSuite
beforeTest
beforeClass
beforeGroups
beforeMethod
test1 from group1
afterMethod
beforeMethod
test11 from group1
afterMethod
afterGroups
beforeMethod
test2 from group2
afterMethod
afterClass
afterTest
PASSED: test1
PASSED: test11
PASSED: test2

===============================================
    Default test
    Tests run: 3, Failures: 0, Skips: 0
===============================================

afterSuite

其他注解使用:

import org.testng.annotations.*;

import static org.testng.Assert.assertEquals;

/**
 * @description:
 * @author: ljx
 * @time: 2020/6/8 14:55
 */
public class FirstTestNg {
    //添加@Test,说明这个方法是一个测试方法
    @Test
    public static void testBaidu() {
        System.out.println("hello TestNG");
    }

    @Test(description = "这是一个注解描述")
    public void methodA() {
        String str = "333";
        System.out.println(str);
    }

//    TestNG的执行用例的方式:先是把每个测试类文件下找到添加了@Test注释的方法,然后把这些测试方法添加到一个测试套件(Test Suite)
//    然后去执行这个Test Suite

    //@BeforeClass/@AfterClass 是在初始化类的时候执行,这就意味着 @BeforeClass/@AfterClass 只会执行一次,而 @BeforeMethod/@AfterMethod 执行次数和 @Test 注解函数个数一样
    @BeforeClass
    public void say() {
        System.out.println("我是前置类");
    }

    @Test
    public void testAdd() throws Exception {

        assertEquals(4 + 4, 8);
    }

    @Test
    public void testDec() throws Exception {
        assertEquals(5 / 5, 1);
    }

    @BeforeMethod
    public void go1() {
        System.out.println("我是前置方法");
    }

    @AfterMethod
    public void go2() {
        System.out.println("我是后置方法");
    }

    @AfterClass
    public void say2() {
        System.out.println("我是后置类");
    }

    //    @Test(timeOut=10) 表示测试方法的运行时间应该低于 10ms,如果超时则测试失败
    @Test(timeOut = 1)
    public void testTime() {
        System.out.println("123");
        int i = 0;
        while (i < 1000000000) {
            i++;
        }
    }

    //@Test(expected=NullPointerException.class) 来指定方法必须抛出 NullPointerException,如果没有抛出异常或者抛出其他异常则测试失败
    @Test(expectedExceptions = NullPointerException.class)
    public void testSub() {
        System.out.println("456");
        throw new NullPointerException();
    }

    //dependsOnMethods 参数来指定依赖方法和方法的执行顺序
    @Test(dependsOnMethods = {"testTime", "testSub"})
    public void testMethond() {
        System.out.println(0);
    }

    //提供数据的一个测试方法。注解的方法必须返回一个Object[] [],其中每个对象 []的测试方法的参数列表中可以分配。该@Test 方法,
    // 希望从这个 DataProvider 的接收数据,需要使用一个 dataProvider 名称等于这个注解的名字
    @DataProvider(name = "user")
    public Object[][] getStr() {
        return new Object[][]{
                {"", "", "账号不能为空"},
                {"admin", " ", "密码不能为空"},
                {" ", "a123456", "账号不能为空"},
                {"ad ", "123456", "账号“ad”不存在"},
                {"admin", "12345", "密码错误"},
        };
    }

    //DataProvider返回的是一个Object的二维数组,二维数组中的每个一维数组都会传递给调用函数,作为参数使用。运行的时候,会发现,
    // @Test标识的test method被执行的次数和object[][]包含的一维数组的个数是一致的,而@Test标识的函数的参数个数,也和object内一维数组内的元素数是一致的
    @Test(dataProvider = "user")
    private void sout(String uname, String pword, String msg) {
        System.out.println(uname + "->" + pword + "->" + msg);

    }

    //    @Factory
//    标记这个方法是一个工厂,方法必须返回的是一个对象
//    @Factory
//    public static Object getAge(){
//        return new Object();
//    }
//TestNG通过设置testng.xml文件能做以下事情
//
//1)创建来源不同包、类、方法的测试套件
//
//2)包括一些选项,例如失败的用例可以重跑。
//
//3)支持使用正则表达式
//
//4)运行把外部参数传入测试方法
//
//5)支持配置多线程的执行环境
    //suite管理多个test,而test管理多个class,最小单元是一个class文件
    //描述了如何给一个测试方法传提参数,参数是{}包裹
    @Test
    @Parameters({"browser", "server"})
    public void getInfo(String browser, String server) {
        System.out.println(browser);
        System.out.println(server);
    }
//添加@Test,说明这个方法是一个测试方法,enabled=false表示跳过该用例不执行
    @Test(enabled = false)
    public static void testBaidu() {
        System.out.println("hello TestNG");
    }

    @Test(priority = 8)
    public void methodA() {
        String str = "333";
        System.out.println(str);
    }
    //默认priority是等于0,而且priority值越小,优先级越高。
    //方法依赖被依赖的方法(B,D) > 无依赖的方法(C,E) > 有依赖的方法(A)
    @Test(priority = 5,dependsOnMethods = {"methodE","methodC"})
    public void methodB() {
        String str = "444";
        System.out.println(str);
    }
//    TestNG的执行用例的方式:先是把每个测试类文件下找到添加了@Test注释的方法,然后把这些测试方法添加到一个测试套件(Test Suite)
//    然后去执行这个Test Suite
    //invocationCount用例执行次数,invocationTimeOut用例执行超时时间,即在2000ms内跑完3次用例否则失败
    @Test(priority = 0,invocationCount = 3,invocationTimeOut = 2000)
    public void methodC() {
        String str = "CCC";
        System.out.println(str);
    }
    @Test(priority = 5)
    public void methodE() {
        String str = "eee";
        System.out.println(str);
    }

}

包括:代码执行顺序通过priority设置,用例执行次数invocationCount,enabled=false跳过用例

timeout用例超时时间,dependsOnMethods方法依赖dependsOnGroups组依赖。

hard依赖:默认为此依赖方式,即其所有依赖的methods或者groups必须全部pass,否则被标识依赖的类或者方法将会被略过,在报告中标识为skip,如后面的范例所示,此为默认的依赖方式;
soft依赖:此方式下,其依赖的方法或者组有不是全部pass也不会影响被标识依赖的类或者方法的运行,注意如果使用此方式,则依赖者和被依赖者之间必须不存在成功失败的因果关系,否则会导致用例失败。此方法在注解中需要加入alwaysRun=true即可,如@Test(dependsOnMethods= {"TestNgLearn1"}, alwaysRun=true);
 

@Test(groups = { "init" })
public void serverStartedOk() {}
 
@Test(groups = { "init" })
public void initEnvironment() {}
 
@Test(dependsOnGroups = { "init.*" })
public void method1() {}

4.断言

TestNG中最常用的一个断言类是Assert.java,里面有多个静态方法,这个类我们习惯叫硬断言。对应的还有一个软断言的类,叫SoftAssert.java,这个类是需要创建实例对象,才能调用相关实例方法进行软断言

硬断言就是,如果运行到折行断言失败,即使该用例,后面还有其他代码行,也不会继续执行下去。

软断言就是,如果一个断言失败,会继续执行这个断言下的其他语句或者断言,软断言最后一定要调用assertAll()方法

这个方法用来抛出 断言的异常信息,,,不加的话,断言的错误信息就出不来

        Assert.assertEquals("a","b");
        //判断两个对象是否相同,忽略排序位置
        String[] st1 = {"Anthony", "Tom", "Jhon"};
        String[] st2 = {"Jhon", "Anthony", "Tom"};
        Assert.assertEqualsNoOrder(st1,st2,"不相同");
        Assert.assertFalse(st1==st2, "两个相等");
        Assert.assertTrue(st1==st2, "两者不相同");
        Assert.assertNotEquals(st1, st2, "两者相等");
        Assert.assertNotNull(st1,"该对象为空");
        //Equals是值比较,而Same是内存地址比较
        Assert.assertSame(st1, st2, "not same");
        Assert.assertNotSame(st1, st2, "same");
        SoftAssert assertion = new SoftAssert();
        assertion.assertEquals(12, 13,"两者不相等");
        System.out.println("Test complete");
        System.out.println(3+8);
        assertion.assertAll();

5.testng.xml

TestNG的执行用例的方式:先是把每个测试类文件下找到添加了@Test注释的方法,然后把这些测试方法添加到一个测试套件(Test Suite),然后去执行这个Test Suite。从宏观上解释测试用例的执行就是这么一个过程。从配置文件上来看,就是执行TestNG.xml文件。直接执行时使用的是默认的testng.xml,我们可以通过自己编写TestNG.xml来运行脚本

TestNG通过设置testng.xml文件能做以下事情

1)创建来源不同包、类、方法的测试套件

2)包括一些选项,例如失败的用例可以重跑。

3)支持使用正则表达式

4)运行把外部参数传入测试方法

5)支持配置多线程的执行环境

testng.xml文件基本配置如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
    <parameter name="browser" value="firfox"></parameter>
    <parameter name="server" value="172.18.40.37"></parameter>
    <test name="Java_Learn">
        <classes>
            <class name="FirstTestNg"/>
            <!-- 添加不同测试类文件 -->
            <class name="FirstTestNg1"/>
            <class name="demo1.FirstTestNg2"/>
        </classes>
    </test> 
</suite>

suite管理多个test,而test管理多个class,最小单元是一个class文件

分组测试:分组测试类似于加标签,每个测试方法给一个标记,当运行时可以根据组来区分运行,如仅运行某组的用例

如下一些方法有些属于ApiTest,有些属于Function Test有些两者都用,有些一个都没有

package com.java.learn;
 
import org.testng.annotations.Test;
 
/**
 * create by Anthony on 2017/11/7
 */
public class TestGroupsDemo {
 
    @Test(groups = {"API Test", "Fucntion Test"})
    public void test01(){
        System.out.println("API Testing and Function testing");
    }
 
    @Test(groups = {"API Test"})
    public void test02(){
        System.out.println("API Testing");
    }
 
    @Test(groups = {"Function Test"})
    public void test03(){
        System.out.println("Function testing");
    }
 
    @Test
    public void test04(){
        System.out.println("not in API and Function testing");
    }
 
}

仅运行API Test

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
 
    <test name="Testng_learn" >
        <groups>
            <run>
                <include name="API Test"/>
            </run>
        </groups>
        <classes>
            <class name="com.java.learn.TestGroupsDemo"/>
        </classes>
    </test> <!-- Java_Learn -->
</suite> <!-- Default Suite -->

两个都运行:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
 
    <test name="Testng_learn" >
        <groups>
            <run>
                <include name="Function Test"/>
                <include name="API Test"/>
            </run>
        </groups>
        <classes>
            <class name="com.java.learn.TestGroupsDemo"/>
        </classes>
    </test> <!-- Java_Learn -->
</suite> <!-- Default Suite -->

excloude标签实现除了这个标签以外的方法

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
 
    <test name="Testng_learn" >
        <groups>
            <run>
                <exclude name="API Test"/>
                <exclude name="Function Test"/>
            </run>
        </groups>
        <classes>
            <class name="com.java.learn.TestGroupsDemo"/>
        </classes>
    </test> <!-- Java_Learn -->
</suite> <!-- Default Suite -->

使用组嵌套,运行APItest和FunctionTest的用一个新的标签all来替代

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
 
    <test name="Testng_learn" >
        <groups>
            <define name="all">
                <include name="API Test"/>
                <include name="Function Test"/>
            </define>
 
            <run>
                <include name="all"/>
            </run>
        </groups>
 
        <classes>
            <class name="com.java.learn.TestGroupsDemo"/>
        </classes>
    </test> <!-- Java_Learn -->
</suite> <!-- Default Suite -->

有些用例如果是不属于任何一个组,这里运行All,这些没有划分到某一个组的用例是不会执行的。

注意在xml标识group,需要将要运行的group加进来,同时还要将被标识这些group的class也加进来,不被加进去的不会运行;

组依赖

testng.xml中添加组依赖

package com.java.learn;
 
import org.testng.Assert;
import org.testng.annotations.Test;
 
/**
 * create by Anthony on 2017/11/9
 */
public class TestDependenceDemo {
 
    @Test(groups = {"tomcat"})
    public void restartTomcatService(){
        System.out.println("Restart the tomcat server when it is down!");
    }
 
    @Test(groups = {"tomcat"})
    public void tomcatServiceIsDown(){
        System.out.println("tomcat service is down!");
    }
 
    @Test(groups = {"app"})
    public void startAppServer(){
        System.out.println("Start App service");
    }
 
    @Test(groups = {"app"})
    public void shutDownApp(){
        System.out.println("Shutdown App service");
    }
}

app组依赖tomcat组

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
 
    <test name="Testng_learn" >
        <groups>
            <dependencies>
                <group name="app" depends-on="tomcat" />
            </dependencies>
        </groups>
 
        <classes>
            <class name="com.java.learn.TestDependenceDemo"/>
        </classes>
 
    </test> <!-- Java_Learn -->
</suite> <!-- Default Suite -->

 

6.参数化测试

 1.使用textng.xml传送参数

public class TestCase1 {

    @Test(enabled=true)
    @Parameters({"param1", "param2"})
    public void TestNgLearn1(String param1, int param2) {
        System.out.println("this is TestNG test case1, and param1 is:"+param1+"; param2 is:"+param2);
        Assert.assertFalse(false);
    }
    
    @Test(dependsOnMethods= {"TestNgLearn1"})
    public void TestNgLearn2() {
        System.out.println("this is TestNG test case2");
    }
}

参数在xml中

<?xml version="1.0" encoding="UTF-8"?>
<suite name="Suite" parallel="false">
  <test name="Test">
    <parameter name="param1" value="1011111" />
    <parameter name="param2" value="10" />
    <classes>
      <class name="com.demo.test.testng.TestCase1"/>
    </classes>
  </test> <!-- Test -->
</suite> <!-- Suite -->

2.使用@DataProvider传递参数

public class TestCase1 {
  //提供参数的方法,用@DataProvider(name = "provideNumbers")注解,函数返回的必然是Object[][]

    @DataProvider(name = "provideNumbers")
    public Object[][] provideData() {

        return new Object[][] { { 10, 20 }, { 100, 110 }, { 200, 210 } };
    }
	
  //使用参数的方法指定provider名字
    @Test(dataProvider = "provideNumbers")
    public void TestNgLearn1(int param1, int param2) {
        System.out.println("this is TestNG test case1, and param1 is:"+param1+"; param2 is:"+param2);
        Assert.assertFalse(false);
    }
    
    
}

7.失败用例重跑

1.查看报告,IDEA需要设置文件输出位置,右键testng文件,选择EditRun Configuration,点击Listeners,勾选使用默认报告

运行用例后,当前目录下生成test-output文件夹,test-output下会生成emailable.html格式报告,和把失败的用例都放在testng-failed.xml

重新执行testng-failed.xml,就相当于是仅执行了失败用例

用代码方式实现是

package demo2;
 
import org.testng.TestNG;
import java.util.ArrayList;
import java.util.List;
 
/**
 * create by Anthony on 2017/11/18
 */
public class RunTestNG {
 
    public static void main(String[] args) throws InterruptedException {
 
          TestNG testNG = new TestNG();
          List<String> suites = new ArrayList<String>();
          suites.add(".\\testng.xml");
          //suites.add(".\\test-output\\testng-failed.xml");
          testNG.setTestSuites(suites);
          testNG.run();
 
          // 等待执行结束,然后去执行失败用例
          TestNG testNG1 = new TestNG();
          List<String> suites1 = new ArrayList<String>();
          Thread.sleep(5000);
          suites1.add(".\\test-output\\testng-failed.xml");
          testNG1.setTestSuites(suites1);
          testNG1.run();
 
 
        }
}

通过执行这个java文件可以实现在cmd下执行用例的需求。

上边的失败重跑方法是全部执行完毕后再执行的,如果想要执行一个测试方法失败后就重试就需要用到Listener了,需要实现

接口IRetryAnallyzer或接口IAnnotationTransformer

接口IretryAnalyzer的方法retry()的返回值作为是否对失败测试用例进行重跑的一个条件。如果retry()结果为true,则该失败测试用例会重跑,同时将本次失败结果修改为Skip;如果结果为false,则失败的测试用例保持失败结果,运行结束。因此,如果你希望失败测试用例重跑的话,需要把IretryAnalyzer的retry()方法重写,插入自己定义的逻辑,设置返回值为true

如果希望所有失败的测试用例都进行重跑,采用retryAnalyzer注解方式对每个测试用例进行注解就比较麻烦。通过实现IAnnotationTransformer接口的方式,可以对全量测试用例的重试类进行设置。
该接口是一个监听器接口,用来修改TestNG注解。

package demo3;
 
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
 
//该接口的作用是提供去实现能够让用例运行失败重跑的设置。实现该接口必须要实现retry(ITestResult 

//result)这个方法。返回值类型是布尔型,如果返回是True,那么就执行失败重跑,返回是false,就不重

//跑。参数result是当前运行的测试用例的结果状态
public class MyRetry implements IRetryAnalyzer{
    // 设置当前失败执行的次数
    private int retryCount = 1;
    // 设置最大失败执行次数
    private static int maxRetryCount = 3;
 
    @Override
    public boolean retry(ITestResult iTestResult) {
        if(retryCount < maxRetryCount) {
            retryCount++;
            return true;
        }
        return false;
    }
}



public class TestNGReRunDemo {
    @Test(retryAnalyzer=MyRetry.class)    
    public void test01(){
        Assert.assertEquals("success","fail");
        System.out.println("test01");
    }
}
package demo3;
 
import org.testng.IAnnotationTransformer;
import org.testng.IRetryAnalyzer;
import org.testng.annotations.ITestAnnotation;
 
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
 
//该接口的作用是在TestNG执行过程中动态改变测试类中Annotation的参数,
//当前这个接口主要是针对@Test注释。
public class MyRetryListener implements IAnnotationTransformer {
 
 
    @Override
    public void transform(ITestAnnotation iTestAnnotation, Class aClass, Constructor constructor, Method method) {
 
        IRetryAnalyzer myRetry = iTestAnnotation.getRetryAnalyzer();
        if (myRetry == null) {
            iTestAnnotation.setRetryAnalyzer(MyRetry.class);
        }
    }
}

xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
    <listeners>
        <listener class-name="demo3.MyRetryListener"/>
    </listeners>
 
    <test name="Testng_learn" >
 
        <classes>
            <class name="demo3.ReRunFailedTestCaseDemo"/>
        </classes>
 
    </test>
</suite>
 

8.测试报告优化

9.在同类不同方法之间或者不同类之间传递变值

 

参考资源:https://blog.csdn.net/u011541946/article/details/78640284

                https://blog.csdn.net/df0128/article/details/83243822

                https://www.cnblogs.com/yixinjishu/p/12835123.html

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值