馃Playwright UI testing con IA

馃ИTesting
馃搮 2023-12-03

En este post, vengo a presentar algo que ha llamado mucho mi atenci贸n y que muy posiblemente sea el futuro de los tests e2e con IA. Se acab贸, estoy cansado de escribir selectores que nos acoplan al DOM y de tests ilegibles. Muchas aplicaciones son testeadas tanto manualmente como con Cypress, pero estas herramientas suelen ser costosas. Adem谩s, Cypress solo permite paralelizar tus tests si pagas, lo cual lo vuelve muy pesado. Este post pretende ense帽ar una herramienta que es una prueba de concepto, m谩s que nada para ilustrar lo cerca que estamos de obtener tests m谩s mantenibles.

Ya hemos visto que para Playwright tenemos una extensi贸n de VSCode que simplifica el uso de tener que escribir selectores, capturando lo que haces en un navegador. Pero eso nos sigue acoplando a la estructura del DOM. Si ma帽ana ponemos selectores m谩s espec铆ficos, estos tests dejar谩n de funcionar, ya que solo ponen las etiquetas lo suficientemente espec铆ficas como para que el selector sea 煤nico en el momento de la creaci贸n del test.

Nota, decir que se necesita ejecutar npx playwright install para que sea efectivo al lanzar los tests con los diferentes navegadores.

Vamos a ver el c贸digo de mi blog que no tiene selectores para ser testeado f谩cilmente:

test.describe("Page should", async () => {
  test("be able to search", async ({ page }) => {
    await page.goto("/");

    const searchInput = "body > div > nav > div > div > div > input";
    await page.fill(searchInput, "test");

    const postResults = await page.$("body > div > nav > div > div > ul");

    await expect(postResults).not.toBeNull();
  });
});

Parece ser que el test sabe demasiado del DOM, ya que no tenemos unos selectores adecuados. Pasa que en mobile el DOM cambia y eso es algo que nos puede romper los tests. As铆 que he descubierto auto-playwright, una librer铆a pensada para lanzar prompts a chat-gpt, para as铆 ser usados como una manera m谩s 贸ptima de ejecutar este test. Ve谩moslo.

test.describe("Page should", async () => {
  const options = {
    // The OpenAI model (https://platform.openai.com/docs/models/overview)
    debug: true,
    model: "gpt-3.5-turbo",
  };
  test("be able to search using AI", async ({ page }) => {
    test.setTimeout(120000);
    await page.goto("/");
    await auto(
      "Action with no result: Fill the search input with the place holder `Buscar posts...` using the value `test`",
      { page },
      options
    );

    const postResults = await auto(
      "Query: We need to wait 3 seconds for the results and then the DOM will display the links, give me the number of them",
      { page },
      options
    );
    await expect(parseInt(postResults.query)).toBeGreaterThan(1);
  });
  // ...
});

Para empezar, hay que registrarse en la p谩gina de OpenAI para sacar un API key para el proyecto, ya que esta librer铆a usa chat gpt por detr谩s. Hay que abonarles un poco de dinero para que empiece a funcionar (me he dejado 10 d贸lares para probarlo), realmente no es muy caro pero s铆 es necesario para empezar a usar la API key. Os muestro c贸mo quedan todas las interacciones con el test:

