jmockit

 

 

行为和状态的测试:
基于行为的mock是站在目标测试代码外面的角度的.通常主要模拟行为。
而基于状态的是站在目标测试代码内部的。我们可以对传入的参数进行检查、匹配,才返回某些结果。Mockup用于state based测试。



一定要理解,单元测试测什么:
单元测试的原则是哪怕你测试的方法中有一行代码,也是有必要写测试方法的。所以不要担心,你测试的业务逻辑最后只剩下边界值测试。你可以根据一些MOCK的返回值比如空,非空,个数等来测试你的业务逻辑是否正确。如果业务方法依赖于第三方类库,缓存,消息队列,DAO层的方法都是可以MOCK的。
以前的思路:Service方法依赖于SDK,针对这些方法的单元测试需要与微博交互返回正确的AccessToken,这样才能Service做有意义的单元测试。这是典型的依赖于第三方或远程调用的场景。
正确的思路:新浪的API已经由新浪团队测试过。我假设它完全正确,只需要模拟它的返回值来测试我的“业务逻辑”在mock指定值下的反应。就象有些Service方法依赖于DAO层,如果DAO层的单元测试充分(比如通过DBUNIT等工具模拟数据),那么在业务层的单元测试中,只需要mock DAO层即可。


Mock的原因:
1)一些依赖单元本身已经(或者将来会有,只是目前还没有 实现而已)拥有自己的单元测试。
2)由于一些特殊原因,在测试环境中,一些并不是很容易快速的执行的单元(因为它们可能会写数据库或者发送邮件等等)。


Dependency是什么?
通过使用mocked声明,可以在指定的测试代码对一些特殊的依赖(比如新浪微博的API)进行 mock模拟,也就是说,一个mocked类型,应该是单元测试中的一个依赖类型,这些类型可以是引用、接口、抽象类、具体的类、final 类等等。
@mocked是去修饰Dependency的。



二种指定要mocked的Dependency:

1.传统的方式:在Expectations里面。

           @Test
public void doBusinessOperationXyz()
{
...

new Expectations() // an "expectation block"
{
Dependency mockInstance; // "Dependency" is our mocked type for this test
...

{
...
// "mockInstance" is a mocked instance automatically provided for use in the test
mockInstance.mockedMethod(...);
...
}
};

...
}





2.但大多数情况下,例如变量都是通过一些特定的注解及注解本身的属性来指定mock,例如 @Mocked, @NonStrict, @Injectable等等。这些注解标记可以在实例的域或者测试方法的参数中使用。
注:不管哪种方式,默认情况下,被mock的类型的所有方法(无论方法或者构造函数的修饰符是否是private,stati,final,native等,这些方法和构造函数都会被mock掉)在测试期间都被 mock实现。如果一个mock类型被声明为类,那么除了java.lang.Object之外,该类的父类将被递归mock。




Expectation:
An expectation is a set of invocations to an specific mocked method/constructor that is relevant for a given test.
为单元测试服务的一组invocations定义。单元测试的replay阶段会invoke这些invocations中的某个match invocation。当单元测试中的方法的签名,运行时的参数(比如invocation的参数值,调用次数)都能match的Expectation中定义的某个invocation时候,该invocation所模拟的行为才会被触发。



Invocation:
单元测试replay阶段invoke一个invocation。如果这个定义在Expectation中的invocation被match到的话。



Record-Replay-Verify模型(jmockit就是遵循这套模型)
任何一个测试至少可以划分三个相互独立的阶段,这些阶段将按顺序执行,一次执行一个阶段

    1.    @Test 
    2.       public void someTestMethod() 
    3.       { 
    4.          // 1. 准备阶段:测试执行之前所需要的所有东西都可以编写在这里 
    5.          ... 
    6.          // 2. 单元测试代码在这里执行,通常是通过调用public方法来执行 
    7.          ... 
    8.          // 3.  验证阶段:验证上面所执行的单元测试代码真正执行了其业务逻辑 
    9.          ... 
    10.       } 

