8. Tests, v fmt, v vet y v doc
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Código: l08-testing, un módulo y sus tests, y l08-failing, tests que fallan a propósito — v test . desde code/v-for-csharp-java/l08-testing.
| C# con xUnit | Java con JUnit 5 | V | |
|---|---|---|---|
| Framework | un paquete: xUnit, NUnit, MSTest | una dependencia: JUnit | integrado |
| Un test | un método con [Fact] |
un método con @Test |
una función llamada test_… en un archivo llamado …_test.v |
| Comprobación | Assert.Equal(expected, actual) |
assertEquals(expected, actual) |
assert actual == expected |
| Antes y después de todos los tests | un fixture, IClassFixture<T> |
@BeforeAll, @AfterAll |
testsuite_begin(), testsuite_end() |
| Probar código privado | InternalsVisibleTo |
un test en el mismo paquete | un test en el mismo módulo |
| Ejecutar | dotnet test |
mvn test, gradle test |
v test ., o v file_test.v |
Fuente: Testing.
Un módulo y sus tests
Sección titulada «Un módulo y sus tests»l08-testing es un proyecto con un módulo, deps, que reúne el parser de versiones de la lección 6 y un grafo de referencias entre proyectos:
l08-testing/├── v.mod├── deps/│ ├── version.v module deps: Version, parse_version│ ├── graph.v module deps: Graph, Graph.parse, reachable│ ├── version_test.v internal tests│ ├── graph_test.v internal tests│ └── s08_cycle_test.v the solution of exercise 1└── tests/ └── ga_test.v external tests, on the ga CSV filesUn archivo de test cuya primera línea es module deps es un test interno: pertenece al módulo, y puede llamar a sus funciones privadas, como parse_release (version_test.v):
// Un test interno: mismo módulo, así que puede llamar a la función privada parse_releasemodule deps
fn test_parse_release() { r := parse_release('9.5.1')! assert r.parts == [9, 5, 1]}
fn test_parse_release_rejects_letters() { parse_release('1.x') or { assert err.msg() == 'strconv.atoi: parsing "x": invalid radix 10 character' return } assert false, 'expected an error'}
fn test_prerelease() ! { v := parse_version('1.0.0-beta.24164.1')! assert v is Prerelease if v is Prerelease { assert v.label == 'beta.24164.1' assert v.release.parts == [1, 0, 0] } assert !v.is_stable()}No hay biblioteca de aserciones: assert acepta cualquier expresión booleana y, si falla, imprime ambos lados de la comparación. Una función de test puede devolver !, y entonces ! tras una llamada hace fallar el test con el error, donde un test de xUnit dejaría escapar la excepción. Un error esperado se comprueba con or, donde xUnit tiene Assert.Throws y JUnit assertThrows.
Un archivo de test que en cambio importa el módulo es un test externo, y solo ve lo que deps hace pub. tests/ga_test.v ejecuta el módulo sobre los archivos reales de ga:
// Un test externo: importa el módulo, y solo ve su API públicaimport depsimport os
const data = os.join_path(@VMODROOT, '..', 'data', 'ga')
fn testsuite_begin() { assert os.exists(data)}
fn test_every_version_parses() ! { lines := os.read_lines(os.join_path(data, 'package_refs.csv'))! mut floating := []string{} for line in lines[1..] { f := line.split(',') v := deps.parse_version(f[2])! if v is deps.Floating { floating << f[1] } } assert floating == ['ModelContextProtocol', 'Microsoft.AspNetCore.SpaProxy']}
fn test_gacli_references() ! { g := deps.Graph.parse(os.read_lines(os.join_path(data, 'project_refs.csv'))!)! reachable := g.reachable('Apps/GaCli/GaCli.fsproj') assert reachable.len == 7 assert 'Common/GA.Core/GA.Core.csproj' in reachable}@VMODROOT es la carpeta del v.mod más cercano, conocida en tiempo de compilación: el test encuentra sus datos desde dondequiera que se ejecute.
v test . encuentra todos los archivos _test.v bajo la carpeta, compila cada uno como un programa aparte, y los ejecuta en paralelo:
---- Testing... ----------------------------------------------------------------OK [1/4] C: 594.6 ms, R: 83.349 ms C:/Users/spare/source/repos/learn/code/v-for-csharp-java/l08-testing/deps/graph_test.vOK [2/4] C: 593.6 ms, R: 84.106 ms C:/Users/spare/source/repos/learn/code/v-for-csharp-java/l08-testing/deps/s08_cycle_test.vOK [3/4] C: 594.1 ms, R: 84.014 ms C:/Users/spare/source/repos/learn/code/v-for-csharp-java/l08-testing/tests/ga_test.vOK [4/4] C: 593.8 ms, R: 84.440 ms C:/Users/spare/source/repos/learn/code/v-for-csharp-java/l08-testing/deps/version_test.v--------------------------------------------------------------------------------Summary for all V _test.v files: 4 passed, 4 total. Elapsed time: 787 ms, on 4 parallel jobs. Comptime: 2376 ms. Runtime: 335 ms.C: es el tiempo de compilación y R: el tiempo de ejecución de cada archivo: 0.6 s de compilación para 0.08 s de tests. El resumen cuenta archivos, no tests. El orden de los archivos, los tiempos y las rutas cambian de una ejecución a otra y de un sistema operativo a otro, así que check.sh ordena las líneas OK y conserva solo la ruta dentro del proyecto antes de comparar.
Una diferencia más en la CI: cuando la variable de entorno CI o GITHUB_JOB está definida, v test oculta las líneas OK, e imprime solo los fallos y el resumen (common.v, líneas 37-43). La primera ejecución de la CI de esta lección falló por eso; VTEST_HIDE_OK=0 las hace volver.
Cuando un test falla
Sección titulada «Cuando un test falla»l08-failing/failing_test.v falla de cuatro maneras:
import strconv
fn testsuite_begin() { println('testsuite_begin')}
fn testsuite_end() { println('testsuite_end')}
fn test_values() { parts := '9.5'.split('.').map(strconv.atoi(it) or { -1 }) assert parts == [9, 5, 1] println('not printed: the test stops at the first failed assert')}
fn test_message() { for s in ['9.5.1', '9.5', '1.0.0-beta.1'] { assert s.split('.').len == 3, 'version ${s}' }}
fn test_propagated_error() ! { lines := strconv.atoi('n/a')! assert lines > 0}
@[assert_continues]fn test_continues() { for s in ['9.5.1', '0.*', '8.*-*'] { assert !s.contains('*'), s } println('printed: assert_continues goes on after a failure')}
fn test_passes() { assert '9.5.1'.split('.').len == 3}v failing_test.v, desde su carpeta, ejecuta un solo archivo:
testsuite_beginfailing_test.v:14: fn test_values > assert parts == [9, 5, 1] Left value (len: 6): `[9, 5]` Right value (len: 9): `[9, 5, 1]`
failing_test.v:20: fn test_message > assert s.split('.').len == 3, 'version ${s}' Left value (len: 1): `2` Right value (len: 1): `3` Message: version 9.5
failing_test.v:25: fn test_propagated_error failed propagation with error: strconv.atoi: parsing "n/a": invalid radix 10 character 25 | lines := strconv.atoi('n/a')!failing_test.v:32: fn test_continues assert !s.contains('*'), s Message: 0.*
failing_test.v:32: fn test_continues assert !s.contains('*'), s Message: 8.*-*
printed: assert_continues goes on after a failuretestsuite_endEl código de salida es 1. Lo que muestra el informe:
assertimprime la expresión y el valor de cada lado, comoAssert.Equalimprime Expected y Actual. Ellenentre paréntesis es la longitud del texto impreso, no del array:[9, 5]tiene 6 caracteres.- Un
assertfallido termina su función de test, como unAssertfallido en xUnit o JUnit:not printedno se imprime. Los demás tests se siguen ejecutando. - El mensaje después de la coma es la única forma de saber qué iteración de un bucle falló. V no tiene
[Theory]ni@ParameterizedTest: un bucle sobre los casos con un mensaje hace ese trabajo. - Un error propagado con
!hace fallar el test, con la línea de la llamada. @[assert_continues]informa de cada fallo y continúa, como elassertAllde JUnit.-assert continuesen la línea de comandos lo hace para todas las funciones.
-stats enumera cada función de test:
OK [1/7] ms NO asserts | main.testsuite_begin() FAIL [2/7] ms 1 assert | main.test_values() FAIL [3/7] ms 2 asserts | main.test_message() FAIL [4/7] ms 0 asserts | main.test_propagated_error() OK [5/7] ms 1 assert | main.test_continues() OK [6/7] ms 1 assert | main.test_passes() OK [7/7] ms NO asserts | main.testsuite_end() Summary for running V tests in "failing_test.v": 3 failed, 3 passed, 6 total. Elapsed time: ms.(check.sh elimina los tiempos, que cambian en cada ejecución.) Dos cosas no cuadran. test_continues está OK, con 1 assert, aunque dos de sus tres asserts fallaron: el archivo sigue fallando, pero esta línea lo oculta. Y el resumen cuenta asserts, no tests: test_message cuenta como 1 superado y 1 fallido.
assert también funciona fuera de los tests, en cualquier función: ahí un fallo es un panic, V panic: Assertion failed.... Con -prod, los asserts se eliminan, como dice la documentación: en mi máquina, v -prod run de un programa cuyo assert 1 + 1 == 3 falla sin -prod imprime la línea siguiente y sale con 0. Debug.Assert en C# desaparece de la misma manera de una compilación Release.
v fmt es el formateador, el equivalente de dotnet format; Java no tiene ninguno en el JDK. No tiene opciones de estilo: las tabulaciones, los espacios y los saltos de línea son los de V. vet/v08_vet.v está mal formateado a propósito:
module main
// Cuenta las referencias de un proyectopub fn count(refs []string) int { return refs.len}
fn main() { println(count( ['GA.Core','GA.Domain.Core'] ))}v fmt -verify comprueba sin escribir, para la CI, como dotnet format --verify-no-changes:
v08_vet.v is not vfmt'edEncountered a total of: 1 formatting errors.El código de salida es 1, y el mensaje real empieza por la ruta absoluta del archivo. v fmt v08_vet.v imprime el archivo formateado, y v fmt -w lo reescribe:
module main
// Cuenta las referencias de un proyectopub fn count(refs []string) int { return refs.len}
fn main() { println(count(['GA.Core', 'GA.Domain.Core']))}El check.sh del curso ejecuta v fmt -verify en todas sus carpetas excepto los snippets rechazados, que v fmt comprueba en cuanto a tipos y rechaza (diario).
v vet señala código sospechoso, un pequeño equivalente de los analizadores de .NET:
v08_vet.v:4: warning: A function name is missing from the documentation of "pub fn count(refs []string) int".v08_vet.v:5: error: Looks like you are using spaces for indentation.Note: You can run `v fmt -w v08_vet.v` to fix these errors automaticallyEl código de salida es 1, por el error; -W convierte también las advertencias en errores. La advertencia se refiere al comentario: V espera que la documentación de una función pública empiece por su nombre, // count devuelve …, como hace Go. v fmt -w corrige la indentación, no el comentario.
v doc genera la documentación de un módulo a partir de los comentarios situados sobre sus declaraciones públicas, como javadoc o los comentarios de documentación XML de C#, sin etiquetas. version.v y graph.v siguen la regla de v vet. Desde l08-testing, v doc -comments deps imprime:
module deps Package versions, as written in the project files of GuitarAlchemist/ga.
fn parse_version(s string) !Version parse_version reads a version, or returns an error when a part isn't a number.fn Graph.parse(lines []string) !Graph Graph.parse reads the lines of project_refs.csv, header included.type Version = Floating | Prerelease | Release Version is one of the three forms.fn (v Version) is_stable() bool is_stable tells whether a version is a release.struct Floating {pub: pattern string} Floating is a version with a wildcard, such as 8.*-*, that NuGet resolves at restore time.struct Graph {mut: refs map[string][]string} Graph holds the project references, from a project to the projects it references.fn (g Graph) reachable(from string) []string reachable returns the projects that a project references, directly or not, sorted.struct Prerelease {pub: release Release label string} Prerelease is a release followed by a label, such as 1.0.0-beta.1.struct Release {pub: parts []int} Release is a version made of numbers only, such as 9.5.1.Las funciones privadas, como parse_release, se omiten, pero el campo privado refs de Graph sí se muestra. -f html o -f md escriben otros formatos.
Puntos clave
Sección titulada «Puntos clave»- Un test es una función
test_en un archivo_test.v;module xlo hace interno,import xexterno.v test .compila cada archivo como un programa aparte y los ejecuta en paralelo. assertimprime ambos lados de una comparación fallida, más un mensaje opcional; termina el test salvo con@[assert_continues], y desaparece con-prod.- Las funciones de test pueden devolver
!; un error esperado se comprueba conor. -statsy los resúmenes cuentan asserts, e informan comoOKde un test con fallos que continuaron: lee las líneas de fallo, y el código de salida.- En la CI,
v testoculta los archivos que pasan salvo conVTEST_HIDE_OK=0. v fmt -verify,v vetyv docvienen con el compilador;v vetquiere comentarios de documentación que empiecen por el nombre.
Ejercicios
Sección titulada «Ejercicios»Graph.reachableno debe entrar en bucle con un ciclo de referencias. Escribe un test paraA → B → A: ¿qué debería devolverreachable('A')?
Solución
l08-testing/deps/s08_cycle_test.v:
// Lección 8, ejercicio 1: un ciclo de referenciasmodule deps
fn test_cycle() ! { g := Graph.parse(['from,to', 'A,B', 'B,A'])! assert g.reachable('A') == ['A', 'B'] assert g.reachable('B') == ['A', 'B']}v deps/s08_cycle_test.v no imprime nada y sale con 0. El conjunto seen detiene el recorrido. A está en su propio resultado, porque B lo referencia: reachable devuelve todos los proyectos alcanzados desde A, y A es uno de ellos. Si eso es correcto es una decisión que el test ahora deja registrada. v test . encuentra el archivo junto con los demás: el resumen de arriba tiene 4 archivos.
- En
failing_test.v,test_messagese detiene en9.5y nunca comprueba1.0.0-beta.1, que tiene 4 partes una vez dividido por los puntos. ¿Cómo ves ambos fallos sin cambiar el bucle?
Solución
Pon @[assert_continues] encima de fn test_message(), o ejecuta v -assert continues failing_test.v para todas las funciones del archivo. El informe tiene entonces un bloque para 9.5, con Left value (len: 1): `2`, y otro para 1.0.0-beta.1, con `4`: '1.0.0-beta.1'.split('.') da ['1', '0', '0-beta', '1']. Con @[assert_continues], -stats informa entonces de test_message como OK, igual que test_continues.
- Corrige
vet/v08_vet.vpara quev vetno señale nada.
Solución
v fmt -w v08_vet.v corrige la indentación. La advertencia necesita un comentario que empiece por el nombre de la función:
// count devuelve el número de referencias de un proyecto.pub fn count(refs []string) int { return refs.len}Los archivos de l08-testing siguen esa regla: v vet . no señala nada ahí.
Fuentes
Sección titulada «Fuentes»- Documentación de V — Testing: Asserts, Asserts that do not abort your program, Test files, Running tests; Tools — v fmt; Writing documentation; la documentación de 0.5.2:
doc/docs.md v help test,v help vet,v help docyv help fmt, impresos por V 0.5.2- xUnit,
dotnet test,dotnet format, Análisis de código en .NET - Guía del usuario de JUnit 5