使用Swift包管理器在单元测试中使用资源

lztngnrs  于 2023-06-21  发布在  Swift
关注(0)|答案(8)|浏览(138)

我试图在单元测试中使用一个资源文件,并使用Bundle.path访问它,但它返回nil。
MyProjectTests.swift中的调用返回nil:

Bundle(for: type(of: self)).path(forResource: "TestAudio", ofType: "m4a")

这是我的项目层次结构。我还尝试将TestAudio.m4a移动到Resources文件夹:

├── Package.swift
├── Sources
│   └── MyProject
│       ├── ...
└── Tests
    └── MyProjectTests
        ├── MyProjectTests.swift
        └── TestAudio.m4a

以下是我的包描述:

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "MyProject",
    products: [
        .library(
            name: "MyProject",
            targets: ["MyProject"])
    ],
    targets: [
        .target(
            name: "MyProject",
            dependencies: []
        ),
        .testTarget(
            name: "MyProjectTests",
            dependencies: ["MyProject"]
        ),
    ]
)

我使用的是Swift 4和Swift Package Manager Description API版本4。

puruo6ea

puruo6ea1#

Swift 5.3

请参阅Apple文档:"Bundling Resources with a Swift Package"

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

  • 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: "Example",
      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 file in Sources/Example/Resources/*
        .process("Resources"),
      ]),
    .testTarget(
      name: "ExampleTests",
      dependencies: [Example],
      resources: [
        // Copy Tests/ExampleTests/Resources directories as-is. 
        // Use to retain directory structure.
        // Will be at top level in bundle.
        .copy("Resources"),
      ]),

报告的问题和可能的解决方法

Xcode中的类似方法是:
1.手动将Resources引用文件夹添加到Xcode项目,
1.添加Xcode构建阶段copy,将Resource放入某个*.bundle目录,
1.添加一些自定义的#ifdef XCODE_BUILD编译器指令,以便Xcode构建使用资源。

#if XCODE_BUILD
extension Foundation.Bundle {
    
    /// Returns resource bundle as a `Bundle`.
    /// Requires Xcode copy phase to locate files into `ExecutableName.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("Foundation.Bundle.module could not load resource bundle: \(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
5lhxktic

5lhxktic2#

SwiftPM(5.1)不支持原生资源yet,但是...
当单元测试运行时,可以预期存储库是可用的,因此只需使用从#file派生的内容加载资源。这适用于SwiftPM的所有现存版本。

let thisSourceFile = URL(fileURLWithPath: #file)
let thisDirectory = thisSourceFile.deletingLastPathComponent()
let resourceURL = thisDirectory.appendingPathComponent("TestAudio.m4a")

在测试之外的情况下,存储库在运行时不存在,仍然可以包含资源,尽管以二进制大小为代价。任何文件都可以通过将其表示为字符串字面量中的base 64数据来嵌入Swift源代码。Workspace是一个开源工具,可以自动化该过程:$ workspace refresh resources .(免责声明:我是作者)。

tuwxkamq

tuwxkamq3#

Bundle.module在设置了正确的文件结构和依赖关系后开始为我工作。

测试目标文件结构:

Package.swift中的依赖设置:

targets: [
    // Targets are the basic building blocks of a package. A target can define a module or a test suite.
    // Targets can depend on other targets in this package, and on products in packages this package depends on.
    .target(
        name: "Parser",
        dependencies: []),
    .testTarget(
        name: "ParserTests",
        dependencies: ["Parser"],
        resources: [
            .copy("Resources/test.txt")
        ]
    ),
]

在项目中的用法:

private var testData: Data {
    let url = Bundle.module.url(forResource: "test", withExtension: "txt")!
    let data = try! Data(contentsOf: url)
    return data
}
fjnneemd

fjnneemd4#

  • Swift 5.2及更早版本的Swift脚本方法 *

Swift包管理器(SwiftPM)

可以在macOS和Linux的SwiftPM单元测试中使用资源,并提供一些额外的设置和自定义脚本。以下是对一种可能方法的描述:

  • SwiftPM尚未提供处理资源的机制。下面是在包中使用测试资源TestResources/的可行方法;和,还提供了一致的TestScratch/目录,用于在需要时创建测试文件。*

设置:

  • PackageName/目录中添加测试资源目录TestResources/
  • 对于Xcode的使用,将测试资源添加到测试包目标的项目“Build Phases”中。
  • 项目编辑器>目标> CxSQLiteFrameworkTests >构建阶段>复制文件:目标资源,+添加文件
  • 对于命令行使用,设置Bash别名,其中包括swift-copy-testresources。
  • 将swift-copy-testresources.swift的可执行版本放在包含$PATH的适当路径上。
  • Ubuntu:nano ~/bin/ swift-copy-testresources.swift
    Bash别名
  • macOS:nano .bash_profile*
alias swiftbuild='swift-copy-testresources.swift $PWD; swift build -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.13";'
alias swifttest='swift-copy-testresources.swift $PWD; swift test -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.13";'
alias swiftxcode='swift package generate-xcodeproj --xcconfig-overrides Package.xcconfig; echo "REMINDER: set Xcode build system."'
  • Ubuntu:nano ~/.profile。append结束。将/opt/swift/current更改为给定系统的Swift安装位置。*
#############
### SWIFT ###
#############
if [ -d "/opt/swift/current/usr/bin" ] ; then
    PATH="/opt/swift/current/usr/bin:$PATH"
fi

alias swiftbuild='swift-copy-testresources.swift $PWD; swift build;'
alias swifttest='swift-copy-testresources.swift $PWD; swift test;'

脚本:swift-copy-testresources.shchmod +x

#!/usr/bin/swift

// FILE: swift-copy-testresources.sh
// verify swift path with "which -a swift"
// macOS: /usr/bin/swift 
// Ubuntu: /opt/swift/current/usr/bin/swift 
import Foundation

func copyTestResources() {
    let argv = ProcessInfo.processInfo.arguments
    // for i in 0..<argv.count {
    //     print("argv[\(i)] = \(argv[i])")
    // }
    let pwd = argv[argv.count-1]
    print("Executing swift-copy-testresources")
    print("  PWD=\(pwd)")
    
    let fm = FileManager.default
    
    let pwdUrl = URL(fileURLWithPath: pwd, isDirectory: true)
    let srcUrl = pwdUrl
        .appendingPathComponent("TestResources", isDirectory: true)
    let buildUrl = pwdUrl
        .appendingPathComponent(".build", isDirectory: true)
    let dstUrl = buildUrl
        .appendingPathComponent("Contents", isDirectory: true)
        .appendingPathComponent("Resources", isDirectory: true)
    
    do {
        let contents = try fm.contentsOfDirectory(at: srcUrl, includingPropertiesForKeys: [])
        do { try fm.removeItem(at: dstUrl) } catch { }
        try fm.createDirectory(at: dstUrl, withIntermediateDirectories: true)
        for fromUrl in contents {
            try fm.copyItem(
                at: fromUrl, 
                to: dstUrl.appendingPathComponent(fromUrl.lastPathComponent)
            )
        }
    } catch {
        print("  SKIP TestResources not copied. ")
        return
    }
            
    print("  SUCCESS TestResources copy completed.\n  FROM \(srcUrl)\n  TO \(dstUrl)")
}

copyTestResources()

测试实用代码

////////////////
// MARK: - Linux
//////////////// 
#if os(Linux)

// /PATH_TO_PACKAGE/PackageName/.build/TestResources
func getTestResourcesUrl() -> URL? {
    guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
        else { return nil }
    let packageUrl = URL(fileURLWithPath: packagePath)
    let testResourcesUrl = packageUrl
        .appendingPathComponent(".build", isDirectory: true)
        .appendingPathComponent("TestResources", isDirectory: true)
    return testResourcesUrl
} 

// /PATH_TO_PACKAGE/PackageName/.build/TestScratch
func getTestScratchUrl() -> URL? {
    guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
        else { return nil }
    let packageUrl = URL(fileURLWithPath: packagePath)
    let testScratchUrl = packageUrl
        .appendingPathComponent(".build")
        .appendingPathComponent("TestScratch")
    return testScratchUrl
}

// /PATH_TO_PACKAGE/PackageName/.build/TestScratch
func resetTestScratch() throws {
    if let testScratchUrl = getTestScratchUrl() {
        let fm = FileManager.default
        do {_ = try fm.removeItem(at: testScratchUrl)} catch {}
        _ = try fm.createDirectory(at: testScratchUrl, withIntermediateDirectories: true)
    }
}

///////////////////
// MARK: - macOS
///////////////////
#elseif os(macOS)

func isXcodeTestEnvironment() -> Bool {
    let arg0 = ProcessInfo.processInfo.arguments[0]
    // Use arg0.hasSuffix("/usr/bin/xctest") for command line environment
    return arg0.hasSuffix("/Xcode/Agents/xctest")
}

// /PATH_TO/PackageName/TestResources
func getTestResourcesUrl() -> URL? {
    let testBundle = Bundle(for: CxSQLiteFrameworkTests.self)
    let testBundleUrl = testBundle.bundleURL
    
    if isXcodeTestEnvironment() { // test via Xcode 
        let testResourcesUrl = testBundleUrl
            .appendingPathComponent("Contents", isDirectory: true)
            .appendingPathComponent("Resources", isDirectory: true)
        return testResourcesUrl            
    }
    else { // test via command line
        guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
            else { return nil }
        let packageUrl = URL(fileURLWithPath: packagePath)
        let testResourcesUrl = packageUrl
            .appendingPathComponent(".build", isDirectory: true)
            .appendingPathComponent("TestResources", isDirectory: true)
        return testResourcesUrl
    }
} 

func getTestScratchUrl() -> URL? {
    let testBundle = Bundle(for: CxSQLiteFrameworkTests.self)
    let testBundleUrl = testBundle.bundleURL
    if isXcodeTestEnvironment() {
        return testBundleUrl
            .deletingLastPathComponent()
            .appendingPathComponent("TestScratch")
    }
    else {
        return testBundleUrl
            .deletingLastPathComponent()
            .deletingLastPathComponent()
            .deletingLastPathComponent()
            .appendingPathComponent("TestScratch")
    }
}

func resetTestScratch() throws {
    if let testScratchUrl = getTestScratchUrl() {
        let fm = FileManager.default
        do {_ = try fm.removeItem(at: testScratchUrl)} catch {}
        _ = try fm.createDirectory(at: testScratchUrl, withIntermediateDirectories: true)
    }
}

#endif

文件位置:

Linux

swift buildswift test期间,进程环境变量PWD提供了一个程序包根目录…/PackageName的路径。将PackageName/TestResources/文件复制到$PWD/.buid/TestResources。如果在测试运行时使用TestScratch/目录,则在$PWD/.buid/TestScratch中创建。

.build/
├── debug -> x86_64-unknown-linux/debug
...
├── TestResources
│   └── SomeTestResource.sql      <-- (copied from TestResources/)
├── TestScratch
│   └── SomeTestProduct.sqlitedb  <-- (created by running tests)
└── x86_64-unknown-linux
    └── debug
        ├── PackageName.build/
        │   └── ...
        ├── PackageNamePackageTests.build
        │   └── ...
        ├── PackageNamePackageTests.swiftdoc
        ├── PackageNamePackageTests.swiftmodule
        ├── PackageNamePackageTests.xctest  <-- executable, not Bundle
        ├── PackageName.swiftdoc
        ├── PackageName.swiftmodule
        ├── PackageNameTests.build
        │   └── ...
        ├── PackageNameTests.swiftdoc
        ├── PackageNameTests.swiftmodule
        └── ModuleCache ...

macOS CLI

.build/
|-- TestResources/
|   `-- SomeTestResource.sql      <-- (copied from TestResources/)
|-- TestScratch/
|   `-- SomeTestProduct.sqlitedb  <-- (created by running tests)
...
|-- debug -> x86_64-apple-macosx10.10/debug
`-- x86_64-apple-macosx10.10
    `-- debug
        |-- PackageName.build/
        |-- PackageName.swiftdoc
        |-- PackageName.swiftmodule
        |-- PackageNamePackageTests.xctest
        |   `-- Contents
        |       `-- MacOS
        |           |-- PackageNamePackageTests
        |           `-- PackageNamePackageTests.dSYM
        ...
        `-- libPackageName.a

macOS Xcode

PackageName/TestResources/文件作为构建阶段的一部分复制到测试包Contents/Resources文件夹中。如果在测试过程中使用,TestScratch/将放在*xctest捆绑包旁边。

Build/Products/Debug/
|-- PackageNameTests.xctest/
|   `-- Contents/
|       |-- Frameworks/
|       |   |-- ...
|       |   `-- libswift*.dylib
|       |-- Info.plist
|       |-- MacOS/
|       |   `-- PackageNameTests
|       `-- Resources/               <-- (aka TestResources/)
|           |-- SomeTestResource.sql <-- (copied from TestResources/)
|           `-- libswiftRemoteMirror.dylib
`-- TestScratch/
    `-- SomeTestProduct.sqlitedb     <-- (created by running tests)

我还在004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref上发布了一个相同方法的GitHubGist

5vf7fwbs

5vf7fwbs5#

从Swift 5.3开始,由于SE-0271,您可以通过在.target声明中添加resources来在Swift包管理器上添加bundle资源。
示例:

.target(
   name: "HelloWorldProgram",
   dependencies: [], 
   resources: [.process(Images), .process("README.md")]
)

如果您想了解更多,我已经写了一篇关于medium文章,讨论这个主题。我没有专门讨论.testTarget,但看看swift的提案,它看起来很像。

a1o7rhls

a1o7rhls6#

我找到了另一个解决方案,看看this file
可以使用路径创建一个bundle,例如:

let currentBundle = Bundle.allBundles.filter() { $0.bundlePath.hasSuffix(".xctest") }.first!
let realBundle = Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/MyProjectTests/Resources")

这有点难看,但如果你想避免Makefile,它是有效的。

cigdeys3

cigdeys37#

我正在使用:

extension Bundle {
    func locateFirst(forResource: String, withExtension: String) -> URL? {
        for b in Bundle.allBundles {
            if let u = b.url(forResource: forResource, withExtension: withExtension) {
                return u
            }
        }
        return nil
    }

}

然后调用locateFirst,它给出第一个项目。例如:

let p12 = Bundle().locateFirst(forResource: "Certificates", withExtension: "p12")!
v64noz0r

v64noz0r8#

A提出了一个简单的解决方案,适用于传统的Swift和未来的Swift:
1.在项目的根目录中添加资产
1.在Swift代码中:ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/payload.json").path
1.在Xcode和Swift中运行,内置终端或github操作🎉https://eon.codes/blog/2020/01/04/How-to-include-assets-with-swift-package-manager/https://github.com/eonist/ResourceHelper/

相关问题