首先,我们有一个准备阶段,单元测试代码所需要的对象和数据都可以在这里创建或者从其他地方加载进来。之后,测试的代码被执行。最后,测试结果和期望结果进行比较。
(1) Record(记录)阶段:在这里将记录下哪些调用会被期望执行,这些都是发生在测试的准备阶段,而且在真正测试代码执行调用之前。
(2) Replay(重播)阶段:我们感兴趣的mock 调用将在这里有机会被执行,就好像真正的单元测试代码被执行一样。这些在Record阶段记录下来的 mock方法 /构造函数调用将在这里重播(执行),尽管这些mock调用在record和replay阶段通常不是一对一对应的。
(3)Verify(验证)阶段:所有的真实调用应该在这里和期望值进行校验,这些动作发生在测试验证阶段




JMockit工具来编写基于行为的测试代码,通常符合下面的经典模板:

import mockit.*; 
... other imports ...

public class SomeTest
{
// 零个或者更多的mock 属性, 这些属性对于整个类的所有测试方法来说是通用的。
@Mocked Collaborator mockCollaborator;
@NonStrict AnotherDependency anotherDependency;
...

@Test
public void testWithRecordAndReplayOnly(mock parameters)
{
// 如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。

new Expectations() // 一个期望块.
{
// 零个或者多个局部 mock 属性域

{
//一个或者多个mock对象(类型)的调用,这些调用会被Expectations记录(Recorded)下来
//一些没有被mock的方法、对象类型等同样可以在这个期望块里面调用
}
};

// 单元测试代码真正业务逻辑在此执行

// 如果需要,可以在这里进行验证代码编写,当然可以利用JUnit/TestNG断言
}

@Test
public void testWithReplayAndVerifyOnly(mock parameters)
{
// 如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。

// 单元测试代码真正业务逻辑在此执行

new Verifications() {{ // 一个验证块
// 一个或者多个mock对象(类型)的调用,这些调用用于验证结果是否正确
//一些没有被mock的方法、对象类型等同样可以在这个验证块里面调用
}};

// 如果需求,这里可以添加其他额外的验证代码,
// 当然,这些验证可以编写在这里,也可以在Verifications块之前
}

@Test
public void testWithBothRecordAndVerify(mock parameters)
{
//如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。

new NonStrictExpectations() { // 同样是一个期望块
//零个或者多个局部 mock 属性域
{
// 一个或者多个mock对象(类型)的调用,这些调用会被Expectations记录(Recorded)下来
}
};

// 单元测试代码真正业务逻辑在此执行

new VerificationsInOrder() {{ // 同样是一个验证块
// 一个或者多个mock对象(类型)的调用,这些调用将期望按照特定的顺序进行比较。
}};

// 如果需求,这里可以添加其他额外的验证代码,
// 当然,这些验证可以编写在这里,也可以在Verifications块之前
}




期望块与验证块的位置及重复:一个测试方法可以包含任意数量(含零个)的期望块,验证块也是一样。




如何声明和使用mock类型:


1)字段,期望块外的字段与期望块内的局部属性字段使用@Mocked来声明Mock类型。

2)参数,方法的参数声明来引入一个Mock类型。

第一种情况,属性字段是属于测试类或者一个mockit.Expectations 子类(一个expectation 期望块的内部的局部属性字段)。
第二种情况,参数必须是属于某个测试方法。    

在所有情况,一个mock 属性字段或者参数声明,都可以通过使用@Mocked声明。
对于方法mock的参数或者 在expectation 期望块中定义的mock属性字段来说,该注解是可选的。
可选的原因:
注解@Mocked(或者其他mock的注解类型,例如 @NonStrict)只是对于定义在测试类中的属性字段域才是必须的,这是为了防止和该测试类的其他不需要mock的字段属性产生冲突而已。




public interface Dependency // an arbitrary custom interface 
{
String doSomething(boolean b);
}

public final class MultiMocksTest<MultiMock extends Dependency & Runnable> //声明类型变量MultiMock(它的作用域是整个测试类)
{
@Mocked MultiMock multiMock;

@Test
public void mockFieldWithTwoInterfaces()
{
new NonStrictExpectations() {{
multiMock.doSomething(false); result = "test";
}};

multiMock.run();
assertEquals("test", multiMock.doSomething(false));

new Verifications() {{ multiMock.run(); }};
}

@Test
public <M extends Dependency & Serializable> void mockParameterWithTwoInterfaces(final M mock) //M(它的作用域是 单个测试方法)
{
new Expectations() {{
mock.doSomething(true); result = ""
}};

assertEquals("", mock.doSomething(true));
}
}





