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

发布于 2024-08-14 08:14:03 字数 333 浏览 9 评论 0原文

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

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

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

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

Some code I am unit testing needs to load a resource file. It contains the following line:

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

In the app it runs just fine, but when run by the unit testing framework pathForResource: returns nil, meaning it could not locate foo.txt.

I've made sure that foo.txt is included in the Copy Bundle Resources build phase of the unit test target, so why can't it find the file?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

故人如初 2024-08-21 08:14:04

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

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

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

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

When the unit test harness runs your code, your unit test bundle is NOT the main bundle.

Even though you are running tests, not your application, your application bundle is still the main bundle. (Presumably, this prevents the code you are testing from searching the wrong bundle.) Thus, if you add a resource file to the unit test bundle, you won't find it if search the main bundle. If you replace the above line with:

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

Then your code will search the bundle that your unit test class is in, and everything will be fine.

ぺ禁宫浮华殁 2024-08-21 08:14:04

Swift 实现:

Swift 2

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

Swift 3、Swift 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分开code> 常规(非单元测试)捆绑路径

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

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

Swift Package Manager (SPM) 4:

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

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

在任何一种情况下,.bundlePath.bundleURL 将返回当前在 macOS 上运行的测试包的路径。然而,Bundle 目前尚未在 Ubuntu Linux 上实现。

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

但是,通过一些努力,可以设置使用 Swift Package Manger 与 macOS Xcode、macOS 命令行和 Ubuntu 命令行环境中的资源的流程。可以在此处找到一个示例: 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref< /a>

另请参阅:在单元中使用资源使用 Swift Package Manager 进行测试

Swift Package Manager (SwiftPM) 5.3+

Swift 5.3 包括 包管理器资源 SE-0271 演变提案,“状态:已实施 (Swift 5.3) ”。 :-)

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

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

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

// 获取 DefaultSettings.plist 文件的路径。
let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist")

// 加载可以位于捆绑包中的资源存档中的图像。
让图像= UIImage(命名:“MyIcon”,在:Bundle.module,兼容:UITraitCollection(userInterfaceStyle:.dark))

// 在已编译的 Metal 着色器库中查找顶点函数。
让着色器 = 尝试 mtlDevice.makeDefaultLibrary(bundle: Bundle.module).makeFunction(name: "vertexShader")

// 加载纹理。
让纹理= MTKTextureLoader(设备:mtlDevice).newTexture(名称:“草”,scaleFactor:1.0,捆绑包:Bundle.module,选项:选项)

示例

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

Xcode 将手动向模块添加一个 Resources 引用文件夹,添加 Xcode 构建阶段 copyResource 放入某个 *.bundle 目录,并为 Xcode 构建添加一个 #ifdef 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

A Swift implementation:

Swift 2

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

Swift 3, Swift 4

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

Bundle provides ways to discover the main and test paths for your configuration:

@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

In Xcode 6|7|8|9, a unit-test bundle path will be in Developer/Xcode/DerivedData something like ...

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

... which is separate from the Developer/CoreSimulator/Devices regular (non-unit-test) bundle path:

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

Also note the unit test executable is, by default, linked with the application code. However, the unit test code should only have Target Membership in just the test bundle. The application code should only have Target Membership in the application bundle. At runtime, the unit test target bundle is injected into the application bundle for execution.

Swift Package Manager (SPM) 4:

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

Note: By default, the command line swift test will create a MyProjectPackageTests.xctest test bundle. And, the swift package generate-xcodeproj will create a MyProjectTests.xctest test bundle. These different test bundles have different paths. Also, the different test bundles may have some internal directory structure and content differences.

In either case, the .bundlePath and .bundleURL will return the path of test bundle currently being run on macOS. However, Bundle is not currently implemented for Ubuntu Linux.

Also, command line swift build and swift test do not currently provide a mechanism for copying resources.

However, with some effort, it is possible to set up processes for using the Swift Package Manger with resources in the macOS Xcode, macOS command line, and Ubuntu command line environments. One example can be found here: 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref

See also: Use resources in unit tests with Swift Package Manager

Swift Package Manager (SwiftPM) 5.3+

