Wie kann ich der App mitteilen, ob Unit-Tests in einem reinen Swift-Projekt ausgeführt werden?

81

Eine ärgerliche Sache beim Ausführen von Tests in Xcode 6.1 ist, dass die gesamte App ihr Storyboard und ihren Root View Controller ausführen und starten muss. In meiner App werden einige Serveraufrufe ausgeführt, die API-Daten abrufen. Ich möchte jedoch nicht, dass die App dies tut, wenn sie ihre Tests ausführt.

Was ist das Beste für mein Projekt, wenn Präprozessor-Makros weg sind, um zu wissen, dass es mit Tests gestartet wurde und kein gewöhnlicher Start? Ich führe sie normal mit command+ Uund auf einem Bot aus.

Pseudocode:

// Appdelegate.swift
if runningTests() {
   return
} else {
   // do ordinary api calls
}
bogen
quelle
"Die gesamte App muss ihr Storyboard und ihren Root View Controller ausführen und starten" ist das richtig? Ich habe es nicht getestet, aber es scheint mir nicht richtig zu sein. Hmm ...
Fogmeister
Ja, die Anwendung wurde nach dem Start ausgeführt und viewdidload für den Root-View-Controller ausgeführt
bogen
Ah, gerade getestet. Ich hätte nicht gedacht, dass das der Fall ist lol. Was ist das, was bei Ihren Tests ein Problem verursacht? Vielleicht gibt es einen anderen Weg, um es zu umgehen?
Fogmeister
Ich muss die App nur über Tests informieren, damit ein Flag wie die alten Präprozessor-Makros funktioniert, aber sie werden nicht schnell unterstützt.
Bogen
Ja, aber warum "brauchen" Sie das? Was lässt Sie denken, dass Sie das tun müssen?
Fogmeister

Antworten:

44

Anstatt zu überprüfen, ob die Tests ausgeführt werden, um Nebenwirkungen zu vermeiden, können Sie die Tests auch ohne die Host-App selbst ausführen. Gehen Sie zu Projekteinstellungen -> wählen Sie das Testziel aus -> Allgemein -> Testen -> Hostanwendung -> wählen Sie 'Keine'. Denken Sie daran, alle Dateien, die Sie zum Ausführen der Tests benötigen, sowie Bibliotheken einzuschließen, die normalerweise vom Host-App-Ziel enthalten sind.

Geben Sie hier die Bildbeschreibung ein

Eivind Rannem Bøhler
quelle
1
Wie kann ich Bridging-Header für das Ziel einschließen, nachdem die Host-Anwendung entfernt wurde?
Bhargav
Versuchte dies, es brach meine Tests, bis ich zu dieser Antwort kam, die genau das Gegenteil vorschlägt
eagle.dan.1349
Wenn ich das mache, kann ich CloudKit (zum Beispiel) testen, sodass die Lösung für mich darin besteht, in applicationDidFinishLaunching zu erkennen, ob ich teste, und wenn JA, dann zurückzukehren, ohne die Hauptklassen der App zuzuweisen.
user1105951
83

Elvinds Antwort ist nicht schlecht, wenn Sie so etwas wie reine "Logiktests" haben möchten. Wenn Sie Ihre enthaltende Hostanwendung weiterhin ausführen möchten, aber Code abhängig davon ausführen oder nicht ausführen möchten, je nachdem, ob Tests ausgeführt werden, können Sie Folgendes verwenden, um festzustellen, ob ein Testpaket injiziert wurde:

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

Ich habe ein bedingtes Kompilierungsflag verwendet, wie in dieser Antwort beschrieben, damit die Laufzeitkosten nur bei Debug-Builds anfallen:

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

Bearbeiten Sie Swift 3.0

if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
    // Code only executes when tests are running
}
Michael McGuire
quelle
3
Funktioniert nicht im neuesten Xcode - Name der Umgebungsvariablen scheint sich geändert zu haben
amleszk
7
Ich bin mir nicht sicher, wann dies nicht mehr funktioniert, aber mit Xcode 7.3 verwende ich jetzt den XCTestConfigurationFilePathUmgebungsschlüssel anstelle von XCInjectBundle.
Ospr
Danke @ospr, ich habe die Antwort bearbeitet, um mit Xcode 7.3 zu arbeiten
Michael McGuire
@tkuichooseyou po ProcessInfo.processInfo.environmenthat keinen Schlüssel XCTestConfigurationFilePath. Können Sie bitte Ihren Code teilen? Dies wird gegen ein UITest-Ziel überprüft
Tal Zion
@TalZion Entschuldigung, ich habe nicht bemerkt, dass Sie für ein UITest-Ziel gedacht sind. Haben Sie die folgende NSClassFromString("XCTest")Methode ausprobiert ?
tkuichooseyou
44