对于一个返回值不为void类型的方法,Expectation中如何模拟方法返回值:
1)其返回值可以通过 Expectations#result属性域
2)Expectations#returns(Object) 方法来进行记录(Recorded)。
如果这个测试需要获取一个异常(exception)或者错误(error)时,_result属性域同样可以使用。很简单,此时只需要将一个 throwable实例赋值给它就可以了。




* Expectations中模拟方法返回多个可能的示例:
场景:下面的代码是要测试的业务方法。该业务方法依赖了第三方的DependencyAbc类。通过mock来分别模拟(1)(2)(3)。

public class UnitUnderTest 
{
(1)private final DependencyAbc abc = new DependencyAbc();

public void doSomething()
{
(2) int n = abc.intReturningMethod();

for (int i = 0; i < n; i++) {
String s;

try {
(3) s = abc.stringReturningMethod();
}
catch (SomeCheckedException e) {
// 处理异常
}

// 这里可以处理其他逻辑
}
}
}



@Test
public void doSomethingHandlesSomeCheckedException() throws Exception
{
new Expectations() {
DependencyAbc abc;

{
(1) new DependencyAbc(); //构造方法的模拟。

(2) abc.intReturningMethod(); result = 3;

(3) abc.stringReturningMethod();
returns("str1", "str2");
result = new SomeCheckedException(); //注意,mock异常只能通过result属性。
}
};

new UnitUnderTest().doSomething();
}



第一个(其实就是 DependencyAbc() 的构造函数调用)实际上会在测试代码中通过一个无参的构造函数来初始化这些依赖,对于这种调用是不需要任何返回值的,除非在构造函数里面抛出一个异常或者 错误(其实构造函数是没有返回值的,所以对它来说记录返回值是没什么意义可说)。第二个期望指定调用intReturningMethod()后将返回值 3。第三个期望就是,调用stringReturningMethod()方法后将按顺序返回3个连续的期望值。





默认mock与默认返回值:
1.对于一个返回值不是void的mock方法,无论是否匹配上在Record阶段定义的调用期望,都应该对该方法提供默认的返回值。Jmockit总 会根据定义返回值的类型返回一个值:对于整型缺省返回0,boolean类型默认为false,collection或者array会默认为empty对 象,而对于引用类型,则默认为null(包括String类型和JDK原始的包装类)。
2.mocked的构造函数和返回值为void的 mocked方法,也提供一个"缺省值",只不过就是简单的return而已,当然没有抛出异常或者错误。




!Expectation invocation与NonStrictExpectations invocation的使用:

Expectation invocation(严格期望  or  严格invocation) :
在期望块new Expectations(){...}中,默认所有被记录下来的期望都是严格的。这意味着,这些期望的调用[必须]在重播阶段被执行,而且需要按照声明的期望指定的[执行顺序执行]。
而且,[也只允许这些调用被执行]。[任何一个没有被记录下来的非期望调用 ] 都会造成测试用例失败。


NonStrictExpectations invocation(非严格期望 or 非严格invocation):
在一个非严格的期望块中,所有的被mock的类型的所有调用都可以在重播阶段执行,即使不是在期望中声明的,比如默认mocked的invocation。这在Expectation中完全不可以的。
也就是说,默认情况下,在重播阶段是否执行mock类型的调用是不会造成测试用例失败的。同样,这种不严格的期望是不要求调用的执行顺序的。



缺省情况下,一个严格的期望会精确匹配重播阶段的一个调用。换而言之,这类型的期望是存在一个隐式的调用次数约束1,就好像它后面紧跟着 times=1这个约束。而另一方面,对于一个非严格的期望,默认是可以匹配重播阶段调用的任意次数的.注意,默认总是可以被显示指定复盖。比如指定times/minTimes /maxTimes 属性字段。