Swift 5.3 includes Package Manager Resources SE-0271 evolution proposal with "Status: Implemented (Swift 5.3)". :-)

Resources aren't always intended for use by clients of the package; one use of resources might include test fixtures that are only needed by unit tests. Such resources would not be incorporated into clients of the package along with the library code, but would only be used while running the package's tests.

  • Add a new resources parameter in target and testTarget APIs to allow declaring resource files explicitly.

SwiftPM uses file system conventions for determining the set of source files that belongs to each target in a package: specifically, a target's source files are those that are located underneath the designated "target directory" for the target. By default this is a directory that has the same name as the target and is located in "Sources" (for a regular target) or "Tests" (for a test target), but this location can be customized in the package manifest.

// 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)

Example

// 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"),
      ]),

Current Issue

Xcode

Bundle.module is generated by SwiftPM (see Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()) and thus not present in Foundation.Bundle when built by Xcode.

A comparable approach in Xcode would be to manually add a Resources reference folder to the module, add an Xcode build phase copy to put the Resource into some *.bundle directory, and add a #ifdef Xcode compiler directive for the Xcode build to work with the resources.

#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
关于从前 2024-08-21 08:14:04

在 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")

With swift Swift 3 the syntax self.dynamicType has been deprecated, use this instead

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

or

let fooTxtURL = testBundle.url(forResource: "foo", withExtension: "txt")
酷到爆炸 2024-08-21 08:14:04

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

输入图像描述这里

Confirm that the resource is added to the test target.

enter image description here

被你宠の有点坏 2024-08-21 08:14:04

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

“在此处输入图像描述"

if you have multiple target in your project then you need to add resources between different target available in the Target Membership and you may need to switch between different Target as 3 steps shown in the figure below

enter image description here

苏大泽ㄣ 2024-08-21 08:14:04

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

I had to ensure this General Testing checkbox was set this General Testing checkbox was set

揽清风入怀 2024-08-21 08:14:04

对于像我这样的人来说,在原始帖子中错过了这一点:

确保 foo.md 包含在单元测试目标的复制捆绑资源构建阶段

在此处输入图像描述

Just for people like me, that missed this point in the original post:

Make sure that foo.md is included in the Copy Bundle Resources build phase of the unit test target

enter image description here

家住魔仙堡 2024-08-21 08:14:04

有一段代码可以在以下位置找到该文件:
如何检查文件是否存在存在于 Swift 的 Documents 目录中吗?

我在测试中使用了它,如下所示,该测试测试我的文件是否已创建并给出其位置以供进一步测试。

        let fileFound = XCTestExpectation (description: "Checking for DB File Found")
    
    
    
    let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
    let url = NSURL(fileURLWithPath: path)
    if let pathComponent = url.appendingPathComponent("filename.ext") {
        let filePath = pathComponent.path
        let fileManager = FileManager.default
        if fileManager.fileExists(atPath: filePath) {
            fileFound.fulfill()
            print("DB FILE AVAILABLE")
        } else {
            print("DB FILE NOT AVAILABLE")
        }
    } else {
        print("DB FILE PATH NOT AVAILABLE")
    }
    
    wait(for: [fileFound], timeout: 5)

这并不是测试文件是否在正确的位置创建,而是测试文件是否已创建。

There is a code which finds the file in :
How to check if a file exists in the Documents directory in Swift?

I used it in a test as follows, which tests that my file is created and gives its location for further testing.

        let fileFound = XCTestExpectation (description: "Checking for DB File Found")
    
    
    
    let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
    let url = NSURL(fileURLWithPath: path)
    if let pathComponent = url.appendingPathComponent("filename.ext") {
        let filePath = pathComponent.path
        let fileManager = FileManager.default
        if fileManager.fileExists(atPath: filePath) {
            fileFound.fulfill()
            print("DB FILE AVAILABLE")
        } else {
            print("DB FILE NOT AVAILABLE")
        }
    } else {
        print("DB FILE PATH NOT AVAILABLE")
    }
    
    wait(for: [fileFound], timeout: 5)

This is not testing that the file is created in the right place, only that it is created.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文