Wahrscheinlich die beste Architektur für UI-Tests



Wahrscheinlich gibt es irgendwo einen idealen Artikel, der das Thema Testarchitektur sofort und vollständig aufzeigt, einfach zu schreiben, zu lesen und zu unterstützen ist und für Anfänger verständlich ist, mit Beispielen für die Implementierung und Anwendungsbereichen. Ich möchte meine Vision dieses „idealen Artikels“ in dem Format anbieten, von dem ich geträumt habe, erst nachdem ich die erste Aufgabe „Autotests schreiben“ erhalten habe. Dazu werde ich über die bekannten und weniger bekannten Ansätze für Web-Autotests sprechen, warum, wie und wann sie verwendet werden sollen, sowie über erfolgreiche Lösungen zum Speichern und Erstellen von Daten.



Hallo Habr! Mein Name ist Diana, ich bin Leiterin der Testgruppe für Benutzeroberflächen und automatisiere seit fünf Jahren Web- und Desktop-Tests. Codebeispiele werden in Java und für das Web sein, aber in der Praxis wurde getestet, dass die Ansätze auf Python mit einem Desktop anwendbar sind.



Am Anfang war es ...



Am Anfang gab es ein Wort, und es gab viele Wörter, und sie füllten alle Seiten gleichmäßig mit Code, unabhängig von Ihren Architekturen und DRY-Prinzipien (wiederholen Sie sich nicht - Sie müssen den Code, den Sie bereits drei Absätze oben geschrieben haben, nicht wiederholen).



Blatt



In der Tat ist die Architektur des "Fußtuchs", auch bekannt als "Blatt", unstrukturierter Code, der auf einem Haufen gehäuft ist, der den Bildschirm gleichmäßig ausfüllt, nicht so schlecht ist und in den folgenden Situationen durchaus anwendbar ist:



  • Ein schneller Klick in drei Zeilen (okay, zweihundertdrei) für sehr kleine Projekte;
  • Für Codebeispiele in der Mini-Demo;
  • Für den ersten Code im "Hello Word" -Stil unter Autotests.


Was müssen Sie tun, um die Architektur der Bettdecke zu erhalten? Schreiben Sie einfach den gesamten erforderlichen Code in eine Datei, eine gemeinsame Zeichenfläche.



import com.codeborne.selenide.Condition;
import com.codeborne.selenide.WebDriverRunner;
import org.testng.annotations.Test;

import static com.codeborne.selenide.Selenide.*;

public class RandomSheetTests {
    @Test
    void addUser() {
        open("https://ui-app-for-autotest.herokuapp.com/");
        $("#loginEmail").sendKeys("test@protei.ru");
        $("#loginPassword").sendKeys("test");
        $("#authButton").click();
        $("#menuMain").shouldBe(Condition.appear);

        $("#menuUsersOpener").hover();
        $("#menuUserAdd").click();

        $("#dataEmail").sendKeys("mail@mail.ru");
        $("#dataPassword").sendKeys("testPassword");
        $("#dataName").sendKeys("testUser");
        $("#dataGender").selectOptionContainingText("");
        $("#dataSelect12").click();
        $("#dataSelect21").click();
        $("#dataSelect22").click();
        $("#dataSend").click();

        $(".uk-modal-body").shouldHave(Condition.text(" ."));

        WebDriverRunner.closeWebDriver();
    }
}


Wenn Sie gerade erst anfangen, sich mit Autotests vertraut zu machen, reicht das "Blatt" bereits aus, um eine einfache Testaufgabe zu erledigen, insbesondere wenn Sie gute Kenntnisse über das Testdesign und eine gute Abdeckung aufweisen. Für Großprojekte ist dies jedoch zu einfach. Wenn Sie also Ambitionen haben, aber keine Zeit haben, jeden Testfall ideal auszuführen, sollte zumindest Ihre Gita ein Beispiel für eine komplexere Architektur haben.



PageObject



Von Gerüchten gehört, dass PageObject veraltet ist? Sie wissen einfach nicht, wie man es kocht!



Die Hauptarbeitseinheit in diesem Muster ist eine „Seite“, dh ein vollständiger Satz von Elementen und Aktionen mit ihnen, z. B. MenuPage - eine Klasse, die alle Aktionen mit einem Menü beschreibt, dh auf Registerkarten klickt, Dropdown-Elemente erweitert usw.







Es ist etwas schwieriger, ein PageObject für das modale Fenster (kurz "modal") der Objekterstellung zu erstellen. Die Menge der Klassenfelder ist klar: alle Eingabefelder, Kontrollkästchen, Dropdown-Listen; und für Methoden gibt es zwei Möglichkeiten: Sie können beide universellen Methoden "Alle Modalfelder ausfüllen", "Alle Modalfelder mit Zufallswerten füllen", "Alle Modalfelder prüfen" und separate Methoden "Namen eingeben", "Namen prüfen" festlegen. "Füllen Sie die Beschreibung aus" und so weiter. Was in einem bestimmten Fall zu verwenden ist, hängt von den Prioritäten ab. Der Ansatz „Eine Methode für das gesamte Modal“ erhöht die Geschwindigkeit beim Schreiben eines Tests, verliert jedoch im Vergleich zum Ansatz „Eine Methode für jedes Feld“ erheblich an Lesbarkeit des Tests.



