开发中接口mock数据

2018-04-12  本文已影响489人  福哥_

一、啰嗦一下

实际开发中,如果网络接口后端还没有提供,就需要我们前端人员自己mock数据,当项目代码量不多
和逻辑复杂程度比较低的时候,mock数据很好处理。但项目很大很大的时候,改接口方法,代码写死
数据等方法就显得不合适了,而且这样风险较大,尤其是多人项目的时候,很容易出现带着debug的
代码上线,这样被fire也是有可能的,那么有没有可以不对业务代码进行太多侵入,又能解决mock数据
的问题,我花了3天(第一天思考、第二天写核心代码、第三天完善代码),做了个小工具,看看能否
解决这个问题。

二、核心代码

#import <Foundation/Foundation.h>

// 按照标准命名了,就不使用这个宏
#define MCRequest(Req) \
- (void) __send##Req##Request {}

// 设置Mock的url地址
#define MCRequestURL(Req, URL) \
- (NSString *) __##Req##MockURLString { return URL; }

// 设置处理网络请求
#define MCHandle(Req, HandleSEL) \
- (void) __handle##Req##Request:(id) data {} \
- (NSString *) __originalHandle##Req { return NSStringFromSelector(HandleSEL); }

@interface MockManager : NSObject

+ (instancetype)shareInstance;

@end
#import "MockManager.h"
#import "Aspects.h"
#import <objc/runtime.h>
#import <objc/message.h>

@interface MockManager ()
@property (nonatomic, strong) NSDictionary *mcDataDic;
@end

@implementation MockManager

#ifdef DEBUG
+ (void) load {
    NSLog(@"MockManager load");

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //XIB、SB
        [UIViewController aspect_hookSelector:@selector(initWithCoder:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo)
         {
             NSString *mockOpenState = [[[MockManager shareInstance].mcDataDic objectForKey:@"Config"] objectForKey:@"MOCKOPENSTATE"];
             if (![mockOpenState isEqualToString:@"#"]) {
                 [self magicMethods:aspectInfo.instance];
             }
         } error:NULL];
        
        //纯代码
        [UIViewController aspect_hookSelector:@selector(init) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo)
         {
             NSString *mockOpenState = [[[MockManager shareInstance].mcDataDic objectForKey:@"Config"] objectForKey:@"MOCKOPENSTATE"];
             if (![mockOpenState isEqualToString:@"#"]) {
                 [self magicMethods:aspectInfo.instance];
             }
         } error:NULL];
    });
}
#endif

