转jMock Cookbook 中文版一

  1. 入门
  2. 定义期望
  3. 模拟方法的返回值
  4. 从模拟方法抛出异常
  5. 匹配参数值
  6. 精确指定期望参数匹配值
  7. 期望方法多于(少于)一次
  8. 期望调用顺序
  9. 期望一个调用在两个其他调用之间
  10. 忽略不相关的模拟对象
  11. 在测试的Set-Up中覆盖期望定义
  12. 匹配对象和方法
  13. 编写新的匹配器
  14. 编写新的行为
  15. 使用脚本轻松定义行为
  16. 使用模拟对象测试多线程代码
  17. 模拟泛型
  18. 模拟抽象的和实际类
  19. 使用非Java语言来使用jMock
  20. 更新jMock 1到jMock 2
  21. 在Maven构建中使用jMock
  22. 理解jMock 2中的方法调度
  23. 在Eclipse 插件测试中模拟类

jMock Cookbook 原文地址:

  1. Get Started
  2. Define Expectations
  3. Return Values from Mocked Methods
  4. Throw Exceptions from Mocked Methods
  5. Match Parameter Values
  6. Precisely Specify Expected Parameter Values
  7. Expect Methods More (or Less) than Once
  8. Expect a Sequence of Invocations
  9. Expect an Invocation Between Two Other Invocations
  10. Ignore Irrelevant Mock Objects
  11. Override Expectations Defined in the Test Set-Up
  12. Match Objects and Methods
  13. Write New Matchers
  14. Write New Actions
  15. Easily Define Actions with Scripts
  16. Test Multithreaded Code with Mock Objects
  17. Mock Generic Types
  18. Mock Abstract and Concrete Classes
  19. Use jMock with Languages Other Than Java
  20. Upgrade from jMock 1 to jMock 2
  21. Use jMock in Maven Builds
  22. Understand method dispatch in jMock 2
  23. Mock Classes in Eclipse Plug-in Tests

入门

在这个简单的例子中我们将写一个模拟对象测试一个发布/订阅消息系统.一个发布者发送消息到零个或者多个接受者,我们想要测试发布者,它涉及测试和它的订阅者的交互.

订阅者接口想这样:

1 interface Subscriber {
2     void receive(String message);
3 }

我们将测试发布者发送一个消息到用一个单独的注册订阅者.为了测试发布者和订阅者之间的结合,我们将使用一个订阅者模拟对象.

建立classpath

为了使用jMock2.5.1,你必须添加下列JAR文件到你的classpath:

  • jmock-2.5.1.jar
  • hamcrest-core-1.1.jar
  • hamcrest-library-1.1.jar
  • 如果使用JUnit,添加相应的版本的JUnit的jar包

编写测试用例

首先我们必须导入jMock类,定义我们的测试固件类并创建一个”Mockery”,它在发布者存在的地方作为上下文.上下文模拟出发布者交互的对象(这里是订阅者),并检查它们在测试期间是否被正确使用.

JUnit3

1 import org.jmock.integration.junit3.MockObjectTestCase;
2 import org.jmock.Expectations;
3  
4 class PublisherTest extends MockObjectTestCase {
5     ...
6 }

JUnit4

 1 import org.jmock.Expectations;
 2 import org.jmock.Mockery;
 3 import org.jmock.integration.junit4.JMock;
 4 import org.jmock.integration.junit4.JUnit4Mockery;
 5  
 6 @RunWith(JMock.class)
 7 class PublisherTest {
 8     Mockery context = new JUnit4Mockery();
 9     ...
10 }

Other

1 import org.jmock.Mockery;
2 import org.jmock.Expectations;
3  
4 class PublisherTest extends TestCase {
5     Mockery context = new Mockery();
6     ...
7 }

现在我们应该编写将执行我们的测试的方法了:

JUnit3

1 public void testOneSubscriberReceivesAMessage() {
2     ...
3 }

JUnit4

1 @Test
2 public void oneSubscriberReceivesAMessage() {
3     ...
4 }

Other

