Probablement la meilleure architecture pour les tests d'interface utilisateur



Probablement, il y a quelque part un article idéal qui révèle immédiatement et complètement le sujet de l'architecture de test, facile à écrire, à lire et à maintenir, et de sorte qu'il soit compréhensible pour les débutants, avec des exemples d'implémentation et de domaines d'application. Je voudrais offrir ma vision de cet «article idéal» dans le format dont je rêvais, seulement après avoir reçu la première tâche «écrire des autotests». Pour ce faire, je parlerai des approches bien connues et moins connues des autotests Web, pourquoi, comment et quand les utiliser, ainsi que des solutions réussies pour stocker et créer des données.



Bonjour, Habr! Je m'appelle Diana, je suis à la tête du groupe de test d'interface utilisateur et j'automatise les tests Web et de bureau depuis cinq ans. Les exemples de code seront en java et pour le web, mais, dans la pratique, cela a été testé, les approches sont applicables à python avec un bureau.



Au début c'était ...



Au début, il y avait un mot, et il y avait beaucoup de mots, et ils remplissaient toutes les pages uniformément de code, quels que soient vos architectures et vos principes DRY (ne vous répétez pas - pas besoin de répéter le code que vous avez déjà écrit trois paragraphes ci-dessus).



Feuille



En fait, l'architecture de la "footcloth", alias "sheet", ou code non structuré empilé dans un tas qui remplit uniformément l'écran, n'est pas si mauvaise et est tout à fait applicable dans les situations suivantes:



  • Un clic rapide en trois lignes (d'accord, deux cent trois) pour les très petits projets;
  • Pour des exemples de code dans la mini démo;
  • Pour le premier code dans le style "Hello Word" parmi les autotests.


Que devez-vous faire pour obtenir l'architecture du drap de lit? écrivez simplement tout le code nécessaire dans un seul fichier, un canevas commun.



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();
    }
}


Si vous commencez tout juste à vous familiariser avec les autotests, alors la «feuille» est déjà suffisante pour effectuer une tâche de test simple, surtout si vous démontrez une bonne connaissance de la conception des tests et une bonne couverture. Mais c'est trop facile pour les projets à grande échelle, donc si vous avez des ambitions, mais que vous n'avez pas le temps d'exécuter idéalement chaque cas de test, alors au moins votre gita devrait avoir un exemple d'architecture plus complexe.



PageObject



Avez-vous entendu parler de rumeurs selon lesquelles PageObject est obsolète? Vous ne savez tout simplement pas comment le cuisiner!



L'unité de travail principale de ce modèle est une «page», c'est-à-dire un ensemble complet d'éléments et d'actions avec eux, par exemple, MenuPage - une classe qui décrit toutes les actions avec un menu, c'est-à-dire des clics sur des onglets, des éléments déroulants en expansion, etc.







Il est un peu plus difficile de composer un PageObject pour la fenêtre modale (pour faire court "modal") de la création de l'objet. L'ensemble des champs de classe est clair: tous les champs d'entrée, cases à cocher, listes déroulantes; et pour les méthodes, il y a deux options: vous pouvez faire les deux méthodes universelles "remplir tous les champs modaux", "remplir tous les champs modaux avec des valeurs aléatoires", "vérifier tous les champs modaux" et des méthodes séparées "remplir le nom", "vérifier le nom", "Remplissez la description" et ainsi de suite. Ce qu'il faut utiliser dans un cas particulier est déterminé par les priorités - l'approche «une méthode pour l'ensemble du mode modal» augmente la vitesse d'écriture d'un test, mais par rapport à l'approche «une méthode pour chaque champ», elle perd beaucoup de lisibilité du test.



Exemple
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();
    }


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



