为什么单元测试中的代码找不到捆绑包资源?

一些代码,我是单元测试需要加载一个资源文件。它包含以下一行:

NSString *path = [[NSBundle mainBundle] pathForResource:@"foo" ofType:@"txt"];

在应用程序中,它运行得很好,但当由单元测试框架运行时,pathForResource:返回nil,这意味着它无法定位foo.txt

我已经确保foo.txt包含在单元测试目标的拷贝Bundle资源构建阶段中,那么为什么它不能找到该文件呢?

59647 次浏览

当单元测试包运行你的代码时,你的单元测试包是主包。

即使您正在运行测试,而不是您的应用程序,您的应用程序包仍然是主要的包。(据推测,这可以防止您正在测试的代码搜索错误的包。)因此,如果您将一个资源文件添加到单元测试包中,那么在搜索主包时将找不到它。如果将上面的行替换为:

NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *path = [bundle pathForResource:@"foo" ofType:@"txt"];

然后,代码将搜索单元测试类所在的包,一切正常。

一个Swift实现:

斯威夫特2

let testBundle = NSBundle(forClass: self.dynamicType)
let fileURL = testBundle.URLForResource("imageName", withExtension: "png")
XCTAssertNotNil(fileURL)

斯威夫特3,斯威夫特4

let testBundle = Bundle(for: type(of: self))
let filePath = testBundle.path(forResource: "imageName", ofType: "png")
XCTAssertNotNil(filePath)

Bundle提供了发现配置的主路径和测试路径的方法:

@testable import Example


class ExampleTests: XCTestCase {
        

func testExample() {
let bundleMain = Bundle.main
let bundleDoingTest = Bundle(for: type(of: self ))
let bundleBeingTested = Bundle(identifier: "com.example.Example")!
                

print("bundleMain.bundlePath : \(bundleMain.bundlePath)")
// …/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Agents
print("bundleDoingTest.bundlePath : \(bundleDoingTest.bundlePath)")
// …/PATH/TO/Debug/ExampleTests.xctest
print("bundleBeingTested.bundlePath : \(bundleBeingTested.bundlePath)")
// …/PATH/TO/Debug/Example.app
        

print("bundleMain = " + bundleMain.description) // Xcode Test Agent
print("bundleDoingTest = " + bundleDoingTest.description) // Test Case Bundle
print("bundleUnderTest = " + bundleBeingTested.description) // App Bundle

在Xcode 6|7|8|9中,单元测试包路径将在Developer/Xcode/DerivedData中,类似于…

/Users/
UserName/
Library/
Developer/
Xcode/
DerivedData/
App-qwertyuiop.../
Build/
Products/
Debug-iphonesimulator/
AppTests.xctest/
foo.txt

... 它与Developer/CoreSimulator/Devices 常规(非单元测试)bundle路径分开:

/Users/
UserName/
Library/
Developer/
CoreSimulator/
Devices/
_UUID_/
data/
Containers/
Bundle/
Application/
_UUID_/
App.app/

还要注意,在默认情况下,单元测试可执行文件与应用程序代码链接在一起。然而,单元测试代码应该只在测试包中具有Target Membership。应用程序代码在应用程序包中应该只有Target Membership。在运行时,单元测试目标包是注入到应用程序包中执行

Swift Package Manager (SPM)

let testBundle = Bundle(for: type(of: self))
print("testBundle.bundlePath = \(testBundle.bundlePath) ")

注意:默认情况下,命令行swift test将创建一个MyProjectPackageTests.xctest测试包。并且,swift package generate-xcodeproj将创建一个MyProjectTests.xctest测试包。这些不同的测试包有不同的路径此外,不同的测试包可能有一些内部目录结构和内容差异

无论哪种情况,.bundlePath.bundleURL将返回当前在macOS上运行的测试包的路径。但是,Bundle目前还没有为Ubuntu Linux实现。

此外,命令行swift buildswift test目前没有提供复制资源的机制。

但是,通过一些努力,可以通过macOS Xcode、macOS命令行和Ubuntu命令行环境中的资源来设置使用Swift Package Manger的进程。一个例子可以在这里找到:004.4'2软件开发Swift包管理器(SPM)与资源Qref

参见:使用Swift Package Manager在单元测试中使用资源

Swift Package Manager (SwiftPM) 5.3

Swift 5.3包括Package Manager Resources SE-0271演变提案"状态:已实现(Swift 5.3)": -)

资源并不总是为包的客户端所使用;资源的一种使用可能包括仅由单元测试所需的测试fixture。这些资源不会与库代码一起合并到包的客户端中,而只会在运行包的测试时使用。