/* 替换VC的Request方法 */
+ (void) magicMethods:(UIViewController *) vc {
    NSString *mockClass = [[[MockManager shareInstance].mcDataDic objectForKey:@"Config"] objectForKey:@"MOCK"];
    if (![mockClass containsString:NSStringFromClass([vc class])]) { //过滤不在Mock列表中的类
        return;
    } else {
        NSLog(@"%@ 正在使用MOCK", vc);
    }
    
    NSMutableArray *methodsArray = [self methodsArray:vc];
    NSLog(@"magicMethods:%@", methodsArray);
    Class theClass = [vc class];
    
    for (NSInteger j=0; j<methodsArray.count; j++) {
        NSString *methodName = (NSString *)methodsArray[j];
    
        //如果存在sendXXXRequest方法
        if (([methodName hasPrefix:@"__send"] || [methodName hasPrefix:@"send"]) && [methodName hasSuffix:@"Request"]) {
            //先取出sendXXXRequest方法中的XXX
            NSString *originalMethodName = [self originalMethodName:methodName];
            NSString *originalMethodNameLowercase = [self stringFirstCharLowercase:originalMethodName];
            NSString *originalMethodNameUppercase = [self stringFirstCharUppercase:originalMethodName];
            
            //过滤ignore方法
            BOOL isIgnore = NO;
            NSString *classString = NSStringFromClass(theClass);
            NSDictionary *classDic = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:classString];
            NSString *ingoreReqName = nil;
            for (NSString *reqName in [classDic allKeys]) {
                NSString *ignore = [[classDic objectForKey:reqName] objectForKey:@"ignore"];
                if ([ignore isEqualToString:@"#"]) {
                    NSString *theOriginal = [self originalMethodName:reqName];
                    if ([[originalMethodName lowercaseString] isEqualToString:[theOriginal lowercaseString]]) { //表明此方法忽略
                        isIgnore = YES;
                        ingoreReqName = reqName;
                        continue;
                    }
                }
            }
            if (isIgnore) {
                NSLog(@"%@ 正在使用MOCK,%@ 方法被忽略", vc, ingoreReqName);
                continue;
            }
            
            //如果类中存在XXX方法,说明使用了MCRequest注解
            if ([methodsArray containsObject:originalMethodNameLowercase]) {
                //如果调用了网络请求方法,这里截获调用,实际上去调用MCRequest生成的sendXXXRequest方法
                [theClass aspect_hookSelector:NSSelectorFromString(originalMethodNameLowercase) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo) {
                    SEL methodNameSEL = NSSelectorFromString(methodName);
                    NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, methodName];
                    NSAssert([vc respondsToSelector:methodNameSEL], assertMsg);
                    
                    objc_msgSend(vc, methodNameSEL, nil, nil);
                 } error:NULL];
            } else if ([methodsArray containsObject:originalMethodNameUppercase]) {
                [theClass aspect_hookSelector:NSSelectorFromString(originalMethodNameUppercase) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo) {
                    SEL methodNameSEL = NSSelectorFromString(methodName);
                    NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, methodName];
                    NSAssert([vc respondsToSelector:methodNameSEL], assertMsg);
                    
                    objc_msgSend(vc, methodNameSEL, nil, nil);
                } error:NULL];
            }
            else {
                //网络请求方法遵循了sendXXXRequest方法,不需要MCRequest注解。不管是否遵循了sendXXXRequest方法,网络请求方法,接下来都会被handleXXXRequest方法截获
            }
        
            //原始处理网络数据方法,查看是否定义了originalHandleXXX,如果定义了,说明处理数据方法被改写,不然说明方法存在
            NSString *originalHandle = [NSString stringWithFormat:@"__originalHandle%@", originalMethodName];
            NSString *theNewHandleSELString = nil;
            if ([methodsArray containsObject:originalHandle]) {
                SEL originalHandleSEL = NSSelectorFromString(originalHandle);
                NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, originalHandle];
                NSAssert([vc respondsToSelector:originalHandleSEL], assertMsg);
                
                NSString *originalHandleSELString = objc_msgSend(vc, originalHandleSEL, nil, nil);
                
                //将handleXXXRequest方法获取originalHandleXXX的实现
                theNewHandleSELString = [NSString stringWithFormat:@"__handle%@Request:", originalMethodName];
                swizzleMethod(theClass, NSSelectorFromString(originalHandleSELString), NSSelectorFromString(theNewHandleSELString));
            } else {
                //不然的话,说明handleXXXRequest:方法存在
                theNewHandleSELString = [NSString stringWithFormat:@"handle%@Request:", originalMethodName];
            }
            
            //如果调用sendXXXRequest,就会被handleXXXRequest:截获  methodName=sendTestRequest,加注解后methodName=__sendGetNetDataRequest
            [theClass aspect_hookSelector:NSSelectorFromString(methodName) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo) {
                //如果配置了MCRequestURL,就读取这个URL。否则去读取本地配置文件
                SEL sendReqURLSELUppercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharUppercase:methodName]]);
                SEL sendReqURLSELLowercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharLowercase:methodName]]);
                
                SEL originalReqURLSELUppercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharUppercase:originalMethodName]]);
                SEL originalReqURLSELLowercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharLowercase:originalMethodName]]);
                
                id returnValue = nil;
                NSString *rapUrlString = nil;
                if ([vc respondsToSelector:sendReqURLSELUppercase]) {
                    rapUrlString = objc_msgSend(vc, sendReqURLSELUppercase, nil, nil);
                } else if([vc respondsToSelector:sendReqURLSELLowercase]) {
                    rapUrlString = objc_msgSend(vc, sendReqURLSELLowercase, nil, nil);
                } else if([vc respondsToSelector:originalReqURLSELUppercase]) {
                    rapUrlString = objc_msgSend(vc, originalReqURLSELUppercase, nil, nil);
                } else if([vc respondsToSelector:originalReqURLSELLowercase]) {
                    rapUrlString = objc_msgSend(vc, originalReqURLSELLowercase, nil, nil);
                }  else {
                    NSString *classString = NSStringFromClass(theClass);
                    NSDictionary *classDic = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:classString];
                    NSDictionary *reqMethodDic = nil;
                    if ([methodName hasPrefix:@"__send"]) {
                        reqMethodDic = [classDic objectForKey:originalMethodNameLowercase];
                    } else {
                        reqMethodDic = [classDic objectForKey:methodName];
                    }
                    if (reqMethodDic != nil) {
                        NSString *serviceString = [reqMethodDic objectForKey:@"service"];
                        if ([serviceString hasPrefix:@"#"] || (serviceString.length == 0)) {
                            NSString *localJsonString = [reqMethodDic objectForKey:@"local"];
                            
                            if (localJsonString == nil || (localJsonString.length == 0)) {
                                returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
                            } else {
                                NSError *error = nil;
                                NSData *jsonData = [localJsonString dataUsingEncoding:NSUTF8StringEncoding];
                                id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData
                                                                             options:NSJSONReadingAllowFragments
                                                                               error:&error];
                                
                                if (!error && (jsonObj != nil)) {
                                    returnValue = jsonObj;
                                } else {
                                    returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
                                }
                            }
                        } else {
                            rapUrlString = serviceString;
                        }
                    } else {
                        returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
                    }
                }
                
                NSError *error = nil;
                if (rapUrlString != nil) {
                    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:rapUrlString]];
                    returnValue = [NSJSONSerialization JSONObjectWithData:data
                                                                  options:NSJSONReadingAllowFragments
                                                                    error:&error];
                }
            
                if (error) {
                    returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
                } else {
                    //XXXMockURLString
                    //这里根据URL(returnValue)做网络请求,然后回调原来的数据处理逻辑
                    
                    SEL theNewHandleSELStringSEL = NSSelectorFromString(theNewHandleSELString);
                    NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, theNewHandleSELString];
                    NSAssert([vc respondsToSelector:theNewHandleSELStringSEL], assertMsg);
                    
                    objc_msgSend(vc, theNewHandleSELStringSEL, returnValue, nil);
                }
             } error:NULL];
        }
    }