L'essentiel est que toutes les actions avec des pages sont encapsulées à l'intérieur des pages (l'implémentation est masquée, seules les actions logiques sont disponibles), ainsi, les fonctions métier sont déjà utilisées dans le test. Et cela, à son tour, vous permet d'écrire vos propres pages pour chaque plate-forme (web, bureau, téléphones mobiles) sans changer les tests.



Le seul dommage est que les interfaces absolument identiques sont rares sur des plates-formes différentes.



Pour réduire l'écart entre les interfaces, il y a une tentation de compliquer les étapes individuelles, elles sont sorties dans des classes intermédiaires distinctes, et les tests deviennent de moins en moins lisibles, jusqu'à deux étapes: "connectez-vous, faites bien", le test est terminé. En plus du web, il n'y avait pas d'interfaces supplémentaires dans nos projets, et nous devons lire des cas plus souvent qu'écrire, par conséquent, par souci de lisibilité, les objets PageObjects historiques ont acquis un nouveau look.



PageObject est un classique que tout le monde connaît. De nombreux articles avec des exemples dans presque tous les langages de programmation peuvent être trouvés le long de cette approche. L'utilisation de PageObject est très souvent utilisée pour juger si un candidat sait quelque chose sur le test des interfaces utilisateur. La plupart des employeurs s'attendent à ce que la plupart des employeurs s'attendent à une mission de test en utilisant cette approche, et une grande partie de celle-ci réside dans des projets de production, même si seul le Web est en test.



Que se passe-t-il d'autre?



Curieusement, pas un seul PageObject!



  • Le modèle ScreenPlay est souvent rencontré, que vous pouvez lire, par exemple, ici . Elle n'a pas pris racine dans notre pays, car utiliser les approches BDD sans impliquer des personnes qui ne savent pas lire le code est une violence insensée contre les automates.
  • js- , PageObject, - , , .
  • - , , ModelBaseTesting, . , .


Et je vous parlerai plus en détail de l'élément de page, qui vous permet de réduire la quantité du même type de code, tout en augmentant la lisibilité et en fournissant une compréhension rapide des tests, même pour ceux qui ne sont pas familiers avec le projet. Et sur lui (avec ses propres blackjacks et préférences, bien sûr!) Les frameworks non-js populaires htmlElements, Atlas et Epam's JDI sont construits.



Qu'est-ce que l'élément de page?



Pour créer le modèle d'élément de page, commencez par l'élément de niveau le plus bas. Comme le dit Wiktionary , un "widget" est une primitive logicielle d'une interface utilisateur graphique qui a une apparence standard et effectue des actions standard. Par exemple, le widget le plus simple "Button" - vous pouvez cliquer dessus, vous pouvez vérifier son texte et sa couleur. Dans le "Champ de saisie", vous pouvez saisir du texte, vérifier quel texte est saisi, cliquer, vérifier l'affichage du focus, vérifier le nombre de caractères saisis, saisir le texte et appuyer sur "Entrée", vérifier l'espace réservé, vérifier la mise en évidence du champ "obligatoire" et du texte d'erreur, et c'est tout, quoi d'autre peut être nécessaire dans un cas particulier. De plus, toutes les actions avec ce champ sont standard sur n'importe quelle page.







Il existe des widgets plus complexes pour lesquels les actions ne sont pas si évidentes, par exemple les tables des matières en arborescence. Lors de leur écriture, vous devez vous baser sur ce que l'utilisateur fait avec cette zone du programme, par exemple:



  • Cliquez sur un élément de la table des matières avec le texte spécifié,
  • Vérifier l'existence d'un élément avec le texte donné,
  • Vérification de l'indentation d'un élément avec le texte donné.


Les widgets peuvent être de deux types: avec un localisateur dans le constructeur et avec un localisateur cousu dans le widget sans possibilité de le changer. La table des matières est généralement une sur la page, sa méthode de recherche sur la page peut être laissée "à l'intérieur" des actions avec la table des matières, cela n'a aucun sens de retirer son localisateur séparément, car le localisateur peut être accidentellement endommagé de l'extérieur, mais il n'y a aucun avantage à son stockage séparé. À son tour, un champ de texte est une chose universelle, au contraire, vous devez travailler avec lui uniquement via le localisateur du constructeur, car il peut y avoir plusieurs champs d'entrée à la fois. Si au moins une méthode apparaît et n'est destinée qu'à un champ de saisie spécial, par exemple, avec un clic supplémentaire sur l'indice de liste déroulante, ce n'est plus seulement un champ de saisie, il est temps de créer votre propre widget pour celui-ci.



Pour réduire le chaos général, les widgets, comme les éléments de page, sont combinés dans les mêmes pages, à partir desquelles, apparemment, le nom Élément de page est composé.



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();
}


