Probablemente la mejor arquitectura para pruebas de UI



Probablemente, en algún lugar hay un artículo ideal que revela de forma inmediata y completa el tema de la arquitectura de prueba, fácil de escribir y leer, y de soporte, y para que sea comprensible para los principiantes, con ejemplos de áreas de implementación y aplicación. Me gustaría ofrecer mi visión de este “artículo ideal” en el formato que soñé, solo después de recibir la primera tarea “escribir autotests”. Para hacer esto, hablaré sobre los enfoques conocidos y no tan conocidos de los autotests web, por qué, cómo y cuándo usarlos, así como sobre soluciones exitosas para almacenar y crear datos.



¡Hola, Habr! Mi nombre es Diana, soy la jefa del grupo de pruebas de interfaz de usuario y llevo cinco años automatizando pruebas web y de escritorio. Los ejemplos de código estarán en java y para la web, pero, en la práctica, se ha probado, los enfoques son aplicables a Python con un escritorio.



Al principio fue ...



Al principio había una palabra, y había muchas palabras, y llenaron todas las páginas de manera uniforme con código, independientemente de sus arquitecturas y principios DRY (no se repita, no es necesario repetir el código que ya escribió tres párrafos arriba).



Sábana



De hecho, la arquitectura del "footcloth", también conocido como "sheet", también conocido como código no estructurado amontonado en un montón que llena uniformemente la pantalla, no es tan malo y es bastante aplicable en las siguientes situaciones:



  • Un clic rápido en tres líneas (bueno, doscientas tres) para proyectos muy pequeños;
  • Para ejemplos de código en la mini demostración;
  • Para el primer código en el estilo "Hello Word" entre autotests.


¿Qué necesitas hacer para obtener la arquitectura de la Sábana? simplemente escriba todo el código necesario en un archivo, un lienzo común.



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 está empezando a familiarizarse con las pruebas automáticas, entonces la "hoja" ya es suficiente para completar una tarea de prueba simple, especialmente si muestra un buen conocimiento del diseño de pruebas y una buena cobertura. Pero esto es demasiado fácil para proyectos a gran escala, por lo que si tiene ambiciones, pero no tiene tiempo para ejecutar idealmente cada caso de prueba, al menos su gita debería tener un ejemplo de una arquitectura más compleja.



PageObject



¿Ha oído rumores de que PageObject está obsoleto? ¡Simplemente no sabes cómo cocinarlo!



La unidad de trabajo principal en este patrón es una "página", es decir, un conjunto completo de elementos y acciones con ellos, por ejemplo, MenuPage - una clase que describe todas las acciones con un menú, es decir, clics en pestañas, desplegar elementos desplegables, etc.







Es un poco más difícil componer un PageObject para la ventana modal (para abreviar "modal") de creación de objetos. El conjunto de campos de clase es claro: todos los campos de entrada, casillas de verificación, listas desplegables; y para los métodos hay dos opciones: puede hacer que ambos métodos universales "llenen todos los campos modales", "llenen todos los campos modales con valores aleatorios", "verifiquen todos los campos modales" y métodos separados "llenen el nombre", "verifiquen el nombre", "Complete la descripción" y así sucesivamente. Qué usar en un caso particular está determinado por las prioridades: el enfoque de "un método para todo el modal" aumenta la velocidad de escritura de una prueba, pero en comparación con el enfoque de "un método para cada campo", pierde mucho en la legibilidad de la prueba.



Ejemplo
Compongamos un objeto de página común para crear usuarios para ambos tipos de pruebas:

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


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



La conclusión es que todas las acciones con páginas están encapsuladas dentro de las páginas (la implementación está oculta, solo las acciones lógicas están disponibles), por lo que las funciones comerciales ya se utilizan en la prueba. Y esto, a su vez, te permite escribir tus propias páginas para cada plataforma (web, escritorio, teléfonos móviles), sin cambiar las pruebas.



La única lástima es que las interfaces absolutamente idénticas son raras en diferentes plataformas.



Para reducir la discrepancia entre las interfaces, existe la tentación de complicar los pasos individuales, se dividen en clases intermedias separadas y las pruebas se vuelven cada vez menos legibles, hasta dos pasos: "iniciar sesión, hacerlo bien", la prueba ha terminado. Además de la web, no había interfaces adicionales en nuestros proyectos, y tenemos que leer casos con más frecuencia que escribir, por lo tanto, en aras de la legibilidad, los PageObjects históricos han adquirido un nuevo aspecto.



