Ir al contenido

Migrar al reporter de Vitest

Vengas de donde vengas, el reporter es una entrada en reporters. No va nada en setupFiles ni en runner: el reporter añade su propio archivo de setup y su runner a cada proyecto de Vitest, en Node y en modo navegador.

// vitest.config.mts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: ['default', ['@probara/vitest-reporter', { projectId: 'SHOP' }]],
},
});

El reporter envía lo que envían las integraciones con Vitest de Qase, Allure, ReportPortal y TestRail, siempre que Probara tenga un lugar para ello: vínculos con casos, el título, la suite, los campos, las etiquetas y los pasos de un caso nuevo, parámetros, comentarios, pasos anidados, archivos, enlaces, y el nombre, el plan, el entorno y las configuraciones de la ejecución. Además envía cada intento de una prueba reintentada o repetida, cosa que ninguna de ellas hace, y mantiene una sola ejecución para toda una sesión watch. Lo que no porta está en lo que el reporter no admite, cada cosa con su motivo.

Vienes deVe a
La salida JUnit de Vitest y probara import junitLa importación JUnit
vitest-qase-reporterQase
allure-vitestAllure
@reportportal/agent-js-vitestReportPortal
La salida JUnit de Vitest y trcli parse_junitTestRail

Si tu pipeline escribe la salida JUnit de Vitest y la envía con probara import junit, el reporter puede sustituir ambos pasos sin desvincular un solo caso: da a cada prueba la clave de automatización y el caso que le da la importación, en Vitest 3, 4 y 5. Después, cada prueba envía lo que el XML no puede contener: cada intento, los pasos, los archivos, los parámetros y las llamadas probara.*. El reporter es el camino recomendado; conserva la importación para un job que solo tiene el XML.

  1. Añade el reporter a la configuración de Vitest (primeros pasos). No hay nada más que definir: la salida JUnit de Vitest siempre nombra el archivo de prueba, igual que las claves por defecto del reporter (keyIncludesFile: true).
  2. Comprueba las claves de ambos sobre las mismas pruebas: deben ser iguales.
  3. En CI, quita --reporter=junit y el paso de importación, y pasa PROBARA_API_TOKEN al paso de pruebas (en CI).
Salida JUnit de VitestAjuste del reporter
--reporter=junit, con o sin addFileAttribute: trueNada: keyIncludesFile: true es el valor por defecto
Escrita con otro ancestorSeparator (Vitest 5)Claves distintas: vincula las pruebas por IDs de caso (vincular)

Ejecuta vitest desde la misma raíz que antes: el archivo de cada clave es relativo a ella, en los dos caminos. keyIncludesFile: false no tiene equivalente en JUnit: da las claves de casos creados a mano o por otra herramienta, sin el archivo (sin el archivo).

Ejecuta la batería una vez con la salida JUnit de Vitest y otra con el archivo de resultados del reporter, con el envío desactivado, y compara las simulaciones de ambos: cada una imprime una línea por resultado, con su estado, su caso y su clave. No hace falta token. Para este archivo de prueba:

tests/refund.test.ts
import { describe, expect, test } from 'vitest';
describe('refund', () => {
test('SHOP-12 refunds an order', () => {
expect(25 - 25).toBe(0);
});
test('keeps the receipt', () => {
expect('receipt').toHaveLength(7);
});
test.todo('refunds a gift card');
});
$ npx vitest run --reporter=junit --outputFile=reports/vitest.xml
$ PROBARA_PROJECT=SHOP npx @probara/cli import junit reports/vitest.xml --dry-run
passed SHOP-12 tests/refund.test.ts > refund > refunds an order
passed - tests/refund.test.ts > refund > keeps the receipt
skipped - tests/refund.test.ts > refund > refunds a gift card
Total: 3 results from 1 file (2 passed, 0 failed, 1 skipped, 0 blocked)
$ PROBARA_ENABLED=false PROBARA_PROJECT=SHOP PROBARA_RESULTS_FILE=probara-results.json npx vitest run
[probara] Wrote 3 results to /work/shop/probara-results.json: send them with probara import results /work/shop/probara-results.json
$ npx @probara/cli import results probara-results.json --dry-run
passed SHOP-12 tests/refund.test.ts > refund > refunds an order
passed - tests/refund.test.ts > refund > keeps the receipt
skipped - tests/refund.test.ts > refund > refunds a gift card
Total: 3 results from 1 file (2 passed, 0 failed, 1 skipped, 0 blocked)

Las líneas son iguales: las mismas claves, los mismos casos y los mismos estados. --reporter=junit sustituye los reporters de la configuración en esa ejecución, así que entonces el reporter no envía nada. En una batería real, escribe cada simulación en un archivo y compáralos con diff: una línea cuya clave cambió es una prueba que recibiría un caso nuevo (comprobar qué se enviaría). Elimina reports/ y el archivo de resultados al terminar.

Antes, dos pasos: Vitest escribe el informe, y la CLI lo envía.

- name: Run the tests
run: npx vitest run --reporter=junit --outputFile=reports/vitest.xml
- name: Send the results
if: ${{ !cancelled() }}
run: npx @probara/cli import junit reports/vitest.xml
env:
PROBARA_API_TOKEN: ${{ secrets.PROBARA_API_TOKEN }}
PROBARA_PROJECT: SHOP

Después, un solo paso: el reporter, registrado en la configuración de Vitest, envía cada resultado cuando termina la ejecución.

- name: Run the tests
run: npx vitest run
env:
PROBARA_API_TOKEN: ${{ secrets.PROBARA_API_TOKEN }}
PROBARA_PROJECT: SHOP

Quita --reporter=junit del comando: sustituye los reporters de la configuración, incluido el del reporter. Si otra herramienta sigue leyendo el XML (un resumen de pruebas del CI), lista en cambio junit junto al reporter:

reporters: ['default', 'junit', '@probara/vitest-reporter'],
outputFile: { junit: 'reports/vitest.xml' },

La ejecución recibe el nombre, el origen y el cierre que le daba la importación (la ejecución).

Qué cambia respecto a la importación JUnit

Sección titulada «Qué cambia respecto a la importación JUnit»

Las claves se mantienen; lo que envía cada prueba crece, y algunos resultados son más exactos:

Temaprobara import junit sobre la salida JUnit de VitestEl reporter
Reintentos (retry)Un resultado por prueba: un aprobado en un reintento es un aprobado sin más; una prueba que falla en todos los intentos conserva su primer falloCada intento, en orden, cada uno con sus propios errores; el caso de la ejecución conserva el último resultado (reintentos)
Repeticiones (repeats)Un resultado por pruebaCada ejecución (repeticiones)
test.todoskippedskipped, con la nota Todo (estados)
context.skip('why')skipped: Vitest no escribe ningún motivoskipped, con la nota Skipped: why
Fallos de expect.softEl mensaje del primer falloCada fallo del intento, en las notas
Salida de consola--attach-output: el <system-out> y el <system-err> de la pruebacaptureOutput: stdout.log y stderr.log de cada intento (salida de consola)
Proyectos de VitestNingún proyecto: la salida JUnit de Vitest no lo nombraUn parámetro project en cada resultado, cuando la configuración tiene más de uno (proyectos de Vitest)
Errores no gestionados (Vitest 5)Se dejan fuera, con una advertencia por archivo; --fail-on-failed-tests sale con 3 por ellosSe dejan fuera, con una advertencia; el código de salida sigue siendo el de Vitest (errores fuera de una prueba)
Cuándo salen los resultadosDespués de toda la ejecución, en un paso posteriorAl final de la ejecución, desde el paso de pruebas; un archivo de resultados guarda lo que no se pudo enviar

Lo mismo en los dos caminos, así que ningún caso se mueve:

  • Los estados de las pruebas aprobadas, fallidas y omitidas, incluido test.fails, y las pruebas que .only o -t dejan fuera, que se omiten en los dos.
  • Los resultados fallidos de un beforeAll o un afterAll que lanza un error, y de un archivo que no carga, con los nombres que les da el escritor JUnit de Vitest (errores fuera de una prueba).
  • Los IDs de caso en los títulos de las pruebas y de los describe, los nombres de las filas de test.each y test.for, y un > con un espacio a cada lado dentro de un título (nombres tal como los escribe Vitest).

El XML contiene el nombre, el resultado, el tiempo y la salida de cada prueba. Lo que las pruebas le cuentan al reporter, y lo que Vitest le da más allá de su salida JUnit, nunca llega a la importación:

  • Cada intento de una prueba reintentada o repetida, cada uno con su estado, su duración y sus errores.
  • Los IDs de caso de probara.id(), que viven en los metadatos propios de la prueba, no en su título (vincular).
  • Los pasos, anidados, con su estado y su duración, y los pasos de un caso nuevo (pasos).
  • Los archivos: probara.attach(), los archivos del propio context.annotate() de Vitest, las capturas de pantalla de los fallos en modo navegador (adjuntos).
  • Los parámetros, los comentarios, los enlaces y los campos de un caso nuevo: los helpers probara.* (metadatos y enlaces).
  • La selección de pruebas: ejecutar solo las pruebas de una ejecución de Probara (selección de pruebas).

Conserva la importación donde el reporter no puede ejecutarse: un job que solo recibe el XML de otro job, o un Vitest anterior a 3.2, que el reporter no admite (la importación se prueba con Vitest 3.2, 4.1 y 5.0). El lado de la importación está en la CLI y en Vitest JUnit.

probara import junit lee la salida de Vitest con su dialecto vitest, que detecta por la raíz del informe (vitest tests), sin ninguna opción. Las versiones de la CLI anteriores a ese dialecto (0.3.0 y anteriores) leen el mismo XML como JUnit genérico, que da a cada prueba la misma clave, con dos diferencias:

ResultadoJUnit genérico (la CLI 0.3.0 y anteriores)El reporter, y el dialecto vitest
Un fallo con el nombre de su archivo, como un archivo que no cargaCon la clave del archivo solo: tests/broken.test.tsEl archivo dos veces: tests/broken.test.ts > tests/broken.test.ts
Un error no gestionado (Vitest 5)Un resultado fallido propioSe deja fuera, con una advertencia

Solo se mueven esos resultados: el siguiente fallo de un archivo así crea un caso con su clave nueva, y el caso anterior no recibe más resultados. Todas las demás pruebas conservan su caso.

Pasar de vitest-qase-reporter es sobre todo cambiar nombres: la entrada del reporter, sus ajustes y las llamadas ctx.qase, que se convierten en llamadas probara.* sin el envoltorio withQase(). Esta sección relaciona cada una tal como la tiene el código de Qase (versión 1.5.1), que en algunos puntos difiere de su documentación (documentado, pero no en el código), y después recoge lo que funciona de otra manera y lo que no se porta, con los motivos.

  1. npm uninstall vitest-qase-reporter && npm i -D @probara/vitest-reporter.
  2. En vitest.config.mts, sustituye ['vitest-qase-reporter', {...}] por ['@probara/vitest-reporter', {...}], traduciendo las opciones (abajo). Elimina qase.config.json y .qaserc: el reporter solo lee sus opciones y las variables PROBARA_*. Quita vitest-qase-reporter/setup de setupFiles si lo añadiste: el reporter añade su propio archivo de setup.
  3. Sustituye en CI las variables QASE_* por variables PROBARA_*: como mínimo PROBARA_API_TOKEN (un token de aplicación de la tarjeta Vitest, primeros pasos) y PROBARA_PROJECT.
  4. Importa probara de @probara/vitest-reporter, vuelve a convertir cada withQase(async ({ qase }) => {...}) en una función de prueba normal, y traduce las llamadas (abajo). Los IDs de caso pasan a ser IDs visibles de Probara (SHOP-12), no números.

Antes:

// vitest.config.mts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: [
'default',
[
'vitest-qase-reporter',
{
mode: 'testops',
testops: {
project: 'SHOP',
run: { title: 'Nightly', complete: true, tags: ['nightly'] },
uploadAttachments: true,
},
},
],
],
},
});

Después:

// vitest.config.mts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: [
'default',
[
'@probara/vitest-reporter',
{ projectId: 'SHOP', run: { name: 'Nightly', tags: ['nightly'] }, captureOutput: true },
],
],
},
});

captureOutput es opcional: adjunta la salida de consola de cada intento, algo que el captureLogs de Qase nunca hizo con Vitest (salida de consola). La ejecución se cierra al final sin complete: true, y uploadAttachments está activado por defecto.

