Skip to main content

Swift Package Testing

Introduction

Testing is a crucial aspect of software development that helps ensure your code works as intended and remains reliable as you make changes. Swift Package Manager (SwiftPM) provides built-in support for testing, making it straightforward to create and run tests for your Swift packages.

In this guide, you'll learn how to:

  • Set up tests in a Swift package
  • Write effective unit tests
  • Run tests from the command line and Xcode
  • Implement test best practices

Understanding Swift Package Tests

Swift Package Manager follows a convention-based approach to testing. When you create a package, it automatically sets up a directory structure that includes a designated place for your tests.

Test Directory Structure

When you create a Swift package, the following structure is created:

MyPackage/
├── Package.swift
├── README.md
├── Sources/
│ └── MyPackage/
│ └── MyPackage.swift
└── Tests/
└── MyPackageTests/
└── MyPackageTests.swift

The Tests directory contains test targets, with each subdirectory representing a separate test target.

Creating Your First Test

Let's start by creating a simple Swift package with tests.

1. Create a New Package

First, let's create a simple package that we can test:

bash
mkdir StringUtils
cd StringUtils
swift package init --type library

This creates a package with a basic structure including a Tests directory.

2. Understanding the Generated Test File

Open the generated test file at Tests/StringUtilsTests/StringUtilsTests.swift. It should look something like this:

swift
import XCTest
@testable import StringUtils

final class StringUtilsTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
XCTAssertEqual(StringUtils().text, "Hello, World!")
}
}

Let's break down what this test file contains:

  • import XCTest: Imports Apple's XCTest framework
  • @testable import StringUtils: Imports your module with the @testable attribute, which allows you to test internal members
  • final class StringUtilsTests: XCTestCase: Defines a test case class that inherits from XCTestCase
  • func testExample(): A test method that must start with the word "test"

3. Adding Functionality to Test

Let's add some functionality to our package that we can test. Open Sources/StringUtils/StringUtils.swift and replace its content with:

swift
public struct StringUtils {
public init() {}

public func reverseString(_ input: String) -> String {
return String(input.reversed())
}

public func isPalindrome(_ input: String) -> Bool {
let cleanString = input.lowercased().filter { $0.isLetter || $0.isNumber }
return cleanString == String(cleanString.reversed())
}
}

4. Writing Tests for Our Functionality

Now, let's update our test file to test these functions:

swift
import XCTest
@testable import StringUtils

final class StringUtilsTests: XCTestCase {
var utils: StringUtils!

override func setUp() {
utils = StringUtils()
}

func testReverseString() throws {
XCTAssertEqual(utils.reverseString("hello"), "olleh")
XCTAssertEqual(utils.reverseString("Swift"), "tfiwS")
XCTAssertEqual(utils.reverseString(""), "")
}

func testIsPalindrome() throws {
XCTAssertTrue(utils.isPalindrome("racecar"))
XCTAssertTrue(utils.isPalindrome("A man a plan a canal Panama"))
XCTAssertFalse(utils.isPalindrome("hello"))
XCTAssertTrue(utils.isPalindrome("")) // Empty string is considered a palindrome
}
}

This test file includes:

  1. A setUp() method that runs before each test to create a fresh StringUtils instance
  2. Two test methods to verify our functionality:
    • testReverseString() to test string reversal
    • testIsPalindrome() to test palindrome detection

Running Tests

You can run tests in two ways: from the command line or using Xcode.

Running Tests from the Command Line

To run tests from the command line, navigate to your package directory and use:

bash
swift test

You should see output similar to:

Building for debugging...
Build complete! (0.22s)
Test Suite 'All tests' started
Test Suite 'StringUtilsTests.xctest' started
Test Suite 'StringUtilsTests' started
Test Case 'StringUtilsTests.testIsPalindrome' passed (0.001 seconds)
Test Case 'StringUtilsTests.testReverseString' passed (0.000 seconds)
Test Suite 'StringUtilsTests' passed at 2023-07-24 14:53:21.456
Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.002) seconds
Test Suite 'StringUtilsTests.xctest' passed at 2023-07-24 14:53:21.456
Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.002) seconds
Test Suite 'All tests' passed at 2023-07-24 14:53:21.456
Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.003) seconds

Running Tests in Xcode

To run tests in Xcode:

  1. Generate an Xcode project:
bash
swift package generate-xcodeproj
  1. Open the generated project:
bash
open StringUtils.xcodeproj
  1. Select the scheme for your test target, then press Command+U or click Product > Test.

Test Best Practices

1. Write Focused Tests

Each test should focus on testing one specific aspect of your code. This makes tests more maintainable and easier to debug.

swift
// Good: Focused test
func testReverseStringWithEmptyInput() {
XCTAssertEqual(utils.reverseString(""), "")
}

// Good: Another focused test
func testReverseStringWithNonEmptyInput() {
XCTAssertEqual(utils.reverseString("hello"), "olleh")
}

2. Use Descriptive Test Names

Test names should clearly describe what's being tested and under what conditions:

swift
func testIsPalindrome_WithValidPalindrome_ReturnsTrue() {
XCTAssertTrue(utils.isPalindrome("racecar"))
}

func testIsPalindrome_WithNonPalindrome_ReturnsFalse() {
XCTAssertFalse(utils.isPalindrome("swift"))
}

