Java单元测试编写案例汇总

相关链接:基础知识储备-java-Java单元测试之Mock实战

 

一、junit入门---环境配置

byte-buddy-1.9.12.jar 
byte-buddy-agent-1.9.3.jar 
hamcrest-core-1.3.jar 
javaassist-3.20.0-GA-jar 
junit-4.12.jar 
mockito-core-2.23.0.jar 
objenesis-2.6.jar 
powermock-api-mockito2-2.0.2.jar 
powermock-api-support-2.0.2.jar
powermock-core-2.0.2.jar 
Powermock-module-junit4-2.0.2.jar 
powermock-module-junit4-common-2.0.2.jar 
powermock-reflect-2.0.2.jar 

常见问题:
1. NoSuchkethodException JavassisttCtclass/getDeclaredClasses
造成该问题的原因是:引用的相关jar包中,存在多个 javassiat.jar 且先解析到了
非javassist-3. 20. 0-GA.jar 那个。而javassist 包在3. 15-GA版本以后才支持该方法。
目前已知的财务条线应用中。在/WebContent/WEB-INF/lib 下存在一个Javassist-3. 3.jar 
需要把他排除出依赖。
2.unsupportVersionException: 偏移量6
造成该问题的原因是,没有按照要求引用ar包。目前CTP Studio中内嵌了1. 6的jar包。
而 javaassiat.jar以及objenesis.jar 对于版本支持是有要求的。版本过高会导致版本不支持,因此请排查自己的jar包有无高版本的这两个jar包。

 二、案例举例

说明:本节所有标题修饰词指的是被测方法中被mock方法,本节挑选了一些典型场景对mock写法进行阐述,可结合mock教程使用. 

 1、 常规静态方法

被测方法

private static String zoneno;
public static String getDarZoneno (String bserino) throws Exception{
if (zoneno==null) {
    String zoneno = TaxAccountDao.getDsrZoneno(bserino);
    return zoneno;
}else{ 
    return zoneno;
    }
}

测试案例
//在案例名称命名时要标明该案例测试的分支情况,不要直接以1, 2, 3代替
//在mock静态方法时,一定费在PrepareForTest里加上该方法所在类,否则mock无效

@RunWith(PowerMockRunner.class)
@PrepareForTest ({TaxAccountDao.class })
public class TestTaxAccount{ 
@Before 
public void Setup ()
//实现对TaxAccountDao.class的mock, 注意!!! 该方法是一个void方法
PowerMockito.mockStatic (TaxAccountDao.class) ;
}

/*
*  测试getDsrzoneno的if分支
*/
@Test
public void testGetDsr2onenoWithIfBranch () throws Exception( 
//if 分支
String besrinom="";
//设定TaxAccountbao.getberzoneno的返回值
PowerMockito.doReturn("6006").when( TaxAccountDao.class,"getbsrzoneno",besrino)
//调用被测方法
result = TaxAccount.getDsrZoneno(besrino);
// 断言
Assert.assertEquals("6006",result) ;
}

/*
* 测试getDarZoneno的else分支
*/
@Test
public void testGetDsrZonenoWithElseBranah () throws Exception( 
//由于zoneno是私有静态属性,而方法中对其进行了判断。因此可以使用反射的方法设置该值
Pield field=TaxAccount.class.getDeclaredField ("zoneno") ;
field.setAccessible(true);
field.set(Taxhccount.class, "6006");
//else 分支
String result=TaxAccount.getDsrZoneno (“”) ;
Assert.assertEquals("6006”,result) ;
//把zoneno置空
field.set(TaxAccount.class,null) ;
       }
}

 

2、常规动态方法

本案例不仅包含了动态方法,还包含了继承方法的mock, 可以供参考
被测方法

public class AjaxcommonDao extends SqlMapClientDaoSupport{ 
public ArrayList getList (String id, Map param) { 
//getSqlMapClientTemplate () 方法是SqlMapClientDaoSupport类的final方法
return (ArrayList) getSq1MapClientTemplate().queryPorList (id,param) ;
    }
}

