Odrobina EarlGrey — testowanie interfejsu użytkownika aplikacji Toptal Talent

Opublikowany: 2022-03-11

Jedną z najważniejszych rzeczy, które możesz zrobić jako tester, aby Twoja praca była wydajniejsza i szybsza, jest zautomatyzowanie testowanej aplikacji. Poleganie wyłącznie na testach ręcznych nie jest możliwe, ponieważ musiałbyś uruchamiać pełny zestaw testów każdego dnia, czasami kilka razy dziennie, testując każdą zmianę wprowadzoną do kodu aplikacji.

W tym artykule opiszemy drogę naszego zespołu do zidentyfikowania EarlGrey 1.0 firmy Google jako narzędzia, które sprawdziło się najlepiej w kontekście automatyzacji aplikacji iOS Toptal Talent. To, że go używamy, nie oznacza, że ​​EarlGrey jest najlepszym narzędziem testowym dla wszystkich – po prostu odpowiada naszym potrzebom.

Dlaczego przeszliśmy na EarlGrey

Przez lata nasz zespół budował różne aplikacje mobilne na iOS i Androida. Na początku rozważaliśmy użycie wieloplatformowego narzędzia do testowania interfejsu użytkownika, które pozwoliłoby nam napisać pojedynczy zestaw testów i wykonać je na różnych mobilnych systemach operacyjnych. Najpierw wybraliśmy Appium, najpopularniejszą dostępną opcję open source.

Ale z biegiem czasu ograniczenia Appium stawały się coraz bardziej oczywiste. W naszym przypadku dwie główne wady Appium to:

  • Wątpliwa stabilność frameworka spowodowała wiele płatków testowych.
  • Stosunkowo powolny proces aktualizacji utrudnił nam pracę.

Aby złagodzić pierwsze niedociągnięcie Appium, napisaliśmy różnego rodzaju poprawki kodu i hacki, aby testy były bardziej stabilne. Jednak nie mogliśmy nic zrobić, aby zająć się drugim. Za każdym razem, gdy wypuszczano nową wersję iOS lub Androida, Appium długo nadrabiało zaległości. I bardzo często, z powodu wielu błędów, pierwsza aktualizacja była bezużyteczna. W rezultacie często byliśmy zmuszeni kontynuować nasze testy na starszej wersji platformy lub całkowicie je wyłączyć do czasu udostępnienia działającej aktualizacji Appium.

Takie podejście było dalekie od ideału i ze względu na te kwestie, wraz z dodatkowymi, których nie będziemy szczegółowo omawiać, postanowiliśmy poszukać alternatyw. Głównymi kryteriami nowego narzędzia testowego były zwiększona stabilność i szybsze aktualizacje . Po pewnym badaniu zdecydowaliśmy się użyć natywnych narzędzi testowych dla każdej platformy.

Tak więc przeszliśmy na Espresso dla projektu Android i EarlGrey 1.0 dla rozwoju iOS. Z perspektywy czasu możemy teraz powiedzieć, że była to dobra decyzja. Czas „stracony” z powodu konieczności napisania i utrzymania dwóch różnych zestawów testów, po jednym dla każdej platformy, został nadrobiony przez brak konieczności badania tak wielu niestabilnych testów i brak przestojów związanych z aktualizacjami wersji.

Lokalna struktura projektu

Musisz dołączyć platformę do tego samego projektu Xcode, co aplikacja, którą tworzysz. Dlatego utworzyliśmy folder w katalogu głównym, aby hostować testy interfejsu użytkownika. Utworzenie pliku EarlGrey.swift jest obowiązkowe podczas instalacji środowiska testowego, a jego zawartość jest wstępnie zdefiniowana.

Aplikacja Toptal Talent: lokalna struktura projektu

EarlGreyBase jest klasą nadrzędną dla wszystkich klas testowych. Zawiera ogólne metody XCTestCase i tearDown , rozszerzone z setUp . W setUp , które będą ogólnie używane w większości testów (więcej o skrótach później), a także ustawiamy kilka flag konfiguracyjnych, które, jak zauważyliśmy, zwiększają stabilność testów:

 // Turn off EarlGrey's network requests tracking since we don't use it and it can block tests execution GREYConfiguration.sharedInstance().setValue([".*"], forConfigKey: kGREYConfigKeyURLBlacklistRegex) GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeyAnalyticsEnabled)

Używamy wzorca projektowego Page Object - każdy ekran w aplikacji ma odpowiadającą mu klasę, w której zdefiniowane są wszystkie elementy UI i ich możliwe interakcje. Ta klasa nazywa się „stroną”. Metody testowe są pogrupowane według cech rezydujących w osobnych plikach i klasach ze stron.

Aby lepiej zorientować się, jak wszystko jest wyświetlane, tak wyglądają ekrany Login i Zapomniane hasło w naszej aplikacji i jak są reprezentowane przez obiekty strony.

Tak wyglądają ekrany logowania i zapomnianego hasła w naszej aplikacji.

