zoukankan      html  css  js  c++  java
  • Jasmine入门(下)

    上一篇 Jasmine入门(上) 介绍了Jasmine以及一些基本的用法,本篇我们继续研究Jasmine的其他一些特性及其用法(注:本篇中的例子均来自于官方文档)。

    Spy

    Spy用来追踪函数的调用历史信息(是否被调用、调用参数列表、被请求次数等)。Spy仅存在于定义它的describe和it方法块中,并且每次在spec执行完之后被销毁。

    示例1:

     1 (function(){
     2     describe("A spy", function() {
     3       var foo, bar = null;
     4 
     5       beforeEach(function() {
     6         foo = {
     7           setBar: function(value) {
     8             bar = value;
     9           }
    10         };
    11 
    12         spyOn(foo, 'setBar'); // 在foo对象上添加spy
    13 
    14         // 此时调用foo对象上的方法,均为模拟调用,因此不会执行实际的代码
    15         foo.setBar(123); // 调用foo的setBar方法
    16         foo.setBar(456, 'another param');
    17       });
    18 
    19       it("tracks that the spy was called", function() {
    20         expect(foo.setBar).toHaveBeenCalled(); //判断foo的setBar是否被调用
    21       });
    22 
    23       it("tracks all the arguments of its calls", function() {
    24         expect(foo.setBar).toHaveBeenCalledWith(123); //判断被调用时的参数
    25         expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
    26       });
    27 
    28       it("stops all execution on a function", function() {
    29         expect(bar).toBeNull();  // 由于是模拟调用,因此bar值并没有改变
    30       });
    31     });
    32 })();

    从示例1中看到,当在一个对象上使用spyOn方法后即可模拟调用对象上的函数,此时对所有函数的调用是不会执行实际代码的。示例1中包含了两个Spy常用的expect:

    toHaveBeenCalled: 函数是否被调用

    toHaveBeenCalledWith: 调用函数时的参数

    and.callThrough()

    那如果说我们想在使用Spy的同时也希望执行实际的代码呢?

    示例2:

     1 (function(){
     2     describe("A spy, when configured to call through", function() {
     3       var foo, bar, fetchedBar;
     4 
     5       beforeEach(function() {
     6         foo = {
     7           setBar: function(value) {
     8             bar = value;
     9           },
    10           getBar: function() {
    11             return bar;
    12           }
    13         };
    14 
    15         spyOn(foo, 'getBar').and.callThrough(); // 与示例1中不同之处在于使用了callThrough,这将时所有的函数调用为真实的执行
    16         //spyOn(foo, 'getBar'); // 可以使用示例1中的模拟方式,看看测试集执行的结果
    17 
    18         foo.setBar(123);
    19         fetchedBar = foo.getBar();
    20       });
    21 
    22       it("tracks that the spy was called", function() {
    23         expect(foo.getBar).toHaveBeenCalled();
    24       });
    25 
    26       it("should not effect other functions", function() {
    27         expect(bar).toEqual(123); // 由于是真实调用,因此bar有了真实的值
    28       });
    29 
    30       it("when called returns the requested value", function() {
    31         expect(fetchedBar).toEqual(123); // 由于是真实调用,fetchedBar也有了真实的值
    32       });
    33     });
    34 })();

     通过在使用spyOn后面增加了链式调用and.CallThrough(),这将告诉Jasmine我们除了要完成对函数调用的跟踪,同时也需要执行实际的代码。

    and.returnValue()

    由于Spy是模拟函数的调用,因此我们也可以强制指定函数的返回值。

    示例3:

     1 (function(){
     2     describe("A spy, when configured to fake a return value", function() {
     3       var foo, bar, fetchedBar;
     4 
     5       beforeEach(function() {
     6         foo = {
     7           setBar: function(value) {
     8             bar = value;
     9           },
    10           getBar: function() {
    11             return bar;
    12           }
    13         };
    14 
    15         spyOn(foo, "getBar").and.returnValue(745); // 这将指定getBar方法返回值为745
    16 
    17         foo.setBar(123);
    18         fetchedBar = foo.getBar();
    19       });
    20 
    21       it("tracks that the spy was called", function() {
    22         expect(foo.getBar).toHaveBeenCalled();
    23       });
    24 
    25       it("should not effect other functions", function() {
    26         expect(bar).toEqual(123);
    27       });
    28 
    29       it("when called returns the requested value", function() {
    30         expect(fetchedBar).toEqual(745);
    31       });
    32     });
    33 })();

    如果被调用的函数是通过从其他函数获取某些值,我们通过使用returnValue模拟函数的返回值。这样做的好处是可以有效的隔离依赖,使测试流程变得更简单。

    and.callFake()

    与returnValue相似,callFake则更进一步,直接通过指定一个假的自定义函数来执行。这种方式比returnValue更灵活,我们可以任意捏造一个函数来达到我们的测试要求。
    示例4:

     1 (function(){
     2     describe("A spy, when configured with an alternate implementation", function() {
     3       var foo, bar, fetchedBar;
     4 
     5       beforeEach(function() {
     6         foo = {
     7           setBar: function(value) {
     8             bar = value;
     9           },
    10           getBar: function() {
    11             return bar;
    12           }
    13         };
    14 
    15         spyOn(foo, "getBar").and.callFake(function() {
    16           return 1001;
    17         });
    18 
    19         foo.setBar(123);
    20         fetchedBar = foo.getBar();
    21       });
    22 
    23       it("tracks that the spy was called", function() {
    24         expect(foo.getBar).toHaveBeenCalled();
    25       });
    26 
    27       it("should not effect other functions", function() {
    28         expect(bar).toEqual(123);
    29       });
    30 
    31       it("when called returns the requested value", function() {
    32         expect(fetchedBar).toEqual(1001);
    33       });
    34     });
    35 })();

    and.throwError()

    throwError便于我们模拟异常的抛出。

     1 (function(){
     2     describe("A spy, when configured to throw an error", function() {
     3       var foo, bar;
     4 
     5       beforeEach(function() {
     6         foo = {
     7           setBar: function(value) {
     8             bar = value;
     9           }
    10         };
    11 
    12         spyOn(foo, "setBar").and.throwError("quux");
    13       });
    14 
    15       it("throws the value", function() {
    16         expect(function() {
    17           foo.setBar(123)
    18         }).toThrowError("quux");
    19       });
    20     });
    21 })();

    and.stub

    示例5:

     1 (function(){
     2     describe("A spy", function() {
     3       var foo, bar = null;
     4 
     5       beforeEach(function() {
     6         foo = {
     7           setBar: function(value) {
     8             bar = value;
     9           },
    10           getBar: function(){
    11             return bar;
    12           }
    13         };
    14 
    15         spyOn(foo, 'setBar').and.callThrough(); // 标记1
    16         spyOn(foo, 'getBar').and.returnValue(999); // 标记2
    17       });
    18 
    19       it("can call through and then stub in the same spec", function() {
    20         foo.setBar(123);
    21         expect(bar).toEqual(123);
    22 
    23         var getValue = foo.getBar();
    24         expect(getValue).toEqual(999);
    25         
    26         foo.setBar.and.stub(); // 相当于'标记1'中的代码变为了spyOn(foo, 'setBar')
    27         foo.getBar.and.stub(); // 相当于'标记2'中的代码变为了spyOn(foo, 'getBar')
    28         bar = null;
    29 
    30         foo.setBar(123);
    31         expect(bar).toBe(null);
    32         expect(foo.setBar).toHaveBeenCalled(); // 函数调用追踪并没有被重置
    33         
    34         getValue = foo.getBar();
    35         expect(getValue).toEqual(undefined);
    36         expect(foo.getBar).toHaveBeenCalled(); // 函数调用追踪并没有被重置
    37       });
    38     });
    39 })();

    其他追踪属性: 

    calls:对于被Spy的函数的调用,都可以在calls属性中跟踪。

    • .calls.any(): 被Spy的函数一旦被调用过,则返回true,否则为false;
    • .calls.count(): 返回被Spy的函数的被调用次数;
    • .calls.argsFor(index): 返回被Spy的函数的调用参数,以index来指定参数;
    • .calls.allArgs():返回被Spy的函数的所有调用参数;
    • .calls.all(): 返回calls的上下文,这将返回当前calls的整个实例数据;
    • .calls.mostRecent(): 返回calls中追踪的最近一次的请求数据;
    • .calls.first(): 返回calls中追踪的第一次请求的数据;
    • .object: 当调用all(),mostRecent(),first()方法时,返回对象的object属性返回的是当前上下文对象;
    • .calls.reset(): 重置Spy的所有追踪数据;

    示例6:

     1 (function(){
     2     describe("A spy", function() {
     3       var foo, bar = null;
     4 
     5       beforeEach(function() {
     6         foo = {
     7           setBar: function(value) {
     8             bar = value;
     9           }
    10         };
    11 
    12         spyOn(foo, 'setBar');
    13       });
    14       
    15       it("tracks if it was called at all", function() {
    16         expect(foo.setBar.calls.any()).toEqual(false);
    17 
    18         foo.setBar();
    19 
    20         expect(foo.setBar.calls.any()).toEqual(true);
    21       });
    22       
    23       it("tracks the number of times it was called", function() {
    24         expect(foo.setBar.calls.count()).toEqual(0);
    25 
    26         foo.setBar();
    27         foo.setBar();
    28 
    29         expect(foo.setBar.calls.count()).toEqual(2);
    30       });
    31       
    32       it("tracks the arguments of each call", function() {
    33         foo.setBar(123);
    34         foo.setBar(456, "baz");
    35 
    36         expect(foo.setBar.calls.argsFor(0)).toEqual([123]);
    37         expect(foo.setBar.calls.argsFor(1)).toEqual([456, "baz"]);
    38       });
    39       
    40       it("tracks the arguments of all calls", function() {
    41         foo.setBar(123);
    42         foo.setBar(456, "baz");
    43 
    44         expect(foo.setBar.calls.allArgs()).toEqual([[123],[456, "baz"]]);
    45       });
    46       
    47       it("can provide the context and arguments to all calls", function() {
    48         foo.setBar(123);
    49 
    50         expect(foo.setBar.calls.all()).toEqual([{object: foo, args: [123], returnValue: undefined}]);
    51       });
    52       
    53       it("has a shortcut to the most recent call", function() {
    54         foo.setBar(123);
    55         foo.setBar(456, "baz");
    56 
    57         expect(foo.setBar.calls.mostRecent()).toEqual({object: foo, args: [456, "baz"], returnValue: undefined});
    58       });
    59       
    60       it("has a shortcut to the first call", function() {
    61         foo.setBar(123);
    62         foo.setBar(456, "baz");
    63 
    64         expect(foo.setBar.calls.first()).toEqual({object: foo, args: [123], returnValue: undefined});
    65       });
    66       
    67       it("tracks the context", function() {
    68         var spy = jasmine.createSpy('spy');
    69         var baz = {
    70           fn: spy
    71         };
    72         var quux = {
    73           fn: spy
    74         };
    75         baz.fn(123);
    76         quux.fn(456);
    77 
    78         expect(spy.calls.first().object).toBe(baz);
    79         expect(spy.calls.mostRecent().object).toBe(quux);
    80       });
    81       
    82       it("can be reset", function() {
    83         foo.setBar(123);
    84         foo.setBar(456, "baz");
    85 
    86         expect(foo.setBar.calls.any()).toBe(true);
    87 
    88         foo.setBar.calls.reset();
    89 
    90         expect(foo.setBar.calls.any()).toBe(false);
    91       });
    92     });
    93 })();

    createSpy

    假如没有函数可以追踪,我们可以自己创建一个空的Spy。创建后的Spy功能与其他的Spy一样:跟踪调用、参数等,但该Spy没有实际的代码实现,这种方式经常会用在对JavaScript中的对象的测试。

    示例7:

     1 (function(){
     2     describe("A spy, when created manually", function() {
     3       var whatAmI;
     4 
     5       beforeEach(function() {
     6         whatAmI = jasmine.createSpy('whatAmI');
     7 
     8         whatAmI("I", "am", "a", "spy");
     9       });
    10 
    11       it("is named, which helps in error reporting", function() {
    12         expect(whatAmI.and.identity()).toEqual('whatAmI');
    13       });
    14 
    15       it("tracks that the spy was called", function() {
    16         expect(whatAmI).toHaveBeenCalled();
    17       });
    18 
    19       it("tracks its number of calls", function() {
    20         expect(whatAmI.calls.count()).toEqual(1);
    21       });
    22 
    23       it("tracks all the arguments of its calls", function() {
    24         expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy");
    25       });
    26 
    27       it("allows access to the most recent call", function() {
    28         expect(whatAmI.calls.mostRecent().args[0]).toEqual("I");
    29       });
    30     });
    31 })(); 

    createSpyObj

    如果需要spy模拟多个函数调用,可以向jasmine.createSpyObj中传入一个字符串数组,它将返回一个对象,你所传入的所有字符串都将对应一个属性,每个属性即为一个Spy。

    示例8:

     1 (function(){
     2     describe("Multiple spies, when created manually", function() {
     3       var tape;
     4 
     5       beforeEach(function() {
     6         tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
     7 
     8         tape.play();
     9         tape.pause();
    10         tape.rewind(0);
    11       });
    12 
    13       it("creates spies for each requested function", function() {
    14         expect(tape.play).toBeDefined();
    15         expect(tape.pause).toBeDefined();
    16         expect(tape.stop).toBeDefined();
    17         expect(tape.rewind).toBeDefined();
    18       });
    19 
    20       it("tracks that the spies were called", function() {
    21         expect(tape.play).toHaveBeenCalled();
    22         expect(tape.pause).toHaveBeenCalled();
    23         expect(tape.rewind).toHaveBeenCalled();
    24         expect(tape.stop).not.toHaveBeenCalled();
    25       });
    26 
    27       it("tracks all the arguments of its calls", function() {
    28         expect(tape.rewind).toHaveBeenCalledWith(0);
    29       });
    30     });
    31 })();

    关于createSpy和createSpyObj,读到这里大家可能很难理解其真正的应用场景。不过没关系,后续的例子中也包含了其用法,大家应该能慢慢理解如何运用它们。

    其他匹配方式

    jasmine.any

    jasmine.any方法以构造器或者类名作为参数,Jasmine将判断期望值和真实值的构造器是否相同,若相同则返回true。

    示例9:

     1 (function(){
     2     describe("jasmine.any", function() {
     3       it("matches any value", function() {
     4         expect({}).toEqual(jasmine.any(Object));
     5         expect(12).toEqual(jasmine.any(Number));
     6       });
     7 
     8       describe("when used with a spy", function() {
     9         it("is useful for comparing arguments", function() {
    10           var foo = jasmine.createSpy('foo');
    11           foo(12, function() {
    12             return true;
    13           });
    14 
    15           expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));
    16         });
    17       });
    18     });
    19 })();

    jasmine.anything

    jamine.anything判断只要不是null或undefined的值,若不是则返回true。

    示例10:

     1 (function(){
     2     describe("jasmine.anything", function() {
     3       it("matches anything", function() {
     4         expect(1).toEqual(jasmine.anything());
     5       });
     6 
     7       describe("when used with a spy", function() {
     8         it("is useful when the argument can be ignored", function() {
     9           var foo = jasmine.createSpy('foo');
    10           foo(12, function() {
    11             return false;
    12           });
    13 
    14           expect(foo).toHaveBeenCalledWith(12, jasmine.anything());
    15         });
    16       });
    17     });
    18 })();

    jasmine.objectContaining

    jasmine.objectContaining用来判断对象中是否存在指定的键值属性对。

    示例11:

     1 describe("jasmine.objectContaining", function() {
     2   var foo;
     3 
     4   beforeEach(function() {
     5     foo = {
     6       a: 1,
     7       b: 2,
     8       bar: "baz"
     9     };
    10   });
    11 
    12   it("matches objects with the expect key/value pairs", function() {
    13     expect(foo).toEqual(jasmine.objectContaining({
    14       bar: "baz"
    15     }));
    16     expect(foo).not.toEqual(jasmine.objectContaining({
    17       c: 37
    18     }));
    19   });
    20 
    21   describe("when used with a spy", function() {
    22     it("is useful for comparing arguments", function() {
    23       var callback = jasmine.createSpy('callback');
    24 
    25       callback({
    26         bar: "baz"
    27       });
    28 
    29       expect(callback).toHaveBeenCalledWith(jasmine.objectContaining({
    30         bar: "baz"
    31       }));
    32       expect(callback).not.toHaveBeenCalledWith(jasmine.objectContaining({
    33         c: 37
    34       }));
    35     });
    36   });
    37 });

    jasmine.arrayContaining

    jasmine.arrayContaining可以用来判断数组中是否有期望的值。

    示例12:

     1 (function(){
     2     describe("jasmine.arrayContaining", function() {
     3       var foo;
     4 
     5       beforeEach(function() {
     6         foo = [1, 2, 3, 4];
     7       });
     8 
     9       it("matches arrays with some of the values", function() {
    10         expect(foo).toEqual(jasmine.arrayContaining([3, 1]));  // 直接在期望值中使用jasmine.arrayContaining达到目的
    11         expect(foo).not.toEqual(jasmine.arrayContaining([6]));
    12       });
    13 
    14       describe("when used with a spy", function() {
    15         it("is useful when comparing arguments", function() {
    16           var callback = jasmine.createSpy('callback'); // 创建一个空的Spy
    17 
    18           callback([1, 2, 3, 4]); // 将数组内容作为参数传入Spy中
    19 
    20           expect(callback).toHaveBeenCalledWith(jasmine.arrayContaining([4, 2, 3]));
    21           expect(callback).not.toHaveBeenCalledWith(jasmine.arrayContaining([5, 2]));
    22         });
    23       });
    24     });
    25 })();

    jasmine.stringMatching

    jasmine.stringMatching用来模糊匹配字符串,在jasmine.stringMatching中也可以使用正则表达式进行匹配,使用起来非常灵活。

    示例13:

     1 describe('jasmine.stringMatching', function() {
     2   it("matches as a regexp", function() {
     3     expect({foo: 'bar'}).toEqual({foo: jasmine.stringMatching(/^bar$/)});
     4     expect({foo: 'foobarbaz'}).toEqual({foo: jasmine.stringMatching('bar')});
     5   });
     6 
     7   describe("when used with a spy", function() {
     8     it("is useful for comparing arguments", function() {
     9       var callback = jasmine.createSpy('callback');
    10 
    11       callback('foobarbaz');
    12 
    13       expect(callback).toHaveBeenCalledWith(jasmine.stringMatching('bar'));
    14       expect(callback).not.toHaveBeenCalledWith(jasmine.stringMatching(/^bar$/));
    15     });
    16   });
    17 });

    不规则匹配(自定义匹配):asymmetricMatch

    某些场景下,我们希望能按照自己设计的规则进行匹配,此时我们可以自定义一个对象,该对象只要包含一个名为asymmetricMatch的方法即可。

    示例14:

     1 describe("custom asymmetry", function() {
     2   var tester = {
     3     asymmetricMatch: function(actual) {
     4       var secondValue = actual.split(',')[1];
     5       return secondValue === 'bar';
     6     }
     7   };
     8 
     9   it("dives in deep", function() {
    10     expect("foo,bar,baz,quux").toEqual(tester);
    11   });
    12 
    13   describe("when used with a spy", function() {
    14     it("is useful for comparing arguments", function() {
    15       var callback = jasmine.createSpy('callback');
    16 
    17       callback('foo,bar,baz');
    18 
    19       expect(callback).toHaveBeenCalledWith(tester);
    20     });
    21   });
    22 });

    注:示例中的asymmetricMatch方法使我们判断字符串以','分割之后,index为1的内容为'bar'。

    Jasmine Clock

    Jasmine中可以使用jasmine.clock()方法来模拟操纵时间。

    要想使用jasmine.clock(),先调用jasmine.clock().install告诉Jasmine你想要在spec或者suite操作时间,当你不需要使用时,务必调用jasmine.clock().uninstall来恢复时间状态。

    示例15:

     1 (function(){
     2     describe("Manually ticking the Jasmine Clock", function() {
     3       var timerCallback;
     4       
     5       beforeEach(function() {
     6         timerCallback = jasmine.createSpy("timerCallback");
     7         jasmine.clock().install();
     8       });
     9       
    10       afterEach(function() {
    11         jasmine.clock().uninstall();
    12       });
    13       
    14       it("causes a timeout to be called synchronously", function() {
    15         setTimeout(function() {
    16           timerCallback();
    17         }, 100);
    18 
    19         expect(timerCallback).not.toHaveBeenCalled();
    20 
    21         jasmine.clock().tick(101);
    22 
    23         expect(timerCallback).toHaveBeenCalled();
    24       });
    25 
    26       it("causes an interval to be called synchronously", function() {
    27         setInterval(function() {
    28           timerCallback();
    29         }, 100);
    30 
    31         expect(timerCallback).not.toHaveBeenCalled();
    32 
    33         jasmine.clock().tick(101);
    34         expect(timerCallback.calls.count()).toEqual(1);
    35 
    36         jasmine.clock().tick(50);
    37         expect(timerCallback.calls.count()).toEqual(1);
    38 
    39         jasmine.clock().tick(50);
    40         expect(timerCallback.calls.count()).toEqual(2);
    41       });
    42       
    43       describe("Mocking the Date object", function(){
    44         it("mocks the Date object and sets it to a given time", function() {
    45           var baseTime = new Date(2013, 9, 23);
    46           
    47           jasmine.clock().mockDate(baseTime);
    48 
    49           jasmine.clock().tick(50);
    50           expect(new Date().getTime()).toEqual(baseTime.getTime() + 50);
    51         });
    52       });
    53     });    
    54 })();

    示例中使用jasmine.clock().tick(milliseconds)来控制时间前进,本例中出现了三种时间控制方式:

    • setTimeout: 定期执行一次,当jasmine.clock().tick()的时间超过了timeout设置的时间时触发
    • setInterval: 定期循环执行,每当jasmine.clock().tick()的时间超过了timeout设置的时间时触发
    • mockDate: 模拟一个指定日期(当不提供基准时间参数时,以当前时间为基准时间)

    异步支持

    Jasmine可以支持spec中执行异步操作,当调用beforeEach, it和afterEach时,函数可以包含一个可选参数done,当spec执行完毕之后,调用done通知Jasmine异步操作已执行完毕。

    示例16:

     1 (function(){
     2     describe("Asynchronous specs", function() {
     3       var value;
     4       
     5       beforeEach(function(done) {
     6         setTimeout(function() {
     7           value = 0;
     8           done();
     9         }, 1);
    10       });
    11       
    12       // 在上面beforeEach的done()被执行之前,这个测试用例不会被执行
    13       it("should support async execution of test preparation and expectations", function(done) {
    14         value++; 
    15         expect(value).toBeGreaterThan(0);
    16         done(); // 执行完done()之后,该测试用例真正执行完成
    17       });
    18       
    19       // Jasmine异步执行超时时间默认为5秒,超过后将报错
    20       describe("long asynchronous specs", function() {
    21           
    22         // 如果要调整指定用例的默认的超时时间,可以在beforeEach,it和afterEach中传入一个时间参数
    23         beforeEach(function(done) {
    24             // setTimeout(function(){}, 2000); // 可以试试如果该方法执行超过1秒时js会报错
    25           done();
    26         }, 1000);
    27 
    28         it("takes a long time", function(done) {
    29           setTimeout(function() {
    30             done();
    31           }, 9000);
    32         }, 10000);
    33 
    34         afterEach(function(done) {
    35           done();
    36         }, 1000);
    37       });
    38     });
    39 })();

    关于用法在代码中已经加了注释,另外补充一点,如果需要设置全局的默认超时时间,可以设置jasmine.DEFAULT_TIMEOUT_INTERVAL的值。

    当异步执行时间超过设置的执行超时时间js将会报错:

    至此为止,我们已经了解了Jasmine的所有用法,Jasmine的基本语法并不复杂,但是想要运用熟练还是需要在实际项目中慢慢实践。

    参考资料

    Jasmine官方:http://jasmine.github.io/2.3/introduction.html

    KeenWon:http://keenwon.com/1218.html

  • 相关阅读:
    React Native配置和使用
    使用ES6语法重构React代码
    git 起点
    Win32API程序中自建按钮
    C语言中数组与指针
    我的第一个博客
    Solr6.5配置中文分词IKAnalyzer和拼音分词pinyinAnalyzer (二)
    Solr6.5在Centos6上的安装与配置 (一)
    PHP版微信公共平台消息主动推送,突破订阅号一天只能发送一条信息限制
    MariaDB+Keepalived双主高可用配置MySQL-HA
  • 原文地址:https://www.cnblogs.com/wushangjue/p/4575826.html
Copyright © 2011-2022 走看看