Ich benutze dies in der Anwendung: didFinishLaunchingWithOptions:

// Return if this is a unit test
if let _ = NSClassFromString("XCTest") {
    return true
}
Jesse
quelle
3
Dies gilt nur für Unit-Tests und nicht für die neuen Xcode 7-UI-Tests.
Jesse
32

Andere, meiner Meinung nach einfacher:

Sie bearbeiten Ihr Schema, um einen booleschen Wert als Startargument an Ihre App zu übergeben. So was:

Legen Sie Startargumente in Xcode fest

Alle Startargumente werden automatisch zu Ihrem hinzugefügt NSUserDefaults.

Sie können das BOOL jetzt wie folgt erhalten:

BOOL test = [[NSUserDefaults standardUserDefaults] boolForKey:@"isTest"];
iCaramba
quelle
2
Es scheint, dass dies der sauberste Weg ist, den ich bisher gefunden habe. Wir müssen nicht alle App-Dateien zum Testziel hinzufügen und müssen uns nicht auf eine seltsame Lösungsprüfung für "XCTestConfigurationFilePath"oder verlassen NSClassFromString("XCTest"). Ich habe diese Lösung in Swift mit einer globalen Funktion implementiertfunc isRunningTests() -> Bool { return UserDefaults.standard.bool(forKey: "isRunningTests") }
Kevin Hirsch
21

Ich glaube, es ist völlig legitim zu wissen, ob Sie in einem Test laufen oder nicht. Es gibt zahlreiche Gründe, warum dies hilfreich sein kann. Wenn ich beispielsweise Tests ausführe, kehre ich frühzeitig von den Methoden zum Starten der Anwendung / zum Beenden der Anwendung im App-Delegaten zurück, sodass die Tests für Code, der für meinen Komponententest nicht relevant ist, schneller beginnen. Aus vielen anderen Gründen kann ich jedoch keinen reinen "Logik" -Test machen.

Ich habe die ausgezeichnete Technik verwendet, die oben von @Michael McGuire beschrieben wurde. Mir ist jedoch aufgefallen, dass Xcode 6.4 / iOS8.4.1 für mich nicht mehr funktioniert (möglicherweise ist es früher kaputt gegangen).

Ich sehe nämlich das XCInjectBundle nicht mehr, wenn ich einen Test in einem Testziel für ein Framework von mir ausführe. Das heißt, ich laufe in einem Testziel, das ein Framework testet.

Unter Verwendung des von @Fogmeister vorgeschlagenen Ansatzes legt jedes meiner Testschemata jetzt eine Umgebungsvariable fest, nach der ich suchen kann.

Geben Sie hier die Bildbeschreibung ein

Dann ist hier ein Code, den ich für eine Klasse namens habe APPSTargetConfiguration, der diese einfache Frage für mich beantworten kann.

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];
}

Die einzige Einschränkung bei diesem Ansatz besteht darin, dass Sie diese Umgebungsvariable nicht erhalten, wenn Sie einen Test über Ihr Haupt-App-Schema ausführen, wie Sie es mit XCTest tun können (dh keines Ihrer Testschemata auswählen).

