Writing in the front

In the process of using XCTests to unit test the Framework, I found that I could not use XCTests for real machine testing, and the project happened to involve functionality that had to be real machine tested.

So I made a simple little tool to supplement it.

The basic idea

Objective: Unit testing using a real machine.

The simple and crude way is to create an App project and drag the Framework project directly into the App project.

Advantages:

  1. All the code and resources in the Framework can be accessed by App.
  2. And then you just write the test code in the App project.

Disadvantages:

  1. Projects can be complicated, even chaotic, and when others take over, they are often confused.
  2. When you write test code, because there are so many test cases, and there are so many more, you can waste a lot of valuable time if you change the UI and change the calling method every time.

So in view of the above two shortcomings, the author of this scheme, a simple optimization.

Optimized scheme:

  1. In the Framework project, add an App Target and use it for real machine testing.
  2. With the TableView, call each test case through the Cell.
  3. When you add test cases, you don’t need to change the original code.

How to do point 3 above?

The author came up with a simple implementation scheme:

  1. Create a test base class, JDAppTestCase, that all test classes will inherit from.
  2. JDAppTestCase provides methods to get all subclasses so that you can add test classes without modifying the original code.
  3. JDAppTestCase provides a way to get “test methods” so that you can add them without modifying the original code.

Modeled after XCTests, this solution is called AppTests.

So the key lies in the JDAppTestCase methods, how to implement.

The specific implementation

The process of creating an App Target will not be described again.

Once created, you need to add a dependency on the original Framework project, as shown in the following figure:

The realization of the JDAppTestCase

There are many ways to get all the subclasses of a class, mainly through the Runtime, one of which is cited here.

To obtain the “test method”, the author agrees:

  • All test methods are based ontestAt the beginning.
  • eachtestMethod is a test case.

We then use the _shortMethodDescription private method to get all the methods and filter out the test methods.

The core code is as follows:

@interface JDAppTestCase : NSObject
- (NSArray<NSString *> *)appTestMethods;
- (NSArray<NSString *> *)subClassNames;
@end

#import <objc/runtime.h>

@implementation JDAppTestCase
// Get all test methods
- (NSArray<NSString *> *)appTestMethods {
    NSString *str = [self performSelector:@selector(_shortMethodDescription)];
    NSArray *components = [str componentsSeparatedByString:@"\n\t\t"];
    NSMutableArray *testMethods = [NSMutableArray new];
    for (NSString *component in components) {
        if ([component containsString:@"test"] && ![component containsString:@"appTestMethods"]) {
            NSRange bRange = [component rangeOfString:@"test"];
            NSRange eRange = [component rangeOfString:@ ";"];
            NSString *method = [component substringWithRange:NSMakeRange(bRange.location, eRange.location - bRange.location)]; [testMethods addObject:method]; }}return testMethods;
}
// Get all subclasses
- (NSArray<NSString *> *)subClassNames {
    int numClasses;
    Class *classes = NULL;
    numClasses = objc_getClassList(NULL.0);
    
    NSMutableArray *subClassNames = [NSMutableArray new];
    if (numClasses >0 ) {
        classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
        numClasses = objc_getClassList(classes, numClasses);
        for (int i = 0; i < numClasses; i++) {
            if (class_getSuperclass(classes[i]) == [self class]){
                [subClassNames addObject:NSStringFromClass(classes[i])];
            }
        }
        free(classes);
    }
    return subClassNames;
}
@end
Copy the code

Improve App display

Create two TableViewControlers to display test methods.

JDAppTestsViewController

Displays all JDAppTestCase subclasses. When you click on the name, all the test methods for that class are displayed.

JDTestMethodsViewController

Displays all methods starting with test in a JDAppTestCase subclass. When I click on the method name, because objc calls the method by sending a message, it’s very easy to call the method with performSelector.

The core code

JDAppTestsViewController

@implementation JDAppTestsViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"appTestsCellID" forIndexPath:indexPath];
    cell.textLabel.text = self.appTestsNames[indexPath.row];
    return cell;
}

#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *name = self.appTestsNames[indexPath.row];
    / / jump
    JDTestMethodsViewController *vc = [[JDTestMethodsViewController alloc] initWithAppTestsName:name];
    [self showViewController:vc sender:nil];
}

#pragma mark - Getters and setters
- (NSArray *)appTestsNames {
    if(! _appTestsNames) { JDAppTestCase *appTests = [JDAppTestCase new]; _appTestsNames = appTests.subClassNames; }return _appTestsNames;
}

@end
Copy the code

JDTestMethodsViewController

@implementation JDTestMethodsViewController
- (instancetype)initWithAppTestsName:(NSString *)name {
    self = [super init];
    if (self) {
        // Get the JDAppTests subclass instance by name
        Class class = NSClassFromString(name);
        self.appTests = [class new];
    }
    return self;
}

#pragma mark - Table view delegate
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"testsMethodsCellID" forIndexPath:indexPath];
    cell.textLabel.text = self.testsMethodNames[indexPath.row];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *name = self.testsMethodNames[indexPath.row];
    SEL selector = NSSelectorFromString(name);
    // Call the method
    if ([self.appTests respondsToSelector:selector]) {
        [self.appTests performSelector:selector]; }}#pragma mark - Getters and setters
- (NSArray *)testsMethodNames {
    if(! _testsMethodNames) { _testsMethodNames = [self.appTests appTestMethods];
    }
    return _testsMethodNames;
}
@end
Copy the code

How to use it?

Add NetworkRequestAppTests to inherit JDAppTestCase.

Add code similar to XCTests, none of which is declared in the header file, just in the.m file.

- (void)testConfigure {
    NSLog(@"configure result %d"[self.networkRequest configure]);
}

- (void)testLogin {
    [self.networkRequest loginWithCompletionHandler:^(BOOL success) {
        NSLog(@"login result %d", success);
    }];
}
Copy the code

Run the App project to check the specific effect

conclusion

The App Target is built in the early stage, and the test code is basically added in the later use, which is convenient to use because it is run on the App and can be used with the performance test.

However, it was annoying to do this every time, so I made a simple Xcode template to automate the process.

reference

Gets all subclasses of the class

Gets the private interface for all methods of a class