1 public void testOneSubscriberReceivesAMessage() {
2     ...
3 }

现在我们开始编写我们的测试的方法体.

我们首先建立我们的测试要执行的上下文.我们创建一个发布者给测试.我们创建一个模拟的订阅者,它会接收消息.我们然后注册订阅者给发布者.最后我们创建一个消息对象来发布.

JUnit3

1 final Subscriber subscriber = mock(Subscriber.class);
2  
3 Publisher publisher = new Publisher();
4 publisher.add(subscriber);
5  
6 final String message = "message";

JUnit4

1 final Subscriber subscriber = context.mock(Subscriber.class);
2  
3 Publisher publisher = new Publisher();
4 publisher.add(subscriber);
5  
6 final String message = "message";

Other

1 final Subscriber subscriber = context.mock(Subscriber.class);
2  
3 Publisher publisher = new Publisher();
4 publisher.add(subscriber);
5  
6 final String message = "message";

下一步我们在模拟的订阅者上定义期望,它指定了在测试运行期间我们期望被调用的方法.我们期望接收方法被使用一个单独的参数调用一次,这个消息会被发送.

JUnit3

1 checking(new Expectations() {{
2     oneOf (subscriber).receive(message);
3 }});

JUnit4

1 context.checking(new Expectations() {{
2     oneOf (subscriber).receive(message);
3 }});

Other

1 context.checking(new Expectations() {{
2     oneOf (subscriber).receive(message);
3 }});

我们然后执行我们想测试的代码.

1 publisher.publish(message);

Other

1 context.assertIsSatisfied();

这是完整的测试

JUnit3

 1 import org.jmock.integration.junit3.MockObjectTestCase;
 2 import org.jmock.Expectations;
 3  
 4 class PublisherTest extends MockObjectTestCase {
 5     public void testOneSubscriberReceivesAMessage() {
 6         // set up
 7         final Subscriber subscriber = mock(Subscriber.class);
 8  
 9         Publisher publisher = new Publisher();
10         publisher.add(subscriber);
11  
12         final String message = "message";
13  
14         // expectations
15         checking(new Expectations() {{
16             oneOf (subscriber).receive(message);
17         }});
18  
19         // execute
20         publisher.publish(message);
21     }
22 }

JUnit4

 1 import org.jmock.integration.junit4.JMock;
 2 import org.jmock.integration.junit4.JUnit4Mockery;
 3 import org.jmock.Expectations;
 4  
 5 @RunWith(JMock.class)
 6 class PublisherTest {
 7     Mockery context = new JUnit4Mockery();
 8  
 9     @Test
10     public void oneSubscriberReceivesAMessage() {
11         // set up
12         final Subscriber subscriber = context.mock(Subscriber.class);
13  
14         Publisher publisher = new Publisher();
15         publisher.add(subscriber);
16  
17         final String message = "message";
18  
19         // expectations
20         context.checking(new Expectations() {{
21             oneOf (subscriber).receive(message);
22         }});
23  
24         // execute
25         publisher.publish(message);
26     }
27 }

Other

 1 import org.jmock.Mockery;
 2 import org.jmock.Expectations;
 3  
 4 class PublisherTest extends TestCase {
 5     Mockery context = new Mockery();
 6  
 7     public void testOneSubscriberReceivesAMessage() {
 8         // set up
 9         final Subscriber subscriber = context.mock(Subscriber.class);
10  
11         Publisher publisher = new Publisher();
12         publisher.add(subscriber);
13  
14         final String message = "message";
15  
16         // expectations
17         context.checking(new Expectations() {{
18             oneOf (subscriber).receive(message);
19         }});
20  
21         // execute
22         publisher.publish(message);
23  
24         // verify
25         context.assertIsSatisfied();
26     }
27 }

指定期望

期望定义在一个双括号块中,它在测试的Mockery上下文中定义期望(在下面例子中标识上下文).jMock2测试大概看起来像如下:

JUnit3

 1 public void testSomeAction() {
 2     ... set up ...
 3  
 4     checking(new Expectations() {{
 5         ... expectations go here ...
 6     }});
 7  
 8     ... code being tested ...
 9  
10     ... assertions ...
11 }