Running 1 test using 1 worker
[1/1] [chromium] 鈥 page.spec.ts:10:7 鈥 Page should 鈥 b
[chromium] 鈥 page.spec.ts:10:7 鈥 Page should 鈥 be able to search using AI
> message {
  role: 'user',
  content: 'This is your task: Action with no result: Fill the search input with the place holder `Buscar posts...` using the value `test`\n' +
    '\n' +
    '* When creating CSS selectors, ensure they are unique and specific enough to select only one element, even if there are multiple elements of the same type (like multiple h1 elements).\n' +
    "* Avoid using generic tags like 'h1' alone. Instead, combine them with other attributes or structural relationships to form a unique selector.\n" +
    '* You must not derive data from the page if you are able to do so by using one of the provided functions, e.g. locator_evaluate.\n' +
    '\n' +
    'Webpage snapshot:\n' +
    '\n' +
    '```\n' +
    `Kevin Hierro - Wolfremium<div class="bg-darkViolet"><nav class="bg-gray-900/90 backdrop-blur-sm  hover:bg-gray-900/95 fixed w-full z-20 top-0 left-0 border-b-2 border-lightGreen/40 hover:border-lightGreen/...` +
    '```\n'
}
> message {
  role: 'assistant',
  content: null,
  function_call: {
    name: 'locateElement',
    arguments: `{\n  "cssSelector": "input[placeholder='Buscar posts...']"\n}`
  }
}
> message {
  role: 'function',
  name: 'locateElement',
  content: '{"elementId":"f00dbab3-f21c-4ec9-8402-fad1e692deb7"}'
}
> message {
  role: 'assistant',
  content: null,
  function_call: {
    name: 'locator_fill',
    arguments: '{\n' +
      '  "value": "test",\n' +
      '  "elementId": "f00dbab3-f21c-4ec9-8402-fad1e692deb7"\n' +
      '}'
  }
}
> message { role: 'function', name: 'locator_fill', content: '{"success":true}' }
> message {
  role: 'assistant',
  content: null,
  function_call: { name: 'resultAction', arguments: '{}' }
}
> message { role: 'function', name: 'resultAction', content: 'null' }
> message {
  role: 'assistant',
  content: 'The search input has been successfully filled with the value "test".'
}
> finalContent The search input has been successfully filled with the value "test".
> lastFunctionResult {}
> message {
  role: 'user',
  content: 'This is your task: Query: We need to wait 3 seconds for the results and then the DOM will display the links, give me the number of them\n' +
    '\n' +
    '* When creating CSS selectors, ensure they are unique and specific enough to select only one element, even if there are multiple elements of the same type (like multiple h1 elements).\n' +
    "* Avoid using generic tags like 'h1' alone. Instead, combine them with other attributes or structural relationships to form a unique selector.\n" +
    '* You must not derive data from the page if you are able to do so by using one of the provided functions, e.g. locator_evaluate.\n' +
    '\n' +
    'Webpage snapshot:\n' +
    '\n' +
    '```\n' +
    'Kevin Hierro - Wolfremium<div class="bg-darkViolet"><nav class="bg-gray-900/90 backdrop-blur-sm  hover:bg-gray-900/...' +
    '```\n'
}
> message {
  role: 'assistant',
  content: null,
  function_call: {
    name: 'locateElement',
    arguments: '{\n  "cssSelector": "ul.absolute a"\n}'
  }
}
> message {
  role: 'function',
  name: 'locateElement',
  content: '{"elementId":"1a274f6c-cf9b-4778-8798-0232b33520a9"}'
}
> message {
  role: 'assistant',
  content: null,
  function_call: {
    name: 'locator_count',
    arguments: '{\n  "elementId": "1a274f6c-cf9b-4778-8798-0232b33520a9"\n}'
  }
}
> message {
  role: 'function',
  name: 'locator_count',
  content: '{"elementCount":5}'
}
> message {
  role: 'assistant',
  content: null,
  function_call: { name: 'resultQuery', arguments: '{\n  "query": "5"\n}' }
}
> message { role: 'function', name: 'resultQuery', content: '{"query":"5"}' }
> message {
  role: 'assistant',
  content: 'There are 5 links displayed on the webpage.'
}
> finalContent There are 5 links displayed on the webpage.
> lastFunctionResult { query: '5' }
  1 passed (20.8s)

Este comportamiento con prompts tiene dos tipos: las acciones y las queries. El DOM enviado esta minificado para reducir el coste de la petici贸n. Este tipo de test tiene dos grandes inconvenientes. El primero es que van a haber problemas de timeout inesperados debido a cu谩nto tarda la librer铆a en gestionar las respuestas del modelo. Por otro lado, trabajamos con un modelo no determinista que no siempre producir谩 el mismo resultado, por lo que si ejecutas el mismo test N veces, habr谩 una ocasi贸n en la que no encuentre nada a pesar de no haber cambios.

La parte que me gusta de este ejemplo son las posibilidades infinitas que puedes hacer a una p谩gina: cambios, rellenar formularios, subidas de ficheros o im谩genes, etc., sin tener t煤 que hacer ni siquiera el c贸digo a mano. Obviamente, hay algo malo, y es que si puedes hacer lo que quieras a disposici贸n de todos, 驴cu谩ntas IA hay que quieran explotar vulnerabilidades de las p谩ginas con muy poco esfuerzo? Es un tema que debatiremos m谩s adelante en un post.

馃彿Conclusi贸n

Dicho esto, creo que es un modelo nuevo e intuitivo para reflejar tests con lo que el negocio necesita, pero que a煤n le queda un largo camino por recorrer. Yo, en mi opini贸n personal, no pondr铆a la calidad de mi software en manos de algo que puede fallar sin un motivo o cambio en una pipeline de CI. Por otro lado, hay alternativas a este ejemplo como ZeroStep, pero en todos los casos hay que pagar. En este caso, opt茅 por la compa帽铆a oficial OpenAI, lo cual me sugiere que el futuro estar谩 en un modelo integrado junto con los servicios de la empresa que, aunque no sea muy bueno, puede ser entrenado para buscar cosas en su propia p谩gina de la empresa. Para poder probarlo, he hecho un repositorio con un ejemplo sencillo de c贸mo usarlo. Espero que os haya gustado y que os haya abierto la mente a nuevas posibilidades.