对于一个严格的期望,所有在重播阶段被期望所匹配的调用,都会隐式被校验(Verify)通过的。剩余的调用则被认为不符合期望(即造成测试失败,哪怕这些调用是针对@Mocked形成的默认mocked的方法。),除非, 这个mock的类型被关联到一个非严格的期望上。所以,使用隐式验证的严格期望需要排除verification块的使用,这个块是用于调用的显式校验的。实际上,在 new Verifications() {...}代码块中,只有那些匹配非严格期望的调用才被允许使用。






从严格到非严格:

1.
除了使用针对期望块的NonStrictExpectations外,还可以使用(@NonStrict注解类属性+Expectation块)做到非严格效果,注意@NonStrict它是针对类的属性非严格,类的属性适用于类中的所有测试方法。
这个非严格的范围比非严格期望块的作用范围大很多。一旦其它的测试方法还想在该属性的基础上使用Expectation块,是不可能的了。如果是这种情况,就需要这样使用(@Mocked注解类属性+ NonStrictExpectations块)。

2.@NonStrict可以避免需要记录调用构造函数,或任何不感兴趣的方法。


3.一旦使用了@NonStrict,Expectation中的invocation就变成了非严格的invocation。可以在replay调用或不调用。


总结三种不同严格性的方式:1)在一个给定的严格期望块中,如果需要指定某一个期望是非严格的,可以调用notStrict()注解方法。2) 对于一个特殊的mock类型/实例,其所有期望都需要是完全非严格的,则可以通过注解@NonStrict将其声明为一个mock属性字段或者参 数。3)如果在一个期望块中,需要所有的期望都是非严格的,则可以使用NonStrictExpectations类。注意,即使使用NonStrictExpectations,它里面的invocation还是必须在replay阶段执行。非严格的含义在于replay阶段调用非期望的方法是否失败。

示例:

public class IntroductionTest {


//不管使用下面的二种注解之一,NonStrictExpectations块中的invocation还是必须在replay的时候调用。只不过比起Expectation块,replay阶段还可以invocate非严格的invocation
//getWinportUrlThrowException而不会失败。
// @NonStrict
@Mocked
private WinportUrlService winportUrlService = null;

@Test
public void testNoExpectations() {
final String memberId = "test2009";
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
}

@Test
public void testWithExpectations() {
final String memberId = "test2009";


new NonStrictExpectations() {
{
//下面的invocation必须出现在replay阶段。
winportUrlService.hasWinport(memberId);
result = false; // 也可以是returns(false);
// 总共可以调用的次数
times = 2;
}
};


// 步骤二、replay
//如果注了下面二句,肯定失败。并且只invocate一次也不行。
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
winportUrlService.getWinportUrlThrowException(memberId);

}
}





public class IntroductionTest {

@NonStrict //使用@NonStrict才可以在replay阶段invocate getWinportUrlThrowException。
private WinportUrlService winportUrlService = null;

@Test
public void testNoExpectations() {
final String memberId = "test2009";
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
}

@Test
public void testWithExpectations() {
final String memberId = "test2009";

new Expectations() {
{
winportUrlService.hasWinport(memberId);
result = false; // 也可以是returns(false);
// 总共可以调用的次数
times = 2;
}
};

// 步骤二、replay 在此阶段

//Expectation中的invocation必须被调用,而且是2次。
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
//如果使用@Mocked,那么下面的调用,会报错。因为隐式验证会对非期望的invocation验证失败。而使用@NonStrict 修改类属性,则下面的invocate没有问题。详情参FullMockTest;如果使用@NonStrict.
winportUrlService.getWinportUrlThrowException(memberId);
}
}





public class IntroductionTest
{

@Mocked
// 使用@Mocked,replay阶段invocate getWinportUrlThrowException会报失败。
private WinportUrlService winportUrlService = null;

@Test
public void testNoExpectations()
{
final String memberId = "test2009";
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
}

@Test
public void testWithExpectations()
{
final String memberId = "test2009";

new Expectations()
{
{
// 期望被mock的调用,以及被调用时返回的结果
winportUrlService.hasWinport(memberId);
result = false; // 也可以是returns(false);
// 总共可以调用的次数
times = 2;
}
};

// 步骤二、replay 在此阶段

// Expectation中的invocation必须被调用,而且是2次。
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
Assert.assertEquals(false, winportUrlService.hasWinport(memberId));
// 如果使用@Mocked,那么下面的调用,会报错。因为隐式验证会对非期望的invocation验证失败。详情参FullMockTest;如果使用@NonStrict.
winportUrlService.getWinportUrlThrowException(memberId);
}
}







