Learn the key

Meaning of unit &UI testing

Unit &UI test execution process exploration and optimization

A preliminary study on the principle of unit-UI testing

1. Unit &UI testing

1.1 What are unit tests

Unit tests check whether each unit of code (such as a class or function) produces the expected results. Unit tests are run independently, not dependent on other modules or components.

1.2 What is UI testing

UI testing is an end-to-end testing process that starts and ends with the application. It replicates the interaction with the application exactly as the user interacts with the application, which is much slower and more resource-intensive to run than unit testing.

1.3 Contents to be tested

& EMSP tests should cover the following:

  • Core functionality: Model classes and methods and their interactions with controllers

  • UI workflow

  • Special boundary conditions

  • Bug processing

1.4 Test Principles (FIRST)

  • Fast: The test module should be Fast and efficient

  • Independent/Isolated: Test modules should be Independent of each other

  • Repeatable: Test instances should be Repeatable and test results should be the same

  • Testing should be fully automated. The output is either “success” or “failure”

  • Timely: Ideally, you should write tests (test-driven development) before writing the production code to be tested.

2. Explore the execution process of unit &UI test

First, create an iOS project using Xcode and check the Include Tests option as shown below:

Before we test the code, how many questions should we ask?

    1. Test whether the code needs to be started before runningAPP?
    1. Does it need to be calledAppDelegateIn thedidFinishLaunchingWithOptionsMethods?

To verify the exact answers to these two questions, make the following breakpoints in the test project, as shown in the figure below:

Then run the testExample method from the testAppDemotests. m file in the test project, as shown below:

First, you can see that the App is started in the emulator and executes at the breakpoint set in the main.m file, as shown below:

After the breakpoint is passed, the program executes at the breakpoint set in the appdelegate. m file, as shown below:

The breakpoint is crossed and the log information output in the test method is printed, as shown below:

Among them:

  • The red box 1The time in: represents the test method execution time
  • The red box 2: indicates the name of the test method
  • The red box 3: Indicates that the test passes and takes time0.001Seconds.

According to the above results, and can very clearly see the program execution when test method is will launch the APP and invokes the execute didFinishLaunchingWithOptions method, in some large project, Usually in didFinishLaunchingWithOptions approach to perform some time consuming method, so that can’t rapid test, in order to solve this problem, can choose to create a FakeAppDelegate, Just return the FakeAppDelegate object in the main function during the test, as shown below:

#import <UIKit/ uikit. h> NS_ASSUME_NONNULL_BEGIN @interface FakeAppDelegate: UIResponder <UIApplicationDelegate> @end NS_ASSUME_NONNULL_END #import "fakeAppdelegate.h" @interface FakeAppDelegate () @end @implementation FakeAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions { return YES; } #pragma mark - UISceneSession lifecycle - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; } - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions { } H > #import "appdelegate. h" #import" fakeAppdelegate. h" int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. BOOL isShouldReturnFakeAppDelegate = NO; appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }Copy the code

You may notice isShouldReturnFakeAppDelegate this local variable has a value of NO, so don’t just doesn’t work? This is what I want to throw the problem, should be how to set the value of isShouldReturnFakeAppDelegate? During the run, how do you know if this is a test method call or the actual APP running? Consider this for a moment, and I’ll share two solutions in the following discussion.

3. Unit &UI test execution process optimization

3.1 Optimization of OC project execution process

3.1.1 useruntime APIjudge

TestAppDemoTests–>XCTestCase–>XCTest –>XCTest –> TestAppDemoTests–>XCTestCase–>XCTest

#import <UIKit/UIKit.h> #import "AppDelegate.h" #import "FakeAppDelegate.h" int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. BOOL isShouldReturnFakeAppDelegate = NSClassFromString(@"XCTest") ! = nil; appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }Copy the code

The following log information is displayed in the appdelegate. m and fakeAppdelegate. m files respectively:

Command +r Runs the App, and the output log information is as follows:

Run the testExample method in the testAppDemotests. m file and output log information as shown below:

3.1.2 Use environment variables to determine