PageObject es un clásico que todo el mundo conoce. Puede encontrar muchos artículos sobre este enfoque con ejemplos en casi cualquier lenguaje de programación. El uso de PageObject se usa muy a menudo para juzgar si un candidato sabe algo sobre probar interfaces de usuario. Realizar una tarea de prueba con este enfoque es lo que espera la mayoría de los empleadores, y gran parte de ella vive en proyectos de producción, incluso si solo se está probando la web.



¿Qué más pasa?



Curiosamente, ¡ni un solo PageObject!



  • El patrón ScreenPlay se encuentra a menudo, sobre el cual puede leer, por ejemplo, aquí . No echó raíces en nuestro país, ya que utilizar enfoques bdd sin involucrar a personas que no pueden leer el código es una violencia sin sentido contra los autómatas.
  • js- , PageObject, - , , .
  • - , , ModelBaseTesting, . , .


Y le contaré con más detalle sobre el elemento de página, que le permite reducir la cantidad del mismo tipo de código, al tiempo que aumenta la legibilidad y proporciona una comprensión rápida de las pruebas, incluso para aquellos que no están familiarizados con el proyecto. Y en él (con sus propios blackjacks y preferencias, ¡por supuesto!) Se construyen los populares frameworks no js htmlElements, Atlas y Epam's JDI.



¿Qué es el elemento de página?



Para crear el patrón de elemento de página, comience con el elemento de nivel más bajo. Como dice Wiktionary , un "widget" es una primitiva de software de una interfaz gráfica de usuario que tiene una apariencia estándar y realiza acciones estándar. Por ejemplo, el "Botón" del widget más simple: puede hacer clic en él, puede verificar su texto y color. En el "Campo de entrada" puede ingresar texto, verificar qué texto ingresó, hacer clic, verificar la pantalla de enfoque, verificar el número de caracteres ingresados, ingresar el texto y presionar "Enter", verificar el marcador de posición, verificar el resaltado del campo "obligatorio" y el texto de error, y listo, qué más puede ser necesario en un caso particular. Además, todas las acciones con este campo son estándar en cualquier página.







Hay widgets más complejos para los que las acciones no son tan obvias, por ejemplo, tablas de contenido de árbol. Al escribirlos, debe basarse en lo que hace el usuario con esta área del programa, por ejemplo:



  • Haga clic en un elemento de la tabla de contenido con el texto especificado,
  • Comprobando la existencia de un elemento con el texto dado,
  • Comprobación de la sangría de un elemento con un texto determinado.


Los widgets pueden ser de dos tipos: con un localizador en el constructor y con un localizador cosido en el widget, sin la posibilidad de cambiarlo. La tabla de contenido suele ser una en la página, su método de búsqueda en la página se puede dejar “dentro” de las acciones con la tabla de contenido, no tiene sentido sacar su localizador por separado, ya que el localizador puede dañarse accidentalmente desde el exterior, y no hay beneficio de almacenarlo por separado. A su vez, un campo de texto es algo universal, por el contrario, necesita trabajar con él solo a través del localizador del constructor, porque puede haber muchos campos de entrada a la vez. Si aparece al menos un método que está destinado a un solo campo de entrada especial, por ejemplo, con un clic adicional en la sugerencia desplegable, esto ya no es solo un campo de entrada, es hora de crear su propio widget para él.



Para reducir el caos general, los widgets, como los elementos de la página, se combinan en las mismas páginas, a partir de las cuales, aparentemente, se compone el nombre Page Element.



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


Para usar todo lo anterior creado en pruebas, debe consultar secuencialmente la página, el widget, la acción, por lo que obtenemos la siguiente construcción:



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


Puede agregar una capa de pasos clásica si es necesario en su marco (la implementación de la biblioteca remota en Java para RobotFramework requiere una clase de paso como entrada, por ejemplo), o si desea agregar anotaciones para informes hermosos. Lo hicimos un generador basado en anotaciones, si te interesa escribe en los comentarios, te lo contamos.



Un ejemplo de una clase de paso de autorización
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);}
}