测试案例
//在案例名称金名时要标明该案例测试的分支情况,不要直接以1, 2, 3代替
// 在mock静态方法时,一定要在PrepareforTest里加上该方法所在类,否则mock无效

@Runwith (PowerMockRunner.class) 
@PreparePorTest ( (SqlMapClientDaoSupport.class} ) 
public class TestajaxCommonDao{
//全局被mock类的准备
SqlMapclientDaoSupport sq1MapClientDaoSupport;
SqlMapC1ientTemplate sqlMapClientTemplate;
ArrayList <object> list;
@Before 
public void Setup () {
//完成mock与数据准备
sqlMapClientDaoSupport=PowerMockito.mock(SqlMapClientDaoSupport.class);
sqlMapClientTemplate=PowerMockito.mock (SqlMapClientTemplate.class);
list= new ArrayList <objeat> (Arrays.asList("0","1","2"));
PowerMookito.spy (AjaxCommonDao.class) ;
}
@Test
public void testGetList () {
//定义gueryForList的行为,返回值在全局进行了定义,此处没有对入参进行约束,所以使用了any PowerMockito.when(sqlMapClientTemplate.queryForList(Mockito.anyString() , Mockito.anyMap() ) ).thenReturn (list) ;
//实例化被测类
AjaxCommonDao ajaxCommonDao = new AjaxCommonDao() ;
//mock getsglMapClientTemplate 该方法是继承父类的final方法,使用如下进行
//首先定义method 
Method methodFoo=PowerMockito.method (Sq1MapClientDaoSupport.class, "getsqlMapClientTemplate") ;
//mock行为,实现InvocationHandler接口,其中return的参数为根据需求要返回的数值
PowerMockito.replace(methodFoo).with(new InvocationBandler() (
@Override 
public Object invoke (Object proxy, Method method, Object [] args)throws Throwable(
return sqlMapClientTemplate;
   }
});
//调用被测方法
ArrayList result = ajaxCommonDao.getList ("0",new HashMap () ) ;
//断言
Assert.assertNotNull (result) ;
    }
}


3、继承方法

继承方法指被mock方法来自被mock类父类,或基于父方法进行重写后的子方法
被测方法(这里以一个op为例)