Beispiel
Page Object :

public class UsersPage {

    @FindBy(how = How.ID, using = "dataEmail")
    private SelenideElement email;
    @FindBy(how = How.ID, using = "dataPassword")
    private SelenideElement password;
    @FindBy(how = How.ID, using = "dataName")
    private SelenideElement name;
    @FindBy(how = How.ID, using = "dataGender")
    private SelenideElement gender;
    @FindBy(how = How.ID, using = "dataSelect11")
    private SelenideElement var11;
    @FindBy(how = How.ID, using = "dataSelect12")
    private SelenideElement var12;
    @FindBy(how = How.ID, using = "dataSelect21")
    private SelenideElement var21;
    @FindBy(how = How.ID, using = "dataSelect22")
    private SelenideElement var22;
    @FindBy(how = How.ID, using = "dataSelect23")
    private SelenideElement var23;
    @FindBy(how = How.ID, using = "dataSend")
    private SelenideElement save;

    @Step("Complex add user")
    public UsersPage complexAddUser(String userMail, String userPassword, String userName, String userGender, 
                                    boolean v11, boolean v12, boolean v21, boolean v22, boolean v23) {
        email.sendKeys(userMail);
        password.sendKeys(userPassword);
        name.sendKeys(userName);
        gender.selectOption(userGender);
        set(var11, v11);
        set(var12, v12);
        set(var21, v21);
        set(var22, v22);
        set(var23, v23);
        save.click();
        return this;
    }

    @Step("Fill user Email")
    public UsersPage sendKeysEmail(String text) {...}

    @Step("Fill user Password")
    public UsersPage sendKeysPassword(String text) {...}

    @Step("Fill user Name")
    public UsersPage sendKeysName(String text) {...}

    @Step("Select user Gender")
    public UsersPage selectGender(String text) {...}

    @Step("Select user variant 1.1")
    public UsersPage selectVar11(boolean flag) {...}

    @Step("Select user variant 1.2")
    public UsersPage selectVar12(boolean flag) {...}

    @Step("Select user variant 2.1")
    public UsersPage selectVar21(boolean flag) {...}

    @Step("Select user variant 2.2")
    public UsersPage selectVar22(boolean flag) {...}

    @Step("Select user variant 2.3")
    public UsersPage selectVar23(boolean flag) {...}

    @Step("Click save")
    public UsersPage clickSave() {...}

    private void set(SelenideElement checkbox, boolean flag) {
        if (flag) {
            if (!checkbox.isSelected()) checkbox.click();
        } else {
            if (checkbox.isSelected()) checkbox.click();
        }
    }
}


:



    @Test
    void addUser() {
        baseRouter.authPage()
                .complexLogin("test@protei.ru", "test")
                .complexOpenAddUser()
                .complexAddUser("mail@test.ru", "pswrd", "TESTNAME", "", true, false, true, true, true)
                .checkAndCloseSuccessfulAlert();
    }


:



    @Test
    void addUserWithoutComplex() {
        //Arrange
        baseRouter.authPage()
                .complexLogin("test@protei.ru", "test");
        //Act
        baseRouter.mainPage()
                .hoverUsersOpener()
                .clickAddUserMenu();
        baseRouter.usersPage()
                .sendKeysEmail("mail@test.ru")
                .sendKeysPassword("pswrd")
                .sendKeysName("TESTNAME")
                .selectGender("")
                .selectVar11(true)
                .selectVar12(false)
                .selectVar21(true)
                .selectVar22(true)
                .selectVar23(true)
                .clickSave();
        //Assert
        baseRouter.usersPage()
                .checkTextSavePopup(" .")
                .closeSavePopup();
    }


. : , , , , — . , , , .



Das Fazit ist, dass alle Aktionen mit Seiten in den Seiten gekapselt sind (die Implementierung ist ausgeblendet, nur logische Aktionen sind verfügbar), sodass Geschäftsfunktionen bereits im Test verwendet werden. Auf diese Weise können Sie Ihre eigenen Seiten für jede Plattform (Web, Desktop, Mobiltelefone) schreiben, ohne die Tests zu ändern.



Schade nur, dass auf verschiedenen Plattformen absolut identische Schnittstellen selten sind.



Um die Diskrepanz zwischen den Schnittstellen zu verringern, besteht die Versuchung, einzelne Schritte zu komplizieren. Sie werden in separate Zwischenklassen unterteilt, und die Tests werden in bis zu zwei Schritten immer weniger lesbar: "Anmelden, gut machen", der Test ist beendet. Zusätzlich zum Web gab es in unseren Projekten keine zusätzlichen Schnittstellen, und wir müssen Fälle häufiger lesen als schreiben. Aus Gründen der Lesbarkeit haben historische PageObjects daher ein neues Aussehen erhalten.



