Opóźnienie / oczekiwanie w przypadku testowym testowania interfejsu użytkownika Xcode

182

Próbuję napisać przypadek testowy przy użyciu nowego testu interfejsu użytkownika dostępnego w Xcode 7 beta 2. Aplikacja ma ekran logowania, w którym nawiązuje połączenie z serwerem, aby się zalogować. Z tym wiąże się opóźnienie, ponieważ jest to operacja asynchroniczna.

Czy istnieje sposób na wywołanie mechanizmu opóźnienia lub oczekiwania w XCTestCase przed przejściem do dalszych kroków?

Brak odpowiedniej dokumentacji i przejrzałem pliki nagłówkowe klas. Nie udało się znaleźć nic z tym związanego.

Wszelkie pomysły / sugestie?

Tejas HS
źródło
13
Myślę, że NSThread.sleepForTimeInterval(1)powinien działać
Kametrixom
Wspaniały! Wygląda na to, że działa. Ale nie jestem pewien, czy jest to zalecany sposób. Myślę, że Apple powinien dać lepszy sposób, aby to zrobić. Może trzeba złożyć radar
Tejas HS
Naprawdę uważam, że to w porządku, to naprawdę najczęstszy sposób na wstrzymanie bieżącego wątku na pewien czas. Jeśli chcesz mieć większą kontrolę, możesz także dostać się do GCD (The dispatch_after, dispatch_queuestuff)
Kametrixom
@Kametrixom Nie zaznaczaj pętli uruchamiania - Apple wprowadziło natywne testy asynchroniczne w wersji Beta 4. Szczegółowe informacje można znaleźć w mojej odpowiedzi .
Joe Masilotti
2
Swift 4.0 -> Thread.sleep (forTimeInterval: 2)
uplearnedu.com

Odpowiedzi:

168

Asynchroniczne testowanie interfejsu użytkownika zostało wprowadzone w Xcode 7 Beta 4. Aby czekać na etykietę z tekstem „Witaj, świecie!” Aby się pojawić, możesz wykonać następujące czynności:

let app = XCUIApplication()
app.launch()

let label = app.staticTexts["Hello, world!"]
let exists = NSPredicate(format: "exists == 1")

expectationForPredicate(exists, evaluatedWithObject: label, handler: nil)
waitForExpectationsWithTimeout(5, handler: nil)

Więcej szczegółów na temat testowania interfejsu użytkownika można znaleźć na moim blogu.

Joe Masilotti
źródło
19
Niestety nie ma sposobu, aby zaakceptować przekroczenie limitu czasu i przejść dalej - waitForExpectationsWithTimeoutautomatycznie nie przejdzie testu, co jest dość niefortunne.
Jedidja
@Jedidja Właściwie to nie dzieje się dla mnie z XCode 7.0.1.
Bastian
@Bastian Hmm ciekawe; Będę musiał to ponownie sprawdzić.
Jedidja
1
to nie działa dla mnie. Oto moja próbka: niech xButton = app.toolbars.buttons ["X"] let exist = NSPredicate (format: „istnieje == 1”) expectationForPredicate (istnieje, ocenionoWithObject: xButton, moduł obsługi: zero) waitForExpectationsWithTimeout (10, moduł obsługi: zero)
emoleumassi
app.launch()Wydaje się po prostu wznowienie aplikację. Czy to konieczne?
Chris Prince
225

Dodatkowo możesz po prostu spać:

sleep(10)

Ponieważ testy UITest działają w innym procesie, działa to. Nie wiem, jak to jest zalecane, ale działa.

mxcl
źródło
2
Czasami potrzebujemy sposobu na opóźnienie i nie chcemy, aby to spowodowało awarię! dzięki
Tai Le
13
Najlepsza odpowiedź, jaką kiedykolwiek widziałem :) Dodałbym + 100 głosów, gdybym mógł :)
Bartłomiej Semańczyk
8
Podoba mi się NSThread.sleepForTimeInterval (0.2), ponieważ możesz określić opóźnienia poniżej drugiej sekundy. (sleep () przyjmuje parametr liczby całkowitej; możliwe są tylko wielokrotności sekundy).
Graham Perks,
5
@GrahamPerks, tak, choć jest też:usleep
mxcl
3
To nie jest kiepska sugestia (nie rozumiesz, jak działa UITesting), ale nawet jeśli była to kiepska sugestia, czasami nie ma sposobu, aby stworzyć oczekiwanie, które działa (system ostrzega kogokolwiek?), Więc to wszystko, co masz.
mxcl
78

iOS 11 / Xcode 9

<#yourElement#>.waitForExistence(timeout: 5)

Jest to świetny zamiennik wszystkich niestandardowych implementacji na tej stronie!

Zapoznaj się z moją odpowiedzią tutaj: https://stackoverflow.com/a/48937714/971329 . Tam opisuję alternatywę oczekiwania na żądania, co znacznie skróci czas działania testów!

