Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent app from creating a viewcontroller when running unit tests

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.

like image 483
Dorian Roy Avatar asked Aug 15 '12 17:08

Dorian Roy


People also ask

What do you have to avoid in tests in unit testing?

Avoid Test Interdependence You, therefore, cannot count on the test suite or the class that you're testing to maintain state in between tests. But that won't always make itself obvious to you. If you have two tests, for instance, the test runner may happen to execute them in the same order each time.

Should you unit test frontend?

Unit testingThis is crucial for any frontend application, testing your components and features against how you expect them to behave in production, leading to a stable codebase and a reliable app for your customers.

Should unit tests be run in parallel?

Running unit tests in parallel can significantly improve the speed at which they run. However, you have to make sure that one test does not affect another in any way. Else your tests are green most of the time, but sometimes one or more tests will fail.

How do I make unit tests automatically?

To generate unit tests, your types must be public. Open your solution in Visual Studio and then open the class file that has methods you want to test. Right-click on a method and choose Run IntelliTest to generate unit tests for the code in your method. IntelliTest runs your code many times with different inputs.


4 Answers

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.

like image 141
Jon Reid Avatar answered Nov 09 '22 06:11

Jon Reid


@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.");
    //http://stackoverflow.com/questions/11974138/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).

like image 31
FreeNickname Avatar answered Nov 09 '22 04:11

FreeNickname


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"
    }()
}
like image 45
uɥƃnɐʌuop Avatar answered Nov 09 '22 06:11

uɥƃnɐʌuop


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.

like image 29
Maciek Czarnik Avatar answered Nov 09 '22 06:11

Maciek Czarnik