关于Verify:

校验用来验证replay阶段是否按照了一定的顺序执行或者验证replay阶段的invocate是否匹配到了期望里面的invocation,即调用次数是否匹配。感觉主要针对replay阶段的正确性进行验证。
Explicit verification使用场合:
严格期望是隐式校验的,所以,不需要在一个显式的校验块重复校验。非严格期望通常在一个校验结构块中通过显式的方式来校验mock类型的调用。就如不久将看到的一样,一个被录制下来的非严格期望仍然可以通过隐式方式来校验,而不需要在校验块中编码调用校验。




Partial Mocked总结:

背景:使用@Mocked, @NonStrict, @Injectable修饰的对象,所有默认方法都是mocked状态。而如果不想对所有方法mocked,那么可以使用Partial Mock来解决。这与严格非严格没有什么关系。主要是针对注解修饰属性形成方法默认为mocked状态这种情况。有些时候,针对第三方API,某个类的部分方法并不希望mocked,而是保持原来的调用。这样的场景是比较特殊的。平常第三方的类或接口都是全部mocked。


This is appropriate for most tests, but in some situations we might need to select only certain methods or constructors to be mocked; or we might prefer to exclude certain methods/constructors from being mocked. Methods/constructors not mocked in an otherwise mocked type will execute normally when called.



二种方式实现:
1.static partial mocking
1) 特征:使用@Mocked(正则字符串)

2)示例:

public class MyTestClass
{
@Mocked("nanoTime") final System system = null;
@Mocked("print") final JComponent component = new JButton();

@Test
//”()“代表构造方法。
public void staticPartialMocking(@Mocked(methods = "()", inverse = true) final Graphics graphics)
{
...

new Expectations() {
@Mocked({"(int)", "doInternal()", "[gs]etValue", "complexOperation(Object)"})
Collaborator mock;

{
graphics.setClip(0, 0, 80, 60);
graphics.translate(-15, -12);
component.print(graphics);

mock.getValue();
}
};

...
}
}




3)关于inverse=true,表示正则匹配到的方法不mocked。默认是false,表示正则匹配到的方法mocked。

For example, while @Mocked("nanoTime") or @Mocked(methods = "nanoTime", inverse = false) select only the System#nanoTime() method to be mocked, the inverse specification @Mocked(methods = "nanoTime", inverse = true) select it as the only method not mocked in the java.lang.System class.


关于正则如何过滤不需要mocked的方法:从当前类一直往上直到Object。整个树里面的方法都需要跟正则匹配。

Finally, notice that when the mocked type is a class, a mocking filter will match methods/constructors defined anywhere in the class hierarchy, from the mocked class up to (but not including) Object


2.Dynamic partial mocking

1)动态部分mocked, 避免了静态使用正则式的工作量及使用字符串不好重构的弊端。

2)特征:不在使用注解的时候显示指定。而是在RRV的第二个阶段,Replay的时候,通过与期望的invocation匹配决定。如果匹配到了,那么就是mocked的方法。如果匹配不到就按照非mocked的方法处理。

An alternative that avoids both problems is to let JMockit figure out whether to execute the real implementation of methods/constructors during the replay phase,based on which invocations were recorded and which were not.

if it matches a recorded expectation, it gets mocked; otherwise, the real implementation gets executed.

3)期望(包括严格期望块与非严格期望块)参数有二种:类和实例

4)示例:

public final class DynamicPartialMockingTest
{
static class Collaborator
{
private final int value;

Collaborator() { value = -1; }
Collaborator(int value) { this.value = value; }

int getValue() { return value; }
final boolean simpleOperation(int a, String b, Date c) { return true; }
static void doSomething(boolean b, String s) { throw new IllegalStateException(); }
}

@Test
public void dynamicallyMockAClass()
{
new Expectations(Collaborator.class) {{
new Collaborator().getValue(); result = 123;
}};

// Mocked:
Collaborator collaborator = new Collaborator();
assertEquals(123, collaborator.getValue());

// Not mocked:
assertTrue(collaborator.simpleOperation(1, "b", null));
assertEquals(45, new Collaborator(45).value);
}

@Test
public void dynamicallyMockAnInstance()
{
final Collaborator collaborator = new Collaborator(2);

new NonStrictExpectations(collaborator) {{
collaborator.simpleOperation(1, "", null); result = false;
Collaborator.doSomething(anyBoolean, "test");
}};

// Mocked:
assertFalse(collaborator.simpleOperation(1, "", null));
Collaborator.doSomething(true, null);

// Not mocked:
assertEquals(2, collaborator.getValue());
assertEquals(45, new Collaborator(45).value);
assertEquals(-1, new Collaborator().value);
}
}





注意
1)实例做为参数,当前类中的方法可以mocking。如果类做为参数,直到Object中的所有方法都可以mocking。注意,不是mocked状态。mocking表示的是一种可以mocking的范围。最终mocked方法是匹配到invocation才是mocked状态。而实例做为参数的范围表示仅在当前类中匹配。类做为参数的范围表示可以在父类中匹配。


2) 与FullMock的一个区别是没有用注解去修饰类属性。
Notice that in these two example tests there is no mock field or mock parameter. Dynamic mocking effectively provides yet another way to specify mocked types. It also lets us turn objects stored in local variables into mocked instances. Such objects can be created with any amount of state in internal instance fields; they will keep that state when mocked.


3) 动态不是让开发人员象静态那样通过正则指定,而是告诉jmockit怎么去区分哪些是mock哪些是real.注意,mocked 和 mocking
An alternative that avoids both problems is to let JMockit figure out whether to execute the real implementation of methods/constructors during the replay phase,




一个示例:
Let's say I have class MyClass with methods x(), y() and z(). Let's say x() calls y(), and y() calls z().
So everytime I test x() both y() and z() are called. In case of mocking the dependencies of MyClass I will have to mock the dependencies behavior inside x(), y() and z().
So if my tests for method x() are testXWhen1(), testXWhen2() and testXWhen3() I will have to repeat the expectations for my dependencies in each of the test methods. In the end, I have some code with the expectations for what happens inside y() and z() repeated for my three test methods. Any solution to avoid this?
One of my ideas was to try to test the actual x() method, but mocking y() and z(). In that case my instance of MyClass should be partly a mock and partly the real MyClass. Is it possible?
Another solution was to be strict about expectations in x(), but not about what happens in y() and z()... I think I can do that with @NonStrict instead of @Mocked, but it's not my favorite solution.(提问者明显混淆了Partial与非严格,这是所有初学者会范的错误)
->

If you want to test method x() then you should mock method y().In That case there's no need to mock z() too 'cause you'll never reach call of z() inside the y() (y is mocked).Test your x, y and z methods in different tests.Use PowerMock.It has createPartialMock method.

->

You can use JMockit's dynamic partial mocking feature, by passing the class or object to be partially mocked in the Expectations/NonStrictExpectations constructor.(由此可见,Partial/Full与 严格/非严格完全不相关,是独立的二个使用场景。)
In general, though, it's best to avoid the use of partial mocking, as it often indicates a lack of cohesion in the code under test, and leads to tests that are harder to understand. (部分Partial是不推荐使用的。因为不好理解。)
For cases where you have a bunch of the same expectations being needed in several tests, there's always the option of creating reusable expectation blocks. You can encapsulate a number of expectations in a named "XyzExpectations" subclass, optionally with parameterized constructors, and instantiate it in any number of tests (the "expectations" subclass actually instantiated must be final). The same can be done with verification blocks.(针对重复Expectation的解决办法。不过,通常x()只需要被测试一次就OK了。没必须出现在三个测试方法里。所以,这样的场景也很少见。)

转载于:https://www.cnblogs.com/highriver/archive/2012/01/10/2318309.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值