Prevent app from creating a viewcontroller when running unit tests
Asked Answered
S

4

11

When I test my app using OCUnit, it sets up the AppDelegate, window and rootViewController as usual before running the tests. My rootViewController then adds itself as an observer for some NSNotifications.

When I test these notifications with isolated test instances and mock observers, the notification handler of the automatically created rootViewController is called as well, which causes some of my tests to fail.

Is there a way to keep OCUnit from creating the rootViewController or make it use a different ViewController class when running in test mode? It would be cool if this could be done without writing special test-related code in my app code.

Semi answered 15/8, 2012 at 17:31 Comment(0)
C
12

Update: What I do today is slightly different from the answer below. See How to Easily Switch Your App Delegate for Testing

It does require adding a little bit of test-specific code to your app code. Here's what I do to avoid my full startup sequence:

Edit the scheme

  • Select the Test action
  • In "Test" select the Arguments tab
  • Disable "Use the Run action's options"
  • Add an environment variable, setting runningTests to YES

Edit your app delegate

  • Add the following to -application:didFinishLaunchingWithOptions: as soon as it makes sense to:

    #if DEBUG
        if (getenv("runningTests"))
            return YES;
    #endif
    
  • Do the same for -applicationDidBecomeActive: but simply return.

Cates answered 16/8, 2012 at 5:31 Comment(7)
Thank you for your solution! I really like it and I use it all the time. I came up with a small improvement (or so I think). I'll be glad if you'll take a look at my answer. Maybe you know a better way to achieve the same thing. Or may be you'll like my idea :)Fania
So why not mock appdelegate ? I better suggest that it would be helpful to mock an appdelegate.Feckless
@RyanChou Can you provide an example?Cates
@JonReid The method was refered from this answer #27930637 I did follow his code snippets. While it seems doesn't work. The solutions you provides could handle the appdelegate methods more flexible.Feckless
@RyanChou That example shows how to replace the app delegate after your tests have begun. But the launch sequence will still fire, unless you do something like what I show.Cates
@JonReid Yeah. Like what you mentioned on the answer. I am so appreciated for your solution. it could work well. Thanks Jon Reid.Feckless
@ScottyBlades I'm guessing you have a more recent project which uses UISceneDelegate. My advice doesn't cover that. Swapping out the scene stuff is complicated due to caching, but check out hacknicity.medium.com/…Cates
C
4

Xcode itself sets environment variables when running tests, so no need to create any in your schemes. If you are already doing so for other purposes, then doing so may be practical. You can, however, use Xcode's environment variables for the purpose of determining whether tests are running. The bulk of the code looks like this in objc, which you could throw into your app delegate:

Option 1:

static BOOL isRunningTests(void) __attribute__((const));

static BOOL isRunningTests(void)
{
    NSDictionary* environment = [[NSProcessInfo processInfo] environment];
    NSString* injectBundle = environment[@"XCInjectBundle"];
    NSLog(@"TSTL %@", [injectBundle pathExtension]);
    return [[injectBundle pathExtension] isEqualToString:@"xctest"] || [[injectBundle pathExtension] isEqualToString:@"octest"];
}

Then simply call isRunningTests() wherever you need to check for tests. This code, however, should really be stored somewhere else, for example, in a TestHelper class:

Option 2:

// TestHelper.h
#import <Foundation/Foundation.h>

extern BOOL isRunningTests(void) __attribute__((const));
// TestHelper.m
#import "TestCase.h"

extern BOOL isRunningTests(void)
{
    NSDictionary* environment = [[NSProcessInfo processInfo] environment];
    NSString* injectBundle = environment[@"XCInjectBundle"];
    NSLog(@"TSTL %@", [injectBundle pathExtension]);
    return [[injectBundle pathExtension] isEqualToString:@"xctest"] || [[injectBundle pathExtension] isEqualToString:@"octest"];
}

Note that we are still using the global variable, and the choice of class name is actually irrelevant. It's just some class where it make sense to keep it.

Option 3:

And in swift, you'll need to wrap it in a class in order to work in both objective-c and swift. You could do it like this:

class TestHelper: NSObject {
    static let isRunningTests: Bool = {
        guard let injectBundle = NSProcessInfo.processInfo().environment["XCInjectBundle"] as NSString? else {
            return false
        }
        let pathExtension = injectBundle.pathExtension

        return pathExtension == "xctest" || pathExtension == "octest"
    }()
}
Coleen answered 22/8, 2015 at 8:26 Comment(1)
This solution worked for me after swapping environment[@"XCInjectBundle"] for environment[@"XCTestBundlePath"].Migraine
F
3

@Jon Reid's solution is great, and I use it in all my projects now, but there is a small problem with it: schemes are not kept in the version control system by default. So when you clone a project from git, tests might fail just because the runningTests environment variable isn't set. And I forget about it all the time.

So, to remind myself about it, I now add a small test to all my projects:

#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>

@interface DMAUnitTestModeTests : XCTestCase

@end

@implementation DMAUnitTestModeTests

- (void)testUnitTestMode {
    BOOL isInUnitTestMode = (BOOL)getenv("runningTests");

    XCTAssert(isInUnitTestMode, @"You have to set a 'runningTests' environment variable in the schemes editor.");
    //https://mcmap.net/q/978415/-prevent-app-from-creating-a-viewcontroller-when-running-unit-tests/11981192#11981192
}

@end

If someone comes up with a better solution, please, let me know :)

Why I posted it as an answer: this is just a small improvement on @Jon Reid's answer (which I really like). I wanted to write it as a comment, but it would be inconvenient to share code this way, so I decided to post it as an answer (despite the fact that it isn't exactly an answer to the question).

Fania answered 4/6, 2015 at 9:47 Comment(0)
A
1

The cleanest way I have seen in RxTodo MVVM example app, it goes like this:

  1. Remove @UIApplication attribute from your application delegate class
  2. Add main.swift file with an implementation like this:

    import UIKit
    import Foundation
    
    final class MockAppDelegate: UIResponder, UIApplicationDelegate {}
    
    private func appDelegateClassName() -> String {
        let isTesting = NSClassFromString("XCTestCase") != nil
        return
        NSStringFromClass(isTesting ? MockAppDelegate.self : AppDelegate.self)
    }
    
    UIApplicationMain(
        CommandLine.argc,
        UnsafeMutableRawPointer(CommandLine.unsafeArgv)
            .bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)),
        NSStringFromClass(UIApplication.self), appDelegateClassName()
    )
    

It's Swift 3 version. For v2 see edit history.

Anishaaniso answered 13/9, 2016 at 9:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.