blackjacx
źródło
Dzięki @daidai zmieniłem tekst :)
blackjacx
1
Tak, to jest wciąż podejście, którego używam XCTestCasei działa jak urok. Nie rozumiem, dlaczego takie podejścia sleep(3)są tutaj tak wysoko oceniane, ponieważ sztucznie wydłuża czas testowania i nie ma żadnej opcji, gdy rośnie Twój zestaw testowy.
blackjacx
Właściwie wymaga Xcode 9, ale działa również na urządzeniach / symulatorach z systemem iOS 10 ;-)
d4Rk
Tak, napisałem to w powyższym nagłówku. Ale teraz większość ludzi powinna była zaktualizować przynajmniej do Xcode 9 ;-)
blackjacx,
77

Xcode 9 wprowadził nowe sztuczki z XCTWaiter

Przypadek testowy czeka jawnie

wait(for: [documentExpectation], timeout: 10)

Instancja kelnera deleguje się do testowania

XCTWaiter(delegate: self).wait(for: [documentExpectation], timeout: 10)

Klasa kelnerów zwraca wynik

let result = XCTWaiter.wait(for: [documentExpectation], timeout: 10)
switch(result) {
case .completed:
    //all expectations were fulfilled before timeout!
case .timedOut:
    //timed out before all of its expectations were fulfilled
case .incorrectOrder:
    //expectations were not fulfilled in the required order
case .invertedFulfillment:
    //an inverted expectation was fulfilled
case .interrupted:
    //waiter was interrupted before completed or timedOut
}

przykładowe użycie

Przed Xcode 9

Cel C

- (void)waitForElementToAppear:(XCUIElement *)element withTimeout:(NSTimeInterval)timeout
{
    NSUInteger line = __LINE__;
    NSString *file = [NSString stringWithUTF8String:__FILE__];
    NSPredicate *existsPredicate = [NSPredicate predicateWithFormat:@"exists == true"];

    [self expectationForPredicate:existsPredicate evaluatedWithObject:element handler:nil];

    [self waitForExpectationsWithTimeout:timeout handler:^(NSError * _Nullable error) {
        if (error != nil) {
            NSString *message = [NSString stringWithFormat:@"Failed to find %@ after %f seconds",element,timeout];
            [self recordFailureWithDescription:message inFile:file atLine:line expected:YES];
        }
    }];
}

STOSOWANIE

XCUIElement *element = app.staticTexts["Name of your element"];
[self waitForElementToAppear:element withTimeout:5];

Szybki

func waitForElementToAppear(element: XCUIElement, timeout: NSTimeInterval = 5,  file: String = #file, line: UInt = #line) {
        let existsPredicate = NSPredicate(format: "exists == true")

        expectationForPredicate(existsPredicate,
                evaluatedWithObject: element, handler: nil)

        waitForExpectationsWithTimeout(timeout) { (error) -> Void in
            if (error != nil) {
                let message = "Failed to find \(element) after \(timeout) seconds."
                self.recordFailureWithDescription(message, inFile: file, atLine: line, expected: true)
            }
        }
    }

STOSOWANIE

let element = app.staticTexts["Name of your element"]
self.waitForElementToAppear(element)

lub

let element = app.staticTexts["Name of your element"]
self.waitForElementToAppear(element, timeout: 10)

ŹRÓDŁO

Przetrząsać
źródło
1
szukam więcej ilustracji dotyczących powyższego przykładu
xcode9
1
Przetestowany. Działa jak marzenie! Dzięki!
Dawid Koncewicz,
32

Od Xcode 8.3 możemy korzystać z XCTWaiter http://masilotti.com/xctest-waiting/

func waitForElementToAppear(_ element: XCUIElement) -> Bool {
    let predicate = NSPredicate(format: "exists == true")
    let expectation = expectation(for: predicate, evaluatedWith: element, 
                                  handler: nil)

    let result = XCTWaiter().wait(for: [expectation], timeout: 5)
    return result == .completed
}

Inną sztuczką jest napisanie waitfunkcji. Podziękowania należą się Johnowi Sundellowi za pokazanie mi tej funkcji

extension XCTestCase {

  func wait(for duration: TimeInterval) {
    let waitExpectation = expectation(description: "Waiting")

    let when = DispatchTime.now() + duration
    DispatchQueue.main.asyncAfter(deadline: when) {
      waitExpectation.fulfill()
    }

    // We use a buffer here to avoid flakiness with Timer on CI
    waitForExpectations(timeout: duration + 0.5)
  }
}

i używaj go jak

func testOpenLink() {
  let delegate = UIApplication.shared.delegate as! AppDelegate
  let route = RouteMock()
  UIApplication.shared.open(linkUrl, options: [:], completionHandler: nil)

  wait(for: 1)

  XCTAssertNotNil(route.location)
}
onmyway133
źródło
11

W oparciu o odpowiedź @ Teda użyłem tego rozszerzenia:

extension XCTestCase {

