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:
- All the code and resources in the Framework can be accessed by App.
- And then you just write the test code in the App project.
Disadvantages:
- Projects can be complicated, even chaotic, and when others take over, they are often confused.
- 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:
- In the Framework project, add an App Target and use it for real machine testing.
- With the TableView, call each test case through the Cell.
- 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:
- Create a test base class, JDAppTestCase, that all test classes will inherit from.
- JDAppTestCase provides methods to get all subclasses so that you can add test classes without modifying the original code.
- 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 on
test
At the beginning. - each
test
Method 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