//    NSLog(@"%@:%@", vc, methodsArray);
}

+ (NSString *) stringFirstCharUppercase:(NSString *) string {
    NSString *firstString = [string substringToIndex:1];
    NSString *lastString = [string substringFromIndex:1];
    NSString *theString = [NSString stringWithFormat:@"%@%@", [firstString uppercaseString], lastString];
    return theString;
}

+ (NSString *) stringFirstCharLowercase:(NSString *) string {
    NSString *firstString = [string substringToIndex:1];
    NSString *lastString = [string substringFromIndex:1];
    NSString *theString = [NSString stringWithFormat:@"%@%@", [firstString lowercaseString], lastString];
    return theString;
}


+ (NSString *) originalMethodName:(NSString *) methodName {
    NSString *originalMethodName = [methodName stringByReplacingOccurrencesOfString:@"send" withString:@""];
    originalMethodName = [originalMethodName stringByReplacingOccurrencesOfString:@"Request" withString:@""];
    originalMethodName = [originalMethodName stringByReplacingOccurrencesOfString:@"_" withString:@""];
    return originalMethodName;
}

+ (NSMutableArray *) methodsArray:(id) object {
    unsigned int methodCount =0;
    Method* methodList = class_copyMethodList([object class], &methodCount);
    NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:methodCount];
    for(int i=0; i<methodCount; i++) {
        Method temp = methodList[i];
        const char* name_s =sel_getName(method_getName(temp));
        [methodsArray addObject:[NSString stringWithUTF8String:name_s]];
    }
    free(methodList);
    return methodsArray;
}

void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)   {
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

+ (instancetype)shareInstance {
    static MockManager *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

- (instancetype) init {
    if (self = [super init]) {
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"MCDataPlist" ofType:@"plist"];
        self.mcDataDic = [[NSDictionary alloc] initWithContentsOfFile:filePath];
        NSLog(@"data2:%@", self.mcDataDic);
    }
    return self;
}

- (id)copy {
    return self;
}

- (id)mutableCopy {
    return self;
}

@end

三、使用方法

//使用方法:
//1、开启和关闭mock
//MCDataPlist.plist -> Config -> MOCKOPENSTATE,配置成#则关闭mock,否则开启mock。
//如果项目上线设置,则mock默认关闭,主要通过DEBUG实现关闭

//2、配置mock哪些类
//MCDataPlist.plist -> Config -> MOCK,配置需要mock扫描的类,多个类用#分割开,例如AController#BController。

//3、接口方法规范
//要求发起网络请求方法要写成sendXXXRequest,接收网络请求方法返回数据的方法写成handleXXXRequest:

//4、配置网络请求的mock地址
//使用宏MCRequestURL(Req, URL)配置请求的mock地址,其中Req是请求的名字,要求首字母大写,URL是请求地址字符串,例如MCRequestURL(GetNetData, @"http://10.141.4.93:8080/mockjs/14/common/omissionInfoSwitch")

//5、接口方法不规范解决方法
//使用宏MCRequest(Req)定义发起网络请求方法,Req为方法名字,首字母要大写,例如- (void) getNetData,要写成MCRequest(GetNetData)
//使用宏MCHandle(Req, HandleSEL)定义接受网络请求返回数据方法,Req为网络请求方法名字,首字母大写。HandleSEL为接受网络请求方法的SEL,例如MCHandle(GetNetData, @selector(useNetData:))

//6、配置本地mock数据
//如果没有MCRequestURL给网络请求方法,也可以MCDataPlist.plist配置本地数据,配置规范是在MCDataPlist.plist -> MockData -> 类名,类名下边配置网络请求方法名字,具体见MCDataPlist.plist
//如果配置了MCRequestURL(Req, URL),则本地mock数据的接口mock数据会被忽略,如果使用本地mock数据,本地数据中service字段配置的url优先于local的json数据

//7、配置忽略
//项目上线,则mock自动失效
//可以在MCDataPlist.plist -> Config -> MOCKOPENSTATE,配置成#,则mock完全关闭
//可以MCDataPlist.plist -> Config -> MOCK不配置任何类名,则mock默认不做任何扫描
//可以MCDataPlist.plist -> MockData -> 类名 -> 网络请求方法名 -> ignore,配置成#,则忽略本网络请求方法
//可以MCDataPlist.plist -> MockData -> 类名 -> 网络请求方法名 -> service,配置成#http:XXX,则忽略此mock的网络请求url配置

四、MCDataPlist.plist配置

屏幕快照 2018-04-12 下午7.14.03.png

五:代码实际应用


ssss.png
上一篇下一篇

猜你喜欢

热点阅读