Opción de Qase (variable QASE_*)Opción de Probara (variable PROBARA_*)
mode: 'testops' (QASE_MODE)Activado siempre que haya un token y un proyecto
mode: 'off'enabled: false (PROBARA_ENABLED=false), o sin token
mode: 'report', fallback (QASE_FALLBACK)resultsFile (PROBARA_RESULTS_FILE) (archivo de resultados)
report.connections.local.path, .format (QASE_REPORT_CONNECTION_PATH, _FORMAT)resultsFile, la ruta de un archivo JSON
mode: 'testops_multi', testops_multi.*projectId más projects y run.ulids (varios proyectos)
debug (QASE_DEBUG)debug (PROBARA_DEBUG)
environment (QASE_ENVIRONMENT), un slug, que se ignora si no se encuentrarun.environment (PROBARA_ENVIRONMENT), un nombre, que se crea si falta
rootSuite (QASE_ROOT_SUITE)suiteUlid (PROBARA_SUITE_ULID): una suite existente, por ULID
captureLogs (QASE_CAPTURE_LOGS): el reporter de Vitest de Qase lo ignoracaptureOutput (PROBARA_CAPTURE_OUTPUT)
statusMapping (QASE_STATUS_MAPPING)statusMapping (PROBARA_STATUS_MAPPING), sin invalid ni disabled
logging.console, logging.file (QASE_LOGGING_CONSOLE, QASE_LOGGING_FILE)Los registros van a stderr; la opción logger para código
testops.api.token (QASE_TESTOPS_API_TOKEN)PROBARA_API_TOKEN (apiToken para código)
testops.api.host (QASE_TESTOPS_API_HOST)baseUrl (PROBARA_BASE_URL), una URL completa
testops.api.timeout (QASE_TESTOPS_API_TIMEOUT), en segundostimeoutMs, en milisegundos, para cada petición
testops.api.retries, testops.api.retryBackoffmaxRetries, para cada petición; el backoff es fijo (reintentos y backoff)
testops.project (QASE_TESTOPS_PROJECT)projectId (PROBARA_PROJECT)
testops.uploadAttachments (QASE_TESTOPS_UPLOAD_ATTACHMENTS)uploadAttachments (PROBARA_UPLOAD_ATTACHMENTS)
testops.attachments.concurrency, .timeoutattachmentConcurrency; las subidas tienen al menos 120 s (tiempos máximos)
testops.run.id (QASE_TESTOPS_RUN_ID)run.ulid (PROBARA_RUN_ULID)
testops.run.title, .description, .tagsrun.name, run.description, run.tags (PROBARA_RUN_*)
testops.run.complete (QASE_TESTOPS_RUN_COMPLETE)closeRun (PROBARA_CLOSE_RUN); una ejecución creada se cierra por defecto
testops.plan.id (QASE_TESTOPS_PLAN_ID)run.plan (PROBARA_PLAN): el ID visible o el nombre del plan
testops.configurations.values (group=value)run.configurations (PROBARA_CONFIGURATIONS=Group=Name)
testops.statusFilter (QASE_TESTOPS_STATUS_FILTER)statusFilter (PROBARA_STATUS_FILTER)
testops.batch.size (QASE_TESTOPS_BATCH_SIZE)chunkSize (de 1 a 500)
(ninguna)run.milestone (PROBARA_MILESTONE): el reporter de Qase no tiene hito

Los ajustes de Qase sin equivalente están en lo que no se porta. Todas las opciones de Probara están en la página de configuración.

QaseProbara
test(addQaseId('pays', [12]), ...), (Qase ID: 12) en un títulotest('SHOP-12 pays', ...): un ID en el título o en un describe (vincular)
addQaseId('pays', [12, 13])'SHOP-12 SHOP-13 pays', o probara.id(['SHOP-12', 'SHOP-13'])
addQaseProjects('pays', { WEB: [3] }), (Qase WEB: 3) en un títuloWEB-3 en el título, con projects: ['WEB'] (varios proyectos)
withQase(async ({ qase }) => {...})La propia función de prueba: probara encuentra por sí solo la prueba en curso
await qase.title('...')probara.title('...')
await qase.suite('Payments - Cards')probara.suite(['Payments', 'Cards'])
await qase.fields({ severity: 'critical' })probara.fields({ severity: 'critical' })
await qase.tags('smoke', 'checkout')probara.tags('smoke', 'checkout')
await qase.comment('...')probara.comment('...')
await qase.parameters({...}), await qase.groupParameters({...})probara.parameters({...})
await qase.step('Pay', fn, 'Paid', 'card=visa')await probara.step('Pay', fn, { expected: 'Paid', data: 'card=visa' })
await qase.attach({ paths: [a, b] })await probara.attach({ name, path }), una vez por archivo
await qase.attach({ name, content, type })await probara.attach({ name, body, contentType })
await qase.annotate(message, options)await context.annotate(message, options): el del propio Vitest; su archivo se envía (anotaciones de Vitest)
(ninguna: el reporter de Vitest de Qase no tiene ignore)probara.ignore()
(ninguna)probara.link() y probara.issue() (enlaces e incidencias)

Quita los marcadores (Qase ID: 12) de los títulos: el reporter los lee como texto, así que se quedarían en la clave de automatización.

Antes:

tests/checkout.test.ts
import { test } from 'vitest';
import { addQaseId, withQase } from 'vitest-qase-reporter/vitest';
test(
addQaseId('pays with a card', [12]),
withQase(async ({ qase }) => {
await qase.fields({ severity: 'critical' });
await qase.tags('smoke');
await qase.step('Pay', async () => {}, 'The order is paid');
await qase.attach({ name: 'order.json', content: '{"id":1042}', type: 'application/json' });
}),
);

Después:

tests/checkout.test.ts
import { probara } from '@probara/vitest-reporter';
import { test } from 'vitest';
test('SHOP-12 pays with a card', async () => {
probara.fields({ severity: 'critical' }).tags('smoke');
await probara.step('Pay', async () => {}, { expected: 'The order is paid' });
await probara.attach({
name: 'order.json',
body: '{"id":1042}',
contentType: 'application/json',
});
});
[
{
"caseDisplayId": "SHOP-12",
"automationKey": "tests/checkout.test.ts > pays with a card",
"status": "passed",
"steps": [{ "action": "Pay", "expected": "The order is paid", "status": "passed" }],
"case": {
"tags": ["smoke"],
"fields": { "severity": "critical" },
"steps": [{ "action": "Pay", "expected": "The order is paid" }]
}
}
]

