运行游戏循环测试

如果游戏应用基于不同的界面框架构建而成,则可能很难实现游戏测试的自动化。借助游戏循环 (Game Loop) 测试,您可以将原生测试与 Test Lab 相集成,并在选定设备上轻松运行这些测试。本指南介绍如何围绕游戏循环测试做准备,以使用 Firebase Test Lab 运行测试。

游戏循环测试简介

什么是游戏循环测试?

游戏循环测试可模拟真实玩家的操作,通过一种快速且可扩缩的方式来验证游戏是否能为您的用户带来出色体验。循环是指对您的游戏应用运行完整测试或运行测试的一部分。您可以在模拟器上本地运行游戏循环测试,也可以在 Test Lab 中的一组设备上运行。游戏循环测试可用于如下目的:

  • 以最终用户玩游戏的方式运行游戏。您可以通过脚本来模拟用户输入,让用户挂机;也可以用 AI 来代替用户(例如,假设您已在一款赛车游戏中实现了 AI,那么就可以设置一个 AI 驾驶员来执行用户输入)。
  • 以最高画质设置运行游戏,看看哪些设备能支持这种设置。
  • 运行技术测试,例如编译多个着色器并运行,检查输出结果是否符合预期。

第 1 步:注册 Test Lab 的自定义网址方案

  1. 在 Xcode 中选择一个项目目标。

  2. 点击 Info(信息)标签页,然后添加新的 URL type(网址类型)。

  3. URL Schemes(网址方案)字段中,输入 firebase-game-loop。 为了注册自定义网址方案,您还可以将其添加到您项目的 Info.plist 配置文件中 <dict> 标记内的任意位置:

    <key>CFBundleURLTypes</key>
     <array>
         <dict>
             <key>CFBundleURLName</key>
             <string></string>
             <key>CFBundleTypeRole</key>
             <string>Editor</string>
             <key>CFBundleURLSchemes</key>
             <array>
                 <string>firebase-game-loop</string>
             </array>
         </dict>
     </array>
    

这样,您的应用就已经配置为使用 Test Lab 运行测试。

第 2 步:视需要配置您的应用

运行多个循环

如果您打算在测试中运行多个循环(也就是场景),则必须指定您希望启动时在应用中运行哪些循环。

在您的应用委托 (app delegate) 中,用以下代码替换 application(_:open:options:) 方法:

Swift

func application(_app: UIApplication,
                 open url: URL
                 options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    let components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
    if components.scheme == "firebase-game-loop" {
        // ...Enter Game Loop Test logic to override application(_:open:options:).
    }
    return true
}

Objective-C

- (BOOL)application:(UIApplication *)app
            openURL:(NSURL *)url
            options:(NSDictionary &lt;UIApplicationOpenURLOptionsKey, id&gt; *)options {
  if ([url.scheme isEqualToString:(@"firebase-game-loop")]) {
      // ...Enter Game Loop Test logic to override application(_:open:options:).
  }
}

当您在测试中运行多个循环时,当前循环将作为参数传递到用于启动应用的网址。您还可以通过解析用于提取自定义网址方案的 URLComponents 对象来获取当前循环编号:

Swift

if components.scheme == "firebase-game-loop" {
    // Iterate over all parameters and find the one with the key "scenario".
    let scenarioNum = Int(components.queryItems!.first(where: { $0.name == "scenario" })!.value!)!
    // ...Write logic specific to the current loop (scenarioNum).
}

Objective-C

if ([url.scheme isEqualToString:(@"firebase-game-loop")]) {
    // Launch the app as part of a game loop.
    NSURLComponents *components = [NSURLComponents componentsWithURL:url
                                             resolvingAgainstBaseURL:YES];
    for (NSURLQueryItem *item in [components queryItems]) {
        if ([item.name isEqualToString:@"scenario"]) {
            NSInteger scenarioNum = [item.value integerValue];
            // ...Write logic specific to the current loop (scenarioNum).
        }
    }
}

提前结束测试

默认情况下,即便所有循环都已执行完毕,在达到 5 分钟的超时时间前,游戏循环测试仍会持续运行。达到超时时间后,测试会结束并取消所有待处理的循环。您可以通过在应用的 AppDelegate 中调用 Test Lab 的自定义网址方案 firebase-game-loop-complete 来加速测试或提前结束测试。例如:

Swift

/// End the loop by calling our custom url scheme.
func finishLoop() {
    let url = URL(string: "firebase-game-loop-complete://")!
    UIApplication.shared.open(url)
}

Objective-C

- (void)finishLoop {
  UIApplication *app = [UIApplication sharedApplication];
  [app openURL:[NSURL URLWithString:@"firebase-game-loop-complete://"]
      options:@{}
completionHandler:^(BOOL success) {}];
}

您的游戏循环测试会终止当前循环,并执行下一个循环。 如果不再有需要运行的其他循环,测试就会结束。

写入自定义测试结果

