【问题标题】:How to let the app know if it's running Unit tests in a pure Swift project?如何让应用知道它是否在纯 Swift 项目中运行单元测试?
【发布时间】:2015-02-14 13:06:42
【问题描述】:

在 Xcode 6.1 中运行测试时,一件烦人的事情是整个应用程序必须运行并启动它的故事板和根视图控制器。在我的应用程序中,这会运行一些获取 API 数据的服务器调用。但是,我不希望应用在运行测试时这样做。

随着预处理器宏的消失,让我的项目知道它是在运行测试而不是普通启动时最好的方法是什么?我通常使用 command + U 并在机器人上运行它们。

伪代码:

// Appdelegate.swift
if runningTests() {
   return
} else {
   // do ordinary api calls
}

【问题讨论】:

  • “整个应用程序必须运行并启动它的故事板和根视图控制器”对吗?我没有测试过,但对我来说似乎不合适。嗯……
  • 是的,应用程序确实完成启动运行以及根视图控制器的 viewdidload
  • 啊,刚刚测试过。没想到是这样的哈哈。是什么导致您的测试出现问题?也许还有其他方法可以解决它?
  • 我只需要让应用程序知道它在运行时会考虑到测试,所以像旧的预处理器宏这样的标志可以工作,但 swift 不支持它们。
  • 是的,但是为什么你“需要”这样做?是什么让您认为您需要这样做?

标签: ios swift xcode xctest


【解决方案1】:

如果您想拥有曾经被称为纯“逻辑测试”的东西,Elvind 的回答还不错。如果您仍想运行包含宿主应用程序但有条件地执行或不执行代码,具体取决于是否运行测试,您可以使用以下方法来检测是否已注入测试包:

if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
     // Code only executes when tests are running
}

我使用了in this answer 所述的条件编译标志,因此运行时成本仅在调试构建中产生:

#if DEBUG
    if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
        // Code only executes when tests are running
    }
#endif

编辑 Swift 3.0

if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
    // Code only executes when tests are running
}

【讨论】:

  • 在最新的 Xcode 中不起作用 - 环境变量名称似乎已更改
  • 不确定它何时停止工作,但在 Xcode 7.3 中,我现在使用 XCTestConfigurationFilePath 环境键而不是 XCInjectBundle
  • 感谢@ospr,我已经编辑了使用 Xcode 7.3 的答案
  • @tkuichooseyou po ProcessInfo.processInfo.environment 没有密钥 XCTestConfigurationFilePath。你能分享你的代码吗?再次检查 UITest 目标
  • 在用于单元测试的 Xcode 9.2 和 Xcode 9.3 beta 中就像魅力一样工作
【解决方案2】:

我在应用程序中使用它:didFinishLaunchingWithOptions:

// Return if this is a unit test
if let _ = NSClassFromString("XCTest") {
    return true
}

【讨论】:

  • 这仅适用于单元测试,不适用于新的 Xcode 7 UI 测试。
【解决方案3】:

您可以在没有宿主应用程序本身的情况下运行测试,而不是检查测试是否正在运行以避免副作用。转到项目设置 -> 选择测试目标 -> 常规 -> 测试 -> 主机应用程序 -> 选择“无”。 只需记住包含运行测试所需的所有文件,以及主机应用目标通常包含的库。

【讨论】:

  • 删除主机应用程序后如何为目标添加桥接头?
  • 试过这个,它打破了我的测试,直到我得到这个建议做相反的答案:stackoverflow.com/a/30939244/1602270
  • 如果我这样做,我可以测试 cloudKit(例如),所以我的解决方案是在 applicationDidFinishLaunching 中检测我是否正在测试,如果是,则返回而不分配主类应用程序。
【解决方案4】:

其他,在我看来更简单的方式:

您编辑方案以将布尔值作为启动参数传递给您的应用。像这样:

所有启动参数都会自动添加到您的NSUserDefaults

您现在可以像这样得到 BOOL:

BOOL test = [[NSUserDefaults standardUserDefaults] boolForKey:@"isTest"];

【讨论】:

  • 看来这是迄今为止我发现的最干净的方法。我们不必将所有应用程序文件添加到测试目标,也不必依赖一些奇怪的解决方案来检查"XCTestConfigurationFilePath"NSClassFromString("XCTest")。我已经在 Swift 中使用全局函数 func isRunningTests() -> Bool { return UserDefaults.standard.bool(forKey: "isRunningTests") } 实现了这个解决方案
  • 在我看来,如果以合理的努力删除主机应用程序为时已晚,那么这个问题的最佳答案。