Pour utiliser tout ce qui a été créé ci-dessus dans les tests, vous devez vous référer séquentiellement à la page, au widget, à l'action, nous obtenons ainsi la construction suivante:



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


Vous pouvez ajouter une couche d'étape classique si cela est nécessaire dans votre framework (l'implémentation de la bibliothèque distante en Java pour RobotFramework nécessite une classe d'étape en entrée, par exemple), ou si vous souhaitez ajouter des annotations pour de beaux rapports. Nous en avons fait un générateur basé sur les annotations, si cela vous intéresse, écrivez dans les commentaires, nous vous le dirons.



Un exemple de classe d'étape d'autorisation
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);}
}




Ces étapes sont très similaires aux étapes à l'intérieur des pages, pratiquement pas différentes. Mais les séparer en classes séparées ouvre la voie à la génération de code, tandis que le lien dur avec la page correspondante n'est pas perdu. En même temps, si vous n'écrivez pas d'étapes dans la page, la signification de l'encapsulation disparaît et si vous n'ajoutez pas de classe d'étapes à pageElement, l'interaction avec la page reste alors séparée de la logique métier.



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





Il est faux de parler de l'architecture du projet sans toucher aux méthodes de fonctionnement pratique avec les données de test.



Le moyen le plus simple est de transmettre les données directement dans le test "telles quelles" ou dans des variables. C'est bien pour l'architecture en feuille, mais les grands projets deviennent compliqués.



Une autre méthode consiste à stocker les données en tant qu'objets, elle s'est avérée être la meilleure pour nous, car elle collecte toutes les données liées à une entité en un seul endroit, supprimant la tentation de tout mélanger et d'utiliser quelque chose au mauvais endroit. De plus, cette méthode présente de nombreuses améliorations supplémentaires qui peuvent être utiles sur des projets individuels.



Pour chaque entité, un modèle la décrivant est créé, qui dans le cas le plus simple contient les noms et les types de champs, par exemple, voici le modèle utilisateur:



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 # 1: si vous avez une architecture d'interaction client-serveur de type repos (des objets json ou xml vont entre le client et le serveur, et non des morceaux de code illisibles), alors vous pouvez google json vers l'objet <votre langage>, probablement le générateur dont vous avez besoin existe déjà ...



Life hack # 2: si vos développeurs de serveurs écrivent dans le même langage de programmation orienté objet, vous pouvez utiliser leurs modèles.



Life hack # 3: si vous êtes un javiste et qu'une entreprise vous permet d'utiliser des bibliothèques tierces, et qu'il n'y a pas de collègues nerveux autour, prédisant beaucoup de douleur pour les hérétiques qui utilisent des bibliothèques supplémentaires au lieu de Java pur et beau, prenez Lombok ! Oui, généralement IDEpeut générer des getters, des setters, des toString et des builders. Mais en comparant nos modèles Lombok et ceux de développement sans Lombok, un profit de centaines de lignes de code "vide" qui ne porte pas de logique métier pour chaque classe est visible. Lorsque vous utilisez un Lombok, vous n'avez pas à battre les mains de ceux qui mélangent les champs et les getters, les setters, la classe est plus facile à lire, vous pouvez vous faire une idée de l'objet à la fois, sans faire défiler trois écrans.