Estos pasos son muy similares a los pasos dentro de las páginas, prácticamente no diferentes. Pero separarlos en clases separadas abre un margen para la generación de código, mientras que el enlace duro con la página correspondiente no se pierde. Al mismo tiempo, si no escribe pasos en la página, el significado de encapsulación desaparece, y si no agrega una clase de pasos a pageElement, la interacción con la página seguirá estando separada de la lógica empresarial.



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





Sería incorrecto hablar de la arquitectura de un proyecto sin tocar los métodos para operar convenientemente con datos de prueba.



La forma más sencilla es pasar datos directamente en la prueba "tal cual" o en variables. Esto está bien para la arquitectura de hojas, pero los proyectos grandes se complican.



Otro método es almacenar datos como objetos, resultó ser el mejor para nosotros, ya que recopila todos los datos relacionados con una entidad en un solo lugar, eliminando la tentación de mezclar todo y usar algo en el lugar equivocado. Además, este método tiene muchas mejoras adicionales que pueden ser útiles en proyectos individuales.



Para cada entidad se crea un modelo que la describe, que en el caso más simple contiene los nombres y tipos de campos, por ejemplo, aquí está el modelo de usuario:



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 tiene una arquitectura de interacción cliente-servidor similar al resto (los objetos json o xml van entre el cliente y el servidor, y no fragmentos de código ilegible), entonces puede google json al objeto <su idioma>, probablemente ya tenga el generador requerido ...



Life hack # 2: si los desarrolladores de su servidor escriben en el mismo lenguaje de programación orientado a objetos, entonces puede usar sus modelos.



Life hack # 3: si eres un javist y una empresa te permite usar bibliotecas de terceros, y no hay colegas nerviosos alrededor, prediciendo mucho dolor para los herejes que usan bibliotecas adicionales en lugar de Java puro y hermoso, ¡toma Lombok ! Sí, normalmente IDEpuede generar getters, setters, toString y builders. Pero al comparar nuestros modelos de Lombok y los de desarrollo sin Lombok, es visible una ganancia de cientos de líneas de código "vacío" que no lleva lógica de negocios para cada clase. Al usar Lombok, no tiene que vencer a quienes mezclan campos y getters, setters, la clase es más fácil de leer, puede hacerse una idea del objeto a la vez, sin tener que desplazarse por tres pantallas.



Por lo tanto, tenemos wireframes de objetos en los que necesitamos estirar los datos de prueba. Los datos se pueden almacenar como variables estáticas finales, por ejemplo, esto puede ser útil para el administrador del sistema principal, a partir del cual se crean otros usuarios. Es mejor usar final, para que no haya la tentación de cambiar los datos en las pruebas, porque entonces la siguiente prueba, en lugar del administrador, puede obtener un usuario “impotente”, sin mencionar la ejecución paralela de pruebas.



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


Para obtener datos que no afecten a otras pruebas, puede utilizar el patrón "prototipo" y clonar su instancia en cada prueba. Decidimos hacerlo más fácil: escribir un método con campos de clase aleatorios, algo como esto:



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


Al mismo tiempo, los métodos que crean aleatoriedad directa se ubican mejor en una clase separada, ya que también se usarán en otros modelos:







En el método para obtener un usuario aleatorio, se usó el patrón "constructor" , que es necesario para no crear un nuevo tipo de constructor para cada conjunto requerido. campos. En su lugar, por supuesto, puede simplemente llamar al constructor deseado.



Este método de almacenamiento de datos utiliza el patrón de objeto de valor, en función del cual puede agregar cualquiera de sus deseos, según las necesidades del proyecto. Puede agregar objetos de guardado a la base de datos y así preparar el sistema antes de la prueba. No puede aleatorizar a los usuarios, sino cargarlos desde archivos de propiedades (y una biblioteca más interesante). Puede usar el mismo usuario en todas partes, pero haga el llamado registro de datos para cada tipo de objeto, en el que el valor del contador de extremo a extremo se agregará al nombre u otro campo único del objeto, y la prueba siempre tendrá su propio testUser_135 único.



Puede escribir su propio Object Storage (grupo de objetos de google y flyweight), desde el cual puede solicitar las entidades necesarias al comienzo de la prueba. El almacén entrega uno de sus objetos listos para usar y lo marca como ocupado. Al final de la prueba, el objeto se devuelve al almacenamiento, donde se limpia según sea necesario, se marca como libre y se entrega a la siguiente prueba. Esto se hace si las operaciones de creación de objetos consumen muchos recursos y, con este enfoque, el almacenamiento funciona independientemente de las pruebas y puede preparar datos para los siguientes casos.