El resultado lleva además order.json (application/json). El ID sale del título y de la clave: añadirlo o quitarlo nunca cambia el caso con el que coincide la prueba (claves de automatización).

El README y las guías de Qase muestran llamadas y ajustes que la versión 1.5.1 no tiene, o que tiene con otro comportamiento. El código copiado de ahí falla o hace otra cosa:

Documentación de QaseCódigo de Qase (1.5.1)Con Probara
test(qase(12, 'pays'), ...), con qase importado de vitest-qase-reporter o de vitest-qase-reporter/vitestNo se exporta ningún qase: solo withQase, addQaseId y addQaseProjectstest('SHOP-12 pays', ...)
qase.title(), qase.fields(), qase.suite() sobre esa importación, en cualquier pruebaSolo ctx.qase, en una función que envuelve withQase()probara.title(), probara.fields(), probara.suite(), en cualquier prueba
qase.ignore()No está en el códigoprobara.ignore()
qase.mute()No está en el códigoNo se porta (abajo)
setupFiles: ['vitest-qase-reporter/setup'], para el perfilador de redEl paquete no lo exporta: Vitest no puede cargarloNada que añadir: el reporter añade su propio archivo de setup
qase.suite('Payments\tCards'), un tabulador entre suitesSolo divide por un guion con un espacio a cada lado ('Payments - Cards')probara.suite(['Payments', 'Cards'])
Pasos anidados en pasosUn paso que contiene pasos se pierde; solo se conservan los internosAnidados tal como se ejecutaron (pasos)
Un adjunto de un pasoSiempre es de la pruebaprobara.attach() dentro de un paso (archivos de un paso)
Las opciones del reporter ganan a las variables QASE_*, que ganan a qase.config.jsonLas variables ganan al archivo, que gana a las opcionesLas opciones y después las variables (precedencia)
testops.run.complete vale true por defectoSin definir, deja la ejecución abiertaUna ejecución creada se cierra por defecto (cierre)
report.connection.*report.connections.*resultsFile
El modo watch reporta al salir o al volver a ejecutar todas las pruebasCada nueva ejecución crea una ejecución nueva, salvo que se defina run.idUna sola ejecución para toda la sesión (modo watch)
api.token por proyecto de testops_multiUn solo token para todos los proyectosUn solo token de aplicación para todos los proyectos de la organización

Qué es distinto respecto a Qase, y por qué

Sección titulada «Qué es distinto respecto a Qase, y por qué»
TemaQaseProbara
PrecedenciaLas variables ganan a qase.config.json, que gana a las opciones del reporterLas opciones de la configuración de Vitest, después las variables PROBARA_*, y después los valores por defecto: lo que dice la configuración es lo que se ejecuta
Archivo de configuraciónqase.config.json o .qasercNinguno: un archivo de configuración con un token podría subirse al repositorio. Las opciones viven en la configuración de Vitest
IDs de casoNúmeros, solo en el título de la propia pruebaIDs visibles (SHOP-12) en el título o en cualquier describe: el ID del Proyecto dirige cada ID a su proyecto
Los helpersctx.qase, dentro de withQase(), hasta que termina el cuerpo de la prueba: Vitest rechaza sus anotaciones en afterEachprobara, importado una vez, funciona en el cuerpo, en beforeEach, en afterEach, en onTestFinished y en las funciones a las que llaman (dónde llamarlos)
PasosPlanos; el resultado esperado y los datos se codifican en el nombre del paso; sin duraciónAnidados tal como se ejecutaron, cada uno con su estado, su duración y su error; los más externos pasan a ser los pasos de un caso nuevo (pasos)
Estadosinvalid para un fallo cuyo error no parece una aserción (una suposición a partir del mensaje)passed, failed, skipped; blocked solo mediante statusMapping. Sin suposiciones a partir del texto del error (estados)
ErroresEl mensaje del primer error, que comment() sustituye; cada traza en un campo propioCada error del intento en las notas, incluidos los fallos de expect.soft; un comentario va primero y no oculta ninguno
Llamadas a fields() y parameters()La última llamada sustituye a las demásSe combinan por nombre
Campos del casoSe envían con cada resultadoSolo cuando el informe crea el caso: un caso existente conserva lo que escribieron las personas (solo casos creados)
ReintentosSolo el intento finalCada intento es un resultado, sin marca de inestable: el historial del caso muestra la inestabilidad (reintentos)
Cierre de la ejecuciónSolo con run.complete: true; sin definir, queda abiertaUna ejecución que crea el reporter se cierra al final, cuando se envió todo; una reutilizada queda abierta (cierre)
ShardsCada shard crea su propia ejecución salvo que reciba un ID de ejecución; cerrar una ejecución compartida queda para otro pasoUna sola ejecución: vitest --merge-reports sobre los blobs de los shards, o probara run create con PROBARA_RUN_ULID (sharding)
Cuando falla el envíofallback a un informe localUn archivo de resultados, que probara import results envía más tarde a las mismas ejecuciones (archivo de resultados)
Cadenas en attach()Una cadena que parece base64 se decodifica ('Hello' sube basura)Una cadena se envía tal cual; pasa un Buffer o un Uint8Array para contenido binario (probara.attach())
attach({ paths })Una lista de rutas; el tipo de contenido es type, no contentTypeUn archivo por llamada, path o body, y contentType
Función de QasePor qué
Enlace público al informe (showPublicReportLink)Un enlace público a los resultados de las pruebas es una decisión de compartir que corresponde a la aplicación Probara, no algo que deba activar un job de CI
Defectos automáticos (testops.defect)Registrar un defecto por cada fallo inunda la lista de defectos; reporta los defectos desde el resultado en Probara
Perfilador de red (profilers: ['network'], networkProfiler)Sus pasos no aportan nada, hasta los 200 pasos de un resultado, y no se pueden distinguir cuando las pruebas se ejecutan en paralelo; adjunta lo importante con probara.attach() (sin perfilador de red)
Enlace de Jira en las ejecuciones (run.externalLink)Probara vincula las incidencias de Jira a los casos y a los defectos, donde vive la trazabilidad; probara.issue() vincula un resultado a una incidencia
qase.mute()Qase lo documenta, pero no está en su código; la clasificación vive en Probara, en los resultados
Crear configuraciones (createIfNotExists)Las configuraciones forman parte de la planificación de un proyecto; una desconocida se rechaza, así que una errata nunca crea una nueva
El estado invalidEs una suposición a partir del texto del error; Probara mantiene cuatro estados y el error completo en las notas
Registrar en un archivo (logging.file)Los registros van a stderr, donde los guarda el CI; redirige stderr para conservar un archivo
Plan y entorno por proyecto (testops_multi.projects[].plan, .environment)Las entidades de planificación pertenecen a un proyecto, donde un nombre desconocido rechazaría el informe: crea primero la ejecución de ese proyecto con probara run create y pásala en run.ulids (una ejecución por proyecto)
Un archivo de configuración (qase.config.json, .qaserc)Un archivo con un token podría subirse al repositorio: el token se queda en el entorno, y las opciones en la configuración de Vitest

