【问题标题】:Why can't code inside unit tests find bundle resources?为什么单元测试中的代码找不到捆绑资源?
【发布时间】:2010-12-25 03:42:48
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: cocoa unit-testing xcode nsbundle octest


    【解决方案1】:

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

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

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

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

    【讨论】:

    • 对我不起作用。仍然是构建包而不是测试包。
    • @Chris 在示例行中,我假设self 指的是主包中的一个类,而不是测试用例类。将 [self class] 替换为主包中的任何类。我将编辑我的示例。
    • @benzado 捆绑包仍然相同(构建),我认为这是正确的。因为当我使用 self 或 AppDelegate 时,两者都位于主包中。当我检查主目标的构建阶段时,两个文件都在其中。但是我想在运行时主包和测试包之间有什么不同。我需要捆绑包的代码位于主捆绑包中。我有以下问题。我正在加载一个 png 文件。通常这个文件不在主包中,因为用户是从服务器下载的。但是对于测试,我想使用测试包中的文件而不将其复制到主包中。
    • @Chris 我在之前的编辑中犯了一个错误,并再次编辑了答案。在测试时,应用程序包仍然是主包。如果要加载单元测试包中的资源文件,则需要将bundleForClass: 与单元测试包中的类一起使用。您应该在单元测试代码中获取文件的路径,然后将路径字符串传递给您的其他代码。
    • 这可行,但我如何区分运行部署和测试部署?基于如果它是一个测试的事实,我需要来自主包中一个类的测试包中的资源。如果它是常规的“运行”,我需要来自主包而不是测试包的资源。有什么想法吗?
    【解决方案2】:

    一个 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。应用程序代码应仅在应用程序包中具有目标成员资格。在运行时,单元测试目标包是injected into the application bundle for execution

    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 测试包。这些不同的测试包具有不同的路径另外,不同的测试包可能有一些内部目录结构和内容差异

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

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

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

    另见:Use resources in unit tests with Swift Package Manager

    Swift 包管理器 (SwiftPM) 5.3

    Swift 5.3 包含 Package Manager Resources SE-0271 进化提案,其“状态:已实施 (Swift 5.3)”。 :-)

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

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

    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
    

    【讨论】:

    • 对于 Swift 4,你也可以使用 Bundle(for: type(of: self))
    【解决方案3】:

    使用 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")
    

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 向测试包添加资源会使测试结果在很大程度上无效。毕竟,一个资源可能很容易在测试目标中而不是在应用目标中,你的测试都会通过,但应用会燃烧起来。
      • @dgatwood 说了什么。它也不能解决问题
      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        我必须确保已设置此常规测试复选框

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-08-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-03-18
          相关资源
          最近更新 更多