Escribir autotests de manera efectiva - Pruebas subcutáneas

Imaginemos una situación hipotética (en la que nos sumergimos regularmente). Se le ha asignado a un proyecto para automatizar la "falla". Le dan un gran plan de prueba con un gran número (¡miles de ellos!) De pruebas "manuales", y dicen que debe hacer algo, y allí mismo. Y también, de forma rápida y estable.

Es demasiado tarde para escribir pruebas unitarias, o incluso pensar en TDD , el código del producto se ha escrito durante mucho tiempo. Su palabra, camarada autotester!

imagen

Afortunadamente, hay un pequeño truco que aumentará la cobertura y hará que las pruebas sean estables y rápidas: las pruebas subcutáneas ("pruebas subcutáneas"), pero lo primero es lo primero.


La esencia del problema


El primer reflejo condicional del automatizador es tomar Selenio (bueno, ya sea allí, Selenide o algún otro prodigio para las pruebas de IU). Este es un estándar de la industria, pero hay muchas razones por las que no despega:

  • Las pruebas de IU son lentas. No hay escapatoria de esto. Pueden ejecutarse en paralelo, archivarse y hacerse un poco más rápido, pero permanecerán lentos.
  • Las pruebas de IU son inestables. En parte porque son lentos. Y también porque el navegador web y la interfaz de usuario no fueron creados para ser controlados por una computadora (esta tendencia está cambiando actualmente, pero no el hecho de que sea buena).
  • UI- — . . ( , , , «» - , ).
  • , , , UI- . . ID XPath . , «» - - . , , — - , .
  • Alguien dirá que alguna funcionalidad simplemente no se puede probar de otra manera. Diré que si hay una funcionalidad que solo puede probarse mediante pruebas de interfaz de usuario (con la excepción de la lógica de la interfaz de usuario), esto puede ser una buena señal de problemas arquitectónicos en el producto.

La única ventaja real de las pruebas de UI es que le permiten "lanzar" controles más o menos útiles sin la necesidad de sumergirse y estudiar el código del producto en sí. Lo cual no es una ventaja a largo plazo. Una explicación más detallada de por qué esto se puede escuchar en esta presentación .

Solución alternativa


Como un caso muy simple, consideremos una aplicación que consiste en un formulario donde puede ingresar un nombre de usuario válido. Si ingresa un nombre de usuario que coincida con las reglas, el usuario se creará en el sistema y se registrará en la base de datos.



El código fuente de la aplicación se puede encontrar aquí: github.com/senpay/login-form . Se le advirtió: en la aplicación hay muchos errores y no hay herramientas y marcos de moda.

Si intenta "arrojar" una hoja de verificación para esta aplicación, puede obtener algo como:
NúmeroPasosResultados previstos
11. Ingrese un nombre de usuario válido
2. Haga clic en el botón "Iniciar sesión"
1.
2. Se crea un nuevo usuario.
21. Ingrese un nombre de usuario vacío
2. Haga clic en el botón "Iniciar sesión"
1.
2. Se muestra el mensaje de error.

¿Se ve simple? ¡Simplemente! ¿Puedo escribir pruebas de IU? Lata. Puede encontrar un ejemplo de las pruebas escritas (junto con un marco completo de tres niveles ) en LoginFormTest.java si va a la etiqueta uitests en git ( git checkout uitests ):

public class LoginFormTest {

    SelenideMainPage sut = SelenideMainPage.INSTANCE;
    private static final String APPLICATION_URL = "http://localhost:4567/index";

    @BeforeClass
    public static void setUpClass() {
        final String[] args = {};
        Main.main(args);
        Configuration.browser = "firefox";
    }

    @Before
    public void setUp() {
        open(APPLICATION_URL);
    }

    @After
    public void tearDown() {
        close();
    }

    @Test
    public void shouldBeAbleToAddNewUser() {
        sut.setUserName("MyCoolNewUser");
        sut.clickSubmit();
        Assert.assertEquals("Status: user MyCoolNewUser was created", sut.getStatus());
        Assert.assertTrue(sut.getUsers().contains("Name: MyCoolNewUser"));
    }

    @Test
    public void shouldNotBeAbleToAddEmptyUseName() {
        final int numberOfUsersBeforeTheTest = sut.getUsers().size();
        sut.clickSubmit();
        Assert.assertEquals("Status: Login cannot be empty", sut.getStatus());
        Assert.assertEquals(numberOfUsersBeforeTheTest, sut.getUsers().size());
    }
}


Algunas métricas para este código:
Tiempo de ejecución: ~ 12 segundos (12 segundos 956 milisegundos la última vez que ejecuté estas pruebas)
Cobertura del código
Clase: 100%
Método: 93.8% (30/32)
Línea: 97.4% (75/77 )

