Swift Unit Testing

A unit test is a small program that checks one piece of your code. The test calls a function with known input and compares the result with the expected answer. A passing test confirms the function works. A failing test points to the exact place where behavior changed.

Think of a quality inspector in a factory. The inspector tests each part before it reaches the customer. Unit tests inspect each function before the app reaches users, and they keep working every time you change the code.

Diagram: The Test Cycle

 +-----------+     +-----------+     +-----------+
 |  Arrange  | --> |    Act    | --> |  Assert   |
 | prepare   |     | call the  |     | compare   |
 | the data  |     | function  |     | with goal |
 +-----------+     +-----------+     +-----------+
                                          |
                              pass (green) / fail (red)

Code Under Test

A shop applies a percentage discount to a price. The function below becomes the target of the tests.

func discountedPrice(price: Double, percent: Double) -> Double {
    price - (price * percent / 100)
}

Writing a Test With XCTest

XCTest ships with Xcode. A test class inherits from XCTestCase, and every method that starts with test runs automatically.

import XCTest
@testable import MyApp

final class PriceTests: XCTestCase {

    func testDiscountReducesPrice() {
        let result = discountedPrice(price: 200, percent: 10)
        XCTAssertEqual(result, 180)
    }

    func testZeroPercentKeepsPrice() {
        XCTAssertEqual(discountedPrice(price: 50, percent: 0), 50)
    }
}

The line @testable import MyApp gives the tests access to the internal code of your app.

Common Assertions

  • XCTAssertEqual(a, b) passes when both values match.
  • XCTAssertTrue(condition) passes when the condition is true.
  • XCTAssertNil(value) passes when the value is nil.
  • XCTAssertThrowsError(try call()) passes when the call throws an error.
  • XCTAssertEqual(a, b, accuracy: 0.01) compares decimal numbers with a small tolerance.

Decimal math needs the accuracy form. Computers store decimals approximately. Exact comparison may fail on harmless rounding differences.

Preparing and Cleaning Up

The setUp method runs before each test, and tearDown runs after each test. Use them to build fresh data for every test.

final class CartTests: XCTestCase {
    var cart: [String] = []

    override func setUp() {
        super.setUp()
        cart = ["Pen", "Book"]
    }

    func testCartStartsWithTwoItems() {
        XCTAssertEqual(cart.count, 2)
    }
}

Every test starts from the same clean cart. One test never disturbs another.

Testing Error Cases

enum LoginError: Error {
    case emptyPassword
}

func login(password: String) throws {
    if password.isEmpty { throw LoginError.emptyPassword }
}

func testEmptyPasswordFails() {
    XCTAssertThrowsError(try login(password: ""))
}

Swift Testing Framework

Newer versions of Xcode include the Swift Testing framework. It uses the @Test attribute and the #expect macro, and it needs no test class.

import Testing

@Test func discountReducesPrice() {
    #expect(discountedPrice(price: 200, percent: 10) == 180)
}

Both frameworks live side by side in one project. You can adopt the new style at your own pace.

Running the Tests

Press Command and U in Xcode to run every test. Green diamonds mark passing tests, and red diamonds mark failures. Package projects run swift test in the terminal.

Habits of Good Tests

  • Test one behavior per method.
  • Name the test after the behavior, such as testZeroPercentKeepsPrice.
  • Keep tests fast and independent.
  • Cover normal input, edge values, and error cases.

Common Mistakes

Tests that depend on the network, the clock, or random numbers fail without warning. Replace those parts with fixed stand-ins to guarantee identical results on every run.

Writing one giant test that checks ten behaviors hides the cause of a failure. Small focused tests name the broken behavior immediately.

Real-World Use

Teams run the full test suite before every release. A change to a price calculation that breaks an old rule shows up as a red test within seconds. Tests act as a safety net that lets developers improve code with confidence.

Key Takeaways

  • A unit test checks one small piece of code against an expected result.
  • XCTest methods start with test and use assertion functions.
  • setUp prepares fresh data for each test.
  • Swift Testing offers a modern syntax with @Test and #expect.