198

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

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

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

我已经确保它foo.txt包含在单元测试目标的Copy Bundle Resources构建阶段,为什么它找不到该文件?

4

6 回答 6

332

当单元测试工具运行您的代码时,您的单元测试包不是主包。

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

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

然后你的代码将搜索你的单元测试类所在的包,一切都会好起来的。

于 2009-12-10T07:40:25.687 回答
104

一个 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 ...与常规(非单元测试)捆绑路径分开:

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

另请注意,默认情况下,单元测试可执行文件与应用程序代码链接。但是,单元测试代码应该只在测试包中具有 Target Membership。应用程序代码应仅在应用程序包中具有目标成员资格。在运行时,单元测试目标包被注入到应用程序包中执行

Swift 包管理器 (SPM) 4:

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

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

在任何一种情况下,.bundlePathand.bundleURL都会返回当前在 macOS 上运行的测试包的路径。但是,Bundle目前还没有为 Ubuntu Linux 实现。

此外,命令行swift build目前swift test不提供复制资源的机制。

但是,通过一些努力,可以在 macOS Xcode、macOS 命令行和 Ubuntu 命令行环境中设置使用 Swift Package Manger 的进程。一个例子可以在这里找到:004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref

另请参阅:使用 Swift 包管理器在单元测试中使用资源

Swift 包管理器 (SwiftPM) 5.3

Swift 5.3 包含带有“状态:已实现(Swift 5.3) ”的包管理器资源 SE-0271进化提案。:-)

资源并不总是供包的客户使用;资源的一种用途可能包括仅单元测试需要的测试夹具。此类资源不会与库代码一起合并到包的客户端中,而只会在运行包的测试时使用。

  • 在和API 中添加一个新resources参数以允许显式声明资源文件。targettestTarget

SwiftPM 使用文件系统约定来确定属于包中每个目标的源文件集:具体而言,目标的源文件是位于目标的指定“目标目录”下的那些。默认情况下,这是一个与目标同名的目录,位于“Sources”(用于常规目标)或“Tests”(用于测试目标)中,但可以在包清单中自定义此位置。

// 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()),因此在Xcode 构建时不存在于Foundation.Bundle中。

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
于 2015-06-06T21:50:22.957 回答
16

使用 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")
于 2016-09-21T08:40:53.367 回答
5

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

在此处输入图像描述

于 2016-09-09T04:10:51.170 回答
2

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

在此处输入图像描述

于 2018-08-29T06:28:26.980 回答
2

我必须确保设置了这个通用测试复选框此通用测试复选框已设置

于 2019-01-25T00:47:43.433 回答