allure-vitest es un reporter que añade su archivo de setup y su runner a cada proyecto de Vitest, y escribe archivos de resultados en allure-results/; un paso aparte los convierte en un informe. El reporter envía en cambio los resultados a Probara, y añade su propio archivo de setup y su runner del mismo modo: cambia las entradas.

// vitest.config.mts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: [
'default',
[
'allure-vitest/reporter',
{
resultsDir: 'allure-results',
links: { issue: { urlTemplate: 'https://jira.example.com/browse/%s' } },
},
],
],
},
});
AllureProbara
resultsDirNada: los resultados van a Probara. Un archivo de resultados guarda lo que no se pudo enviar
links.issue.urlTemplateissueUrlTemplate (PROBARA_ISSUE_URL_TEMPLATE) (probara.issue())
environmentInforun.environment (un nombre) y el origen de la ejecución, detectado del CI (la ejecución)
globalLabels, ALLURE_LABEL_*run.tags (PROBARA_RUN_TAGS) para la ejecución; las etiquetas de un caso nuevo salen de probara.tags()
ALLURE_TESTPLAN_PATHrunCasesOnly con PROBARA_RUN_ULID (selección de pruebas)
reportMatchers, categories, listenersNo se portan (lo que el reporter no admite)

Las llamadas de Allure son asíncronas (await allure.tags('smoke')); las de Probara vuelven en el acto, salvo probara.attach() y un probara.step() con un cuerpo asíncrono, que esperas con await.

Allure (allure-js-commons)Probara (probara.*)
@allure.id:12 en un título, allureId('12')SHOP-12 en el título o en un describe, o id('SHOP-12')
displayName('...')title('...')
description('...')fields({ description: '...' })
severity('critical'), layer('api')fields({ severity: 'critical', layer: 'api' })
tag('smoke'), tags(...)tags('smoke')
epic, feature, story, label(name, value)fields({ Feature: 'Checkout' }): un campo personalizado con ese nombre, o tags(...)
parentSuite, suite, subSuitesuite(['Payments', 'Cards'])
parameter('user', 'ana')parameters({ user: 'ana' })
step('Pay', async () => {...})step('Pay', async () => {...})
logStep('Paid')step('Paid'): un paso sin cuerpo, aprobado
attachment(name, content, { contentType })attach({ name, body: content, contentType })
attachmentPath(name, path)attach({ name, path })
link(url, name)link(url, name)
issue('PAY-7')issue('PAY-7'), con issueUrlTemplate
tms('12')id('SHOP-12')
reporters: [
'default',
['@probara/vitest-reporter', { issueUrlTemplate: 'https://jira.example.com/browse/%s' }],
],
tests/payments.test.ts
import { probara } from '@probara/vitest-reporter';
import { test } from 'vitest';
test('refunds an order', async () => {
probara.fields({ severity: 'critical' }).issue('PAY-7');
await probara.step('Open the order', async () => {});
await probara.step('Refund it', async () => {});
});
[
{
"automationKey": "tests/payments.test.ts > refunds an order",
"steps": [
{ "action": "Open the order", "status": "passed" },
{ "action": "Refund it", "status": "passed" }
],
"links": [{ "url": "https://jira.example.com/browse/PAY-7", "name": "PAY-7" }],
"case": {
"fields": { "severity": "critical" },
"steps": [{ "action": "Open the order" }, { "action": "Refund it" }]
}
}
]
TemaAllureProbara
Estadosfailed para una aserción, broken para cualquier otro errorfailed para ambos, con el error en las notas: sin suposiciones a partir del tipo de error (estados)
test.skip, un describe omitidoNo se reportanResultados omitidos, como los tiene la salida JUnit de Vitest
AsercionesCada expect es un paso (reportMatchers, activado por defecto)Los pasos son las llamadas a probara.step()
Una llamada donde no corre ninguna pruebaUna llamada en beforeAll o en el cuerpo de un describe va a cada prueba de la suiteSe deja fuera, con una advertencia: ahí no corre una sola prueba (dónde llamar a los helpers)
Un hook que lanza un errorSu error es un error global del informe, junto a las pruebasEl veredicto de Vitest: un beforeEach o afterEach que falla hace fallar su prueba, un beforeAll o afterAll que falla es un resultado fallido de su describe o de su archivo (errores fuera de una prueba)
ReintentosUn resultado por prueba, el intento finalCada intento es un resultado de la ejecución (reintentos)
Modo watchArchivos de resultados nuevos en cada nueva ejecución, que se acumulan en resultsDirUna sola ejecución para toda la sesión (modo watch)
Campos del casoEtiquetas en cada resultadoCampos y etiquetas del caso que crea un informe; un caso existente conserva los suyos (solo casos creados)
test.concurrent en modo navegadorUna llamada puede ir a la prueba equivocada después de un await (lo dice su README)Una llamada se descarta con una advertencia, nunca se registra en otra prueba (modo navegador)

@reportportal/agent-js-vitest es un objeto reporter en reporters, con un archivo de setup que listas en setupFiles para su ReportingApi. Sustituye ambos por la entrada del reporter de arriba:

// vitest.config.mts
import RPReporter from '@reportportal/agent-js-vitest';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['@reportportal/agent-js-vitest/setup'],
reporters: [
'default',
new RPReporter({
apiKey: process.env.RP_API_KEY,
endpoint: 'https://rp.example.com/api/v2',
project: 'shop',
launch: 'Nightly',
}),
],
},
});
ReportPortal (variable)Probara (variable)
apiKey, oauthPROBARA_API_TOKEN: un token de aplicación de la tarjeta Vitest (primeros pasos)
endpointbaseUrl (PROBARA_BASE_URL), la URL de la aplicación sin ruta
projectprojectId (PROBARA_PROJECT)
launchrun.name (PROBARA_RUN_NAME)
descriptionrun.description (PROBARA_RUN_DESCRIPTION)
attributes ([{ key: 'build', value: '42' }])run.tags (PROBARA_RUN_TAGS=build:42,nightly): las etiquetas son nombres simples, con dos puntos incluidos
launchId (RP_LAUNCH_ID)run.ulid (PROBARA_RUN_ULID)
launchUuidPrintprobara run create imprime el ULID en el que reporta cada shard (una ejecución existente)
restClientConfig (timeout, retries, proxy)timeoutMs, maxRetries; un proxy mediante las variables de Node (red)
headersUn fetch propio (tu propio cliente HTTP)
debugdebug (PROBARA_DEBUG)
extendTestDescriptionWithLastErrorNada que definir: cada error de un intento está en sus notas
skippedIssue, rerun, rerunOf, mode: 'DEBUG'No se portan (lo que el reporter no admite)
ReportPortal (ReportingApi)Probara (probara.*)
ReportingApi.attachment(task, { name, type, content })attach({ name, body, contentType }), con un Buffer o texto, o attach({ name, path })
ReportingApi.description(task, text)fields({ description: text }), para un caso nuevo
ReportingApi.attributes(task, [{ key, value }])parameters({ [key]: value }) en el resultado, o tags(value) en un caso nuevo
ReportingApi.testCaseId(task, id)No se porta: vincula un caso por su ID (vincular)
ReportingApi.log(task, message, level)No se porta: comment() para una línea en las notas, captureOutput para la consola
console.log(), console.error(), como registroscaptureOutput: stdout.log y stderr.log de cada intento (salida de consola)
El ReportingApi global del archivo de setupprobara, importado: no hay ningún task que pasar

Una cadena en base64 como content se enviaría como texto: pasa a attach() la path del archivo, o sus bytes como un Buffer.

tests/checkout.test.ts
import { probara } from '@probara/vitest-reporter';
import { test } from 'vitest';
test('pays with a card', async () => {
probara.fields({ description: 'Pays with a saved card' }).parameters({ browser: 'chromium' });
await probara.attach({
name: 'response',
body: '{"status":"paid"}',
contentType: 'application/json',
});
});
[
{
"automationKey": "tests/checkout.test.ts > pays with a card",
"parameters": { "browser": "chromium" },
"case": { "description": "Pays with a saved card" }
}
]

El resultado lleva además response.json (application/json).

Qué funciona distinto respecto a ReportPortal

Sección titulada «Qué funciona distinto respecto a ReportPortal»
TemaReportPortalProbara
API dentro de la pruebaReportingApi, global desde su archivo de setup, o pasando el task de la pruebaprobara, importado; cada llamada pertenece a la prueba que la hizo (metadatos)
test.concurrentEl ReportingApi global sigue a la última prueba que empezó, así que una llamada puede ir a otra pruebaCada llamada va a su propia prueba
ReintentosSolo el intento finalCada intento es un resultado (reintentos)
test.todoOmitida, con el atributo todoOmitida, con la nota Todo
Un describe omitidoUn elemento de suite omitido; sus pruebas no se reportanCada prueba es un resultado omitido
ErroresEl primer error, como registro y en la descripciónCada error del intento, en las notas de su resultado
EstructuraUn elemento de suite por cada describeCada caso nuevo en una suite con el nombre de su archivo (casos que faltan)
Modo watchEl launch termina con la primera ejecución, y las siguientes no tienen ningunoUna sola ejecución para toda la sesión (modo watch)

TestRail no tiene reporter para Vitest: un pipeline escribe la salida JUnit de Vitest, y después la CLI de Python de TestRail, trcli parse_junit, la envía tras la ejecución. El reporter sustituye ambos pasos, y envía lo que el XML no puede contener: pasos, archivos, comentarios y cada intento.

Ventana de terminal
npx vitest run --reporter=junit --outputFile=reports/vitest.xml
trcli -h https://example.testrail.io --project "Shop" -u ci@example.com -k "$TESTRAIL_KEY" \
parse_junit --title "Nightly" --case-matcher name -f reports/vitest.xml --close-run

Con el reporter, basta con el paso de pruebas:

Ventana de terminal
npx vitest run
trcliProbara
-h (host), -u, -p, -kbaseUrl para un Probara autoalojado; PROBARA_API_TOKEN
--project (un nombre)projectId (PROBARA_PROJECT), el ID del Proyecto
--title, --run-descriptionrun.name, run.description
--run-idrun.ulid (PROBARA_RUN_ULID)
--plan-id, --config-ids, --milestone-idrun.plan, run.configurations, run.milestone, por ID visible o nombre (la ejecución)
--close-runEl comportamiento por defecto para una ejecución que crea el reporter; closeRun para una reutilizada
--case-matcher name (C123 en el nombre)SHOP-12 en el título o en un describe (vincular)
--case-matcher auto (classname.name)La clave de automatización: el archivo, los describes y el título
-y (crear secciones y casos)createMissingCases, activado por defecto; las suites salen del archivo de prueba
--suite-id, --section-idsuiteUlid (PROBARA_SUITE_ULID): la suite bajo la que van los casos creados
--case-fields type_id:3probara.fields({ type: 'Regression' }) en la prueba, por nombre (metadatos)
-a, --assign (emails)assignFailedTo (PROBARA_ASSIGN_FAILED_TO) (asignar resultados fallidos)
-b, --batch-sizechunkSize (de 1 a 500)
-t, --timeout (segundos)timeoutMs (milisegundos, para cada intento de una petición)
--proxy, --noproxyHTTPS_PROXY y NO_PROXY con NODE_USE_ENV_PROXY=1 (proxies)
-v (verbose)debug (PROBARA_DEBUG)
Propiedad testrail_result_commentprobara.comment()
Propiedad testrail_attachmentprobara.attach()
Propiedad testrail_result_stepprobara.step()
Tematrcli sobre la salida JUnit de VitestProbara
Pruebas omitidasRetestOmitidas
test.todoRetest: Vitest la escribe como omitidaOmitida, con la nota Todo
ReintentosUn resultado por pruebaCada intento (reintentos)
Describes anidadosUna sección por archivo de prueba, plana: Vitest escribe una suite por archivoLa suite de un caso creado es su archivo; probara.suite() nombra suites anidadas
DuracionesRedondeadas a segundos (--allow-ms para milisegundos)Milisegundos, y el inicio de cada intento
Un ID de caso desconocidoAborta la subidaEse resultado no se registra, y el registro lo nombra; los demás sí
Una subida fallidaDeshace la ejecución, los casos y las secciones que creóNo se deshace nada; un archivo de resultados guarda lo que no se envió
ShardsUna subida del archivo de cada shard, con un patrónvitest --merge-reports sobre los blobs, o probara run create, PROBARA_RUN_ULID en cada shard y probara run close (sharding)

