How do I migrate from SenTestingKit/OCUnit to XCTest?

herzbube picture herzbube · Nov 16, 2013 · Viewed 15k times · Source

I am in the process of migrating my project from Xcode 4.6.3 to Xcode 5.0.2. The project's unit tests were developed with SenTestingKit/OCUnit. Now when I am running the tests in Xcode 5, I get an error from the RunUnitTests script telling me that

RunUnitTests is obsolete.

Possibly related is this note in the Xcode 5 release notes:

SenTestingKit and OCUnit are deprecated. Use the migrator to move to XCTest.

Unfortunately, I have not been able to find out more about this mysterious "migrator". Possibly my google-fu is lacking [again], so my main question is: How do I migrate unit tests from SenTestingKit/OCUnit to the new XCTest (with or without the "migrator")?

A secondary question, in case migrating is a complicated business: Is it possible to get Xcode 5 to run unit tests that are still based on SenTestingKit/OCUnit? After all these are merely deprecated, so they should still be around and functional.

Answer

herzbube picture herzbube · Nov 17, 2013

Thanks to Shaggy Frog's answer we know that the mysterious "migrator" mentioned in the Xcode release notes is a wizard launched by selecting "Edit > Refactor > Convert To XCTest". I am going to write about my experience with this wizard in two parts. The first part is an incomplete answer to the primary question, the second part answers the secondary question.


Part 1: How to migrate from OCUnit to XCTest

The first thing you need to realize is that for the wizard to work, you need to select a unit test target. If you have the main target selected, the wizard simply does not list any targets to convert.

Once I found out about this, I was able to step through the wizard, but in my case the end result was still a spectacular failure! The wizard claimed that no source changes were necessary and that only build settings needed to be updated to migrate to XCTest. In the end, the wizard did not even manage to do that correctly: It did remove the reference to the SenTestingKit framework, but it did not put in a reference to the XCTest framework.

Anyway, what follows is a list of the changes that I had to manually make because the wizard failed to make them for me. If the wizard works better for you, you may not need to do all of these things.

  1. Remove the "Run Script" build phase from the unit test target
  2. Change the base class of all test case classes from SenTestCase to XCTestCase
  3. Change the imported header from <SenTestingKit/SenTestingKit.h> to <XCTest/XCTest.h>
  4. In test target's Build Settings, change Wrapper Extension from octest to xctest.
  5. Rename all assert macros from ST* to XCT* (e.g. STAssertTrue becomes XCTAssertTrue)
  6. Exception to the above: STAssertEquals needs to be renamed to XCTAssertEqual (note the missing "s" at the end). You will know that you have forgotten about this if you get this compiler warning: warning: implicit declaration of function 'XCTAssertEquals' is invalid in C99
  7. The new XCTest assert macros do not allow nil to be passed as the failure description. For instance, XCTAssertNotNil(anObject, nil) is not possible and must be changed to XCTAssertNotNil(anObject). You will know that you have this problem when you get this compiler error: error: called object type 'NSString *' is not a function or function pointer.
  8. If you do need to pass a failure description, the new XCTest assert macros require a constant expression for the format specifier, just as the NSString class method stringWithFormat: does. You will know that you have this problem when you get this compiler error: error: expected ')'. Some examples:
NSString* formatSpecifier = @"%@";
NSString* failureDescription = @"foo";
// These are OK
XCTAssertNotNil(anObject, @"foo")
XCTAssertNotNil(anObject, @"%@", failureDescription)
// These are not OK
XCTAssertNotNil(anObject, failureDescription);
XCTAssertNotNil(anObject, formatSpecifier, failureDescription);

Last but not least, as already mentioned further up, the reference to the XCTest framework needs to be added to the unit test target. You will know that you have forgotten this if you get linker errors such as Undefined symbols for architecture i386: "_OBJC_CLASS_$_XCTestCase", referenced from: foo.

Xcode 6 update: Linking against XCTest is no longer required in Xcode 6 (in fact XCTest is not even listed as an available framework anymore). Instead set the build setting CLANG_ENABLE_MODULES to YES (exposed in the UI as "Enable Modules (C and Objective-C)"). This will cause clang to automatically link against XCTest when it sees an #import <XCTest/XCTest.h> statement. Details are available in the "Modules" section of the clang documentation.


Part 2: How to run OCUnit tests in Xcode 5

At this point I got a linker error that made me realize that my mission to migrate to XCTest had failed. The reason: XCTest is not part of SDK 6.1, but I am still building my project with base SDK iOS 6.1 (this SO answer explains how to integrate SDK 6.1 into Xcode 5).

Since I am unable to continue with the migration, my solution for the moment is therefore to keep my unit tests based on SenTestingKit/OCUnit, until I find the time to upgrade my app to iOS 7. This is what I had to do in order to get the unit tests to run:

  1. Remove the "Run Script" build phase from the unit test target. This is all that is required to let Xcode execute unit tests via the "Test" action ( + U) while the unit test target is selected.
  2. This is not ideal, though, because I don't want to switch targets just to execute unit tests. Instead I want to execute unit tests while the main target is selected. The second step therefore is to modify the main target's Xcode scheme so that when I run the "Test" action, the unit test target's tests are executed instead.

The final solution is not as good as in Xcode 4.x where the unit tests were executed automatically every time that I ran the main target's "Run" or "Build" action. Unfortunately, it seems that I can't get this to work without a "Run Script" build phase.