Jak powiadomić aplikację, czy uruchamia testy jednostkowe w czystym projekcie Swift?


86

Jedną irytującą rzeczą podczas uruchamiania testów w Xcode 6.1 jest to, że cała aplikacja musi działać i uruchamiać swój scenorys i kontroler widoku głównego. W mojej aplikacji uruchamia to niektóre wywołania serwera, które pobierają dane API. Jednak nie chcę, aby aplikacja robiła to podczas uruchamiania testów.

Gdy zniknęły makra preprocesora, co jest najlepsze dla mojego projektu, aby był świadomy, że został uruchomiony na podstawie testów, a nie zwykłego uruchomienia? Uruchamiam je normalnie z command+ Ui na bocie.

Pseudo kod:

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

„Cała aplikacja musi działać i uruchamiać swój scenorys i główny kontroler widoku”, czy to prawda? Nie testowałem tego, ale nie wydaje mi się to właściwe. Hmm ...
Fogmeister

Tak, aplikacja zakończyła uruchamianie jest uruchamiana, a także viewdidload dla głównego kontrolera widoku
bogen,

Ach, właśnie przetestowany. Nie sądziłem, że tak było w przypadku lol. Co w tym jest, że powoduje problem w twoich testach? Może jest inny sposób obejścia tego?
Fogmeister,

Muszę tylko powiadomić aplikację o jej uruchomieniu z myślą o testach, aby flaga taka jak stare makra preprocesora działała, ale nie są obsługiwane w trybie szybkim.
bogen

Tak, ale dlaczego „musisz” to robić? Co sprawia, że ​​myślisz, że musisz to zrobić?
Fogmeister

Odpowiedzi:


44

Zamiast sprawdzać, czy testy są uruchomione, aby uniknąć skutków ubocznych, możesz uruchomić je bez samej aplikacji hosta. Przejdź do Ustawienia projektu -> wybierz cel testu -> Ogólne -> Testowanie -> Aplikacja hosta -> wybierz „Brak”. Pamiętaj tylko, aby uwzględnić wszystkie pliki potrzebne do uruchomienia testów, a także biblioteki zwykle zawarte w celu aplikacji Host.

wprowadź opis obrazu tutaj


1
Jak dołączyć nagłówki mostkowania dla celu po usunięciu aplikacji hosta?
Bhargav

1
Próbowałem tego, zepsuło moje testy, aż dotarłem do odpowiedzi, która proponuje zrobić coś
zupełnie

jeśli to zrobię, mogę przetestować cloudKit (na przykład), więc rozwiązaniem dla mnie jest wykrycie w applicationDidFinishLaunching, jeśli testuję, a jeśli TAK, to wrócę bez przydzielania głównych klas aplikacji.
user1105951

86

Odpowiedź Elvinda nie jest zła, jeśli chcesz mieć coś, co zwykło się nazywać „testami logiki”. Jeśli nadal chcesz uruchomić swoją aplikację hosta, ale warunkowo wykonać lub nie wykonać kodu, w zależności od tego, czy testy są uruchomione, możesz skorzystać z następujących czynności, aby wykryć, czy pakiet testowy został wstrzyknięty:

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

Użyłem flagi kompilacji warunkowej, jak opisano w tej odpowiedzi, aby koszt czasu wykonania był ponoszony tylko w kompilacjach debugowania:

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

Edytuj Swift 3.0

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

3
Nie działa w najnowszym Xcode - nazwa zmiennej środowiskowej chyba się zmieniła
amleszk

7
Nie jestem pewien, kiedy to przestało działać, ale w Xcode 7.3 używam teraz XCTestConfigurationFilePathklucza środowiska zamiast XCInjectBundle.
ospr

Dzięki @ospr, zredagowałem odpowiedź, aby pracować z Xcode 7.3
Michael McGuire

@tkuichooseyou po ProcessInfo.processInfo.environmentnie ma klucza XCTestConfigurationFilePath. Czy możesz udostępnić swój kod? Jest to sprawdzane pod kątem celu UITest
Tal Zion

@TalZion przepraszam, nie zdawałem sobie sprawy, że chodziło Ci o cel UITest. Czy wypróbowałeś NSClassFromString("XCTest")poniższą metodę?
tkuichooseyou

44

Używam tego w aplikacji: didFinishLaunchingWithOptions:

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

3
Dotyczy to tylko testów jednostkowych, a nie nowych testów interfejsu użytkownika Xcode 7.
Jesse

34

Inny, moim zdaniem prostszy sposób:

Edytuj swój schemat, aby przekazać wartość logiczną jako argument uruchamiania do swojej aplikacji. Lubię to:

Ustaw argumenty uruchamiania w Xcode

Wszystkie argumenty uruchamiania są automatycznie dodawane do pliku NSUserDefaults.

Możesz teraz uzyskać BOOL jak:

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

2
Wygląda na to, że to najczystszy sposób, jaki do tej pory znalazłem. Nie musimy dodawać wszystkich plików aplikacji do celu testowego i nie musimy polegać na jakimś dziwnym rozwiązaniu sprawdzającym "XCTestConfigurationFilePath"lub NSClassFromString("XCTest"). Wdrożyłem to rozwiązanie w Swift z funkcją globalnąfunc isRunningTests() -> Bool { return UserDefaults.standard.bool(forKey: "isRunningTests") }
Kevin Hirsch,

21