PageObject ist ein Klassiker, den jeder kennt. Sie finden viele Artikel zu diesem Ansatz mit Beispielen in fast jeder Programmiersprache. Die Verwendung von PageObject wird sehr häufig verwendet, um zu beurteilen, ob ein Kandidat etwas über das Testen von Benutzeroberflächen weiß. Das Durchführen einer Testaufgabe mit diesem Ansatz ist das, was die meisten Arbeitgeber erwarten, und ein Großteil davon lebt in Produktionsprojekten, selbst wenn nur das Web testet.



Was passiert sonst noch?



Seltsamerweise kein einziges PageObject!



  • Das ScreenPlay-Muster ist häufig anzutreffen, über das Sie beispielsweise hier lesen können . Es hat in unserem Land keine Wurzeln geschlagen, da die Verwendung von bdd-Ansätzen ohne Einbeziehung von Personen, die den Code nicht lesen können, eine sinnlose Gewalt gegen Automaten darstellt.
  • js- , PageObject, - , , .
  • - , , ModelBaseTesting, . , .


Und ich werde Sie ausführlicher über das Seitenelement informieren, mit dem Sie die Menge des gleichen Codetyps reduzieren, die Lesbarkeit verbessern und ein schnelles Verständnis der Tests auch für diejenigen bereitstellen können, die mit dem Projekt nicht vertraut sind. Und darauf (natürlich mit eigenen Blackjacks und Vorlieben!) Werden die beliebten Nicht-JS-Frameworks htmlElements, Atlas und Epams JDI erstellt.



Was ist ein Seitenelement?



Beginnen Sie mit dem Element der untersten Ebene, um das Seitenelementmuster zu erstellen. Wie Wiktionary sagt , ist ein "Widget" ein Software-Grundelement einer grafischen Benutzeroberfläche, die ein Standard-Erscheinungsbild hat und Standardaktionen ausführt. Zum Beispiel das einfachste Widget "Button" - Sie können darauf klicken, Sie können den Text und die Farbe überprüfen. Im "Eingabefeld" können Sie Text eingeben, überprüfen, welcher Text eingegeben wurde, klicken, die Fokusanzeige überprüfen, die Anzahl der eingegebenen Zeichen überprüfen, den Text eingeben und "Eingabe" drücken, den Platzhalter überprüfen, die Hervorhebung des "obligatorischen" Felds und des Fehlertextes überprüfen und fertig. Was kann in einem bestimmten Fall noch benötigt werden? Darüber hinaus sind alle Aktionen mit diesem Feld auf jeder Seite Standard.







Es gibt komplexere Widgets, für die die Aktionen nicht so offensichtlich sind, z. B. Bauminhaltsverzeichnisse. Wenn Sie sie schreiben, müssen Sie darauf aufbauen, was der Benutzer mit diesem Bereich des Programms macht, zum Beispiel:



  • Klicken Sie auf ein Element des Inhaltsverzeichnisses mit dem angegebenen Text.
  • Überprüfen der Existenz eines Elements mit dem angegebenen Text,
  • Überprüfen der Einrückung eines Elements mit einem bestimmten Text.


Es gibt zwei Arten von Widgets: mit einem Locator im Konstruktor und mit einem in das Widget eingenähten Locator, ohne dass es geändert werden kann. Das Inhaltsverzeichnis befindet sich normalerweise auf der Seite. Die Suchmethode auf der Seite kann "innerhalb" der Aktionen mit dem Inhaltsverzeichnis belassen werden. Es ist nicht sinnvoll, den Locator separat herauszunehmen, da der Locator von außen versehentlich beschädigt werden kann, aber die separate Speicherung keinen Nutzen bringt. Ein Textfeld ist wiederum eine universelle Sache, im Gegenteil, Sie müssen nur über den Locator des Konstruktors damit arbeiten, da es viele Eingabefelder gleichzeitig geben kann. Wenn mindestens eine Methode angezeigt wird, die nur für ein spezielles Eingabefeld vorgesehen ist, z. B. mit einem zusätzlichen Klick auf den Dropdown-Hinweis, ist dies nicht mehr nur ein Eingabefeld, sondern es ist Zeit, ein eigenes Widget dafür zu erstellen.



Um das allgemeine Chaos zu verringern, werden Widgets wie Seitenelemente zu denselben Seiten zusammengefasst, aus denen anscheinend der Name Seitenelement besteht.



public class UsersPage {

    public Table usersTable = new Table();