Add the environment variable IS_TESTING in Debug mode of Test under the project Scheme, as shown below:

Then write the following code in the main.m file to get the value of the environment variable IS_TESTING

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "FakeAppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        BOOL isShouldReturnFakeAppDelegate = [[NSProcessInfo processInfo].environment[@"IS_TESTING"] boolValue];;
        
        appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Copy the code

Command +r Runs the App, and the output log information is as follows:

Run the testExample method in the testAppDemotests. m file and output log information as shown below:

3.2 SwiftEngineering execution process optimization

  • Create a simpleSwiftengineeringTestSwiftDemo, as shown in the figure below:

  • Will find inSwiftIt’s not in the worksmain.swiftfile

  • createmain.swiftFile and write the following code in this file:
Import UIKit var appDelegateClsName = NSStringFromClass(appDelegate.self) import UIKit var appDelegateClsName = NSStringFromClass(appDelegate.self) If NSClassFromString("XCTest")! = nil { appDelegateClsName = NSStringFromClass(FakeAppDelegate.self) } //2. According to the environment variables determine whether is executing test method if ProcessInfo. ProcessInfo. Environment [] "IS_TESTING" = = "true" {appDelegateClsName = NSStringFromClass(FakeAppDelegate.self) } let argv = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<CChar>.self, capacity: Int(CommandLine.argc)) _ = UIApplicationMain(CommandLine.argc, argv, nil, appDelegateClsName)Copy the code
  • inTestConfigure the environment variables as shown in the following figure

  • The running results are as follows:

4. A preliminary study on the principle of unit-UI testing

After running the test project with Command + U, open the compiled application as shown below, and you will see the AutoTestingDemoUITests-Runner application.

The application will also be installed in the emulator, as shown below:

AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo: # AutoTestingDemo

That is, is it possible to run test code in an IPA package as long as these dynamic libraries are included? Where did the XCTest. Framework come from? It is actually obtained from the following path in Xcode:

  • Xctest. framework (emulator device) : /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framew ork

  • Xctest. framework (real device) : /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework

Once you have added xcTest. framework to your project, you can test your code in your project without creating a test target. Start by creating an xcconfig file, as shown below:

Then configure it in the xcconfig file as follows:

//1. Set the directory HEADER_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.frame work/Headers" //2. //2.1 Traditional OTHER_LDFLAGS = $(Inherited) -f "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" -framework "XCTest" / / / / 3 2.2. Configure RPATH to resolve crashes image not found LD_RUNPATH_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks"Copy the code

The main project then references the xcconfig file, as shown below:

&emsp creates an AppTests class with code like this:

#import <XCTest/ xctest. h> NS_ASSUME_NONNULL_BEGIN @interface AppTests: XCTestCase - (void)testExample1; - (void)testExample2; #import "tests. h" @implementation AppTests - (void)testExample1 { NSLog(@"-------testExample1-------"); } - (void)testExample2 { NSLog(@"-------testExample2-------"); } @endCopy the code

Then write code in the viewController.m file as follows:

#import "ViewController.h" #import <XCTest/XCTest.h> #import "AppTests.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// Manage XCTestSuite to manage test cases XCTestSuite *suite = [XCTestSuite defaultTestSuite]; AppTests *testCase = [AppTests testCaseWithSelector:@selector(testExample1)]; [suite addTest:testCase]; For (XCTest *test in suite.tests) {[test runTest]; } } @endCopy the code

Then run the program and click the screen. The log output is as follows:

There is a problem with the above code, however. When the screen is clicked again, the application crashes directly, as shown below:

If you want to run the test case multiple times, you can call it in the following way

- (void)touchesBegan:(NSSet< uittouch *> *)touches withEvent:(UIEvent *)event {// Used to manage the test case suite XCTestSuite * = [XCTestSuite testSuiteForTestCaseClass: AppTests. Class]; AppTests *testCase = [AppTests new]; [suite addTest:testCase]; For (XCTest *test in suite.tests) {[test runTest]; }}Copy the code

Run the program and click the screen. The output information of the console is as follows:

To be continued…