3. Use Setup and Teardown

Use setUp() for initialization code and tearDown() for cleanup:

swift
class MyTests: XCTestCase {
var sut: SystemUnderTest!
var mockDatabase: MockDatabase!

override func setUp() {
super.setUp()
mockDatabase = MockDatabase()
sut = SystemUnderTest(database: mockDatabase)
}

override func tearDown() {
sut = nil
mockDatabase = nil
super.tearDown()
}

// Tests here...
}

4. Use XCTAssert Functions Appropriately

XCTest provides various assertion functions for different scenarios:

swift
// Test equality
XCTAssertEqual(result, expectedValue)

// Test boolean conditions
XCTAssertTrue(condition)
XCTAssertFalse(condition)

// Test for nil/non-nil
XCTAssertNil(value)
XCTAssertNotNil(value)

// Test if code throws or doesn't throw
XCTAssertNoThrow(try function())
XCTAssertThrowsError(try function())

Advanced Testing Techniques

Testing Asynchronous Code

Swift tests can handle asynchronous code using expectations:

swift
func testAsyncFunction() {
// Create an expectation
let expectation = XCTestExpectation(description: "Async operation completes")

// Call your async function
asyncFunction { result in
// Verify the result
XCTAssertEqual(result, expectedValue)

// Fulfill the expectation
expectation.fulfill()
}

// Wait for the expectation to be fulfilled
wait(for: [expectation], timeout: 1.0)
}

Testing Different Swift Versions

You can specify the Swift version compatibility in your Package.swift file:

swift
// swift-tools-version:5.5
import PackageDescription

let package = Package(
name: "MyPackage",
platforms: [
.macOS(.v10_15),
.iOS(.v13)
],
// ...
)

To test across different Swift versions, you can use CI services like GitHub Actions to run your tests in different environments.

Real-World Example: Testing a URL Builder

Let's create a more practical example: a URL builder utility with comprehensive tests.

First, let's create our URL builder in Sources/StringUtils/URLBuilder.swift:

swift
public struct URLBuilder {
private var components: URLComponents

public init(baseURL: String) {
self.components = URLComponents(string: baseURL) ?? URLComponents()
}

public mutating func addQueryItem(name: String, value: String) {
if components.queryItems == nil {
components.queryItems = []
}
components.queryItems?.append(URLQueryItem(name: name, value: value))
}

public mutating func setPath(_ path: String) {
components.path = path.hasPrefix("/") ? path : "/" + path
}

public func build() -> URL? {
return components.url
}
}

Now, let's write tests for this URL builder in Tests/StringUtilsTests/URLBuilderTests.swift:

swift
import XCTest
@testable import StringUtils

final class URLBuilderTests: XCTestCase {
func testInitWithValidURL() {
let builder = URLBuilder(baseURL: "https://example.com")
XCTAssertEqual(builder.build()?.absoluteString, "https://example.com")
}

func testInitWithInvalidURL() {
let builder = URLBuilder(baseURL: "not a url")
XCTAssertNil(builder.build())
}

func testAddQueryItem() {
var builder = URLBuilder(baseURL: "https://example.com")
builder.addQueryItem(name: "search", value: "swift")
XCTAssertEqual(builder.build()?.absoluteString, "https://example.com?search=swift")

builder.addQueryItem(name: "page", value: "1")
XCTAssertEqual(builder.build()?.absoluteString, "https://example.com?search=swift&page=1")
}

func testSetPath() {
var builder = URLBuilder(baseURL: "https://example.com")

// Path without leading slash
builder.setPath("api/v1/users")
XCTAssertEqual(builder.build()?.absoluteString, "https://example.com/api/v1/users")

// Path with leading slash
builder.setPath("/api/v2/users")
XCTAssertEqual(builder.build()?.absoluteString, "https://example.com/api/v2/users")
}

func testComplexURL() {
var builder = URLBuilder(baseURL: "https://api.example.com")
builder.setPath("/v1/search")
builder.addQueryItem(name: "q", value: "swift programming")
builder.addQueryItem(name: "lang", value: "en")
builder.addQueryItem(name: "limit", value: "10")

// Verify the final URL
let expectedURL = "https://api.example.com/v1/search?q=swift%20programming&lang=en&limit=10"
XCTAssertEqual(builder.build()?.absoluteString, expectedURL)
}
}

These tests validate that our URL builder works as expected in various scenarios, including handling invalid inputs and creating complex URLs.

Summary

In this guide, you've learned how to:

  • Set up and organize tests in a Swift package
  • Write effective unit tests using XCTest
  • Run tests both from the command line and in Xcode
  • Apply best practices for writing maintainable tests
  • Test both simple and more complex functionality

Testing is a vital part of software development that helps ensure your code works correctly and continues to work as you make changes. By incorporating testing into your Swift package development process, you can build more reliable and maintainable libraries.

Additional Resources

Exercises

  1. Add tests for edge cases to the StringUtils struct (e.g., testing with multi-byte characters, empty strings).
  2. Create a new utility function in the package and write tests for it before implementing the function (test-driven development).
  3. Extend the URL builder to handle URL fragments (the part after #) and write tests for this new functionality.
  4. Implement a simple mock for a network service and write tests using this mock.


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)