Quick Start Guide to Kiwi iOS Automation Testing Framework

testkuaibao|Software Testing Self-Learning WeChat Public Account

1, 2, 4 Every Day at 9:00, Don’t Miss It

Introduction

  Kiwi is a behavior-driven testing framework suitable for iOS development, designed to provide a simple and easy-to-use BDD (Behavior Driven Development) library.

Installation

  Install using Cocoapods, add the following configuration in the test Target:

pod 'Kiwi', '3.0.0'

Basic Usage

  Let’s look at a complete code example:

#import <Kiwi/Kiwi.h>  SPEC_BEGIN(KiwiTest)    describe(@"descibeString", ^{        context(@"contextString", ^{            beforeAll(^ {                NSLog(@"beforeAll");            });            afterAll(^ {                NSLog(@"afterAll");            });            beforeEach(^ {                NSLog(@"beforeEach");            });            afterEach(^ {                NSLog(@"afterEach");            });            it(@"test1", ^{                [[theValue(1) should] equal:@(1)];                NSLog(@"test1");            });            it(@"test2", ^{                [[theValue(2) should] equal:@(2)];                NSLog(@"test2");            });        });    });  SPEC_END

  Tests written using Kiwi will not appear in the Xcode test case list until the test cases are run (regardless of success or failure), which is less intuitive than XCTest.

  After the test cases run successfully, two test cases will be generated, as shown in the figure below:

Quick Start Guide to Kiwi iOS Automation Testing Framework

  The log output is shown in the figure below:

Quick Start Guide to Kiwi iOS Automation Testing Framework

  From the log output, the equivalence relationship between APIs can be basically observed:

Quick Start Guide to Kiwi iOS Automation Testing Framework

  Kiwi XCTest beforeAll() setUp() afterAll() tearDown() it() testXXX() [[theValue(xxx) should] equal:yyy] XCTAssertEqual(xx, yyy, @””)

  At the same time, Kiwi also provides two new methods, beforeEach and afterEach, which are triggered before and after each test case execution, making it convenient to do some initialization and cleanup before and after each test case execution.

  Introduction to Common Kiwi APIs

  Kiwi provides rich logical judgment APIs, usage as follows:

  1. Boolean Judgment

  [[theValue(boolVar) should] beYes]

  [[theValue(boolVar) shouldNot] beYes]

  [[theValue(boolVar) should] beNo]

  [[theValue(boolVar) shouldNot] beNo]

  Equivalent to XCTAssertTrue(boolVar) and XCTAssertFalse(boolVar).

  2. Value Judgment

  [[theValue(1) should] equal:@(1)]

  [[@”string1″ shouldNot] equal:@”string”]

  Equivalent to XCTAssertEqual and XCTAssertNotEqual.

  3. Nil Judgment

  [[oc_object should] beNil];

  [[oc_object shouldNot] beNil];

  Equivalent to XCTAssertNil and XCTAssertNotNil.

  The above APIs have the same usage as XCTest, but differ in syntax; XCTest is more like C language API forms, while Kiwi is more like OC API forms, making the semantics more straightforward.

  4. Delay Judgment

  The expectFutureValue API is very useful, usually used in asynchronous scenarios, such as network requests, timers, etc. Tests written after the delay judgment become synchronous judgments.

  // After 30 seconds, the result value should be YES

  [[expectFutureValue(@(result)) shouldAfterWaitOf(30.0)] equal:@(YES)];

  For example:

  __block BOOL result = NO;

  dispatch_async(dispatch_get_global_queue(0, 0), ^{

   // Do some time-consuming operations

   result = YES;

  });

  // After 10 seconds, check if the result value is YES

  [[expectFutureValue(@(result)) shouldAfterWaitOf(10.0)] equal:@(YES)];

  // After the previous judgment ends, check the following test case

  [[@”string1″ shouldNot] equal:@”string”]

  Kiwi Advanced Usage

  1. Stub

  Used to replace the execution process or return result of a specified method, Kiwi provides rich stub APIs as follows:

  API Introduction

- (void)stub:(SEL)aSelector;- (void)stub:(SEL)aSelector withBlock:(id (^)(NSArray *params))block;- (void)stub:(SEL)aSelector withArguments:(id)firstArgument, ...;  - (void)stub:(SEL)aSelector andReturn:(id)aValue;  - (void)stub:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, ...;  - (void)stub:(SEL)aSelector andReturn:(id)aValue times:(NSNumber *)times afterThatReturn:(id)aSecondValue;  + (void)stub:(SEL)aSelector;  + (void)stub:(SEL)aSelector withBlock:(id (^)(NSArray *params))block;  + (void)stub:(SEL)aSelector withArguments:(id)firstArgument, ...;  + (void)stub:(SEL)aSelector andReturn:(id)aValue;  + (void)stub:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, ...;  + (void)stub:(SEL)aSelector andReturn:(id)aValue times:(NSNumber *)times afterThatReturn:(id)aSecondValue;

  Carefully observing, it is not difficult to find that the above APIs are divided into two categories: instance methods and class methods. Here are a few commonly used ones:

  - (void)stub:(SEL)aSelector withBlock:(id (^)(NSArray *params))block;

  When calling aSelector, the original logic will not be executed, but the block you passed will be executed.

  - (void)stub:(SEL)aSelector andReturn:(id)aValue

  When calling aSelector, it will not return the original value, but will return the value you passed.

  - (void)stub:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, …

  When calling aSelector and the parameter is the one you specified, it will not return the original value, but will return the value you passed.

  Usage Scenario

  For example, if you want to test a complex method A, which calls some methods (B, C, D) from other modules, and method A will handle different logic based on the different return values of methods B, C, and D, how can you write test cases to verify that method A meets expectations in different logical branch processing results?

  This scenario is difficult to handle in XCTest because you cannot handle the internal details of method A, which is like a black box to the person writing the test case.

  However, in Kiwi, as long as you call the corresponding stub method for B, C, and D before calling method A, method A’s internal B, C, and D methods can execute the specified block and return the specified values according to the previous stubbing agreements.

  Example as follows:

@implementation Runner    - (BOOL)start {        // The return value is determined by the current device environment and login account        BOOL enabled = [ConfigABTest enabled];        if (enable) {            return YES;        }        return NO;    }  @end

  We need to test whether [Runner.instance start] returns the correct value in different scenarios,

  while this method internally needs to call [ConfigABTest enabled] method. For example, in my current test device or test account,

  [ConfigABTest enabled] can only return YES, and the test case in XCTest cannot verify whether [Runner.instance start] returns the correct value when [ConfigABTest enabled] returns NO without changing the test device or account.

  In Kiwi, this is very easy to do; just write the following test code.

// Test YES scenario    [ConfigABTest stub:@selector(enabled) andReturn:theValue(YES)];    BOOL result = [Runner.instance start];    [[theValue(result) should] beYes];    // Test NO scenario    [ConfigABTest stub:@selector(enabled) andReturn:theValue(NO)];    BOOL result = [Runner.instance start];    [[theValue(result) should] beNo];

  2. Mock

  This is easier to understand. In network request scenarios, it is often necessary to mock data to verify whether the client can correctly handle various data returned by the server. In Kiwi, mocking an object is generating an object that behaves almost identically to the source object.

  API Introduction

// NSObject + KiwiMockAdditions.h    /// Class for mocking    + (id)mock;    // KWMock.h    /// Mock a specified class and generate an object    + (id)mockForClass:(Class)aClass;    /// Mock an object that conforms to a specified protocol    + (id)mockForProtocol:(Protocol *)aProtocol;     /// Generate a mock object of the specified type based on the specified object    + (id)partialMockForObject:(id)object;

  Usage Scenario

  For example, if you need to write test cases for the [MyDownloader downloadWithURL:handler:] method.

@protocol MyDownloadDelegate <NSObject>     @required     /// Notify download result     - (void)downloadComplete:(NSString * _Nullable)dstPath error:(NSError * _Nullable)error;    @end    @implementation MyDownloader    - (void)downloadWithURL:(NSString *)url handler:(id<MyDownloadDelegate>)delegate {        // Process download using the underlying network library        [XXXNetwork downloadWithUrl:url completeHandler:^(NSString *path, NSError *error) {            // Transform the path            // Transform the error            [delegate downloadComplete:path error:error];        }];    }  @end

  Since [MyDownloader downloadWithURL:handler:] requires a protocol parameter, I cannot define a class to implement the specified protocol just for this parameter, so this is where mocking comes into play.

it(@"Test Download", ^{        AnyDeclaredClass *mocked = [KWMock mockForProtocol:@protocol(MyDownloadDelegate)];        [mocked stub:@selector(downloadComplete:error:) withBlock:^id(NSArray *params) {            resultImage = [params[0] isEqual:[NSNull null]] ? nil : params[0];            resultError = [params[1] isEqual:[NSNull null]] ? nil : params[1];            return nil;        }];        MyDownloader *downloader = [[MyDownloader alloc] init];        [downloader downloadWithURL:@"" handler:mockDelegate];    });

  When executing the line of code [delegate downloadComplete:path error:error]; in [MyDownloader downloadWithURL:handler:], it will execute the block specified in [mocked stub:withBlock:], avoiding the need to create a new class.

Personal Experience

  I personally think the biggest benefit of writing test cases is that problems can be discovered earlier. The later a problem is discovered, the more it costs to correct it. But why can’t we stick to it? The main reason is that as the scale of the App continues to grow, the scenarios where the XCTest testing framework can be applied are getting fewer, and its effectiveness is very limited. For example, business scenarios where data is obtained through network requests; XCTest cannot customize response data to verify the robustness of our code; also, our module code calls other module code, and we cannot verify whether our module code can correctly handle different return values from other module codes, etc.

  Now, Kiwi provides powerful Stub and Mock capabilities, and the difficulties mentioned above no longer exist. Since using Kiwi to develop test cases, as long as the test cases can run through each iteration, there are basically no issues when going live. Even if problems are discovered after going live, as long as cases are supplemented in time, it can prevent the same problems from occurring again.

Source: Images and text from the internet, please contact us for deletion if there is infringement.

Quick Start Guide to Kiwi iOS Automation Testing Framework
Web Performance Testing Demand Analysis, What Should Be Done Specifically?
What is a Test Case? How to Design It?
What is ZenTao in Software Testing? How to Use It?
About Business Function Testing in Black Box Testing

Like this article? Click to see more, sharing is even better!

Leave a Comment