您可以对游戏循环测试进行配置,以便将自定义测试结果写入您设备的文件系统。这样,当测试开始运行时,Test Lab 会将结果文件存储在测试设备中的 GameLoopsResults 目录下(您必须自行创建此目录)。测试结束后,Test Lab 会将 GameLoopResults 目录中的所有文件移至项目的存储桶。设置测试时,请注意以下事项:

  • 无论文件类型、大小或数量如何,所有结果文件都会上传。

  • 只有在测试中的所有循环都运行完毕之后,Test Lab 才会处理您的测试结果,因此,如果您的测试包含多个写入输出结果的循环,请确保将各循环的结果以附加的形式添加到一个唯一的结果文件中,或者分别为每个循环创建一个结果文件。这样可以避免覆盖上一个循环的结果。

如需设置测试以写入自定义测试结果,请执行以下操作:

  1. 在应用的 Documents 目录下,创建一个名为 GameLoopResults 的目录。

  2. 在应用代码的任意位置(例如,您的应用委托)添加以下内容:

    Swift

    /// Write to a results file.
    func writeResults() {
      let text = "Greetings from game loops!"
      let fileName = "results.txt"
      let fileManager = FileManager.default
      do {
    
      let docs = try fileManager.url(for: .documentDirectory,
                                     in: .userDomainMask,
                                     appropriateFor: nil,
                                     create: true)
      let resultsDir = docs.appendingPathComponent("GameLoopResults")
      try fileManager.createDirectory(
          at: resultsDir,
          withIntermediateDirectories: true,
          attributes: nil)
      let fileURL = resultsDir.appendingPathComponent(fileName)
      try text.write(to: fileURL, atomically: false, encoding: .utf8)
      } catch {
        // ...Handle error writing to file.
      }
    }
    

    Objective-C

    /// Write to a results file.
    - (void)writeResults:(NSString *)message {
        // Locate and create the results directory (if it doesn't exist already).
        NSFileManager *manager = [NSFileManager defaultManager];
        NSURL* url = [[manager URLsForDirectory:NSDocumentDirectory
                                      inDomains:NSUserDomainMask] lastObject];
        NSURL* resultsDir = [url URLByAppendingPathComponent:@"GameLoopResults"
                                                 isDirectory:YES];
        [manager createDirectoryAtURL:resultsDir
          withIntermediateDirectories:NO
                           attributes:nil
                                error:nil];
    
        // Write the result message to a text file.
        NSURL* resultFile = [resultsDir URLByAppendingPathComponent:@"result.txt"];
        if ([manager fileExistsAtPath:[resultFile path]]) {
            // Append to the existing file
            NSFileHandle *handle = [NSFileHandle fileHandleForWritingToURL:resultFile
                                                                     error:nil];
            [handle seekToEndOfFile];
            [handle writeData:[message dataUsingEncoding:NSUTF8StringEncoding]];
            [handle closeFile];
        } else {
            // Create and write to the file.
            [message writeToURL:resultFile
                     atomically:NO
                       encoding:NSUTF8StringEncoding error:nil];
        }
    }
    

第 3 步:对应用签名

  1. 确保应用中的所有工件均已签名。例如,您可以指定签名设置(如预配配置文件和身份),通过 Xcode 执行此操作。如需了解详情,请参阅:Apple 代码签名

第 4 步:打包应用以备上传

为您的应用生成 IPA 文件(稍后您需要找到此文件)。

  1. 在随即显示的下拉菜单中,点击 Product(产品)> Archive(归档)。 选择最新的归档文件,然后点击 Distribute App(分发应用)。

  2. 在随即显示的窗口中,点击 Development(开发)> Next(下一步)

  3. 可选:如需加快构建速度,请取消选择 Rebuild from Bitcode(从 Bitcode 重新构建)选项,然后点击 Next(下一步)。Test Lab 不需要精简或重建应用即可运行测试,因此您可以放心地停用该选项。

  4. 点击 Export(导出),然后输入要在其中下载应用 IPA 文件的目录。

第 5 步:验证应用签名

  1. 验证应用签名,具体方法是解压缩 .ipa 文件,然后运行 codesign --verify --deep --verbose /path/to/MyApp.app,其中“MyApp”是解压缩后的文件夹内的应用名称(因项目而异)。预期输出为 MyApp.app: valid on disk

第 6 步:在本地运行测试

您可以在本地运行您的测试,以便在使用 Test Lab 运行该测试之前检查其行为。如需在本地测试,请在模拟器中加载您的游戏应用并运行以下命令:

xcrun simctl openurl SIMULATOR_UDID firebase-game-loop://
  • 您可以通过运行 instruments -s devices 命令找到模拟器的 UDID。

  • 如果只有一个模拟器正在运行,请输入特殊字符串 "booted" 来替代 SIMULATOR_UDID

如果测试包含多个循环,您可以通过将循环编号传递给 scenario 标志来指定要运行的循环。请注意,在本地运行测试时,一次只能运行一个循环。例如,如果要运行循环 1、2 和 5,则必须为每个循环运行单独的命令:

xcrun simctl openurl SIMULATOR_UDID firebase-game-loop://?scenario=1
xcrun simctl openurl SIMULATOR_UDID firebase-game-loop://?scenario=2
xcrun simctl openurl SIMULATOR_UDID firebase-game-loop://?scenario=5

后续步骤

使用 Firebase 控制台gcloud CLI 运行测试。