    public InputLine email = new InputLine(By.id("dataEmail"));
    public InputLine password = new InputLine(By.id("dataPassword"));
    public InputLine name = new InputLine(By.id("dataName"));
    public DropdownList gender = new DropdownList(By.id("dataGender"));
    public Checkbox var11 = new Checkbox(By.id("dataSelect11"));
    public Checkbox var12 = new Checkbox(By.id("dataSelect12"));
    public Checkbox var21 = new Checkbox(By.id("dataSelect21"));
    public Checkbox var22 = new Checkbox(By.id("dataSelect22"));
    public Checkbox var23 = new Checkbox(By.id("dataSelect23"));
    public Button save = new Button(By.id("dataSend"));

    public ErrorPopup errorPopup = new ErrorPopup();
    public ModalPopup savePopup = new ModalPopup();
}


Um alle oben in Tests erstellten Elemente verwenden zu können, müssen Sie nacheinander auf die Seite, das Widget und die Aktion verweisen. Daher erhalten wir die folgende Konstruktion:



    @Test
    public void authAsAdmin() {
        baseRouter
                .authPage().email.fill("test@protei.ru")
                .authPage().password.fill("test")
                .authPage().enter.click()
                .mainPage().logoutButton.shouldExist();
    }


Sie können eine klassische Stufenebene hinzufügen, wenn dies in Ihrem Framework erforderlich ist (die Implementierung der Remotebibliothek in Java für RobotFramework erfordert beispielsweise eine Schrittklasse als Eingabe) oder wenn Sie Anmerkungen für schöne Berichte hinzufügen möchten. Wir haben es zu einem annotationsbasierten Generator gemacht. Wenn Sie interessiert sind, schreiben Sie in die Kommentare, wir werden es Ihnen sagen.



Ein Beispiel für eine Autorisierungsschrittklasse
public class AuthSteps{

    private BaseRouter baseRouter = new BaseRouter();

    @Step("Sigh in as {mail}")
    public BaseSteps login(String mail, String password) {
        baseRouter
                .authPage().email.fill(mail)
                .authPage().password.fill(password)
                .authPage().enter.click()
                .mainPage().logoutButton.shouldExist();
        return this;
    }
    @Step("Fill E-mail")
    public AuthSteps fillEmail(String email) {
        baseRouter.authPage().email.fill(email);
        return this;
    }
    @Step("Fill password")
    public AuthSteps fillPassword(String password) {
        baseRouter.authPage().password.fill(password);
        return this;
    }
    @Step("Click enter")
    public AuthSteps clickEnter() {
        baseRouter.authPage().enter.click();
        return this;
    }
    @Step("Enter should exist")
    public AuthSteps shouldExistEnter() {
        baseRouter.authPage().enter.shouldExist();
        return this;
    }
    @Step("Logout")
    public AuthSteps logout() {
        baseRouter.mainPage().logoutButton.click()
                .authPage().enter.shouldExist();
        return this;
    }
}
public class BaseRouter {
//    ,      ,     
    public AuthPage authPage() {return page(AuthPage.class);}
    public MainPage mainPage() {return page(MainPage.class);}
    public UsersPage usersPage() {return page(UsersPage.class);}
    public VariantsPage variantsPage() {return page(VariantsPage.class);}
}




Diese Schritte sind den Schritten auf den Seiten sehr ähnlich, praktisch nicht anders. Die Aufteilung in separate Klassen eröffnet jedoch Spielraum für die Codegenerierung, während die feste Verknüpfung mit der entsprechenden Seite nicht verloren geht. Wenn Sie keine Schritte in die Seite schreiben, verschwindet die Bedeutung der Kapselung. Wenn Sie pageElement keine Klasse von Schritten hinzufügen, bleibt die Interaktion mit der Seite weiterhin von der Geschäftslogik getrennt.



, , . . , , , « , ». — , page object , !





Es ist falsch, über die Architektur des Projekts zu sprechen, ohne die Methoden für den bequemen Umgang mit Testdaten zu berühren.



Am einfachsten ist es, Daten direkt im Test "wie sie sind" oder in Variablen zu übergeben. Dies ist in Ordnung für die Blattarchitektur, aber große Projekte werden chaotisch.



Eine andere Methode ist das Speichern von Daten als Objekte. Es hat sich als die beste für uns herausgestellt, da alle Daten, die sich auf eine Entität beziehen, an einem Ort gesammelt werden, wodurch die Versuchung beseitigt wird, alles zu verwechseln und etwas am falschen Ort zu verwenden. Darüber hinaus bietet diese Methode viele zusätzliche Verbesserungen, die für einzelne Projekte hilfreich sein können.



Für jede Entität wird ein Modell erstellt, das sie beschreibt und im einfachsten Fall die Namen und Feldtypen enthält. Hier ist beispielsweise das Benutzermodell:



public class User {
    private Integer id;
    private String mail;
    private String name;
    private String password;
    private Gender gender;

    private boolean check11;
    private boolean check12;
    private boolean check21;
    private boolean check22;
    private boolean check23;

    public enum Gender {
        MALE,
        FEMALE;