Ainsi, nous avons des wireframes d'objets sur lesquels nous devons étirer les données de test. Les données peuvent être stockées en tant que variables statiques finales, par exemple, cela peut être utile pour l'administrateur système principal, à partir duquel d'autres utilisateurs sont créés. Il est préférable d'utiliser final, pour qu'il n'y ait pas de tentation de changer les données dans les tests, car alors le test suivant, à la place de l'administrateur, peut obtenir un utilisateur «impuissant», sans parler de l'exécution parallèle des tests.



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


Pour obtenir des données qui n'affectent pas les autres tests, vous pouvez utiliser le modèle "prototype" et cloner votre instance dans chaque test. Nous avons décidé de faciliter les choses: d'écrire une méthode qui randomise les champs de la classe, quelque chose comme ceci:



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


Dans le même temps, les méthodes qui créent un caractère aléatoire direct sont mieux placées dans une classe distincte, car elles seront également utilisées dans d'autres modèles:







dans la méthode d'obtention d'un utilisateur aléatoire, le modèle "constructeur" a été utilisé , ce qui est nécessaire pour ne pas créer un nouveau type de constructeur pour chaque ensemble requis des champs. Au lieu de cela, bien sûr, vous pouvez simplement appeler le constructeur souhaité.



Cette méthode de stockage des données utilise le modèle d'objet de valeur, sur la base duquel vous pouvez ajouter n'importe lequel de vos souhaits, en fonction des besoins du projet. Vous pouvez ajouter des objets de sauvegarde à la base de données et ainsi préparer le système avant le test. Vous ne pouvez pas randomiser les utilisateurs, mais les charger à partir de fichiers de propriétés (et d'une autre bibliothèque sympa). Vous pouvez utiliser le même utilisateur partout, mais créer le soi-disant registre de données pour chaque type d'objet, dans lequel la valeur du compteur de bout en bout sera ajoutée au nom ou à un autre champ unique de l'objet, et le test aura toujours son propre testUser_135 unique.



Vous pouvez écrire votre propre Object Storage (pool d'objets google et flyweight), à partir duquel vous pouvez demander les entités nécessaires au début du test. L'entrepôt donne l'un de ses objets prêts à l'emploi et le marque comme occupé. À la fin du test, l'objet est renvoyé au stockage, où il est nettoyé si nécessaire, marqué comme libre et remis au test suivant. Ceci est fait si les opérations de création d'objets sont très gourmandes en ressources, et avec cette approche, le stockage fonctionne indépendamment des tests et peut préparer des données pour les cas suivants.



Création de données



Pour les cas d'édition utilisateur, vous aurez certainement besoin d'un utilisateur créé que vous éditerez et, en général, le test d'édition ne se soucie pas de l'origine de cet utilisateur. Il existe plusieurs façons de le créer:



  • appuyez sur les boutons avec vos mains avant le test,
  • laisser les données du test précédent,
  • déployer avant le test à partir de la sauvegarde,
  • créer en cliquant sur les boutons directement dans le test,
  • utilisez l'API.


Toutes ces méthodes ont des inconvénients: si avant le test, vous devez entrer quelque chose dans le système manuellement, alors c'est un mauvais test, et donc on les appelle des autotests, qu'ils doivent agir aussi indépendamment des mains humaines que possible.



L'utilisation des résultats du test précédent viole le principe d'atomicité et ne vous permet pas d'exécuter le test séparément, vous devrez exécuter tout le lot et les tests d'interface utilisateur ne sont pas si rapides. Il est considéré comme une bonne forme d'écrire des tests de manière à ce que chacun puisse être exécuté dans un splendide isolement et sans danses supplémentaires. De plus, un bogue dans la création d'un objet ayant abandonné le test précédent ne garantit pas du tout un bogue dans l'édition, et dans une telle construction, le test d'édition tombera ensuite, et il est impossible de savoir si l'édition fonctionne.



Utiliser la sauvegarde (une image sauvegardée de la base de données) avec les données nécessaires au test est déjà une approche plus ou moins bonne, surtout si la sauvegarde est déployée automatiquement ou si les tests eux-mêmes mettent les données dans la base de données. Cependant, la raison pour laquelle cet objet particulier est utilisé dans le test n'est pas évidente, les problèmes d'intersection de données peuvent également commencer par un grand nombre de tests. Parfois, la sauvegarde cesse de fonctionner correctement en raison d'une mise à jour de l'architecture de la base de données, par exemple, si vous devez exécuter des tests sur une ancienne version et que la sauvegarde contient déjà de nouveaux champs. Vous pouvez lutter contre cela en organisant un stockage de sauvegarde pour chaque version de l'application. Parfois, la sauvegarde cesse d'être à nouveau valide en raison de la mise à jour de l'architecture de la base de données - de nouveaux champs apparaissent régulièrement, la sauvegarde doit donc être mise à jour régulièrement. Et soudainement ça peut êtrequ'exactement un tel utilisateur unique de sauvegarde ne plante jamais, et si l'utilisateur vient d'être créé ou si le nom lui a été donné un peu au hasard, vous trouverez un bogue. C'est ce qu'on appelle «l'effet pesticide», le test arrête d'attraper les bugs, car l'application est «habituée» aux mêmes données et ne tombe pas, et il n'y a pas d'écarts sur le côté.



Si l'utilisateur est créé dans le test par des clics sur la même interface, alors le pesticide diminue et la non-évidence de l'apparence de l'utilisateur disparaît. Les inconvénients sont similaires à l'utilisation des résultats du test précédent: la vitesse est moyenne, et même s'il y a un bug dans la création, même le plus petit (surtout un bug de test, par exemple, le localisateur du bouton de sauvegarde va changer), alors nous ne saurons pas si l'édition fonctionne.



Enfin, une autre façon de créer un utilisateur consiste à utiliser http-API à partir du test, c'est-à-dire qu'au lieu de cliquer sur les boutons, envoyez immédiatement une requête pour créer l'utilisateur souhaité. Ainsi, le pesticide est réduit au maximum, la provenance de l'utilisateur est évidente, et la vitesse de création est bien plus élevée qu'en cliquant sur des boutons. Les inconvénients de cette méthode sont qu'elle ne convient pas aux projets sans json ou xml dans le protocole de communication entre le client et le serveur (par exemple, si les développeurs écrivent en utilisant gwt et ne veulent pas écrire une API supplémentaire pour les testeurs). Il est possible, lors de l'utilisation de l'API, de perdre une partie de la logique exécutée par le panneau d'administration et de créer une entité invalide. L'API peut changer, entraînant l'échec des tests, mais généralement cela est connu et personne n'a besoin de changements pour des raisons de changements, il s'agit très probablement d'une nouvelle logique qui devra encore être vérifiée.Il est également possible qu'il y ait un bogue au niveau de l'API, mais pas une seule méthode autre que les sauvegardes prêtes à l'emploi n'est à l'abri de cela, il est donc préférable de combiner des approches pour créer des données.



Ajouter une goutte d'API



Parmi les méthodes de préparation des données, l'API http pour les besoins actuels d'un test séparé et le déploiement d'une sauvegarde pour les données de test supplémentaires qui ne changent pas dans les tests, par exemple, les icônes pour les objets, afin que les tests de ces objets ne se bloquent pas lorsque les icônes sont chargées, nous conviennent le mieux.



Pour créer des objets via l'API en Java, il s'est avéré plus pratique d'utiliser la bibliothèque restAssured, bien qu'elle ne soit pas vraiment destinée à cela. Je veux partager quelques puces trouvées, vous en savez plus - écrivez!



La première douleur est l'autorisation dans le système. Sa méthode doit être sélectionnée séparément pour chaque projet, mais il y a un point commun: l'autorisation doit être placée dans la spécification de la demande, par exemple:



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();
    }
}