    // Based on https://stackoverflow.com/a/33855219
    func waitFor<T>(object: T, timeout: TimeInterval = 5, file: String = #file, line: UInt = #line, expectationPredicate: @escaping (T) -> Bool) {
        let predicate = NSPredicate { obj, _ in
            expectationPredicate(obj as! T)
        }
        expectation(for: predicate, evaluatedWith: object, handler: nil)

        waitForExpectations(timeout: timeout) { error in
            if (error != nil) {
                let message = "Failed to fulful expectation block for \(object) after \(timeout) seconds."
                self.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true)
            }
        }
    }

}

Możesz użyć tego w ten sposób

let element = app.staticTexts["Name of your element"]
waitFor(object: element) { $0.exists }

Pozwala także czekać na zniknięcie elementu lub zmianę dowolnej innej właściwości (za pomocą odpowiedniego bloku)

waitFor(object: element) { !$0.exists } // Wait for it to disappear
Ben Lings
źródło
+1 bardzo szybki i wykorzystuje predykat blokowy, który moim zdaniem jest o wiele lepszy, ponieważ standardowe wyrażenia predykatów czasami nie działały dla mnie, na przykład podczas oczekiwania na pewne właściwości na XCUIElements itp.
lawicko
10

Edytować:

Właśnie przyszło mi do głowy, że w Xcode 7b4 testy interfejsu użytkownika mają teraz expectationForPredicate:evaluatedWithObject:handler:

Oryginalny:

Innym sposobem jest obracanie pętli biegu przez określony czas. Naprawdę przydatne tylko, jeśli wiesz, ile czasu (szacowanego) będziesz musiał czekać

Obj-C: [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow: <<time to wait in seconds>>]]

Szybki: NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: <<time to wait in seconds>>))

Nie jest to szczególnie przydatne, jeśli musisz przetestować niektóre warunki, aby kontynuować test. Aby uruchomić sprawdzanie warunkowe, użyj whilepętli.

enmiller
źródło
Jest to dla mnie czyste i bardzo przydatne, szczególnie np. Czekając na uruchomienie aplikacji, żądając wstępnie załadowanych danych i wykonując czynności związane z logowaniem / wylogowaniem. Dziękuję Ci.
felixwcf
4

Poniższy kod działa tylko z celem C.

- (void)wait:(NSUInteger)interval {

    XCTestExpectation *expectation = [self expectationWithDescription:@"wait"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:interval handler:nil];
}

Wystarczy wywołać tę funkcję, jak podano poniżej.

[self wait: 10];
arango_86
źródło
Błąd -> wyłapał „NSInternalInconsistencyException”, „Naruszenie interfejsu API - wywołanie zostało wykonane bez oczekiwania.”
FlowUI. SimpleUITesting.com,
@ iOSCalendarpatchthecode.com, czy znalazłeś alternatywne rozwiązanie?
Maks.
@Max. Czy możesz skorzystać z innych na tej stronie?
FlowUI. SimpleUITesting.com,
@ iOSCalendarpatchthecode.com Nie, potrzebuję tylko opóźnienia bez żadnego elementu do sprawdzenia. Potrzebuję więc alternatywy.
Max
@Max Użyłem wybranej odpowiedzi na tej stronie. To zadziałało dla mnie. Może możesz zapytać ich, czego konkretnie szukasz.
FlowUI. SimpleUITesting.com,
4

W moim przypadku sleepstworzyłem efekt uboczny, więc użyłemwait

let _ = XCTWaiter.wait(for: [XCTestExpectation(description: "Hello World!")], timeout: 2.0)
yoAlex5
źródło
0

Zgodnie z interfejsem API dla XCUIElement .existsmożna sprawdzić, czy zapytanie istnieje, czy nie, więc w niektórych przypadkach przydatna może być następująca składnia!

let app = XCUIApplication()
app.launch()

let label = app.staticTexts["Hello, world!"]
while !label.exists {
    sleep(1)
}

Jeśli masz pewność, że Twoje oczekiwania zostaną spełnione, możesz spróbować to uruchomić. Należy zauważyć, że awaria może być lepsza, jeśli oczekiwanie jest zbyt długie, w takim przypadku należy waitForExpectationsWithTimeout(_,handler:_)użyć posta @Joe Masilotti.

Reid
źródło
0

sen zablokuje wątek

„Żadne przetwarzanie w pętli uruchomieniowej nie występuje, gdy wątek jest zablokowany”.

możesz użyć waitForExistence

let app = XCUIApplication()
app.launch()

if let label = app.staticTexts["Hello, world!"] {
label.waitForExistence(timeout: 5)
}
zdravko zdravkin
źródło
0

Spowoduje to opóźnienie bez uśpienia wątku lub spowodowania błędu przekroczenia limitu czasu:

let delayExpectation = XCTestExpectation()
delayExpectation.isInverted = true
wait(for: [delayExpectation], timeout: 5)

Ponieważ oczekiwanie jest odwrócone, upłynie czas oczekiwania.

Ken Murphy
źródło