        public String getVisibleText() {
            switch (this) {
                case MALE:
                    return "";
                case FEMALE:
                    return "";
            }
            return "";
        }
    }
}


Life Hack Nr. 1: Wenn Sie eine restliche Architektur für die Client-Server-Interaktion haben (JSON- oder XML-Objekte befinden sich zwischen Client und Server und keine unlesbaren Codeteile), können Sie JSON zu <Ihrer Sprache> -Objekt googeln, wahrscheinlich haben Sie bereits den erforderlichen Generator ...



Life Hack # 2: Wenn Ihre Serverentwickler in derselben objektorientierten Programmiersprache schreiben, können Sie ihre Modelle verwenden.



Life Hack # 3: Wenn Sie ein Javist sind und ein Unternehmen Ihnen die Verwendung von Bibliotheken von Drittanbietern ermöglicht und keine nervösen Kollegen in der Nähe sind, die Ketzern, die zusätzliche Bibliotheken anstelle von reinem und schönem Java verwenden, große Schmerzen vorhersagen, nehmen Sie Lombok ! Ja, normalerweise IDEkann Getter, Setter, toString und Builder generieren. Beim Vergleich unserer Lombok-Modelle und Entwicklungsmodelle ohne Lombok wird jedoch ein Gewinn von Hunderten von Zeilen "leeren" Codes sichtbar, der nicht für jede Klasse Geschäftslogik enthält. Wenn Sie einen Lombok verwenden, müssen Sie nicht die Hände derer schlagen, die Felder und Getter mit Setzern mischen. Die Klasse ist leichter zu lesen. Sie können sich sofort ein Bild vom Objekt machen, ohne durch drei Bildschirme scrollen zu müssen.



Wir haben also Drahtgitter von Objekten, auf denen wir die Testdaten strecken müssen. Daten können als endgültige statische Variablen gespeichert werden. Dies kann beispielsweise für den Hauptsystemadministrator nützlich sein, unter dem andere Benutzer erstellt werden. Es ist besser, final zu verwenden, damit keine Versuchung besteht, die Daten in Tests zu ändern, da der nächste Test anstelle des Administrators einen „machtlosen“ Benutzer erhalten kann, ganz zu schweigen vom parallelen Start von Tests.



public class Users {
    public static final User admin = User.builder().mail("test@protei.ru").password("test").build();
}


Um Daten zu erhalten, die keine Auswirkungen auf andere Tests haben, können Sie das Muster "Prototyp" verwenden und Ihre Instanz in jedem Test klonen. Wir haben beschlossen, es einfacher zu machen: eine Methode zu schreiben, die die Felder der Klasse zufällig sortiert, ungefähr so:



    public static User getUserRandomData() {
        User user = User.builder()
                .mail(getRandomEmail())
                .password(getShortLatinStr())
                .name(getShortLatinStr())
                .gender(getRandomFromEnum(User.Gender.class))
                .check11(getRandomBool())
                .check21(getRandomBool())
                .check22(getRandomBool())
                .check23(getRandomBool())
                .build();
//business-logic: 11 xor 12 must be selected
        if (!user.isCheck11()) user.setCheck12(true); 
        if (user.isCheck11()) user.setCheck12(false);
        return user;
    }


Gleichzeitig sollten Methoden, die eine direkte Zufälligkeit erzeugen, in eine separate Klasse eingeordnet werden, da sie auch in anderen Modellen verwendet werden:







Bei der Methode zum Erhalten eines zufälligen Benutzers wurde das Muster "Builder" verwendet , das erforderlich ist, um nicht für jeden erforderlichen Satz einen neuen Konstruktortyp zu erstellen Felder. Stattdessen können Sie natürlich einfach den gewünschten Konstruktor aufrufen.



Bei dieser Methode zum Speichern von Daten wird das Wertobjektmuster verwendet, auf dessen Grundlage Sie je nach den Anforderungen des Projekts einen beliebigen Wunsch hinzufügen können. Sie können der Datenbank Speicherobjekte hinzufügen und so das System vor dem Test vorbereiten. Sie können Benutzer nicht zufällig sortieren, sondern sie aus den Eigenschaftendateien (und einer anderen coolen Bibliothek) laden). Sie können überall denselben Benutzer verwenden, aber für jeden Objekttyp die sogenannte Datenregistrierung erstellen, in der der Wert des End-to-End-Zählers zum Namen oder einem anderen eindeutigen Feld des Objekts hinzugefügt wird und der Test immer einen eigenen eindeutigen testUser_135 hat.



Sie können Ihren eigenen Objektspeicher (Google Object Pool und Flyweight) schreiben, von dem Sie zu Beginn des Tests die erforderlichen Entitäten anfordern können. Das Lager gibt eines seiner arbeitsfertigen Objekte und markiert es als besetzt. Am Ende des Tests wird das Objekt in den Speicher zurückgebracht, wo es bei Bedarf gereinigt, als frei markiert und dem nächsten Test übergeben wird. Dies erfolgt, wenn das Erstellen von Objekten sehr ressourcenintensiv ist und der Speicher bei diesem Ansatz unabhängig von den Tests arbeitet und Daten für die folgenden Fälle vorbereiten kann.