Ahora supongamos que las Pruebas automáticas funcionales se pueden escribir en el nivel "inmediatamente debajo" de la IU. Esta técnica se llama Pruebas subcutáneas ("pruebas subcutáneas" - pruebas que prueban inmediatamente por debajo del nivel de la lógica de visualización) y fue propuesta por Martin Fowler hace bastante tiempo [ 1 ].

Cuando las personas piensan en autotests "no UI", a menudo piensan inmediatamente en REST / SOAP o su API. Pero la API (interfaz de programación de aplicaciones) es un concepto mucho más amplio, que no necesariamente afecta a HTTP y otros protocolos pesados.

Si elegimos un código de producto , podemos encontrar algo interesante:
public class UserApplication {

    private static IUserRepository repository = new InMemoryUserRepository();
    private static UserService service = new UserService(); {
        service.setUserRepository(repository);
    }

    public Map<String, Object> getUsersList() {
        return getUsersList("N/A");
    }

    public Map<String, Object> addUser(final String username) {
        final String status = service.addUser(username);
        final Map<String, Object> model = getUsersList(status);
        return model;
    }

    private Map<String, Object> getUsersList(String status) {
        final Map<String, Object> model = new HashMap<>();
        model.put("status", status);
        model.put("users", service.getUserInfoList());
        return model;
    }
}


Cuando hacemos clic en una IU, se llama a uno de estos métodos, o se agrega un nuevo objeto Usuario, o se devuelve una lista de objetos Usuario ya creados. ¿Qué pasa si usamos estos métodos directamente ? Después de todo, ¡esta es una API real! Y lo más importante, REST y otras API también funcionan con el mismo principio: llaman a un cierto método de "nivel de controlador".

Usando estos métodos directamente, podemos escribir una prueba más simple y mejor:
public class UserApplicationTest {

    private UserApplication sut;

    @Before
    public void setUp() {
       sut = new UserApplication();
    }

    @Test
    public void shouldBeAbleToAddNewUser() {
        final Map<String, Object> myCoolNewUser = sut.addUser("MyCoolNewUser");
        Assert.assertEquals("user MyCoolNewUser was created", myCoolNewUser.get("status"));
        Assert.assertTrue(((List) myCoolNewUser.get("users")).contains("Name: MyCoolNewUser"));
    }

    @Test
    public void shouldNotBeAbleToAddEmptyUseName() {
        final Map<String, Object> usersBeforeTest = sut.getUsersList();
        final int numberOfUsersBeforeTheTest = ((List) usersBeforeTest.get("users")).size();
        final Map<String, Object> myCoolNewUser = sut.addUser("");
        Assert.assertEquals("Login cannot be empty", myCoolNewUser.get("status"));
        Assert.assertEquals(numberOfUsersBeforeTheTest, ((List) myCoolNewUser.get("users")).size());
    }
}


Este código está disponible bajo las subpruebas de etiquetas :

git checkout subctests


¿Intentamos recopilar métricas?
Tiempo de ejecución: ~ 21 milisegundos
Cobertura del código :
Clase: 77.8%
Método: 78.1 (30/32)
Línea: 78.7 (75/77)

Perdimos un poco de cobertura, ¡pero la velocidad de las pruebas aumentó 600 veces!

¿Qué tan importante / importante es la pérdida de cobertura en este caso? Depende de la situación. Perdimos un pequeño código de pegamento, que puede ser (o no) importante (recomiendo determinar qué código se pierde como ejercicio).

¿Esta pérdida de cobertura justifica la introducción de pruebas de peso pesado en el nivel de IU? También depende de la situación. Podemos, por ejemplo:
  • Agregue una prueba de IU para verificar el código de pegamento, o
  • Si no esperamos cambios frecuentes en el código de pegamento, déjelo sin pruebas automáticas, o
  • Si tenemos algún tipo de prueba "manual", existe una gran posibilidad de que el probador note problemas con el código de pegamento, o
  • Proponer algo más (mismo despliegue de Canarias)


Finalmente


  • No se requiere que las pruebas automáticas funcionales se escriban a nivel de UI o REST / SOAP API. El uso de "Pruebas subcutáneas" en muchas situaciones probará la misma funcionalidad con mayor velocidad y estabilidad.
  • Una de las desventajas del enfoque es una cierta pérdida de cobertura.
  • Una forma de evitar perder la cobertura es el " Modelo de pruebas de características "
  • Pero incluso con la pérdida de cobertura, el aumento de la velocidad y la estabilidad es significativo.


Una versión en inglés del artículo está disponible aquí .

All Articles