Vous pouvez ajouter la possibilité d'enregistrer des cookies pour un utilisateur spécifique, puis le nombre de demandes au serveur diminuera. La deuxième extension possible de cette méthode consiste à enregistrer les cookies reçus pour le test en cours et à les envoyer au pilote du navigateur, en ignorant l'étape d'autorisation. Les gains sont en secondes, mais si vous les multipliez par le nombre de tests, vous pouvez plutôt bien accélérer!



Il y a un chignon pour la démarche et de beaux rapports, faites attention à la ligne .addFilter(new BeautifulRest()):



Classe BeautifulRest


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




Les modèles d'objets s'intègrent parfaitement à restAssured, puisque la bibliothèque elle-même gère la sérialisation et la désérialisation des modèles en json / xml (conversion des formats json / xml en un objet d'une classe donnée).



    @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);
    }


Si vous envisagez plusieurs étapes consécutives pour créer des objets, vous pouvez remarquer l'identité du code. Pour réduire le même code, vous pouvez écrire une méthode générale de création d'objets.



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


Encore une fois sur les opérations de routine



Dans le cadre de la vérification de l'édition d'un objet, nous ne nous soucions généralement pas de la façon dont l'objet est apparu dans le système - via api ou à partir d'une sauvegarde, ou a-t-il été créé par un test d'interface utilisateur. Les actions importantes sont de trouver un objet, de cliquer sur l'icône «modifier» dessus, d'effacer les champs et de les remplir avec de nouvelles valeurs, de cliquer sur «enregistrer» et de vérifier si toutes les nouvelles valeurs ont été correctement enregistrées. Toutes les informations inutiles qui ne sont pas directement liées au test doivent être supprimées dans des méthodes distinctes, par exemple, dans la classe step.



    @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);
    }