public class AjaxCommonSqlMapOp extends OperationStep{ 
@Override 
Protected int execute (IContext context) throws TranFailException(){
        List<?> list = null;
        String id = (String)getInputValue(context, "id");
        String params = (String) getInputValue(context, "params");
        Map <String,String> param = new HashMap<~>();
if (params != null && !"".equals(params)){
        param=URLDecoder.decode (params) ; 
        String [] tl=params.split ("&") ;
        for (int i= O;i<t1.length;i++) { 
        String[] t2=tl[i].split("-") ;
if (t2.length> 1) (
        param.put(t2[0],t2[1]) ;
          }
     }    
)
try{ 
AjaxCommanservice ajaxCommonService=(AjaxCommonService)Componentractory.getComponentByItsName("AjaxCommonService") ;
list = ajaxCommonService.getList(id,param) ;
context.setValueAt ("list",list) ;
context.setValueAt ("retcode", "0") ;
context.setValueAt ("msg”, "处理成功!") ;
return 0; 
}catch (Exception e) (
        context.setValue ("retcode", "-1") ;
        context.setValue ("msg”, "处理异常!") ;
        return -1; 
        }
    }
}

测试案例
//在案例名称命名时要标明该案例测试的分支情况,不要直接以1, 2, 3代替
//在mock继承方法时,一定要在PrepareForTest里加上该方法所在类,否则mock无效    

@Runwith (PowerMockRunner.class) 
@PrepareForTest(ComponentFactory.class, OperationStep.class} )
public class TestAjaxCommonSqlMapOp( 
//全局定义
AjaxCommonService ajaxCommonService;
AjaxCommonDao ajaxCommonDao;
IContext context;
OperationStep operationStep;
ArrayList list = new ArrayList(Arrays.asList("1","2"));

@Before 
public void Setup (
//mock静态类
PowerMockito.mockStatic(ComponentFactory.class));
//mock动态类
ajaxCommonService = PowerMockito.mock(AjaxCommonService.class);
ajaxCommonDao = PowerMockito.mock(AjaxCommonDao.class); 
context=PowerMockito.mock(IConcext.class);
//调用真实的set方法向AjaxCommonService注入一个被mock的AjaxCommonDao
PowerMockito.doCallRealMethod().when(ajaxCommonService).setAjaxCommonDao(Mookito.any(AjaxCommonDao.class);
}

/*
* 测试execute 
*/
@Test
public void testExecuteithNoIfCondition() throws Exception{
//定义入参类别
Class clazz=new Class[] (IContext.class,String.class};
//定义被mock的方法,该方法是父类方法
Method method=PowerMockito.method (Operationstep.class, "getInputValue",clazz) ;
//替换方法行为,其中可以看到在InvocationHandler里的invoke方法里可以根据进参对数据进行一些逻辑判断,以返回不同的参数。该情况可以应对对某方法的多次调用情况
PowerMockito.replace(method).with(new InvocationHandler() {
@Override 
public Object invoke (Object proxy, Method method, Object[] args)throws Throwable{
    String param= String.valueOf(args[1]);
 if (params.equal("id")){
    return  "001194760";
}else if (params.equal("params")){ 
    return "";
   }
    return "";
   }
});
//静态方法的行为定义,静态方法要先doReturn doNothing doThrow 再when
PowerMockito.doReturn(ajaxCommonService).when(ComponentPactory.class, "getComponentByltsName",  Mockito.anyString());
//动态方法要先when 再thenReturn thenThrow
PowerMockito.when(ajaxCommunService.getList(Mockito.anyString(), Mockito.anyMap()).thenReturn(list);
PowerMockito.doNothing().when(context).setValueAt(Mockito.anyString(), Mockito,anyobject());
//调用被测方法
int result = new AjaxCommonSqlMapOp().execute(context);
//断言
Assert.assertEquals(0,result);
   }
}


4、异常分支

被测方法
注意:要想抛出异常,就要在原方法throws Exceptlion, 否则会报Checked exception ls invalld for this method。当然,可以抛出已知的具体异常类


public static void RevAccount (String baerino, String userId) throws Exception{ 
        List derInfoList = TaxAccountDao.getDsrI异常nfoProo (bserino, "1") :
        String serino=(String) derInfoList.get (5) );
try( 
        TaxAocountLoggger.log (bserino, "反交易,获取主机交易参数”) :
        List dsrParInfo= getDarparInfo () ;
        TaxAccountLogger.log (bserino, "主机交易,反交易”) ;
        accDsrOperation ("1",dsrParInfo,bserino,userId,dsrInfoList) ;
        TaxAccountLogger.log (bserino, "新状态") ;
        TaxAccountDMO.updateStatusAfterBooking (bserino, "1") ;
        TaxAccountLogger.log (bserino, “更新账务”) ;
        TaxAacountDAO. UpdateAccountAfterBooking (bserino, "1") ;
        catch (TaxAccountException e) { 
throw e ;
     }
catch (DsrPailException e) { 
DsrRevException (bserino) ;
throw e: 
    }
catch (DsrRuntimeException e) {
DsrExceptionDeal (bserino, "1",serino) :
throw e;
    }
catch (DsrException e) {
DesExceptionDeal (bserino, "1",serino) :
throw e;
    }
catch (Exception e) (
throw e; 
    }
}}

测试案例

@Runwith (PowerMockRunner.class)
@PrepareForTest ( {TaxAccountDAO.class))
public class TestTaxAccount{ 
@Before 
public void Setup () throws Exception{
PowerMockito.mockStatic (TaxAccountDAO.class) ;
}
//对于抛出异常,要在expected里写明抛出的异常类型
@Test(expected=TaxAccountException.class)
Public void teatRevhocountwithTataaountException() throws Exception{
//定义抛出异常的情况
PowerMoakito.when(TaxAccount DJO.getDerParInfo()).thenThrow (new TaxAccountException(""));
//调用被测方法
TaxAccount.RevAccount("", "") ;
  }
}

5、私有方法

说明:被mock方法是private
被测方法

protected Map getbsrReturnMsg (CTEOpetation op, String segNo) {
    Map retValMap = new HashMap ();
try(
    KeyedCollection data = op.getkeyedCollection();
    int eCount = data.getElements().size();
//isLogging是一个private方法
if (isLogging()) (
    log.error (“上送顺序号为:”+segNo+“---”);
    log.error (segNo+""+"返回数据");
for (int i=0;i<eCount,i++) (
    DataElement de = data.getElementAt(i);
if (de instanceof DataElement) { 
    retValMap.put(de.getName(),de.getValue()) ;
    log.error(sqeNo+“ ”+de.getName()+":"+de.getValue());
}
if (de instanceof IndexedCollection) (
    IndexedCollection icoll = (IndexedCollection) de;
    List list = new ArrayList();
for(int j=0; j < icoll.size();j++) (
    KeyedCollection kColl=(KeyedCollection) iColl.getElementAt(j);
    Iterator it = kColl.getElements().keySet().iterator();
    Map temp = new HashMap(); 
while (it.hasNext()) (
    String key = (String) it.next());
    String value = (String)kColl.getValueAt(key);
    temp.put(key,value) ;
if (isLogging()) { 
    log.error(segNo++key+":"+value) ;
    list.add(temp);
    retValMap.put (iColl.getName(),list);
              }
    }
} catch (CTEObjectNoFoundException) {
    throw new FMSRuntimeException (e) ;
   }
return retValMap;

处理思路


@Runwith(PowerMockitoRunner.class)
@PrepareForTest((DSROperationService.class} )
public class TestDSROperationService{ 
    @Test
    public void testGetDerReturnMsg () { 
    //方法1
    DSROperationService dsrOpsrationService = PowerMockito.spy (new   DSROperationService() ) ;
    PowerMockito.when(dsrOpsrationService, "isLogging").thenReturn("1");

    //方法2用method方法
    Method method=PowerMockito.method(DSROperationService.class, "isLogging");
    PowerMockito.replace(methodl).with((proxy,method,args)->{ return 1;});
    //验证思路
    //思路1有返回值的
    Assert.assertEquals(1,isLogging()) ;
    //思路2 无返回值
    PowerMockito.verifyPrivate(dsrOpsrationservice Mockito.times(1)).invoke("isLogging");
}


6、受保护的方法

这里指被mock方法是被protected修饰的方法,使用method即可解决
被测方法

public  Map exeantebsropstep (String opName, String tradeId, Map inputvalMap, String segNo) throws DerException{ 
… 省略
try{
    dsrOp.execute();
  //见3. 5节被测室例,其是一个protected方法
   retValMap=getDsrReturnMsg (dsrOp,seqNo) ;
   dsrop.close();
   }
…省略
}

测试案例

@RunWith(PowerMockitoRunner.class)
@PrepareForTest( (DSROperationService.class} }
public class testDSROperationService{ 
    Map map;
@Before 
public void SetUp(){
     map= new HashMap();
)
@Test 
public void testExeuteDsrOpStep(){
    DSROperationService dsrOpsrationService-PowerMockito.spy (new DSROperationService());
    //入参的类别集合
    Class[] clazz=new Class[]{CTEOpetation.class,String.class};
    Method method;
    PowerMockito.method(people.class, "getDsrReturnMsg",clazz);
    PowerMockito.replace(methodl).with((proxy,method,args)->{return map}; 
});
   … 省略
  }
}

7、局部变量方法

说明:这里的局部变量方法指的是,在被测方法内部新建类,并调用该类某个方法。本节讨论如何
对这种情况实施mock 

被测方法

@override
publig int execute (CTEOperation operation) throws Exeeption(){
. . . 省略
//被测方法内部新建HttpClient类和PostMethod类,且无入口可以注入该类
    Httpclient httpClient = new HttpClient();
    PostMethod method = new Posthethod(url)); 
try{
…省略
    httpClient.setTimeout(Integer.valueOf("60000"));
    httpClient.execteMethod(method);
  }
}

测试方法

@Runwith(PowerMockitoRunner.class) 
public class testFaccAccCvvQueryOpStep{ 
    public void testExecute() { 
    …省略
    //准备替换对象
    HttpClient  httpClient = PowerMockito.mock(HttpClient.class));
    // 在生命同期中,只要new Httpclient对象,且调用的是其无参构造器,就将mock好的对象返回
    //其余形式的API请查看 mock教程中 PowerMock与构造器一节
    PowerMockito.whenNew(HttpClient.class).withNoArguments().thenReturn(httpClient); 
    …省略
    }
  }

8、 多次调用方法

说明:这里指的多次调用方法主要指的是被多次调用且返回值不相同的方法,以下代码仅涉及相关演示,其余忽路

被测方法

public Map executedsropStep(string opName,string tradeId, Map 
    inputValMap,string segNo) throws Exception( 
    CTEOperation dsrop = null;
try(
    dsrop = (CTEOperation)CTEOpertation.readobject(opName);
    …省略
KeyedCollection keyedData = dsrop.getkeyedCollection();
    int eCount = data.getElements().size();
    for (int i = 0,i <eCount ; i++) {
    DataElement de=KeyData.getElementAt(i);
    if (de instange of DataElement){ 
    log.error (segNo+""+de.getName() +":"+de.getValue()) ; //待mock方法,通过for循环不断调用
. . . 省略
}
. . . 省路
}

测试案例


public void testExecutedsrOpStep() {
    Map<String, String> valueMap = new HashMap();
@Before
public void Setup()(
    List <string> keyList = new ArrayList(Arrays.asList("TRANSOK", "ERR NO", "MSG NO"));
    List<Object> value=new ArrayList (Arrays.asList("0", "1111", "交易成功”));
}

…省略
//方法1 直接用thenReturn
//优点:书写简单:
//缺点:按照调用次数i依次返回第i个返回值,若调用次数超过了返回值的列表长度,则一直返回般后一个,不灵活
PowerMockito.when(dataElement.getElementAt(Mockito.anyInt())).thenReturn(dataelemnt);
PowerMockito.when(dataElement.getName()).thenReturn("TRANSOK", "ERROR NO", "MS G_NO");
PowerMockito.when(dataElement.getName()).thenReturn("0", "1111","交易成功");
//------------------------------------------------------------------
// 方法2 使用Answer接口
//优点:灵活    缺点:代码量大、但可复用
PowerMockito.when(data.getElementAt(Mockito.anyInt())).then (new Answer<Datalement>(){
@Override
public DataElement answer (InvocationonMock invocationOnMock) throws  Throwable( 
//获腔入参,关于invocationOnMock的更多Api请查春mock文档
// 这里可以根据入参的索引来返回对应的值
final int index = (int)invocationOnMock.getArguments()[0];
if (index <keyList.size())(
    PowerMockito.when(dataELement.getName()).thenReturn(keyList.get (index));
    PowerMockito.when(dataElement.getValue()).thenReturn(valueList.get (index));
    return dataElement;
    }
});
//--------------------------------------------------------------------
//方法3 使用method 
//优点:灵活     缺点:代码量大、但可复用
Method method = PowerMockito.method (DataElement.class, "getName") ;
PowerMockito.replace(method).with (new InvocationHandler () {
@Override
public object invoke (Objeat proxy, Method method, Object[] args) throws Throwable{
if (args[0] < keyList.size()){ 
    return keyList.get(args[0] ); 
  }
    return "";
}
});

9、私有变量处理方法

说明:有时我们需要获取被mock类中的全局属性,而一般来说这些属性一般都是private的,因此如这些类本身不提供set或get方法时,就会非常麻烦的。以下提供几种解法。

被测方法

private static string zoneno;
public statie string getDsrZaneno (String bserino) throws Exception( 
if (zoneno-null) ( 
  String zoneno = TaxAccountDao.getDsrZoneno (bserino) ;
 return zoneno;
} else{
 return zoneno;
}

测试案例

// 如上可发现,zonenc在为null和非null时会触发不同的分支,而zoneno本身是全局私有登量,且没有set方法,
publie void  testGetDsrzoneno () {
…省略
// 方法1 采用了java.lang.reflect的方法  缺点:代码量较大
Field field = Taxhocount.class.getDeolaredField ("zoneno");
field.sethocessible (true) ,
field.set (Taxocount.class, "6006") ;
//方法2: 利用NemberMadifier 
MemberModifier.fimld/PasgivBookingAaaount.alass, "zoneno").set (pasgivBookingAccount, "01"); 
//方法3: 利用whiteBox 当然,能set就可以get
Whitebox.setInternalstate (PassivBookingAccount.class, "zoneno", "6006") ; 
... 省略
}

10、存在于被mock类中依赖方法

说明:当被测方法中存在的依赖方法与被测方法处在同一个类,且该类存在外部依赖,此时若使用mock, 会造成被测方法也被mock掉。此时,需要使用spy句法。
注意:如果被依赖方法是静态方法,此时需要在@PrepareForTest加上该类,而这会导致Jacoco 统计不到该类的覆盖率,因此如果出现这种情,尽量不要写该被测方法,以免造成其他方法不计数
被测方法


public static void RevAccount(String bserino, String userId) throws Exception{ 
    List dsrInfolist = TaxAccountDM.getDsrI异常nfoProc (bserino, "1");
    String serino = (String) dsrInfoList.get(5) ;
try(
    TaxAccountLoggger.log (bserino, "反交易,获取主机交易参数”);
    List dsrParInfo= getDsrParInfo () )
    TaxAccountLogger.log (bserino, 主机交易,反交易”);
    accDsroperation (1l,dsrParInfo,bserino,userId,dsrInfoList) ;
    TaxAccount Logger.log (bserino, 更新状态”);
    TaxnecountDAO.updateStatusAfterBooking (bserino, "1") ;
    TaxAccount Logger.log (bserino, “更新账务”) ;
    TaxAccountDAO. UpdateAccountAfterBooking (bserino, "1") ;
catch (TaxAccountException e) {
    throw e; 
}catch (DsrPailException e) {
    DsrRevException (bserino) ;
    throw e;
}catch (DsrRuntimeException e) (
    DsrExceptionDeal (bserino, "1",serino) ;
    throw e; 
}
catch (DsrException e) {
    DesExceptionDeal (bserino, "1,serino) :
    throw e;
}
catch (Exception e) { 
    throw e;
    }
}
}

测试案例

public  class TestTaxAccount{ 
@Before
public void setUp () {
PowerMockito.spy (TaxAccount.class) ;
 }
@Test
public void testRevWithNormalCondition () throws Exception(
//抑制方法运行
PowerMockito.doNothing().when (TaxAccount.class, "accDsrOperation", Mockito.String() ,Mockito.anyList() , Mockito.anyString() , Mockito.anyString() , Mockito.anyList()) ;
…省略 对TaxAccountDAO几个方法的mock 
TaxAccount.RevAccount ("", "") ;
  }
}



 

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java中,可以使用JUnit框架进行异常单元测试。一个例子可以是这样的: ``` @RunWith(PowerMockRunner.class) @PrepareForTest({TaxAccountDAO.class}) public class TestTaxAccount { @Before public void setup() throws Exception { PowerMockito.mockStatic(TaxAccountDAO.class); } // 使用@Test注解标记这是一个测试方法,并且在expected参数中指定期望抛出的异常类型 @Test(expected = TaxAccountException.class) public void testRevAccountWithTaxAccountException() throws Exception { // 定义抛出异常的情况 PowerMockito.when(TaxAccountDAO.getDerParInfo()).thenThrow(new TaxAccountException()); // 调用被测方法 TaxAccount.RevAccount("", #### 引用[.reference_title] - *1* [详解Java单元测试Junit框架实例](https://download.csdn.net/download/weixin_38557935/12782179)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Java单元测试编写案例汇总](https://blog.csdn.net/liuchangjie0112/article/details/109789490)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

痴书先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值