JUnit4

 1 public void testSomeAction() {
 2     ... set up ...
 3  
 4     context.checking(new Expectations() {{
 5         ... expectations go here ...
 6     }});
 7  
 8     ... code being tested ...
 9  
10     ... assertions ...
11 }

Other

 1 public void testSomeAction() {
 2     ... set up ...
 3  
 4     context.checking(new Expectations() {{
 5         ... expectations go here ...
 6     }});
 7  
 8     ... code being tested ...
 9  
10     context.assertIsSatisfied();
11  
12     ... other assertions ...
13 }

一个期望块可以包含任何数量个期望.每个期望都有如下结构:

1 invocation-count (mock-object).method(argument-constraints);
2     inSequence(sequence-name);
3     when(state-machine.is(state-name));
4     will(action);
5     then(state-machine.is(new-state-name));

除了调用次数和模拟对象,所有语句都是可选的.你可以给一个期望你希望多的inSequence2, when3, will 和 then语句.

下列测试定义了几个期望:

JUnit3

 1 public void testReturnsCachedObjectWithinTimeout() {
 2     checking(new Expectations() {{
 3         oneOf (clock).time(); will(returnValue(loadTime));
 4         oneOf (clock).time(); will(returnValue(fetchTime));
 5  
 6         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
 7  
 8         oneOf (loader).load(KEY); will(returnValue(VALUE));
 9     }});
10  
11     Object actualValueFromFirstLookup = cache.lookup(KEY);
12     Object actualValueFromSecondLookup = cache.lookup(KEY);
13  
14     assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
15     assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
16 }

JUnit4

 1 @Test public void
 2 returnsCachedObjectWithinTimeout() {
 3     context.checking(new Expectations() {{
 4         oneOf (clock).time(); will(returnValue(loadTime));
 5         oneOf (clock).time(); will(returnValue(fetchTime));
 6  
 7         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
 8  
 9         oneOf (loader).load(KEY); will(returnValue(VALUE));
10     }});
11  
12     Object actualValueFromFirstLookup = cache.lookup(KEY);
13     Object actualValueFromSecondLookup = cache.lookup(KEY);
14  
15     assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
16     assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
17 }

Other

 1 public void testReturnsCachedObjectWithinTimeout() {
 2     context.checking(new Expectations() {{
 3         oneOf (clock).time(); will(returnValue(loadTime));
 4         oneOf (clock).time(); will(returnValue(fetchTime));
 5  
 6         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
 7  
 8         oneOf (loader).load(KEY); will(returnValue(VALUE));
 9     }});
10  
11     Object actualValueFromFirstLookup = cache.lookup(KEY);
12     Object actualValueFromSecondLookup = cache.lookup(KEY);
13  
14     context.assertIsSatisfied();
15     assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
16     assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
17 }

因为期望可以定义在匿名内部类中,所以被存储在本地变量中并在期望块中引用的任何模拟对象(或者其他值)都必须是final.把模拟对象存储在实例 变量中并在测试中为那些值定义常量非常方便,上例就是这样.常量比它们不命名,用原值被使用有更多好处,可以使测试更容易理解.

一个测试可以包含多个期望块.后面的块中的期望被附加到早期的块中.这可以用来说明在测试中响应调用时期望调用什么时候会发生.举例来说,上述测试可以如下重写,来更加明确表达什么时候缓存会加载一个将被载入的对象,什么时候会返回一个缓存复本:

JUnit3

 1 public void testReturnsCachedObjectWithinTimeout() {
 2     checking(new Expectations() {{
 3         oneOf (clock).time(); will(returnValue(loadTime));
 4         oneOf (loader).load(KEY); will(returnValue(VALUE));
 5     }});
 6  
 7     Object actualValueFromFirstLookup = cache.lookup(KEY);
 8  
 9     checking(new Expectations() {{
10         oneOf (clock).time(); will(returnValue(fetchTime));
11         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
12     }});
13  
14     Object actualValueFromSecondLookup = cache.lookup(KEY);
15  
16     assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
17     assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
18 }

