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:
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:
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 membersfinal class StringUtilsTests: XCTestCase
: Defines a test case class that inherits fromXCTestCase
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:
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:
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:
- A
setUp()
method that runs before each test to create a freshStringUtils
instance - Two test methods to verify our functionality:
testReverseString()
to test string reversaltestIsPalindrome()
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:
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:
- Generate an Xcode project:
swift package generate-xcodeproj
- Open the generated project:
open StringUtils.xcodeproj
- 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.
// 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:
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:
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:
// 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:
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-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
:
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
:
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
- Swift Package Manager Documentation
- XCTest Documentation
- Testing Tips & Tricks in Xcode
- Advanced Testing in Xcode
Exercises
- Add tests for edge cases to the
StringUtils
struct (e.g., testing with multi-byte characters, empty strings). - Create a new utility function in the package and write tests for it before implementing the function (test-driven development).
- Extend the URL builder to handle URL fragments (the part after #) and write tests for this new functionality.
- 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! :)