idStar
quelle
5
Wäre es nicht hilfreicher, wenn wir es für alle Schemata in "Test" hinzufügen, anstatt es in "Ausführen" hinzuzufügen? Auf diese Weise funktionieren isRunningTests in allen Schemata.
Vishal Singh
@ VishalSingh Ja, ich glaube das ist sauberer. Haben Sie diesen Ansatz ausprobiert? Lassen Sie uns wissen, ob es für Sie genauso gut funktioniert hat.
idStar
16
var isRunningTests: Bool {
    return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

Verwendung

if isRunningTests {
    return "lena.bmp"
}
return "facebook_profile_photo.bmp"
neoneye
quelle
Im Gegensatz zu einigen anderen Antworten funktioniert diese. Vielen Dank.
n13
8

Kombinierter Ansatz von @Jessy und @Michael McGuire

(Als akzeptierte Antwort hilft Ihnen nicht bei der Entwicklung eines Frameworks)

Also hier ist der Code:

#if DEBUG
        if (NSClassFromString(@"XCTest") == nil) {
            // Your code that shouldn't run under tests
        }
#else
        // unconditional Release version
#endif
m8labs
quelle
1
Das ist viel schöner! Kürzere und Framework-kompatible!
Blackjacx
Arbeitete an einem Swift-Paket und das funktionierte für mich
D. Greg
4

Hier ist eine Möglichkeit, die ich in Swift 4 / Xcode 9 für unsere Unit-Tests verwendet habe. Es basiert auf Jesses Antwort .

Es ist nicht einfach zu verhindern, dass das Storyboard überhaupt geladen wird. Wenn Sie dies jedoch zu Beginn von didFinishedLaunching hinzufügen, wird Ihren Entwicklern klar, was los ist:

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

(Sie sollten so etwas natürlich nicht für UI-Tests tun, bei denen die App wie gewohnt gestartet werden soll!)

JosephH
quelle
3

Sie können Laufzeitargumente je nach Schema hier an die App übergeben ...

Geben Sie hier die Bildbeschreibung ein

Aber ich würde fragen, ob es tatsächlich benötigt wird oder nicht.

Fogmeister
quelle
3

Dies ist der schnelle Weg, dies zu tun.

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
  }
}

Und so benutzt du es:

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

Hier ist der vollständige Artikel: https://medium.com/@theinkedengineer/check-if-app-is-running-unit-tests-the-swift-way-b51fbfd07989

Firas Safa
quelle
Die for-Schleife kann folgendermaßen beschleunigt werden:return threadDictionary.allKeys.anyMatch { ($0 as? String)?.split(separator: ".").contains("xctest") == true }
Roger Oba
2

Einige dieser Ansätze funktionieren nicht mit UITests und wenn Sie im Grunde genommen mit dem App-Code selbst testen (anstatt einem UITest-Ziel bestimmten Code hinzuzufügen).

Am Ende habe ich eine Umgebungsvariable in der setUp-Methode des Tests festgelegt:

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];

Beachten Sie, dass dies für meine Tests sicher ist, da ich derzeit keine anderen launchEnvironment-Werte verwende. In diesem Fall möchten Sie natürlich zuerst alle vorhandenen Werte kopieren.

Dann suche ich in meinem App-Code nach dieser Umgebungsvariablen, wenn ich während eines Tests einige Funktionen ausschließen möchte:

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

Hinweis - danke für den Kommentar von RishiG, der mir diese Idee gegeben hat; Ich habe das nur zu einem Beispiel erweitert.

ODB
quelle
1

Die Methode, die ich verwendet hatte, funktionierte in Xcode 12 Beta 1 nicht mehr. Nachdem ich alle Build-basierten Antworten auf diese Frage ausprobiert hatte, ließ ich mich von der Antwort von @ ODB inspirieren. Hier ist eine Swift-Version einer ziemlich einfachen Lösung, die sowohl für reale Geräte als auch für Simulatoren funktioniert. Es sollte auch ziemlich "Release Proof" sein.

In Testaufbau einfügen:

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

In App einfügen:

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

Um es zu benutzen:

    if isTesting {
        // Only if testing
    } else {
        // Only if not testing
    }
Chuck H.
quelle
0

Arbeitete für mich:

Ziel c

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

Schnell: ProcessInfo.processInfo.environment["DYLD_INSERT_LIBRARIES"]?.contains("libXCTTargetBootstrapInject") ?? false

user3761851
quelle
0

Fügen Sie zuerst eine Variable zum Testen hinzu:

Geben Sie hier die Bildbeschreibung ein

und verwenden Sie das in Ihrem Code:

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

Anscheinend müssen wir in Xcode12 im Umgebungsschlüssel suchen, XCTestBundlePathanstatt XCTestConfigurationFilePathwenn Sie den neuen verwendenXCTestPlan

MZapatae
quelle