Uważam, że chęć sprawdzenia, czy bierzesz udział w teście, czy nie, jest całkowicie uzasadniona. Istnieje wiele powodów, dla których może to być pomocne. Na przykład podczas uruchamiania testów wcześnie wracam z metod uruchamiania aplikacji / will-finish-launching w delegacie aplikacji, dzięki czemu testy zaczynają się szybciej dla kodu niezwiązanego z moim testem jednostkowym. Jednak nie mogę przejść testu „logiki” z wielu innych powodów.

Używałem doskonałej techniki opisanej powyżej przez @Michael McGuire. Jednak zauważyłem, że przestał działać dla mnie w okolicach Xcode 6.4 / iOS8.4.1 (być może wcześniej się zepsuł).

Mianowicie, nie widzę już XCInjectBundle podczas uruchamiania testu wewnątrz celu testowego dla mojej struktury. Oznacza to, że działam wewnątrz celu testowego, który testuje strukturę.

Tak więc, stosując podejście sugerowane przez @Fogmeister, każdy z moich schematów testowych ustawia teraz zmienną środowiskową, którą mogę sprawdzić.

wprowadź opis obrazu tutaj

Następnie mam kod na zajęciach o nazwie, APPSTargetConfigurationktóry może odpowiedzieć na to proste pytanie.

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

Jedynym zastrzeżeniem związanym z tym podejściem jest to, że jeśli uruchomisz test z głównego schematu aplikacji, na co pozwoli XCTest (to znaczy nie wybierzesz jednego ze schematów testowych), nie otrzymasz tego zestawu zmiennych środowiskowych.


5
zamiast dodawać go w polu „Uruchom”, czy nie byłoby bardziej pomocne, gdybyśmy dodali go w polu „Test” dla wszystkich schematów? W ten sposób isRunningTests będzie działać we wszystkich schematach.
Vishal Singh

@VishalSingh Tak, uważam, że jest czystszy. Czy wypróbowałeś to podejście? Daj nam znać, czy to zadziałało równie dobrze dla Ciebie.
idStar

16
var isRunningTests: Bool {
    return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

Stosowanie

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

W przeciwieństwie do innych odpowiedzi, ta działa. Dziękuję Ci.
n13

8

Połączone podejście @Jessy i @Michael McGuire

(Jako przyjęta odpowiedź nie pomoże ci przy tworzeniu frameworka)

Oto kod:

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

1
To jest o wiele przyjemniejsze! Krótszy i kompatybilny z ramami!
blackjacx

Pracowałem nad Swift Package i to zadziałało dla mnie
D. Greg,

4

Oto sposób, w jaki używałem w Swift 4 / Xcode 9 do naszych testów jednostkowych. Opiera się na odpowiedzi Jessego .

Nie jest łatwo w ogóle uniemożliwić załadowanie storyboardu, ale jeśli dodasz to na początku didFinishedLaunching, wtedy programistom będzie jasne, co się dzieje:

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

(oczywiście nie powinieneś robić niczego takiego w przypadku testów interfejsu użytkownika, w których chcesz, aby aplikacja uruchamiała się normalnie!)


3

Możesz przekazać argumenty środowiska uruchomieniowego do aplikacji w zależności od schematu tutaj ...

wprowadź opis obrazu tutaj

Ale kwestionowałbym, czy jest to rzeczywiście potrzebne.


3

To jest szybki sposób na zrobienie tego.

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

A tak z tego korzystasz:

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

Oto cały artykuł: https://medium.com/@theinkedengineer/check-if-app-is-running-unit-tests-the-swift-way-b51fbfd07989


To for loop można spowolnić w następujący sposób:return threadDictionary.allKeys.anyMatch { ($0 as? String)?.split(separator: ".").contains("xctest") == true }
Roger Oba

2

Niektóre z tych podejść nie działają z UITests i jeśli zasadniczo testujesz za pomocą samego kodu aplikacji (zamiast dodawać określony kod do celu UITest).

Skończyło się na ustawieniu zmiennej środowiskowej w metodzie setUp testu:

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

Zauważ, że jest to bezpieczne dla moich testów, ponieważ obecnie nie używam żadnych innych wartości launchEnvironment; jeśli to zrobisz, możesz oczywiście najpierw skopiować wszelkie istniejące wartości.

Następnie w kodzie aplikacji szukam tej zmiennej środowiskowej, jeśli / kiedy chcę wykluczyć niektóre funkcje podczas testu:

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

Uwaga - dzięki za komentarz RishiG, który dał mi ten pomysł; Właśnie rozszerzyłem to na przykład.


2

Metoda, której używałem, przestała działać w Xcode 12 beta 1. Po wypróbowaniu wszystkich odpowiedzi opartych na kompilacji na to pytanie, zainspirowała mnie odpowiedź @ ODB. Oto wersja Swift dość prostego rozwiązania, które działa zarówno na rzeczywistych urządzeniach, jak i symulatorach. Powinien również być dość „dowód zwolnienia”.

Wstaw w konfiguracji testowej:

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

Wstaw do aplikacji:

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

Aby z niego skorzystać:

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

0

Pracował dla mnie:

Cel C

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

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


0

Najpierw dodaj zmienną do testowania:

wprowadź opis obrazu tutaj

i użyj tego w swoim kodzie:

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

0

Najwyraźniej w Xcode12 musimy szukać w kluczu środowiska XCTestBundlePathzamiast tego, XCTestConfigurationFilePathczy używasz nowegoXCTestPlan

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.