Creación de datos



Para los casos de edición de usuarios, definitivamente necesitará un usuario creado que editará y, en general, a la prueba de edición no le importa de dónde proviene este usuario. Hay varias formas de crearlo:



  • presione los botones con las manos antes de la prueba,
  • dejar datos de la prueba anterior,
  • implementar antes de la prueba desde la copia de seguridad,
  • crear haciendo clic en los botones directamente en la prueba,
  • utilizar la API.


Todos estos métodos tienen inconvenientes: si necesita ingresar algo en el sistema manualmente antes de la prueba, entonces esta es una mala prueba y, por lo tanto, se llaman autotests, porque deben actuar lo más independientemente posible de las manos humanas.



El uso de los resultados de la prueba anterior viola el principio de atomicidad y no le permite ejecutar la prueba por separado, tendrá que ejecutar todo el lote y las pruebas de interfaz de usuario no son tan rápidas. Se considera una buena forma escribir pruebas de tal manera que cada una se pueda ejecutar en un espléndido aislamiento y sin bailes adicionales. Además, un error en la creación de un objeto que dejó caer la prueba anterior no garantiza en absoluto un error en la edición, y en tal construcción, la prueba de edición será la siguiente, y es imposible saber si la edición funciona.



El uso de una copia de seguridad (una imagen guardada de la base de datos) con los datos necesarios para la prueba ya es un enfoque más o menos bueno, especialmente si la copia de seguridad se implementa automáticamente o si las pruebas mismas colocan los datos en la base de datos. Sin embargo, no es obvio por qué se usa este objeto en particular en la prueba, los problemas de intersección de datos también pueden comenzar con una gran cantidad de pruebas. A veces, la copia de seguridad deja de funcionar correctamente debido a una actualización de la arquitectura de la base de datos, por ejemplo, si necesita ejecutar pruebas en una versión anterior y la copia de seguridad ya contiene nuevos campos. Puede combatir esto organizando un almacenamiento de respaldo para cada versión de la aplicación. A veces, la copia de seguridad deja de ser válida nuevamente debido a la actualización de la arquitectura de la base de datos; aparecen nuevos campos regularmente, por lo que la copia de seguridad debe actualizarse regularmente. Y de repente puede serque exactamente un solo usuario de copia de seguridad nunca falla, y si el usuario se acaba de crear o el nombre se le da al azar, encontrará un error. A esto se le llama el "efecto pesticida", la prueba deja de atrapar bichos, porque la aplicación está "acostumbrada" a los mismos datos y no cae, y no hay desviaciones al lado.



Si el usuario se crea en la prueba mediante clics en la misma interfaz, entonces el pesticida disminuye y desaparece la no obviedad de la apariencia del usuario. Las desventajas son similares a usar los resultados de la prueba anterior: la velocidad es regular, e incluso si hay un error en la creación, incluso el más pequeño (especialmente un error de prueba, por ejemplo, el localizador del botón guardar cambiará), entonces no sabremos si la edición funciona.



Finalmente, otra forma de crear un usuario es a través de la http-API de la prueba, es decir, en lugar de hacer clic en los botones, envía inmediatamente una solicitud para crear el usuario deseado. Por lo tanto, el pesticida se reduce tanto como sea posible, es obvio de dónde vino el usuario y la velocidad de creación es mucho mayor que al hacer clic en los botones. Las desventajas de este método son que no es adecuado para proyectos sin json o xml en el protocolo de comunicación entre el cliente y el servidor (por ejemplo, si los desarrolladores escriben usando gwt y no quieren escribir una api adicional para probadores). Es posible, al usar la API, perder una parte de la lógica ejecutada por el panel de administración y crear una entidad no válida. La API puede cambiar, haciendo que las pruebas fallen, pero generalmente esto se sabe y nadie necesita cambios por el bien de los cambios, lo más probable es que esta sea una nueva lógica que aún tendrá que verificarse.También es posible que haya un error en el nivel de la API, pero ningún método aparte de las copias de seguridad listas para usar está a salvo de esto, por lo que es mejor combinar enfoques para crear datos.



Agregar una API de gota