Il est important de ne pas s'emballer, car un test composé uniquement d'actions «complexes» devient moins lisible et plus difficile à reproduire sans fouiller dans le code.



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


Si pratiquement les mêmes tests apparaissent dans la suite, qui ne diffèrent que dans la préparation des données (par exemple, vous devez vérifier que les trois types d'utilisateurs "différents" peuvent effectuer les mêmes actions, ou il existe différents types d'objets de contrôle, pour chacun desquels vous devez vérifier création d'objets dépendants identiques, ou vous devez vérifier le filtrage par dix types de statuts d'objet), vous ne pouvez toujours pas déplacer les parties répétitives dans une méthode distincte. Pas du tout si la lisibilité est importante pour vous!



Au lieu de cela, vous devez lire sur les tests basés sur les données, pour Java + TestNG, ce sera quelque chose comme ceci:



    @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)
        );
    }


Il utilise la bibliothèque Data Supplier , qui est un add-on sur le fournisseur de données TestNG qui vous permet d'utiliser des collections typées au lieu d'Object [] [], mais l'essence est la même. Ainsi, nous obtenons un test, qui est exécuté autant de fois qu'il reçoit des données d'entrée.



conclusions



Ainsi, pour créer un projet volumineux mais pratique d'autotests d'interface utilisateur, vous avez besoin de:



  • Décrivez tous les petits widgets rencontrés dans l'application,
  • Collectez les widgets en pages,
  • Créer des modèles pour toutes sortes d'entités,
  • Ajouter des méthodes pour générer toutes sortes d'entités basées sur des modèles,
  • Envisagez une méthode appropriée pour créer des entités supplémentaires
  • Facultatif: générer ou collecter manuellement des fichiers d'étape,
  • Ecrivez des tests pour que dans la section des actions principales d'un test particulier il n'y ait pas d'actions complexes, seulement des opérations évidentes avec des widgets.


C'est fait, vous avez créé un projet basé sur PageElement avec des méthodes simples pour stocker, générer et préparer des données. Vous disposez désormais d'une architecture facilement maintenable, suffisamment gérable et flexible. Un testeur expérimenté et un débutant June peuvent facilement naviguer dans le projet, car les tests automatiques au format d'actions de l'utilisateur sont les plus pratiques à lire et à comprendre.



Des exemples de code de l'article sous la forme d'un projet fini sont ajoutés au git .



All Articles