Cada una de estas funciones se deja fuera, con su motivo:

FunciónEnPor qué
Actualizar casos existentestrcli (--update-existing-cases, --update-strategy)Un informe nunca cambia un caso existente: lo que hay escrito ahí es de las personas (solo casos creados)
Una marca de inestableVitest (diagnostic().flaky)Cada intento es un resultado: un caso cuyo historial mezcla fallos y aprobados en una ejecución es inestable, sin necesidad de marca (reintentos)
Aserciones como pasosAllure (reportMatchers)Cada expect sería un paso, hasta los 200 pasos de un resultado, y desplazaría a los pasos que dicen lo que hace la prueba: envuelve lo importante en probara.step()
Los estados broken e invalidAllure (broken), Qase (invalid)Los dos son una suposición a partir del tipo o del texto del error; Probara mantiene cuatro estados (passed, failed, skipped, blocked) y el error completo en las notas
Parámetros enmascarados, ocultos y excluidosAllure (los modos de parameter())Los parámetros se muestran con el resultado y nunca forman parte de la clave: deja fuera un secreto en lugar de enmascararlo
Niveles de registro y una API de registroReportPortal (ReportingApi.log()), Allure (logStep() con un estado)Probara no tiene una entidad de registro: captureOutput guarda la salida de consola de cada intento, probara.comment() escribe una línea en las notas, y el estado de un paso es lo que hizo su cuerpo
Defectos, enlaces de Jira en las ejecuciones, enlaces públicos a informesQase (testops.defect, run.externalLink, showPublicReportLink)Los defectos y su clasificación viven en Probara, en los resultados; probara.issue() vincula un resultado a una incidencia; un enlace público a los resultados es una decisión de compartir, no un ajuste de CI
Un perfilador de redQase (profilers: ['network'])Sus pasos no aportan nada, hasta los 200 pasos de un resultado, y no se pueden distinguir cuando las pruebas se ejecutan en paralelo (sin perfilador de red)
Categorías, problemas conocidos, pruebas silenciadasAllure (categories), ReportPortal (skippedIssue), Qase (qase.mute(), solo documentado)La clasificación vive en Probara, en los resultados y los defectos, no en un ajuste de CI
Sustituir la clave de automatizaciónAllure (historyId(), testCaseId()), ReportPortal (testCaseId()), trcli (custom_automation_id)La clave sale del archivo y de los nombres, igual que la de probara import junit: vincula un caso por su ID (claves de automatización)
Un propietarioAllure (owner())Un caso de Probara no tiene campo de propietario: asigna en su lugar los resultados fallidos (asignar resultados fallidos)
Campos de resultadotrcli (--result-fields, testrail_result_field)Los resultados de Probara no tienen campos personalizados: los parámetros (probara.parameters()) y los enlaces (probara.link()) llevan los datos adicionales
Pasos y llamadas desde los hooks de una suiteAllure (una llamada en beforeAll va a cada prueba de la suite)Una llamada donde no corre una sola prueba no pertenece a ninguna prueba: se deja fuera con una advertencia, no se copia en cada una. Crea los pasos importantes en la prueba o en su beforeEach; un hook que lanza un error se sigue mostrando (errores fuera de una prueba)
Un archivo de configuraciónQase (qase.config.json, .qaserc), trcli (config.yml)Un archivo con un token podría subirse al repositorio: el token se queda en el entorno, y las opciones en la configuración de Vitest (configuración)
Benchmarks y coberturaVitest (vitest bench, --coverage)Probara no tiene resultados de benchmarks ni de cobertura: el reporter envía resultados de pruebas
Pruebas de tiposVitest (vitest typecheck)Ninguna prueba del reporter las cubre, así que no se promete nada para ellas
Launches de repetición y de depuraciónReportPortal (rerun, rerunOf, mode: 'DEBUG')Vuelve a reportar en la misma ejecución con run.ulid: cada caso de la ejecución conserva el último resultado y su historial. Para probar una configuración, comprueba qué se enviaría
Iniciar sesión con OAuthReportPortal (oauth)El CI reporta con un token de aplicación de la organización, que no ocupa ningún puesto y sobrevive a la persona que lo creó
Archivos y errores globalesAllure (globalAttachment(), globalError())Los archivos y los errores pertenecen a un resultado: adjúntalos desde la prueba que los produjo
Descripciones en HTMLAllure (descriptionHtml())La descripción es texto; Probara le da formato
ListenersAllure (listeners)Ningún hook cambia un resultado antes de enviarlo; statusMapping y statusFilter cubren los estados (mapeo de estados)
Desactivar la verificación TLStrcli (--insecure)No se puede desactivar: pasa a Node tu autoridad de certificación (autoridades de certificación propias)
Deshacer una subida fallidatrcliUn resultado registrado se conserva: deshacerlo perdería los que sí llegaron
Referencias en las ejecucionestrcli (--test-run-ref)La ejecución registra la URL del build de CI, la rama y el commit; las incidencias se vinculan a los resultados con probara.issue()

@probara/vitest-reporter sigue el versionado semántico. Lee el changelog antes de actualizarlo. Una versión mayor nueva de Vitest puede cambiar los nombres que da a las pruebas, y con ellos sus claves de automatización: antes de actualizar Vitest, lee de Vitest 4 a 5 y compara las claves.

  • 0.x (ahora): el reporter es joven. Una versión menor (0.2.0) puede cambiar opciones, líneas del registro o valores por defecto; una versión de parche (0.1.1) solo corrige errores. Fija la versión en package.json (o conserva el lockfile) y actualiza a propósito.
  • 1.0 y posteriores: cambios incompatibles solo en una versión mayor.
  • @probara/vitest-reporter depende de @probara/core de la misma línea de versiones; instala solo el reporter y deja que traiga su core.