JUnit4

 1 @Test public void
 2 returnsCachedObjectWithinTimeout() {
 3     context.checking(new Expectations() {{
 4         oneOf (clock).time(); will(returnValue(loadTime));
 5         oneOf (loader).load(KEY); will(returnValue(VALUE));
 6     }});
 7  
 8     Object actualValueFromFirstLookup = cache.lookup(KEY);
 9  
10     context.checking(new Expectations() {{
11         oneOf (clock).time(); will(returnValue(fetchTime));
12         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
13     }});
14  
15     Object actualValueFromSecondLookup = cache.lookup(KEY);
16  
17     assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
18     assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
19 }

Other

 1 public void testReturnsCachedObjectWithinTimeout() {
 2     context.checking(new Expectations() {{
 3         oneOf (clock).time(); will(returnValue(loadTime));
 4         oneOf (loader).load(KEY); will(returnValue(VALUE));
 5     }});
 6  
 7     Object actualValueFromFirstLookup = cache.lookup(KEY);
 8  
 9     context.checking(new Expectations() {{
10         oneOf (clock).time(); will(returnValue(fetchTime));
11         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
12     }});
13  
14     Object actualValueFromSecondLookup = cache.lookup(KEY);
15  
16     context.assertIsSatisfied();
17     assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
18     assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
19 }

期望不必须被在测试方法体中定义.你可以在helper方法或者setUp方法中定义期望来消除重复或使测试代码更加清晰.

JUnit3

 1 public void testReturnsCachedObjectWithinTimeout() {
 2     initiallyLoads(VALUE);
 3  
 4     Object valueFromFirstLookup = cache.lookup(KEY);
 5  
 6     cacheHasNotExpired();
 7  
 8     Object valueFromSecondLookup = cache.lookup(KEY);
 9  
10     assertSame("should have returned cached object",
11                valueFromFirstLookup, valueFromSecondLookup);
12 }
13  
14 private void initiallyLoads(Object value) {
15     checking(new Expectations() {{
16         oneOf (clock).time(); will(returnValue(loadTime));
17         oneOf (loader).load(KEY); will(returnValue(value));
18     }});
19 }
20  
21 private void cacheHasNotExpired() {
22     checking(new Expectations() {{
23         oneOf (clock).time(); will(returnValue(fetchTime));
24         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
25     }});
26 }

JUnit4

 1 @Test public void
 2 returnsCachedObjectWithinTimeout() {
 3     initiallyLoads(VALUE);
 4  
 5     Object valueFromFirstLookup = cache.lookup(KEY);
 6  
 7     cacheHasNotExpired();
 8  
 9     Object valueFromSecondLookup = cache.lookup(KEY);
10  
11     assertSame("should have returned cached object",
12                valueFromFirstLookup, valueFromSecondLookup);
13 }
14  
15 private void initiallyLoads(Object value) {
16     context.checking(new Expectations() {{
17         oneOf (clock).time(); will(returnValue(loadTime));
18         oneOf (loader).load(KEY); will(returnValue(value));
19     }});
20 }
21  
22 private void cacheHasNotExpired() {
23     context.checking(new Expectations() {{
24         oneOf (clock).time(); will(returnValue(fetchTime));
25         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
26     }});
27 }

Other

 1 public void testReturnsCachedObjectWithinTimeout() {
 2     initiallyLoads(VALUE);
 3  
 4     Object valueFromFirstLookup = cache.lookup(KEY);
 5  
 6     cacheHasNotExpired();
 7  
 8     Object valueFromSecondLookup = cache.lookup(KEY);
 9  
10     context.assertIsSatisfied();
11     assertSame("should have returned cached object",
12                valueFromFirstLookup, valueFromSecondLookup);
13 }
14  
15 private void initiallyLoads(Object value) {
16     context.checking(new Expectations() {{
17         oneOf (clock).time(); will(returnValue(loadTime));
18         oneOf (loader).load(KEY); will(returnValue(value));
19     }});
20 }
21  
22 private void cacheHasNotExpired() {
23     context.checking(new Expectations() {{
24         oneOf (clock).time(); will(returnValue(fetchTime));
25         allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
26     }});
27 }


