Testing
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
XCTest is Apple’s testing framework, built into Xcode and used for unit, integration, performance and UI tests alike. Objective-C’s dynamism makes it unusually easy to test: any object can be swapped for a stand-in at run time, and a protocol-typed dependency can be satisfied by a hand-written fake with no framework at all.
XCTestCase
A test case is a class; each test is a method whose name begins with test and which takes no arguments and
returns void. Discovery is automatic — there is no registration step.
// PersonTests.m
#import <XCTest/XCTest.h>
#import "Person.h"
@interface PersonTests : XCTestCase
@property (nonatomic, strong) Person *person;
@end
@implementation PersonTests
- (void)setUp {
[super setUp]; // always call super first
self.person = [[Person alloc] initWithName:@"Ada" age:36];
}
- (void)tearDown {
self.person = nil;
[super tearDown]; // always call super last
}
- (void)testGreetingIncludesName {
XCTAssertEqualObjects([self.person greeting], @"Hello, Ada!");
}
- (void)testAgeIsStored {
XCTAssertEqual(self.person.age, 36);
}
@end
setUp runs before each test method and tearDown after, so every test starts from a clean state — tests must never depend on each other or on execution order. The class-level +setUp/+tearDown run once per
class, for genuinely expensive shared fixtures.
Name tests for what they assert. testGreetingIncludesName tells you what broke from the failure list alone;
testGreeting does not.
The Assertion Family
| Assertion | Checks |
|---|---|
|
The expression is true. |
|
Explicit boolean form — clearer intent. |
|
Scalar equality ( |
|
Object equality ( |
|
The negations. |
|
Floating-point comparison within a tolerance. |
|
Nil checks. |
|
Ordering. |
|
An exception is raised. |
|
No exception is raised. |
|
Unconditional failure — for unreachable branches. |
The Equal / EqualObjects distinction is the classic mistake:
NSString *a = @"hello";
NSString *b = [NSString stringWithFormat:@"hel%@", @"lo"];
XCTAssertEqual(a, b); // FAILS -- compares pointers
XCTAssertEqualObjects(a, b); // passes -- compares contents
Every assertion takes an optional trailing format string, and using it turns a failure report into a diagnosis:
XCTAssertEqual(results.count, 3,
@"expected 3 results for query %@, got %lu",
query, (unsigned long)results.count);
Testing Asynchronous Code
XCTestExpectation makes the test wait for a callback:
- (void)testDownloadDeliversData {
XCTestExpectation *expectation =
[self expectationWithDescription:@"download completes"];
[self.downloader fetchURL:self.testURL completion:^(NSData *data, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(data);
XCTAssertGreaterThan(data.length, 0);
[expectation fulfill]; // must be called, or the test times out
}];
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
XCTFail(@"timed out: %@", error);
}
}];
}
Variations worth knowing:
// Expect exactly N fulfilments.
expectation.expectedFulfillmentCount = 3;
// Fail if it is fulfilled MORE than expected -- catches duplicate callbacks.
expectation.assertForOverFulfill = YES;
// Invert it: the test PASSES only if this never fires.
expectation.inverted = YES;
// Wait on a KVO key path reaching a value.
[self keyValueObservingExpectationForObject:download keyPath:@"isFinished" expectedValue:@YES];
// Wait for a notification.
[self expectationForNotification:MYKDownloaderDidFinishNotification object:nil handler:nil];
// Enforce ordering between several expectations.
[self waitForExpectations:@[ first, second ] timeout:5.0 enforceOrder:YES];
Keep timeouts short. A long timeout turns a genuine hang into a slow suite rather than a fast failure.
Performance Tests
- (void)testSortPerformance {
NSArray *input = [self largeRandomArray];
[self measureBlock:^{
[input sortedArrayUsingSelector:@selector(compare:)];
}];
}
// With explicit metrics and options
- (void)testParsePerformance {
XCTMeasureOptions *options = [XCTMeasureOptions defaultOptions];
options.iterationCount = 10;
[self measureWithMetrics:@[ [XCTClockMetric new], [XCTMemoryMetric new] ]
options:options
block:^{
[self.parser parse:self.largeDocument];
}];
}
measureBlock: runs the block ten times and reports the average. The first run establishes a baseline
(set it in the test’s result inspector); later runs fail if they regress beyond the allowed deviation. Keep
setup outside the measured block, or you are timing the setup.
Mocking and Test Doubles
Hand-Written Fakes
Objective-C’s protocols make this the simplest and most robust option — no framework, no magic, and a compile-time check that the fake matches the contract:
@interface FakeDownloader : NSObject <MYKDownloading>
@property (nonatomic, strong) NSData *stubbedData;
@property (nonatomic, strong) NSError *stubbedError;
@property (nonatomic, assign) NSUInteger fetchCallCount;
@end
@implementation FakeDownloader
- (void)fetchURL:(NSURL *)url completion:(void (^)(NSData *, NSError *))completion {
self.fetchCallCount++;
completion(self.stubbedData, self.stubbedError); // synchronous: no waiting
}
@end
- (void)testViewModelHandlesFailure {
FakeDownloader *fake = [[FakeDownloader alloc] init];
fake.stubbedError = [NSError errorWithDomain:MYKErrorDomain code:2 userInfo:nil];
ViewModel *vm = [[ViewModel alloc] initWithDownloader:fake];
[vm refresh];
XCTAssertEqual(fake.fetchCallCount, 1);
XCTAssertEqualObjects(vm.errorMessage, @"Could not refresh.");
}
This is why dependencies should be injected and typed by protocol rather than constructed in place — it is the single change that makes most Objective-C code testable. See Protocols and Delegation.
OCMock
OCMock builds mocks at run time using the message-forwarding machinery, which is handy when you cannot inject a dependency:
#import <OCMock/OCMock.h>
- (void)testWithMock {
// A strict/nice mock of a class
id mockDownloader = OCMClassMock([MYKDownloader class]);
OCMStub([mockDownloader isReady]).andReturn(YES);
// A protocol mock
id mockDelegate = OCMProtocolMock(@protocol(MYKDownloaderDelegate));
Controller *c = [[Controller alloc] initWithDownloader:mockDownloader];
c.delegate = mockDelegate;
[c start];
// Verify an interaction occurred
OCMVerify([mockDelegate downloaderDidFinish:[OCMArg any]]);
// A partial mock: a real object with some methods overridden
MYKDownloader *real = [[MYKDownloader alloc] init];
id partial = OCMPartialMock(real);
OCMStub([partial isReady]).andReturn(NO);
[partial stopMocking]; // restore the original behaviour
}
OCMock leans on swizzling and forwarding, so it interacts badly with code that does the same, and its failures can be hard to read. Prefer a hand-written fake where injection is possible, and keep OCMock for legacy code you cannot restructure. See Dynamic Method Resolution and Forwarding.
Testing Private Methods
You generally should not — test the public behaviour instead. When you must, a category in the test file declares the method without touching the production header:
// In PersonTests.m
@interface Person (Testing)
- (NSString *)internalFormattedName;
@end
- (void)testInternalFormatting {
XCTAssertEqualObjects([self.person internalFormattedName], @"ADA");
}
The declaration is enough; the implementation is already there, and the dynamic dispatch finds it.
Running Tests
# Everything, on a simulator
xcodebuild test -workspace MyApp.xcworkspace -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15'
# On macOS
xcodebuild test -project MyLib.xcodeproj -scheme MyLib -destination 'platform=macOS'
# A single class or a single method
xcodebuild test -scheme MyApp -only-testing:MyAppTests/PersonTests
xcodebuild test -scheme MyApp -only-testing:MyAppTests/PersonTests/testGreetingIncludesName
# Skip a slow suite
xcodebuild test -scheme MyApp -skip-testing:MyAppTests/IntegrationTests
# With coverage, then read the result bundle
xcodebuild test -scheme MyApp -enableCodeCoverage YES -resultBundlePath ./TestResults.xcresult
xcrun xccov view --report ./TestResults.xcresult
# Under a sanitizer
xcodebuild test -scheme MyApp -enableAddressSanitizer YES
xcodebuild test -scheme MyApp -enableThreadSanitizer YES
In Xcode: ⌘U runs everything, ⌃⌥⌘U the test under the cursor, ⌃⌥⌘G re-runs the last one. Enable Gather coverage data in the scheme’s Test action to see per-line coverage in the editor gutter.
Running the suite under Thread Sanitizer periodically is one of the highest-value things a CI pipeline can do for concurrent Objective-C — see Build and Tooling.
Testing on Linux with GNUstep
XCTest is not part of GNUstep, but two routes exist for Foundation-only code.
swift-corelibs-xctest, the open-source XCTest, can be built against GNUstep and gives you the familiar API with manual test registration:
#import <XCTest/XCTest.h>
#import "Calculator.h"
@interface CalculatorTests : XCTestCase
@end
@implementation CalculatorTests
- (void)testAddition {
Calculator *c = [[Calculator alloc] init];
XCTAssertEqual([c add:2 to:3], 5);
}
@end
// GNUstep has no automatic discovery: register explicitly.
int main(int argc, const char *argv[]) {
@autoreleasepool {
return XCTMain(@[ [CalculatorTests defaultTestSuite] ]);
}
}
clang `gnustep-config --objc-flags` -lXCTest \
CalculatorTests.m Calculator.m -o tests `gnustep-config --base-libs`
./tests
GNUstep’s own ObjectTesting.h is the lighter alternative — a small set of pass()/PASS_EQUAL()
macros used by GNUstep itself, with no XCTest dependency:
#import "ObjectTesting.h"
int main(void) {
@autoreleasepool {
Calculator *c = [[Calculator alloc] init];
PASS([c add:2 to:3] == 5, "addition works");
PASS_EQUAL([c description], @"Calculator", "description is correct");
}
return 0;
}
The practical strategy for cross-platform code: keep business logic in a Foundation-only core that both toolchains can compile and test, and confine UIKit/AppKit code — which only Apple platforms can run — to a thin, separately tested layer.
See Also
-
Protocols and Delegation — protocol-typed dependencies, which make code testable.
-
Dynamic Method Resolution and Forwarding — the machinery mocking frameworks use.
-
Build and Tooling —
xcodebuild, sanitizers and coverage. -
Errors and Exceptions — assertions versus test assertions.