Datenerstellung



Für Benutzerbearbeitungsfälle benötigen Sie auf jeden Fall einen erstellten Benutzer, den Sie bearbeiten, und im Allgemeinen ist es dem Bearbeitungstest egal, woher dieser Benutzer stammt. Es gibt verschiedene Möglichkeiten, es zu erstellen:



  • Drücken Sie vor dem Test die Tasten mit den Händen.
  • Daten aus dem vorherigen Test hinterlassen,
  • Bereitstellung vor dem Test aus dem Backup,
  • Erstellen Sie durch Klicken auf die Schaltflächen direkt im Test,
  • Verwenden Sie die API.


Alle diese Methoden haben Nachteile: Wenn Sie vor dem Test etwas manuell in das System eingeben müssen, ist dies ein schlechter Test. Daher werden sie als Autotests bezeichnet, da sie so unabhängig wie möglich von menschlichen Händen sein sollten.



Die Verwendung der Ergebnisse des vorherigen Tests verstößt gegen das Atomizitätsprinzip und ermöglicht es Ihnen nicht, den Test separat durchzuführen. Sie müssen die gesamte Charge ausführen, und UI-Tests sind nicht so schnell. Es wird als gute Form angesehen, Tests so zu schreiben, dass sie in hervorragender Isolation und ohne zusätzliche Tänze ausgeführt werden können. Darüber hinaus garantiert ein Fehler bei der Erstellung eines Objekts, das den vorherigen Test gelöscht hat, überhaupt keinen Fehler bei der Bearbeitung, und bei einer solchen Konstruktion fällt der Bearbeitungstest als nächstes, und es ist unmöglich herauszufinden, ob die Bearbeitung funktioniert.



Die Verwendung von Backup (ein gespeichertes Image der Datenbank) mit den für den Test erforderlichen Daten ist bereits ein weniger guter Ansatz, insbesondere wenn das Backup automatisch bereitgestellt wird oder wenn die Tests selbst die Daten in die Datenbank stellen. Warum dieses spezielle Objekt im Test verwendet wird, ist jedoch nicht offensichtlich. Datenüberschneidungsprobleme können auch mit einer großen Anzahl von Tests beginnen. Manchmal funktioniert die Sicherung aufgrund einer Aktualisierung der Datenbankarchitektur nicht mehr ordnungsgemäß, z. B. wenn Sie Tests mit einer alten Version ausführen müssen und die Sicherung bereits neue Felder enthält. Sie können dem entgegenwirken, indem Sie für jede Version der Anwendung einen Sicherungsspeicher organisieren. Manchmal ist die Sicherung aufgrund der Aktualisierung der Datenbankarchitektur nicht mehr gültig. Es werden regelmäßig neue Felder angezeigt, sodass die Sicherung regelmäßig aktualisiert werden muss. Und plötzlich kann es seindass genau solch ein einzelner Benutzer aus dem Backup niemals abstürzt, und wenn der Benutzer gerade erstellt wurde oder der Name ihm ein wenig zufällig gegeben wurde, würden Sie einen Fehler finden. Dies wird als "Pestizideffekt" bezeichnet. Der Test fängt an, Fehler zu erkennen, da die Anwendung für dieselben Daten "verwendet" wird und nicht abfällt und es keine Abweichungen zur Seite gibt.



Wenn der Benutzer im Test durch Klicken auf dieselbe Benutzeroberfläche erstellt wird, nimmt das Pestizid ab und die Nicht-Offensichtlichkeit des Erscheinungsbilds des Benutzers verschwindet. Die Nachteile ähneln der Verwendung der Ergebnisse des vorherigen Tests: Die Geschwindigkeit ist mittelmäßig, und selbst wenn ein Fehler in der Erstellung vorliegt, ändert sich selbst der kleinste (insbesondere ein Testfehler, z. B. ändert sich der Locator der Schaltfläche Speichern), und wir wissen nicht, ob die Bearbeitung funktioniert.



Eine andere Möglichkeit, einen Benutzer zu erstellen, ist die http-API aus dem Test, dh anstatt auf die Schaltflächen zu klicken, senden Sie sofort eine Anfrage zum Erstellen des gewünschten Benutzers. Dadurch wird das Pestizid so weit wie möglich reduziert, es ist offensichtlich, woher der Benutzer kam, und die Erstellungsgeschwindigkeit ist viel höher als beim Klicken auf Schaltflächen. Die Nachteile dieser Methode sind, dass sie nicht für Projekte ohne json oder xml im Kommunikationsprotokoll zwischen Client und Server geeignet ist (z. B. wenn die Entwickler mit gwt schreiben und keine zusätzliche API für Tester schreiben möchten). Bei Verwendung der API ist es möglich, eine vom Admin-Panel ausgeführte Logik zu verlieren und eine ungültige Entität zu erstellen. Die API kann sich ändern, was dazu führt, dass die Tests fehlschlagen. In der Regel ist dies jedoch bekannt, und niemand muss Änderungen vornehmen. Dies ist höchstwahrscheinlich eine neue Logik, die noch überprüft werden muss.Es ist auch möglich, dass auf API-Ebene ein Fehler auftritt, aber keine andere Methode als vorgefertigte Sicherungen ist hiervon sicher. Daher ist es besser, Ansätze zum Erstellen von Daten zu kombinieren.