模拟方法的返回值

如果你没有明确指定,jMock会从任何没有void返回类型的方法返回一个适当的值.在大多数测试中你需要明确定义来自模拟调用的返回值.

简单的例子

你可以通过在一个期望的”will”子句内使用returnValue方法来从模拟方法返回值.

1 oneOf (calculator).add(2, 2); will(returnValue(5));

如果你尝试返回一个错误类型的值,jMock会测试失败.

通过集合返回迭代器

returnIterator方法通过一个集合返回一个迭代器.

1 final List<Employee> employees = new ArrayList<Employee>();
2 employees.add(alice);
3 employees.add(bob);
4  
5 context.checking(new Expectations() {{
6     oneOf (department).employees(); will(returnIterator(employees));
7 }});

returnIterator方法一个方便的重载让你可以指定内联元素:

1 context.checking(new Expectations() {{
2     oneOf (department).employees(); will(returnIterator(alice, bob));
3 }});

注意使用returnIterator和使用returnValue来返回一个迭代器之间的不同.使用returnValue每次会返回同一个 Iterator:一旦所有迭代器的元素被销毁,进一步调用会返回同一个耗尽的迭代器.returnIterator方法每次被调用都会返回一个新的迭代 器.

在连续调用中返回不同值

在不同的调用上返回不同的值有两种方法.第一是定义多个期望并从每个中返回一个不同值:

1 oneOf (anObject).doSomething(); will(returnValue(10));
2 oneOf (anObject).doSomething(); will(returnValue(20));
3 oneOf (anObject).doSomething(); will(returnValue(30));

第一次调用doSomething会返回10,第二次返回20,第三次返回30.