  • targettestTarget api中添加一个新的resources参数,以允许显式声明资源文件。

SwiftPM使用文件系统约定来确定属于包中每个目标的源文件集:具体地说,目标的源文件是位于指定的“目标目录”下面的源文件。对于目标。默认情况下,这是一个与目标名称相同的目录,位于“Sources"(对于常规目标)或“测试”;(对于测试目标),但是这个位置可以在包清单中定制。

// Get path to DefaultSettings.plist file.
let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist")


// Load an image that can be in an asset archive in a bundle.
let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))


// Find a vertex function in a compiled Metal shader library.
let shader = try mtlDevice.makeDefaultLibrary(bundle: Bundle.module).makeFunction(name: "vertexShader")


// Load a texture.
let texture = MTKTextureLoader(device: mtlDevice).newTexture(name: "Grass", scaleFactor: 1.0, bundle: Bundle.module, options: options)

例子

// swift-tools-version:5.3
import PackageDescription


targets: [
.target(
name: "CLIQuickstartLib",
dependencies: [],
resources: [
// Apply platform-specific rules.
// For example, images might be optimized per specific platform rule.
// If path is a directory, the rule is applied recursively.
// By default, a file will be copied if no rule applies.
.process("Resources"),
]),
.testTarget(
name: "CLIQuickstartLibTests",
dependencies: [],
resources: [
// Copy directories as-is.
// Use to retain directory structure.
// Will be at top level in bundle.
.copy("Resources"),
]),

最新一期

Xcode

Bundle.module由SwiftPM生成(参见Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()),因此在Foundation中不存在。Bundle时由Xcode构建。

在Xcode中类似的方法是手动向模块中添加Resources引用文件夹,添加一个Xcode构建阶段copy,将Resource放入某个*.bundle目录中,并添加一个#ifdef Xcode编译器指令,以便Xcode构建与资源一起工作。

#if Xcode
extension Foundation.Bundle {
  

/// Returns resource bundle as a `Bundle`.
/// Requires Xcode copy phase to locate files into `*.bundle`
/// or `ExecutableNameTests.bundle` for test resources
static var module: Bundle = {
var thisModuleName = "CLIQuickstartLib"
var url = Bundle.main.bundleURL
    

for bundle in Bundle.allBundles
where bundle.bundlePath.hasSuffix(".xctest") {
url = bundle.bundleURL.deletingLastPathComponent()
thisModuleName = thisModuleName.appending("Tests")
}
    

url = url.appendingPathComponent("\(thisModuleName).bundle")
    

guard let bundle = Bundle(url: url) else {
fatalError("Bundle.module could not load: \(url.path)")
}
    

return bundle
}()
  

/// Directory containing resource bundle
static var moduleDir: URL = {
var url = Bundle.main.bundleURL
for bundle in Bundle.allBundles
where bundle.bundlePath.hasSuffix(".xctest") {
// remove 'ExecutableNameTests.xctest' path component
url = bundle.bundleURL.deletingLastPathComponent()
}
return url
}()
  

}
#endif

确认资源已添加到测试目标。

enter image description here

在swift swift 3中,self.dynamicType语法已弃用,请改用此语法

let testBundle = Bundle(for: type(of: self))
let fooTxtPath = testBundle.path(forResource: "foo", ofType: "txt")

let fooTxtURL = testBundle.url(forResource: "foo", withExtension: "txt")

如果你的项目中有多个目标,那么你需要在目标会员中可用的不同目标之间添加资源,你可能需要在不同的目标之间切换,如下图所示的3个步骤

enter image description here

我必须确保这个通用测试复选框设置为this General Testing checkbox was set .

就像我这样的人,在最初的帖子中错过了这一点:

确保foo。md包含在单元测试目标的Copy Bundle Resources构建阶段中

enter image description here