W dalszej części artykułu przedstawimy zawartość kodu obiektu strony logowania.

Niestandardowe metody użytkowe

Sposób, w jaki EarlGrey synchronizuje działania testowe z aplikacją, nie zawsze jest doskonały. Na przykład może próbować kliknąć przycisk, który nie jest jeszcze załadowany w hierarchii interfejsu użytkownika, powodując niepowodzenie testu. Aby uniknąć tego problemu, stworzyliśmy niestandardowe metody, które czekają, aż elementy pojawią się w pożądanym stanie, zanim wejdziemy z nimi w interakcję.

Oto kilka przykładów:

 static func asyncWaitForVisibility(on element: GREYInteraction) { // By default, EarlGrey blocks test execution while // the app is animating or doing anything in the background. //https://github.com/google/EarlGrey/blob/master/docs/api.md#synchronization GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeySynchronizationEnabled) element.assert(grey_sufficientlyVisible()) GREYConfiguration.sharedInstance().setValue(true, forConfigKey: kGREYConfigKeySynchronizationEnabled) } static func waitElementVisibility(for element: GREYInteraction, timeout: Double = 15.0) -> Bool { GREYCondition(name: "Wait for element to appear", block: { var error: NSError? element.assert(grey_notNil(), error: &error) return error == nil }).wait(withTimeout: timeout, pollInterval: 0.5) if !elementVisible(element) { XCTFail("Element didn't appear") } return true }

Inną rzeczą, której EarlGrey nie robi samodzielnie, jest przewijanie ekranu, aż żądany element stanie się widoczny. Oto jak możemy to zrobić:

 static func elementVisible(_ element: GREYInteraction) -> Bool { var error: NSError? element.assert(grey_notVisible(), error: &error) if error != nil { return true } else { return false } } static func scrollUntilElementVisible(_ scrollDirection: GREYDirection, _ speed: String, _ searchedElement: GREYInteraction, _ actionElement: GREYInteraction) -> Bool { var swipes = 0 while !elementVisible(searchedElement) && swipes < 10 { if speed == "slow" { actionElement.perform(grey_swipeSlowInDirection(scrollDirection)) } else { actionElement.perform(grey_swipeFastInDirection(scrollDirection)) } swipes += 1 } if swipes >= 10 { return false } else { return true } }

Inne metody narzędziowe, których brakuje w interfejsie API EarlGrey, które zidentyfikowaliśmy, to zliczanie elementów i odczytywanie wartości tekstowych. Kod tych narzędzi jest dostępny na GitHub: tutaj i tutaj.

Stubbing wywołań API

Aby upewnić się, że unikniemy fałszywych wyników testów spowodowanych problemami z serwerem zaplecza, używamy biblioteki OHHTTPStubs do symulowania wywołań serwera. Dokumentacja na ich stronie głównej jest dość prosta, ale zaprezentujemy, jak kodujemy odpowiedzi w naszej aplikacji, która korzysta z GraphQL API.

 class StubsHelper { static let testURL = URL(string: "https://[our backend server]")! static func setupOHTTPStub(for request: StubbedRequest, delayed: Bool = false) { stub(condition: isHost(testURL.host!) && hasJsonBody(request.bodyDict())) { _ in let fix = appFixture(forRequest: request) if delayed { return fix.requestTime(0.1, responseTime: 7.0) } else { return fix } } } static let stubbedEmail = "[email protected]" static let stubbedPassword = "password" enum StubbedRequest { case login func bodyDict() -> [String: Any] { switch self { case .login: return EmailPasswordSignInMutation( email: stubbedTalentLogin, password: stubbedTalentPassword ).makeBodyIdentifier() } } func statusCode() -> Int32 { return 200 } func jsonFileName() -> String { let fileName: String switch self { case .login: fileName = "login" } return "\(fileName).json" } } private extension GraphQLOperation { func makeBodyIdentifier() -> [String: Any] { let body: GraphQLMap = [ "query": queryDocument, "variables": variables, "operationName": operationName ] // Normalize values like enums here, otherwise body comparison will fail guard let normalizedBody = body.jsonValue as? [String: Any] else { fatalError() } return normalizedBody } }

Ładowanie kodu pośredniczącego odbywa się poprzez wywołanie metody setupOHTTPStub :

 StubsHelper.setupOHTTPStub(for: .login)

Składanie wszystkiego razem

W tej sekcji zademonstrujemy, w jaki sposób wykorzystujemy wszystkie opisane powyżej zasady, aby napisać rzeczywisty test logowania end-to-end.

 import EarlGrey final class LoginPage { func login() -> HomePage { fillLoginForm() loginButton().perform(grey_tap()) return HomePage() } func fillLoginForm() { ElementsHelper.waitElementVisibility(emailField()) emailField().perform(grey_replaceText(StubsHelper.stubbedTalentLogin)) passwordField().perform(grey_tap()) passwordField().perform(grey_replaceText(StubsHelper.stubbedTalentPassword)) } func clearAllInputs() { if ElementsHelper.elementVisible(passwordField()) { passwordField().perform(grey_tap()) passwordField().perform(grey_replaceText("")) } emailField().perform(grey_tap()) emailField().perform(grey_replaceText("")) } } private extension LoginPage { func emailField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement(with: grey_accessibilityLabel("Email"), file: file, line: line) } func passwordField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement( with: grey_allOf([ grey_accessibilityLabel("Password"), grey_sufficientlyVisible(), grey_userInteractionEnabled() ]), file: file, line: line ) } func loginButton(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement(with: grey_accessibilityID("login_button"), file: file, line: line) } } class BBucketTests: EarlGreyBase { func testLogin() { StubsHelper.setupOHTTPStub(for: .login) LoginPage().clearAllInputs() let homePage = LoginPage().login() GREYAssertTrue( homePage.assertVisible(), reason: "Home screen not displayed after successful login" ) } }

Uruchamianie testów w CI

Używamy Jenkins jako naszego systemu ciągłej integracji i przeprowadzamy testy interfejsu użytkownika dla każdego zatwierdzenia w każdym żądaniu ściągnięcia.

fastlane scan wykorzystujemy do wykonywania testów w CI i generowania raportów. Przydatne jest dołączanie zrzutów ekranu do tych raportów w przypadku nieudanych testów. Niestety, scan nie zapewnia tej funkcjonalności, więc musieliśmy to zrobić na zamówienie.

W funkcji tearDown() , czy test się nie powiódł i zapisujemy zrzut ekranu symulatora iOS, jeśli tak się stało.

 import EarlGrey import XCTest import UIScreenCapture override func tearDown() { if testRun!.failureCount > 0 { // name is a property of the XCTest instance // https://developer.apple.com/documentation/xctest/xctest/1500990-name takeScreenshotAndSave(as: name) } super.tearDown() } func takeScreenshotAndSave(as testCaseName: String) { let imageData = UIScreenCapture.takeSnapshotGetJPEG() let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) let filePath = "\(paths[0])/\(testCaseName).jpg" do { try imageData?.write(to: URL.init(fileURLWithPath: filePath)) } catch { XCTFail("Screenshot not written.") } }

Zrzuty ekranu są zapisywane w folderze Simulator i będziesz musiał je stamtąd pobrać, aby dołączyć je jako artefakty kompilacji. Używamy Rake do zarządzania naszymi skryptami CI. W ten sposób zbieramy artefakty testowe:

 def gather_test_artifacts(booted_sim_id, destination_folder) app_container_on_sim = `xcrun simctl get_app_container #{booted_sim_id} [your bundle id] data`.strip FileUtils.cp_r "#{app_container_on_sim}/Documents", destination_folder end

Kluczowe dania na wynos

Jeśli szukasz szybkiego i niezawodnego sposobu na zautomatyzowanie testów iOS, nie szukaj dalej niż EarlGrey. Jest rozwijany i utrzymywany przez Google (czy muszę mówić więcej?) i pod wieloma względami przewyższa inne dostępne obecnie narzędzia.

Będziesz musiał trochę pomajstrować z frameworkiem, aby przygotować metody użytkowe promujące stabilność testów. Aby to zrobić, możesz zacząć od naszych przykładów niestandardowych metod narzędziowych.

Zalecamy testowanie na danych z kodami, aby upewnić się, że testy nie zakończą się niepowodzeniem, ponieważ serwer zaplecza nie ma wszystkich danych testowych, których można by się spodziewać. Użyj OHHTTPStubs lub podobnego lokalnego serwera WWW, aby wykonać zadanie.

Podczas uruchamiania testów w CI, upewnij się, że dostarczasz zrzuty ekranu dla przypadków zakończonych niepowodzeniem, aby ułatwić debugowanie.

Być może zastanawiasz się, dlaczego nie przeprowadziliśmy jeszcze migracji do EarlGrey 2.0, a oto krótkie wyjaśnienie. Nowa wersja została wydana w zeszłym roku i obiecuje pewne ulepszenia w stosunku do wersji 1.0. Niestety, kiedy przyjęliśmy EarlGrey, wersja 2.0 nie była szczególnie stabilna. Dlatego nie przeszliśmy jeszcze na v2.0. Jednak nasz zespół z niecierpliwością oczekuje poprawki błędu w nowej wersji, abyśmy mogli w przyszłości przeprowadzić migrację naszej infrastruktury.

Zasoby online

Poradnik EarlGrey Pierwsze kroki na stronie głównej GitHub to miejsce, od którego chcesz zacząć, jeśli rozważasz strukturę testową dla swojego projektu. Znajdziesz tam łatwy w użyciu przewodnik po instalacji, dokumentację API narzędzia oraz podręczną ściągawkę, zawierającą listę wszystkich metod frameworka w sposób prosty w użyciu podczas pisania testów.

Aby uzyskać dodatkowe informacje na temat pisania automatycznych testów na iOS, możesz również zapoznać się z jednym z naszych poprzednich wpisów na blogu.