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.
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.
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.
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.
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.
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
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
.
@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).
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"
}()
}
The cleanest way I have seen in RxTodo MVVM example app, it goes like this:
@UIApplication
attribute from your application delegate classAdd 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With