Cualquiera de estos necesita una versión mayor a partir de la 1.0, y una menor antes:

  • Una opción o una variable PROBARA_* eliminada o renombrada, o con otro significado.
  • Un valor por defecto cambiado (cerrar ejecuciones, crear los casos que faltan, keyIncludesFile, lo que se adjunta).
  • Un helper probara.* eliminado, o con otro comportamiento.
  • Un estado con el que se envía un intento, cambiado para el mismo intento.
  • Cualquier cambio en la clave de automatización que recibe una prueba, con keyIncludesFile activado o desactivado. Una clave distinta desvincula la prueba de su caso: la siguiente ejecución crea uno nuevo.
  • Dejar de admitir una versión de Vitest o de Node.js que sigue teniendo soporte de sus autores.

Añadir una opción, un helper, una línea de registro o un archivo que se sube no es incompatible. Las líneas del registro en stderr son para personas: no las analices con código.

El algoritmo de la clave es la clave de automatización v1, congelada en @probara/core y fijada con vectores de referencia: ninguna versión del reporter la cambia. Lo que añade el reporter es cómo una prueba de Vitest se convierte en las partes de la clave, igual que probara import junit lee la salida JUnit de Vitest (claves):

  • el archivo de prueba relativo a rootDir (la raíz de Vitest), cuando keyIncludesFile está activado;
  • los títulos de los describe y el de la prueba, cada uno una parte, como los nombra la salida JUnit de Vitest, sin los IDs de caso;
  • una fila de test.each o test.for, por el título que le da el formato de Vitest.

Esa correspondencia también forma parte del contrato: una versión del reporter que cambia la clave de una prueba es un cambio incompatible, señalado como tal en el changelog. Una versión de Vitest que formatea los títulos de otra forma también cambia claves (abajo), tanto para el reporter como para probara import junit.

Para comprobar una actualización de cualquiera de los dos, compara lo que enviarían ambas versiones: ejecuta la batería con el envío desactivado una vez por versión, cada una con su propio archivo de resultados, y compara las simulaciones con diff (comprobar qué se enviaría). Cualquier clave que cambie es una prueba que recibiría un caso nuevo.

NecesitaVersiones admitidas
vitest3.2 y posteriores (el rango de peer ^3.2.0 || ^4.0.0 || ^5.0.0): probado en 3.2.7, 4.1.11, 5.0.2
Modo navegadorEl proveedor de tu versión de vitest: probado en 3.2.7, 4.0.18, 4.1.11 y 5.0.2
Node.js22.12 y posteriores (el campo engines)

Vitest 3.2 es la primera versión con anotaciones de prueba, que el reporter lee, de ahí el rango de peer. El modo navegador tiene su propia configuración por cada línea de Vitest (modo navegador). 0.1.0 es la primera versión del reporter: todavía no hay nada desde lo que migrar, y si vienes de probara import junit sobre la salida JUnit de Vitest, las claves se mantienen.

Cambian algunos títulos de test.each y test.for, y con ellos sus claves. Vitest 5 formatea de otra manera los valores que pone en un título:

La pruebaVitest 3 y 4Vitest 5
test.each([{ name: 'Ana' }])('greets $name')greets 'Ana'greets Ana
test.each([{ note }])('saves $note'), una nota de 48 caracteressaves 'Leave the parcel with the neighbour a…'saves Leave the parcel with the neighbour at …
test.each([[{ note }]])('stores %o')stores { note: 'Leave the parcel with the neighbour at number 12' }stores { …(1) }
test.each([[new Map([['eur', 12]])]])('reads %o')reads Map{ 'eur' => 12 }reads Map { 'eur' => 12 }
Números, objetos y arrays cortos, %s de una cadenaIgualIgual

Las claves de esas filas, tal como las nombra Vitest 5:

tests/rows.test.ts
import { test } from 'vitest';
const note = 'Leave the parcel with the neighbour at number 12';
test.each([{ name: 'Ana' }])('greets $name', () => {});
test.each([[{ note }]])('stores %o', () => {});
[
{ "automationKey": "tests/rows.test.ts > greets Ana" },
{ "automationKey": "tests/rows.test.ts > stores { …(1) }" }
]
  • Una clave que cambia es un caso nuevo. El caso anterior conserva su historial, y los resultados de la clave nueva van a un caso nuevo. Compara las claves antes de actualizar, como arriba: cada línea que cambia es una de esas filas.
  • Las filas pueden coincidir. { …(1) } representa cualquier objeto de una sola clave demasiado largo para imprimirse, así que dos filas así reciben el mismo título y comparten un caso. Pon en el título un valor corto de cada fila ('stores $id').
  • probara import junit cambia del mismo modo: lee los nombres que Vitest 5 escribe en su salida JUnit, así que los dos caminos siguen dando a una prueba la misma clave.
  • Las carpetas por defecto cambian: attachmentsDir pasa de .vitest-attachments a .vitest/attachments (dónde se guardan las copias), y los blobs de un job con sharding pasan de .vitest-reports a .vitest/blob, que es lo que lee vitest --merge-reports por defecto. Un job de CI que los lleva, o que los limpia, necesita las rutas nuevas (lleva los blobs y los adjuntos).
  • El modo navegador tiene un paquete de proveedor nuevo, @vitest/browser-playwright, con provider: playwright() (modo navegador).
  • Las etiquetas de Vitest (4.1 y posteriores) se suman a las etiquetas de los casos que crea un informe; Vitest rechaza una etiqueta que su configuración no declara (etiquetas de Vitest).
  • Los cuerpos de texto de context.annotate() necesitan bodyEncoding: 'utf-8' a partir de Vitest 4.1.3: si no, Vitest marca el cuerpo como base64 (anotaciones de Vitest).

Cualquier actualización de un pipeline con sharding

Sección titulada «Cualquier actualización de un pipeline con sharding»

vitest --merge-reports solo lee los blobs de su propia versión de Vitest: actualiza a la vez los shards y el job que fusiona, en un único cambio del pipeline (la misma versión de Vitest en todas partes).

Relacionado: Primeros pasos · Vincular · CLI · Fuente: migrating-from-junit.md, migrating-from-qase.md, coming-from-other-tools.md, upgrade.md