【解决方案5】:
var isRunningTests: Bool {
    return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

用法

if isRunningTests {
    return "lena.bmp"
}
return "facebook_profile_photo.bmp"

【讨论】:

【解决方案6】:

我相信想知道您是否在测试中运行是完全合法的。有很多原因可以帮助您。例如,在运行测试时,我会提前从 App Delegate 中的 application-did/will-finish-launching 方法返回,从而使测试开始更快,因为代码与我的单元测试无关。然而,由于许多其他原因,我不能进行纯粹的“逻辑”测试。

我曾经使用上面@Michael McGuire 描述的出色技术。但是,我注意到它在 Xcode 6.4/iOS8.4.1 附近停止了对我的工作(也许它更早坏了)。

也就是说,在我的框架的测试目标中运行测试时,我不再看到 XCInjectBundle。也就是说,我在一个测试框架的测试目标中运行。

因此,利用@Fogmeister 建议的方法,我的每个测试方案现在都设置了一个我可以检查的环境变量。

然后,这是我在一个名为 APPSTargetConfiguration 的类上的一些代码,它可以为我回答这个简单的问题。

static NSNumber *__isRunningTests;

+ (BOOL)isRunningTests;
{
    if (!__isRunningTests) {
        NSDictionary *environment = [[NSProcessInfo processInfo] environment];
        NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
        __isRunningTests = @([isRunningTestsValue isEqualToString:@"YES"]);
    }

    return [__isRunningTests boolValue];
}

使用这种方法的一个警告是,如果您从主应用程序方案运行测试,正如 XCTest 允许的那样(也就是说,不选择您的测试方案之一),您将不会获得此环境变量设置。

【讨论】:

  • 而不是把它添加到'Run'中,如果我们将它添加到'Test'中对所有方案来说不是更有帮助吗?这样,isRunningTests 将适用于所有方案。
  • @VishalSingh 是的,我相信这样更干净。你有没有尝试过这种方法?让我们知道它是否同样适合您。
  • 安全警告:我会把它包装在一个大的#if DEBUG 子句中,否则你会让“测试”后门进入生产环境
【解决方案7】:

这是快速的方法。

extension Thread {
  var isRunningXCTest: Bool {
    for key in self.threadDictionary.allKeys {
      guard let keyAsString = key as? String else {
        continue
      }

      if keyAsString.split(separator: ".").contains("xctest") {
        return true
      }
    }
    return false
  }
}

这就是你使用它的方式:

if Thread.current.isRunningXCTest {
  // test code goes here
} else {
  // other code goes here
}

这里是全文:https://medium.com/@theinkedengineer/check-if-app-is-running-unit-tests-the-swift-way-b51fbfd07989

【讨论】:

  • 那个for循环可以这样Swiftilized:return threadDictionary.allKeys.anyMatch { ($0 as? String)?.split(separator: ".").contains("xctest") == true }
  • anyMatch 应该是 contains(where:),并且您也不需要返回,因为它现在是一个班轮:threadDictionary.allKeys.contains { ($0 as? String)?.split(separator: ".").contains("xctest") == true }
  • 还会补充一点,如果您想检测它是否是标记为情节提要入口点的视图控制器的生命周期功能中的单元测试,这将不起作用。线程字典还不包含这些键。在我的初始视图控制器的断点上,我可以看到 NSClassFromString 变体有效,ProcessInfo 变体也有效。
【解决方案8】:

@Jessy 和@Michael McGuire 的组合方法

(在开发框架时,公认的答案对您没有帮助)

代码如下:

#if DEBUG
        if (NSClassFromString(@"XCTest") == nil) {
            // Your code that shouldn't run under tests
        }
#else
        // unconditional Release version
#endif

【讨论】:

  • 这更好!更短且兼容框架!
  • 正在开发一个 Swift 包,这对我有用
【解决方案9】:

现在可以使用了

if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
            // Code only executes when tests are not running
}

该选项在 Xcode 12.5 中已弃用

首先添加变量进行测试:

并在您的代码中使用它:

 if ProcessInfo.processInfo.environment["IS_UNIT_TESTING"] == "1" {
                 // Code only executes when tests are running
 } 

【讨论】:

    【解决方案10】:

    这是我在 Swift 4 / Xcode 9 中用于单元测试的一种方法。它基于Jesse's answer

    要完全阻止故事板被加载并不容易,但是如果你在 didFinishedLaunching 的开头添加它,那么它会让你的开发人员非常清楚发生了什么:

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions:
                     [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        #if DEBUG
        if let _ = NSClassFromString("XCTest") {
            // If we're running tests, don't launch the main storyboard as
            // it's confusing if that is running fetching content whilst the
            // tests are also doing so.
            let viewController = UIViewController()
            let label = UILabel()
            label.text = "Running tests..."
            label.frame = viewController.view.frame
            label.textAlignment = .center
            label.textColor = .white
            viewController.view.addSubview(label)
            self.window!.rootViewController = viewController
            return true
        }
        #endif
    

    (对于您确实希望应用正常启动的 UI 测试,您显然不应该这样做!)

    【讨论】:

      【解决方案11】:

      您可以根据此处的方案将运行时参数传递给应用程序...

      但我会怀疑它是否真的需要。

      【讨论】:

        【解决方案12】:

        我一直使用的方法在 Xcode 12 beta 1 中停止工作。在尝试了这个问题的所有基于构建的答案后,我受到了@ODB 答案的启发。这是一个相当简单的解决方案的 Swift 版本,适用于真实设备和模拟器。它也应该是相当“发布证明”。

        插入测试设置:

        let app = XCUIApplication()
        app.launchEnvironment.updateValue("YES", forKey: "UITesting")
        app.launch()
        

        在应用中插入:

        let isTesting: Bool = (ProcessInfo.processInfo.environment["UITesting"] == "YES")
        

        使用它:

            if isTesting {
                // Only if testing
            } else {
                // Only if not testing
            }
        

        【讨论】:

          【解决方案13】:

          其中一些方法不适用于 UITest,如果您基本上是使用应用代码本身进行测试(而不是将特定代码添加到 UITest 目标中)。

          我最终在测试的 setUp 方法中设置了一个环境变量:

          XCUIApplication *testApp = [[XCUIApplication alloc] init];
          
          // set launch environment variables
          NSDictionary *customEnv = [[NSMutableDictionary alloc] init];
          [customEnv setValue:@"YES" forKey:@"APPS_IS_RUNNING_TEST"];
          testApp.launchEnvironment = customEnv;
          [testApp launch];
          

          请注意,这对我的测试是安全的,因为我目前没有使用任何其他的 launchEnvironment 值;如果你这样做了,你当然会想先复制任何现有的值。

          然后在我的应用程序代码中,如果/当我想在测试期间排除某些功能时,我会查找此环境变量:

          BOOL testing = false;
          ...
          if (! testing) {
              NSDictionary *environment = [[NSProcessInfo processInfo] environment];
              NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
              testing = [isRunningTestsValue isEqualToString:@"YES"];
          }
          

          注意 - 感谢 RishiG 的评论给了我这个想法;我只是将其扩展为一个示例。

          【讨论】:

            【解决方案14】:

            显然在 Xcode12 中,如果您使用新的 XCTestPlan,我们需要在环境键 XCTestBundlePath 中搜索而不是 XCTestConfigurationFilePath

            【讨论】:

              【解决方案15】:

              我在我的 SwiftUI Scene.body (Xcode 12.5) 中使用它:

              if UserDefaults.standard.value(forKey: "XCTIDEConnectionTimeout") == nil {
                // not unit testing
              } else {
                // unit testing
              }
              

              【讨论】:

                【解决方案16】:

                为我工作:

                目标-C

                [[NSProcessInfo processInfo].environment[@"DYLD_INSERT_LIBRARIES"] containsString:@"libXCTTargetBootstrapInject"]

                斯威夫特: ProcessInfo.processInfo.environment["DYLD_INSERT_LIBRARIES"]?.contains("libXCTTargetBootstrapInject") ?? false

                【讨论】:

                  猜你喜欢
                  • 2021-06-06
                  • 2014-09-16
                  • 1970-01-01
                  • 2021-07-31
                  • 1970-01-01
                  • 2015-04-12
                  • 2010-09-28
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多