然而,重复定义期望调用,所以会增加维护负担.一个更好的方法是使用onConsecutiveCalls方法来在不同的调用中返回不同值(或者执行一个不同的行为

atLeast(1).of (anObject).doSomething();
   will(onConsecutiveCalls(
       returnValue(10),
       returnValue(20),
       returnValue(30)));

这个期望被期望至少调用一次,而不是三次,如果你想要,稍后你可以很容易添加更多效果.

从模拟方法抛出异常

使用throwException方法从一个模拟方法抛出一个异常.

1 allowing (bank).withdraw(with(any(Money.class)));
2     will(throwException(new WithdrawalLimitReachedException());

如果你尝试返回一个和调用方法不和谐的检查时异常jMock会发现它并以一个描述性错误消息测试失败. 它允许你从任何方法抛出任何RuntimeException或Error.

1 allowing (bank).withdraw(Money.ZERO);


匹配参数值

你可以指定当一个方法调用不同的参数时不同的行为被执行:

1 allowing (calculator).add(1,1); will(returnValue(3));
2 allowing (calculator).add(2,2); will(returnValue(5));
3 allowing (calculator).sqrt(-1); will(throwException(new IllegalArgumentException());

你可以指定同样的方法使用不同参数被调用不同次数:

1 one   (calculator).load("x"); will(returnValue(10));


匹配器

大多数时候期望指定原参数值和调用方法实际参数值做等于比较.例如:

1 allowing (calculator).add(2,2); will(returnValue(5));

然而有时,你需要定义更加宽松约束给参数值来表达测试目的,或者来忽略那些不相关测试行为的参数,例如:

1 allowing (calculator).sqrt(with(lessThan(0)); will(throwException(new IllegalArgumentException());
2 oneOf (log).append(with(equal(Log.ERROR)), with(stringContaining("sqrt"));

宽松的参数约束可以通过为每个参数指定匹配器来定义.通过在上面例子中的lessThan, equal 和 stringContaining工厂方法创建的匹配器 ,来确保期望易读.每个工厂方法结果必须通过一个with方法调用包装.

一个使用参数匹配器的期望必须使用with方法包装每个参数,不管它是匹配器函数还是一个常量值.

通常使用的匹配器工厂方法定义在Expectations类中.更多的匹配器被定义在org.hamcrest.Matchers类的静态方法中.如果你需要它们,静态导入那些匹配器到你的测试代码:

import static org.hamcrest.Matchers.*;

如果需要,匹配器可以联合起来使规范更严紧或者更宽松.匹配器集是可扩展的,所以你可以写新的匹配器来铺盖不常用的测试场景.

基本匹配器

对象相等

最常用的匹配器是equal,它指定被接受的参数必须等于一个给定的值.例如:下列代码指定doSomething方法必须被使用一个为1的参数来调用.

oneOf (mock).doSomething(with(equal(1)));

equalTo约束使用期望值的equals方法来比较期望和实际值相等.Null值会预先被检验,所以指定equal(null)或者应用匹配器 给一个null实际值是安全的.数组被视为特殊的例子:两个数组如果它们数量相同,并且所有它们的元素都被认为相等,那么它们被认为相等.

对象相同

same匹配器指定参数实际值必须和期望值是同一对象.它比equal更加严紧,但经常是描述你想要什么参数参考引用行为对象.下列代码指定doSomething方法会使用一个被期望相同对象的参考作为参数来调用.

Object expected = new Object();
 
oneOf (mock).doSomething(with(same(expected)));

作为经验来说,使用equal给值对象,使用same给行为对象.

子串

stringContaining匹配器指定期望参数必须是一个包含给定子串的字符串. 下列代码指定doSomething方法必须被使用一个包含”hello”文本的字符串参数来调用.

oneOf (mock).doSomething(with(stringContaining("hello")));

stringContaining匹配器对于测试字符串内容并孤立测试与具体的标点符号和格式是特别有用的.例如,上面代码接受下列任何参数值:”hello world”, “hello, world”, “hello!”等.

Null或非Null

aNull(Class)和aNonNull(Class)匹配器分别指定一个参数为null或者非null. 下列代码指定”doSomething”方法必须使用两个字符串调用,第一个参数为null,第二个必须为非null.

oneOf (mock).doSomething(with(aNull(String.class)), aNonNull(String.class)));

任何

any(Class)约束指定任何给定的类型的值都允许.这对于忽略和测试方案不相关的参数很有用.明智的使用any约束确保你的测试很灵活,当测 试代码改变时不需要经常维护.下列代码指定”doSomething”方法必须使用两个参数被调用,第一个等于1,第二个在测试中忽略.

oneOf (mock).doSomething(with(eq(1)), with(any(String.class)));

使用误差的数值相等

一个重载版本的equal约束指定浮点值使用一些误差作为舍入误差等于另一个值.下列代码指定”doSomething”方法会使用一个1加或减0.002的值参数被调用.

oneOf (mock).doSomething(with(equal(1, 0.002)));

组合匹配器

匹配器可以组合创建更加严紧或者更加宽松的规范. 组合匹配器本身也是匹配器,因此可以进一步联合.

Not — 逻辑否

not匹配器指定实际参数必须不匹配给定匹配器.下列代码指定”doSomething”方法必须使用一个不等于1的参数调用.

oneOf (mock).doSomething(with(not(eq(1)));

AllOf — 逻辑和

allOf匹配器指定实际参数必须满足给定的所有匹配器作为参数.下列代码指定”doSomething”方法必须使用一个包含”hello”文本和”world”文本的字符串被调用.

oneOf (mock).doSomething(with(allOf(stringContaining("hello"),
stringContaining("world"))));

AnyOf — 逻辑或

anyOf匹配器指定实际参数必须满足给定的匹配器中的至少一个作为参数.下列代码指定”doSomething”方法必须使用一个包含”hello”文本或者”howdy”文本的字符串被调用.

oneOf (mock).doSomething(with(anyOf(stringContains("hello"),
stringContains("howdy"))));
原文地址:https://www.cnblogs.com/johnson-blog/p/3890615.html