The iOS application I’m working on is complicated and consists of multiple frameworks (which correspond to targets in the Xcode project), each one for a separate piece of functionality. Therefore we also have a number of testing targets, roughly one test target per framework. XCTest is the standard testing framework for iOS applications in Xcode, however it’s quite limited, it only has a number of basic assertions. It’s useful to create our own higher-level assertions to make the testing code more expressive. Then the question becomes, how to share those common assertions between multiple test frameworks? Well, create a new framework (target) with those assertions, of course! Alas, it’s not that simple when you need to import XCTest in that framework. I’ll show here how I made it work.
functestMagicNumberShouldBeWithinCertainBounds(){letmagicNumber=Framework.magicNumberXCTAssertWithinBounds(magicNumber,8...9000,"The magic number should really be within the specified bounds to work")}/// Asserts that `x` is within the given `bounds`.funcXCTAssertWithinBounds<T:Comparable>(_x:T,_bounds:ClosedRange<T>,_message:@autoclosure()->String="",file:StaticString=#file,line:UInt=#line){XCTAssertTrue(bounds.contains(x),"Expected \(x) to be within the bounds \(bounds): \(message())",file:file,line:line)}
As you see the problem is in importing XCTest in a regular framework. Linking with XCTest doesn’t work.
The fix is to set “Framework Search Paths” (FRAMEWORK_SEARCH_PATHS) of the target to: "$(PLATFORM_DIR)/Developer/Library/Frameworks" and $(inherited) (the inherited item should be there if you’re using CocoaPods because it sets its own paths list, and it’s a good idea for compatibility anyway). See commit 1f514f8dfa6f3721b21ca4766803575b10d9321e. Build succeeded!
This works for most of the standard assertions like XCTAssert, XCTAssertEqual, XCTAssertNoThrow, but not if you want to use XCTUnwrap.
Using XCTUnwrap
If you need to assert that an Optional value is not nil in tests and get its unwrapped value, use XCTUnwrap:
12345
/// Asserts that the string `s` is at least of the length `minCount`. If `s` is `nil`, the assertion fails.funcXCTAssertStringHasMinimalCount(_s:String?,_minCount:Int,_message:@autoclosure()->String="",file:StaticString=#file,line:UInt=#line)throws{lets=tryXCTUnwrap(s,"Expected string to have at least \(minCount) characters, but it was nil: \(message())",file:file,line:line)XCTAssertGreaterThanOrEqual(s.count,minCount,"Expected string \(s) to have at least \(minCount) characters, but it had \(s.count): \(message())",file:file,line:line)}
However this will not work in our test extensions framework:
123
XCTestExtensions/XCTestExtensions/XCTestExtensions.swift:18:17:error:useofunresolvedidentifier'XCTUnwrap'lets=tryXCTUnwrap(s,"Expected string to have at least \(minCount) characters, but it was nil: \(message())",file:file,line:line)^~~~~~~~~
I tried linking the framework with XCTest and/or libswiftXCTest.tbd, but neither worked. So where is this function defined?
So XCTUnwrap isn’t present in the Objective-C headers, but only in the swift module — that must be the cause of the build error. There are two steps to fix this:
Set “Import Paths” (in “Swift Compiler – Search Paths”) (SWIFT_INCLUDE_PATHS) to "$(PLATFORM_DIR)/Developer/usr/lib" and $(inherited) (just in case). Now the code compiles, but linking fails:
ld: warning: Could not find or use auto-linked library 'XCTestSwiftSupport'
Undefined symbols for architecture x86_64:
"XCTest.XCTAssertTrue(_: @autoclosure () throws -> Swift.Bool, _: @autoclosure () -> Swift.String, file: Swift.StaticString, line: Swift.UInt) -> ()", referenced from:
XCTestExtensions.XCTAssertWithinBounds<A where A: Swift.Comparable>(_: A, _: Swift.ClosedRange<A>, _: @autoclosure () -> Swift.String, file: Swift.StaticString, line: Swift.UInt) -> () in XCTestExtensions.o
"XCTest.XCTAssertGreaterThanOrEqual<A where A: Swift.Comparable>(_: @autoclosure () throws -> A, _: @autoclosure () throws -> A, _: @autoclosure () -> Swift.String, file: Swift.StaticString, line: Swift.UInt) -> ()", referenced from:
XCTestExtensions.XCTAssertStringHasMinimalCount(_: Swift.String?, _: Swift.Int, _: @autoclosure () -> Swift.String, file: Swift.StaticString, line: Swift.UInt) throws -> () in XCTestExtensions.o
"XCTest.XCTUnwrap<A>(_: @autoclosure () throws -> A?, _: @autoclosure () -> Swift.String, file: Swift.StaticString, line: Swift.UInt) throws -> A", referenced from:
XCTestExtensions.XCTAssertStringHasMinimalCount(_: Swift.String?, _: Swift.Int, _: @autoclosure () -> Swift.String, file: Swift.StaticString, line: Swift.UInt) throws -> () in XCTestExtensions.o
"__swift_FORCE_LOAD_$_XCTestSwiftSupport", referenced from:
__swift_FORCE_LOAD_$_XCTestSwiftSupport_$_XCTestExtensions in XCTestExtensions.o
(maybe you meant: __swift_FORCE_LOAD_$_XCTestSwiftSupport_$_XCTestExtensions)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Set “Library Search Paths” (in “Search Paths”) (LIBRARY_SEARCH_PATHS) to the same two values: "$(PLATFORM_DIR)/Developer/usr/lib" and $(inherited). See commit aef611b773b8ee79720774238ef586058e9edcd9. Build succeeded!