Entre los métodos para preparar datos, la http-API para las necesidades actuales de una prueba separada y la implementación de una copia de seguridad para datos de prueba adicionales que no cambian en las pruebas, por ejemplo, iconos para objetos, para que las pruebas de estos objetos no se bloqueen cuando se cargan los iconos, son los más adecuados para nosotros.



Para crear objetos a través de la API en Java, resultó más conveniente utilizar la biblioteca restAssured, aunque en realidad no está diseñado para esto. Quiero compartir un par de fichas encontradas, ya sabes, ¡escribe!



El primer dolor es la autorización en el sistema. Su método debe seleccionarse por separado para cada proyecto, pero hay una cosa en común: la autorización debe colocarse en la especificación de la solicitud, por ejemplo:



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


Puede agregar la capacidad de guardar cookies para un usuario específico, luego disminuirá la cantidad de solicitudes al servidor. La segunda posible extensión de este método es guardar las Cookies recibidas para la prueba actual y enviarlas al controlador del navegador, omitiendo el paso de autorización. La ganancia es de segundos, pero si los multiplica por el número de pruebas, ¡puede acelerar bastante bien!



Hay un moño para la marcha y hermosos informes, preste atención a la línea .addFilter(new BeautifulRest()):



Hermosa clase de descanso


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




Los modelos de objetos encajan perfectamente en restAssured, ya que la propia biblioteca maneja la serialización y deserialización de modelos en json / xml (conversión de formatos json / xml a un objeto de una clase dada).



    @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 observa varios pasos seguidos para crear objetos, puede ver la identidad del código. Para reducir el mismo código, puede escribir un método general para crear objetos.



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


Una vez más sobre las operaciones de rutina



Como parte de la verificación de la edición de un objeto, generalmente no nos importa cómo apareció el objeto en el sistema, a través de una API o una copia de seguridad, o fue creado por una prueba de interfaz de usuario. Las acciones importantes son encontrar un objeto, hacer clic en el icono "editar", borrar los campos y completarlos con nuevos valores, hacer clic en "guardar" y comprobar si todos los nuevos valores se han guardado correctamente. Toda la información innecesaria que no esté directamente relacionada con la prueba debe eliminarse en métodos separados, por ejemplo, en la clase de pasos.



    @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 importante no dejarse llevar, ya que una prueba que consta únicamente de acciones "complejas" se vuelve menos legible y más difícil de reproducir sin tener que profundizar en el código.



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


Si aparecen prácticamente las mismas pruebas en la suite, que difieren solo en la preparación de datos (por ejemplo, debe verificar que los tres tipos de usuarios "diferentes" pueden realizar las mismas acciones, o hay diferentes tipos de objetos de control, para cada uno de los cuales debe verificar creación de objetos dependientes idénticos, o necesita verificar el filtrado por diez tipos de estados de objeto), aún no puede mover las partes repetidas a un método separado. ¡De ninguna manera si la legibilidad es importante para usted!



En su lugar, debe leer sobre las pruebas basadas en datos, para Java + TestNG será algo como esto:



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


Utiliza la biblioteca del proveedor de datos , que es un complemento sobre el proveedor de datos TestNG que le permite usar colecciones escritas en lugar de Object [] [], pero la esencia es la misma. Por lo tanto, obtenemos una prueba, que se ejecuta tantas veces como recibe datos de entrada.



conclusiones



Entonces, para crear un proyecto grande pero conveniente de autotests de interfaz de usuario, necesita:



  • Describe todos los pequeños widgets encontrados en la aplicación,
  • Recopile widgets en páginas,
  • Crea modelos para todo tipo de entidades,
  • Agregar métodos para generar todo tipo de entidades basadas en modelos,
  • Considere un método adecuado para crear entidades adicionales
  • Opcional: generar o recopilar archivos de pasos manualmente,
  • Escribe pruebas para que en la sección de las acciones principales de una prueba en particular no haya acciones complejas, solo operaciones obvias con widgets.


Hecho, ha creado un proyecto basado en PageElement con métodos simples para almacenar, generar y preparar datos. Ahora tiene una arquitectura que se puede mantener fácilmente, administrar y lo suficientemente flexible. Tanto un evaluador experimentado como un principiante, June pueden navegar fácilmente por el proyecto, ya que las pruebas automáticas en el formato de acciones del usuario son más convenientes de leer y comprender.



Los ejemplos de código del artículo en forma de proyecto terminado se agregan al git .



All Articles