Fügen Sie einen Tropfen API hinzu



Unter den Methoden zum Vorbereiten von Daten ist die http-API für die aktuellen Anforderungen eines separaten Tests und das Bereitstellen einer Sicherung für zusätzliche Testdaten, die sich in Tests nicht ändern, z. B. Symbole für Objekte, damit Tests dieser Objekte beim Laden der Symbole nicht abstürzen, für uns am besten geeignet.



Um Objekte über die API in Java zu erstellen, erwies es sich als am bequemsten, die restAssured-Bibliothek zu verwenden, obwohl dies nicht wirklich vorgesehen ist. Ich möchte ein paar gefundene Chips teilen, wissen Sie mehr - schreiben Sie!



Der erste Schmerz ist die Autorisierung im System. Die Methode muss für jedes Projekt separat ausgewählt werden, aber es gibt eine Gemeinsamkeit: Die Autorisierung muss in die Anforderungsspezifikation aufgenommen werden, zum Beispiel:



public class ApiSettings {
    private static String loginEndpoint="/login";

    public static RequestSpecification testApi() {
        RequestSpecBuilder tmp = new RequestSpecBuilder()
                .setBaseUri(testConfig.getSiteUrl())
                .setContentType(ContentType.JSON)
                .setAccept(ContentType.JSON)
                .addFilter(new BeautifulRest())
                .log(LogDetail.ALL);
        Map<String, String> cookies = RestAssured.given().spec(tmp.build())
                .body(admin)
                .post(loginEndpoint).then().statusCode(200).extract().cookies();
        return tmp.addCookies(cookies).build();
    }
}


Sie können die Möglichkeit hinzufügen, Cookies für einen bestimmten Benutzer zu speichern. Dadurch wird die Anzahl der Anforderungen an den Server verringert. Die zweite mögliche Erweiterung dieser Methode besteht darin, die empfangenen Cookies für den aktuellen Test zu speichern und sie an den Browsertreiber zu werfen, wobei der Autorisierungsschritt übersprungen wird. Die Verstärkung beträgt Sekunden, aber wenn Sie sie mit der Anzahl der Tests multiplizieren, können Sie ziemlich gut beschleunigen!



Es gibt ein Brötchen für den Gang und schöne Berichte, achten Sie auf die Linie .addFilter(new BeautifulRest()):



BeautifulRest Klasse


public class BeautifulRest extends AllureRestAssured {
        public BeautifulRest() {}

        public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext filterContext) {
            AllureLifecycle lifecycle = Allure.getLifecycle();
            lifecycle.startStep(UUID.randomUUID().toString(), (new StepResult()).setStatus(Status.PASSED).setName(String.format("%s: %s", requestSpec.getMethod(), requestSpec.getURI())));
            Response response;
            try {
                response = super.filter(requestSpec, responseSpec, filterContext);
            } finally {
                lifecycle.stopStep();
            }
            return response;
        }
}




Objektmodelle passen perfekt in restAssured, da die Bibliothek selbst die Serialisierung und Deserialisierung von Modellen in json / xml (Konvertierung von json / xml-Formaten in ein Objekt einer bestimmten Klasse) übernimmt.



    @Step("create user")
    public static User createUser(User user) {
        String usersEndpoint = "/user";
        return RestAssured.given().spec(ApiSettings.testApi())
                .when()
                .body(user)
                .post(usersEndpoint)
                .then().log().all()
                .statusCode(200)
                .body("state",containsString("OK"))
                .extract().as(User.class);
    }


Wenn Sie mehrere Schritte hintereinander zum Erstellen von Objekten in Betracht ziehen, können Sie die Identität des Codes feststellen. Um denselben Code zu reduzieren, können Sie eine allgemeine Methode zum Erstellen von Objekten schreiben.



    public static Object create(String endpoint, Object model) {
        return RestAssured.given().spec(ApiSettings.testApi())
                .when()
                .body(model)
                .post(endpoint)
                .then().log().all()
                .statusCode(200)
                .body("state",containsString("OK"))
                .extract().as(model.getClass());
    }

    @Step("create user")
    public static User createUser(User user) {
                  create(User.endpoint, user);
    }


Noch einmal über Routineoperationen



