Testing: Test Types, XCTest, and Writing Tests That Actually Catch Bugs
Survey the major categories of testing an Objective-C codebase needs — unit, integration, UI, regression, API, performance, memory, and concurrency testing — and then go deep on the XCTest framework itself: XCTestCase, the assertion family, setUp/tearDown, expectations, asynchronous tests, and performance tests.
Every lesson so far in this course has been about writing Objective-C code. This lesson is about proving that code works — and keeps working. A method that compiles cleanly and runs correctly once, in the debugger, under your careful eye, is not the same thing as a method that is verified to behave correctly, automatically, every time anyone changes anything nearby, for years, across a team of developers who never personally watched it run. That gap is what testing closes. Objective-C's testing story centers on XCTest, Apple's built-in testing framework, bundled with Xcode and integrated directly into the build-and-run workflow — write a test method, press Cmd+U, and Xcode runs every test in the project and reports, method by method, which passed and which failed. This lesson first surveys the different kinds of testing a real project needs, since 'testing' is not one activity but several with different goals, and then teaches the XCTest mechanics — XCTestCase, the XCTAssert family, setUp/tearDown, expectations for asynchronous code, and performance tests — that make writing all of them possible in practice.
Unit testing: verifying one thing in isolation
A unit test verifies a single unit of code — typically one method, or one small cluster of tightly related methods on one class — in isolation from the rest of the system. 'In isolation' is the operative phrase: a well-written unit test for a discount-calculation method does not spin up a network connection, does not touch a real database, and does not depend on any other class's correctness except the one class actually under test. If the discount calculation is wrong, the unit test for it should fail; if a completely unrelated networking class is broken, that same unit test should still pass, because it never went anywhere near the network.
This isolation is what makes unit tests fast and precise. A well-designed unit test suite for a large app can run hundreds or thousands of individual tests in a few seconds, because none of them are waiting on anything slow — no network round-trip, no disk I/O, no sleep() calls. And when a unit test fails, the failure points at exactly one class and usually exactly one method, because that's the only thing the test was exercising in the first place. This precision is also why unit tests are, in practice, the largest category of tests in most real test suites: they are cheap to write, cheap to run, and cheap to diagnose when they fail.
Achieving that isolation is not automatic, though — it's the direct payoff of the architectural habits this course has been building toward all along. A class that depends on a concrete NSURLSession or a concrete Core Data stack baked directly into its own implementation is hard to unit test in isolation, because testing it drags the network or the database along for the ride. A class that depends on a protocol-typed collaborator, injected from the outside, can be unit tested with a lightweight substitute standing in for that collaborator instead — which is exactly the subject of the next lesson in this module.
Integration testing: verifying components together
Where a unit test deliberately isolates one class, an integration test deliberately does the opposite: it exercises several real components working together, checking that the seams between them behave correctly. A unit test might verify that a JSON-parsing method correctly turns a dictionary into a model object; an integration test might verify that the networking layer, the JSON parser, and the persistence layer, all wired together as they would be in the real app, correctly take a server response all the way through to a saved record on disk.
Integration tests catch a category of bug that unit tests structurally cannot: bugs in how two individually-correct components fail to cooperate. Each piece might pass every one of its own unit tests in isolation and still, once actually connected, misbehave — a subtly mismatched assumption about who owns a piece of data, a threading assumption one side makes that the other side violates, a data format one component produces that the other doesn't quite parse the way its author assumed. These are, almost by definition, bugs that only show up when the real components actually touch each other.
Integration tests are correspondingly slower and less precise about pinpointing a failure than unit tests are — when one fails, you know the *seam* between two or more components is wrong, but narrowing that down to exactly which side is at fault, or which specific interaction, takes more digging than a failing unit test does. Most real projects therefore keep a large base of fast, precise unit tests and a smaller number of integration tests focused specifically on the seams that have historically been fragile or that carry the most risk if they break.
UI testing, regression testing, and API testing
UI testing drives the app's actual interface the way a person would — tapping buttons, entering text, scrolling lists — and asserts on what appears on screen afterward. Apple's XCUITest, built on the same XCTest foundation this lesson teaches below, automates exactly this: it launches the app in a simulator or on a device, locates elements by accessibility identifier or label, simulates taps and gestures, and asserts that the expected screen, label, or alert actually appeared. UI tests are the slowest and most brittle category covered here — a layout change can break a UI test that was checking correct behavior all along — but they are also the only category that verifies the full stack really works end to end, exactly the way a user will actually experience it, and this course does not go deeper into XCUITest's specific API beyond this overview.
Regression testing is not a distinct testing technique so much as a *discipline* about how existing tests get used over time: whenever a bug is found and fixed, a new test is written that specifically reproduces that bug, and that test is kept in the suite permanently, running on every future build. The point is not to test the fix once — it's to guarantee, automatically and forever afterward, that the same bug can never silently creep back in through some later, unrelated change. A growing regression suite is, in a very real sense, a growing, executable history of every mistake the codebase has already learned not to make twice.
API testing focuses specifically on a networking layer: does a request get built correctly, does the code correctly parse a success response into the right model objects, and does it correctly handle an error response, a malformed response, or a timeout. Because real API tests should not depend on a live server being reachable and behaving predictably during a test run, API tests typically test against *stubbed* responses — pre-canned data standing in for what a real server would return — rather than making a genuine network call, so the test verifies the app's own request-building and response-handling logic without also depending on network availability or a specific server's current behavior. This overlaps directly with test doubles, covered fully in the next lesson.
Performance testing, memory testing, and concurrency testing
Performance testing measures how long an operation takes, or how much of some resource (CPU, memory) it consumes, and asserts that it stays within an acceptable range — rather than merely asserting the operation produced the correct *result*. This matters because correctness and speed are genuinely separate properties: a sorting method can return a perfectly correct sorted array while being ten times slower than it should be, and only a performance test, not a correctness test, will ever catch that. XCTest has dedicated support for this, covered in full further down this lesson, that establishes a timing baseline and automatically flags future runs that regress against it.
Memory testing verifies that code does not leak memory or create retain cycles — the exact hazards the earlier Memory Debugging lesson covered with Instruments and the Memory Graph Debugger. Where that lesson was about interactively hunting down a specific leak by hand, memory testing is about folding that same verification into the automated test suite: a test that creates an object, lets every strong reference to it go out of scope, and then asserts that the object was actually deallocated — catching a reintroduced retain cycle automatically on the next test run, the same way a regression test catches a reintroduced logic bug.
Concurrency testing verifies that code involving asynchronous work or multiple threads — built with the blocks and Grand Central Dispatch mechanisms from the earlier lessons on those topics — behaves correctly under those conditions specifically: that a completion handler actually fires, that it fires with the right result, that work dispatched to a background queue doesn't corrupt shared state a foreground consumer is reading at the same time, and that no deadlock occurs. Concurrency bugs are notoriously timing-dependent and can pass a hundred times in a row before failing once under slightly different scheduling, which makes them some of the hardest bugs to catch by casual manual testing — and exactly why XCTest's expectations mechanism, covered in depth below, exists specifically to test asynchronous code reliably rather than by guesswork.
XCTestCase: the foundation every test sits on
Every XCTest test case is a class that subclasses XCTestCase, Apple's base class for a group of related tests. Inside that class, every instance method whose name begins with the word test and takes no parameters is automatically discovered and run as an individual test by Xcode's test runner — no registration step, no separate list of tests to maintain; the naming convention alone is what makes a method a test.
A single XCTestCase subclass typically groups together all the tests for one thing being tested — one class, or one tightly related area of functionality — with each individual test method verifying one specific behavior or scenario. This grouping is what setUp and tearDown, covered next, actually operate on: they run around each test *method* within a given XCTestCase subclass, not once for the whole class.
Xcode runs each test method as an independent test, reports pass or fail for each one individually in the Test Navigator, and — critically — keeps going even after one test method fails, so a single broken test doesn't hide the results of every other test in the suite. This is part of why test isolation, covered next under setUp and tearDown, matters so much: if test methods secretly depended on each other's side effects, running them independently and in whatever order Xcode chooses would produce unreliable, order-dependent results.
The XCTAssert family: what actually makes a test pass or fail
A test method that runs to completion without calling any assertion function passes trivially — XCTest has no way to know a test failed unless the test itself says so. The XCTAssert... family of functions is exactly that mechanism: each one checks a specific condition and, if that condition is not met, records a test failure at that exact line, with a clear message, while letting the rest of the test method keep running.
XCTAssertTrue(expression) and XCTAssertFalse(expression) check a boolean condition directly. XCTAssertEqual(a, b) checks that two values are equal — for objects, this typically means isEqual: — and is by far the most commonly reached-for assertion, since most tests boil down to 'did this computation produce the value I expected.' XCTAssertNil(expression) and XCTAssertNotNil(expression) check specifically for nil versus a non-nil value, which matters because a method that's supposed to return nil on failure and an object on success needs exactly this distinction tested, not a general-purpose equality check. XCTAssertThrows and XCTAssertNoThrow check whether an expression raises an exception. Every one of these has an optional final message: parameter, a string that appears alongside the failure to explain, in plain language, what specifically went wrong — worth adding whenever the assertion alone wouldn't make an obviously failing test easy to diagnose.
A well-written test method usually contains a small number of closely related assertions — ideally testing one specific behavior — rather than sprawling across many unrelated checks. A test with too many unrelated assertions in one method makes a failure harder to diagnose, since you can't immediately tell, from the test's name alone, which of several unrelated things actually broke; it also loses the benefit that unrelated failures ought to be reported as unrelated failures rather than getting bundled into one signal.
setUp and tearDown: fresh state for every test
XCTest calls -setUp before every single test method in a given XCTestCase subclass, and -tearDown after every single test method, whether that test passed or failed. This is not once per class — it is once per test method, and that repetition is the entire point. setUp is where you create whatever objects a test needs (the object under test, and any test doubles it depends on) fresh, from scratch, for that one test method alone; tearDown is where you release or reset anything that needs explicit cleanup before the next test method runs.
The reason this matters as much as it does comes down to test isolation, mentioned earlier: without a fresh setUp before every test, one test method's leftover state — an object it mutated, a singleton it reconfigured, a file it wrote to disk — could silently leak into the next test method that happens to run afterward, making that next test's pass or fail outcome depend on which test ran before it rather than on whether its own logic is correct. A test suite where results depend on execution order is not trustworthy; a developer can no longer run a single test in isolation and trust the result, which defeats much of the point of having fast, precise unit tests in the first place. setUp and tearDown, run around every single test method without exception, are XCTest's guarantee against exactly that failure mode.
In practice, setUp commonly creates the system-under-test as a fresh instance stored in an instance variable, so every test method in the class can refer to it without repeating the construction code; tearDown commonly sets that instance variable back to nil (releasing it under ARC) and undoes anything with a side effect outside the object graph itself, such as resetting a shared default or removing a temporary file the test created.
Expectations and asynchronous tests
A synchronous test method is straightforward: call the method, assert on its return value, done. An asynchronous method — one that takes a completion handler, built with the blocks mechanism from the earlier lesson, and calls it later, possibly after dispatching work to a background queue with Grand Central Dispatch — breaks that pattern completely, because the test method itself would return and finish before the completion handler has had any chance to run, and XCTest would report the test as trivially passed without ever having actually checked anything.
XCTestExpectation solves this directly. A test creates an expectation object — typically via -expectationWithDescription:, with a human-readable string describing what's being waited for — calls the asynchronous method under test, and inside that method's completion handler, once the real result has actually arrived, performs its assertions and then calls -fulfill on the expectation to signal that the awaited event has happened. The test method itself then calls -waitForExpectations:timeout:, which pauses the test — blocking that test method's execution but not the app's main run loop the way a naive sleep() would — until every expectation passed to it has been fulfilled, or until the given timeout elapses, whichever comes first.
If every expectation fulfills before the timeout, the test proceeds normally, having genuinely exercised the asynchronous code path and asserted on its real result rather than assuming success. If the timeout elapses first — because the completion handler never fired, or fired with a delay the test wasn't willing to tolerate — XCTest fails the test at that point with a timeout failure, which is itself a meaningful and often bug-revealing result: a completion handler that should fire quickly but doesn't is very often a real bug (a deadlock, a forgotten dispatch back to a queue, a network call that silently never completes) rather than a flaky test.
Performance tests and test doubles: previews of what comes next
XCTest measures performance through -measureBlock:, given a block of code to run repeatedly; XCTest runs that block several times, records how long each run took, and compares the result against a stored baseline. The first time a performance test runs, or whenever a developer explicitly resets the baseline, Xcode asks the developer to accept the current timing as the new baseline; every subsequent run compares fresh measurements against that saved baseline and fails the test if performance has regressed beyond an acceptable tolerance. Newer XCTest APIs generalize this with XCTMetric to measure other resources besides raw wall-clock time, such as memory allocations, but the underlying idea is identical: establish a number you know is acceptable today, and let the automated suite catch it automatically if that number quietly gets worse over time, the same way a regression test catches a reintroduced logic bug rather than a reintroduced performance bug.
The last item on both this lesson's survey and its XCTest tour — test doubles — is deliberately left as only a preview here. A test double is a broad category name for any object substituted in for a real collaborator during a test, specifically to keep that test fast, deterministic, and isolated from things like the network or the filesystem, rather than testing against a class's actual, full real implementation. That category covers several genuinely distinct kinds of substitute object — fakes, stubs, spies, and mocks — each with its own precise meaning and its own best use, and getting those four terms right, with worked examples of each, is substantial enough a topic that it is the entire subject of the next lesson, Mocking and Dependency Injection, rather than something to rush through here.
An XCTestCase subclass: setUp, tearDown, and a test method
Any method starting with test and taking no arguments is discovered and run automatically. setUp runs before, and tearDown after, every single test method — not once for the whole class. Always call [super setUp] and [super tearDown] so XCTestCase's own bookkeeping still happens.
@interface CalculatorTests : XCTestCase
@property (nonatomic, strong) Calculator *calculator;
@end
@implementation CalculatorTests
- (void)setUp {
[super setUp];
self.calculator = [[Calculator alloc] init];
}
- (void)tearDown {
self.calculator = nil;
[super tearDown];
}
- (void)testAdditionOfTwoPositiveNumbers {
NSInteger result = [self.calculator addNumber:2 toNumber:3];
XCTAssertEqual(result, 5, @"2 + 3 should equal 5");
}
@endCode Examples
This single test method exercises several distinct XCTAssert... functions, each checking a different kind of condition: XCTAssertNotNil/XCTAssertNil check presence versus absence, XCTAssertTrue checks a boolean, and XCTAssertEqualObjects checks object equality via isEqual: (the object-pointer counterpart to XCTAssertEqual, which is meant for primitive scalars). Each assertion carries its own descriptive message, so a failure at any one line immediately tells you, in plain language, which specific expectation was violated rather than just which line number failed. XCTAssertEqual and XCTAssertEqualObjects are easy to confuse: XCTAssertEqual is for C scalars (compared with ==), while XCTAssertEqualObjects is for Objective-C objects (compared with isEqual:). Using XCTAssertEqual on two NSString * pointers would compare pointer identity, not string content, and can pass or fail for the wrong reason.
XCTAssertEqualObjects(validUser.name, @"Alice", @"Name should be stored unchanged");
fetchUserProfileForID:completion: is asynchronous — it likely dispatches to a background queue via Grand Central Dispatch and calls its completion block later, possibly back on the main queue. The test creates an XCTestExpectation before calling it, performs its real assertions inside the completion block once the actual result has arrived, and calls fulfill to record that the expected event happened. waitForExpectationsWithTimeout:handler: then pauses the test method until fulfill is called or 5 seconds pass, whichever happens first, so the test genuinely waits for and verifies the real asynchronous result instead of finishing instantly and vacuously passing. Expected result: Test Case '-[ProfileServiceTests testFetchUserProfileSucceeds]' passed (0.312 seconds).
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *timeoutError) { ... }];
Common mistake: Forgetting to call waitForExpectationsWithTimeout:handler: after creating the expectation means the test method returns immediately, before the completion handler (and its assertions) ever run, and XCTest reports a trivial, meaningless pass.
measureBlock: runs the given block several times in a row, measuring the wall-clock time of each run, and compares the resulting average against a stored performance baseline for this test. The first run establishes the baseline (Xcode prompts the developer to accept it); every later run flags a failure automatically if sorting this array has gotten meaningfully slower, catching a performance regression the same way an ordinary assertion catches a logic regression — without anyone having to remember to check timing by hand. A trivial XCTAssertEqual is included inside the block purely to confirm the operation actually completed correctly on each measured run — a performance test that silently measures a broken operation is worse than useless, since it would give you confidently wrong numbers.
[self measureBlock:^{ ... }];
testEmptyCartHasZeroTotal depends on self.cart genuinely being empty when it runs. Because setUp creates a brand-new ShoppingCart before every test method — including this one — that assumption holds no matter what order Xcode chooses to run these two tests in, and no matter whether testAddingItemIncreasesTotal happened to run first. Without setUp giving each test its own fresh cart, testEmptyCartHasZeroTotal's result would depend on test execution order, which is exactly the fragile, unreliable pattern setUp and tearDown exist to prevent.
self.cart = [[ShoppingCart alloc] init];
Common mistake: Creating the cart once as a class-level singleton or a static variable instead of inside setUp would let state from one test (like the added item) leak into the next test that happens to run afterward.
- (void)testUserValidation {
User *validUser = [[User alloc] initWithName:@"Alice" age:30];
XCTAssertNotNil(validUser, @"A valid name and age should produce a user");
XCTAssertTrue([validUser isValid], @"User with valid fields should be valid");
XCTAssertEqualObjects(validUser.name, @"Alice", @"Name should be stored unchanged");
User *invalidUser = [[User alloc] initWithName:@"" age:-5];
XCTAssertNil(invalidUser, @"Empty name and negative age should fail construction");
}- (void)testFetchUserProfileSucceeds {
XCTestExpectation *expectation =
[self expectationWithDescription:@"fetchUserProfile completion handler fires"];
[self.profileService fetchUserProfileForID:@"user_42"
completion:^(UserProfile *profile, NSError *error) {
XCTAssertNil(error, @"Fetch should succeed without an error");
XCTAssertNotNil(profile, @"A successful fetch should return a profile");
XCTAssertEqualObjects(profile.userID, @"user_42", @"Returned profile should match the requested ID");
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *timeoutError) {
if (timeoutError) {
XCTFail(@"fetchUserProfile did not call its completion handler within 5 seconds");
}
}];
}- (void)testSortingPerformance {
NSArray<NSNumber *> *unsortedNumbers = [self largeUnsortedNumberArray];
[self measureBlock:^{
NSArray<NSNumber *> *sorted =
[unsortedNumbers sortedArrayUsingSelector:@selector(compare:)];
XCTAssertEqual(sorted.count, unsortedNumbers.count);
}];
}@interface ShoppingCartTests : XCTestCase
@property (nonatomic, strong) ShoppingCart *cart;
@end
@implementation ShoppingCartTests
- (void)setUp {
[super setUp];
self.cart = [[ShoppingCart alloc] init];
}
- (void)tearDown {
self.cart = nil;
[super tearDown];
}
- (void)testAddingItemIncreasesTotal {
[self.cart addItem:[Item itemWithPrice:9.99]];
XCTAssertEqualWithAccuracy(self.cart.total, 9.99, 0.001, @"Total should equal the single item's price");
}
- (void)testEmptyCartHasZeroTotal {
XCTAssertEqualWithAccuracy(self.cart.total, 0.0, 0.001, @"A brand-new cart should start at zero");
}
@endWhy teams invest in a test suite at all
A test suite pays for itself the moment a codebase has more than one person changing it, or lives long enough that its own original author no longer remembers every corner of it. Without automated tests, verifying that a change hasn't broken anything means manually re-checking behavior by hand — slow, incomplete, and easy to skip under deadline pressure. With a solid test suite, the same verification is a few seconds of automated, exhaustive re-checking on every single build, catching a regression the moment it's introduced rather than weeks later when a user reports it. This is why the discipline covered in this lesson — writing focused unit tests, keeping them isolated with setUp/tearDown, testing asynchronous code properly with expectations instead of skipping it as 'too hard to test,' and tracking performance baselines automatically — compounds so heavily over a project's lifetime: each test, once written, keeps working for free on every future change, for as long as the project lives.
Writing a test method with no XCTAssert... call at all, relying on 'it didn't crash' as success.
Assert explicitly on the actual result: XCTAssertEqual(result, expectedValue, ...).
XCTest only fails a test when an assertion inside it fails. A test with no assertions passes trivially no matter what the code under test actually did, giving false confidence.
Creating the object under test once in a class-level property outside setUp, and reusing it across every test method.
Create a fresh instance inside setUp, and release it in tearDown, so every test method starts from identical, known state.
Reusing one shared instance across test methods lets one test's side effects silently leak into the next, making results depend on execution order and defeating test isolation.
Calling an asynchronous, completion-handler-based method and asserting immediately afterward, with no XCTestExpectation.
Create an XCTestExpectation, assert and fulfill it inside the completion handler, and call waitForExpectationsWithTimeout:handler: to actually wait for the real result.
The test method returns before an asynchronous completion handler has had any chance to run, so assertions placed right after the call are checking nothing real and the test passes vacuously.
Treating a passing unit test suite as proof the whole app works correctly end to end.
Combine unit tests with integration and UI tests, since each category catches a different, non-overlapping class of bug.
Unit tests deliberately isolate one component at a time and cannot, by design, catch bugs that only appear when several real components interact — that is what integration and UI testing exist for.
What distinguishes a unit test from an integration test? A. A unit test isolates a single component while an integration test exercises several real components working together B. A unit test runs on a device while an integration test only runs in the simulator C. There is no real difference; the terms are interchangeable D. A unit test always tests UI code while an integration test always tests networking code
A. A unit test isolates a single component while an integration test exercises several real components working together — Unit tests deliberately isolate one unit of code from its collaborators; integration tests deliberately exercise real collaborators together to verify the seams between them.
Every test method in an XCTestCase subclass is automatically discovered and run based on what convention? A. It must be registered in a separate test manifest file B. Its name must begin with the word test and it must take no parameters C. It must be marked with an @test attribute D. It must return a BOOL indicating pass or fail
B. Its name must begin with the word test and it must take no parameters — XCTest discovers test methods purely by naming convention: any no-argument instance method whose name starts with test is treated as an individual test.
Why does XCTest call setUp and tearDown before and after every single test method, rather than once for the whole class? A. It is a performance optimization to reduce total test run time B. To give each test method fresh, known state, so one test's leftover state can't silently affect another C. Because Objective-C requires it for memory management D. To reduce the number of XCTAssert calls a test needs
B. To give each test method fresh, known state, so one test's leftover state can't silently affect another — Running setUp/tearDown around every test method enforces test isolation, so results never depend on which other tests happened to run before them.
What problem does XCTestExpectation solve? A. It makes assertions run faster B. It lets a test correctly wait for and assert on the result of asynchronous, completion-handler-based code C. It automatically generates mock objects for a test D. It measures a test's memory usage
B. It lets a test correctly wait for and assert on the result of asynchronous, completion-handler-based code — Without an expectation, a test method calling an asynchronous method would return before the completion handler ran, making any assertions after the call meaningless; XCTestExpectation and waitForExpectations:timeout: fix that.
What does XCTest's measureBlock: performance testing compare a run's timing against? A. A hardcoded number of milliseconds set by the developer in code B. A stored baseline established from a previous accepted run C. The timing of a different, unrelated test D. Nothing; it only prints the timing without judging pass or fail
B. A stored baseline established from a previous accepted run — measureBlock: compares fresh measurements against a saved baseline (established the first time, or reset explicitly) and fails the test if performance regresses beyond an acceptable tolerance.
In the survey of testing types, what specifically does API testing typically avoid, and why? A. It avoids assertions, relying only on manual inspection B. It avoids making real network calls, typically testing against stubbed responses instead, so the test doesn't depend on a live server C. It avoids testing error responses, focusing only on success cases D. It avoids XCTestCase entirely and uses a separate framework
B. It avoids making real network calls, typically testing against stubbed responses instead, so the test doesn't depend on a live server — API tests usually test against stubbed, pre-canned responses rather than a real network call, so the test verifies request-building and response-handling logic without depending on network availability or a live server's current behavior.
Write an XCTestCase subclass named TemperatureConverterTests that tests a TemperatureConverter class with a method - (double)celsiusToFahrenheit:(double)celsius. Include a setUp method that creates a fresh TemperatureConverter, a test that asserts 0°C converts to 32°F, and a test that asserts 100°C converts to 212°F, using an appropriate XCTAssert... function for comparing floating-point values.
@interface TemperatureConverterTests : XCTestCase @property (nonatomic, strong) TemperatureConverter *converter; @end @implementation TemperatureConverterTests - (void)setUp { [super setUp]; self.converter = [[TemperatureConverter alloc] init]; } - (void)tearDown { self.converter = nil; [super tearDown]; } - (void)testZeroCelsiusIsThirtyTwoFahrenheit { double result = [self.converter celsiusToFahrenheit:0.0]; XCTAssertEqualWithAccuracy(result, 32.0, 0.001, @"0C should equal 32F"); } - (void)testOneHundredCelsiusIsTwoHundredTwelveFahrenheit { double result = [self.converter celsiusToFahrenheit:100.0]; XCTAssertEqualWithAccuracy(result, 212.0, 0.001, @"100C should equal 212F"); } @end
Floating-point results should be compared with XCTAssertEqualWithAccuracy(actual, expected, accuracy, message), not XCTAssertEqual, since exact floating-point equality is unreliable.