Bei der Überprüfung der Bearbeitung eines Objekts ist es uns im Allgemeinen egal, wie das Objekt im System angezeigt wurde - über die API oder aus dem Backup oder wurde es durch einen UI-Test erstellt. Wichtige Aktionen sind, ein Objekt zu finden, auf das Symbol „Bearbeiten“ zu klicken, die Felder zu löschen und mit neuen Werten zu füllen, auf „Speichern“ zu klicken und zu überprüfen, ob alle neuen Werte korrekt gespeichert wurden. Alle unnötigen Informationen, die nicht direkt mit dem Test zusammenhängen, sollten in separaten Methoden entfernt werden, z. B. in der Schrittklasse.



    @Test
    void checkUserVars() {        
//Arrange
        User userForTest = getUserRandomData();
       
 //         , 
 //      -  , 
 //   ,   
        usersSteps.createUser(userForTest);
        authSteps.login(userForTest);
       
 //Act
        mainMenuSteps
                .clickVariantsMenu();
       
 //Assert
        variantsSteps
                .checkAllVariantsArePresent(userForTest.getVars())
                .checkVariantsCount(userForTest.getVarsCount());
        
//Cleanup
        usersSteps.deleteUser(userForTest);
    }


Es ist wichtig, sich nicht mitreißen zu lassen, da ein Test, der nur aus "komplexen" Aktionen besteht, weniger lesbar und schwieriger zu reproduzieren ist, ohne den Code zu durchsuchen.



    @Test
    void authAsAdmin() {
        authSteps.login(Users.admin);
//  ,    .     . 
//   ,   ? 


Wenn in der Suite praktisch dieselben Tests angezeigt werden, die sich nur in der Aufbereitung der Daten unterscheiden (z. B. müssen Sie überprüfen, ob alle drei Arten von "unterschiedlichen" Benutzern dieselben Aktionen ausführen können, oder es gibt unterschiedliche Arten von Steuerobjekten, für die Sie jeweils prüfen müssen Wenn Sie identische abhängige Objekte erstellen oder die Filterung nach zehn Arten von Objektstatus überprüfen müssen, können Sie die sich wiederholenden Teile immer noch nicht in eine separate Methode verschieben. Überhaupt nicht, wenn Ihnen Lesbarkeit wichtig ist!



Stattdessen müssen Sie sich über datengesteuerte Tests informieren. Für Java + TestNG sieht es ungefähr so ​​aus:



    @Test(dataProvider = "usersWithDifferentVars")
    void checkUserDifferentVars(User userForTest) {
        //Arrange
        usersSteps.createUser(userForTest);
        authSteps.login(userForTest);
        //Act
        mainMenuSteps
                .clickVariantsMenu();
        //Assert
        variantsSteps
                .checkAllVariantsArePresent(userForTest.getVars())
                .checkVariantsCount(userForTest.getVarsCount());
    }

 //         . 
 // ,   -.
    @DataSupplier(name = "usersWithDifferentVars")
    public Stream<User> usersWithDifferentVars(){
        return Stream.of(
            getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(false),
            getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(false),
            getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(false),
            getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(true),
            getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(false),
            getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(true),
            getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(true),
            getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(true)
        );
    }


Es verwendet die Datenlieferantenbibliothek , ein Add-On über den TestNG-Datenprovider, mit dem Sie typisierte Sammlungen anstelle von Object [] [] verwenden können, aber das Wesentliche ist dasselbe. Somit erhalten wir einen Test, der so oft ausgeführt wird, wie er Eingabedaten empfängt.



Schlussfolgerungen



Um ein großes, aber praktisches Projekt von Autotests für Benutzeroberflächen zu erstellen, benötigen Sie:



  • Beschreiben aller kleinen Widgets in der Anwendung.
  • Sammeln Sie Widgets in Seiten,
  • Erstellen Sie Modelle für alle Arten von Entitäten.
  • Fügen Sie Methoden hinzu, um alle Arten von Entitäten basierend auf Modellen zu generieren.
  • Betrachten Sie eine geeignete Methode zum Erstellen zusätzlicher Entitäten
  • Optional: Schrittdateien manuell generieren oder sammeln,
  • Schreiben Sie Tests so, dass es im Abschnitt der Hauptaktionen eines bestimmten Tests keine komplexen Aktionen gibt, sondern nur offensichtliche Operationen mit Widgets.


Fertig, Sie haben ein PageElement-basiertes Projekt mit einfachen Methoden zum Speichern, Generieren und Vorbereiten von Daten erstellt. Sie haben jetzt eine leicht zu wartende Architektur, die verwaltbar und flexibel genug ist. Sowohl ein erfahrener Tester als auch ein Anfänger im Juni können problemlos im Projekt navigieren, da Autotests im Format von Benutzeraktionen am bequemsten zu lesen und zu verstehen sind.



Codebeispiele aus dem Artikel in Form eines fertigen Projekts werden dem Git hinzugefügt .



All Articles