8. Tests, v fmt, v vet et v doc
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Code : l08-testing, un module et ses tests, et l08-failing, des tests qui échouent exprès — v test . depuis code/v-for-csharp-java/l08-testing.
| C# avec xUnit | Java avec JUnit 5 | V | |
|---|---|---|---|
| Framework | un paquet : xUnit, NUnit, MSTest | une dépendance : JUnit | intégré |
| Un test | une méthode avec [Fact] |
une méthode avec @Test |
une fonction nommée test_… dans un fichier nommé …_test.v |
| Vérifier | Assert.Equal(expected, actual) |
assertEquals(expected, actual) |
assert actual == expected |
| Avant et après tous les tests | une fixture, IClassFixture<T> |
@BeforeAll, @AfterAll |
testsuite_begin(), testsuite_end() |
| Tester du code privé | InternalsVisibleTo |
un test dans le même package | un test dans le même module |
| Exécuter | dotnet test |
mvn test, gradle test |
v test ., ou v file_test.v |
Source : Testing.
Un module et ses tests
Section intitulée « Un module et ses tests »l08-testing est un projet avec un seul module, deps, qui regroupe l’analyseur de versions de la leçon 6 et un graphe de références entre projets :
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 fichier de test dont la première ligne est module deps est un test interne : il appartient au module, et peut appeler ses fonctions privées, comme parse_release (version_test.v) :
// Un test interne : même module, donc il peut appeler la fonction privée 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()}Il n’y a pas de bibliothèque d’assertions : assert prend n’importe quelle expression booléenne, et en cas d’échec affiche les deux côtés de la comparaison. Une fonction de test peut renvoyer !, et un ! après un appel fait alors échouer le test avec l’erreur, là où un test xUnit laisserait l’exception s’échapper. Une erreur attendue se vérifie avec or, là où xUnit a Assert.Throws et JUnit assertThrows.
Un fichier de test qui importe au contraire le module est un test externe, et ne voit que ce que deps rend pub. tests/ga_test.v exécute le module sur les vrais fichiers de ga :
// Un test externe : il importe le module, et ne voit que son API publiqueimport 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 est le dossier du v.mod le plus proche, connu à la compilation : le test trouve ses données d’où qu’on le lance.
v test . trouve tous les fichiers _test.v sous le dossier, compile chacun comme un programme séparé, et les exécute en parallèle :
---- 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: est le temps de compilation et R: le temps d’exécution de chaque fichier : 0,6 s de compilation pour 0,08 s de tests. Le résumé compte des fichiers, pas des tests. L’ordre des fichiers, les temps et les chemins changent d’une exécution à l’autre et d’un OS à l’autre, donc check.sh trie les lignes OK et ne garde que le chemin à l’intérieur du projet avant de comparer.
Une différence de plus en CI : quand la variable d’environnement CI ou GITHUB_JOB est définie, v test masque les lignes OK, et n’affiche que les échecs et le résumé (common.v, lignes 37-43). La première exécution en CI de cette leçon a échoué à cause de ça ; VTEST_HIDE_OK=0 les fait réapparaître.
Quand un test échoue
Section intitulée « Quand un test échoue »l08-failing/failing_test.v échoue de quatre façons :
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, depuis son dossier, exécute un seul fichier :
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_endLe code de sortie est 1. Ce que montre le rapport :
assertaffiche l’expression et la valeur de chaque côté, commeAssert.Equalaffiche Expected et Actual. Lelenentre parenthèses est la longueur du texte affiché, pas du tableau :[9, 5]fait 6 caractères.- Un
assertqui échoue termine sa fonction de test, comme unAssertqui échoue dans xUnit ou JUnit :not printedn’est pas affiché. Les autres tests s’exécutent quand même. - Le message après la virgule est le seul moyen de savoir quelle itération d’une boucle a échoué. V n’a ni
[Theory]ni@ParameterizedTest: une boucle sur des cas avec un message fait ce travail. - Une erreur propagée avec
!fait échouer le test, avec la ligne de l’appel. @[assert_continues]signale chaque échec et continue, comme leassertAllde JUnit.-assert continuesen ligne de commande le fait pour toutes les fonctions.
-stats liste chaque fonction 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 retire les temps, qui changent à chaque exécution.) Deux choses ne collent pas. test_continues est OK, avec 1 assert, alors que deux de ses trois asserts ont échoué : le fichier échoue quand même, mais cette ligne le cache. Et le résumé compte des asserts, pas des tests : test_message compte pour 1 réussi et 1 échoué.
assert fonctionne aussi hors des tests, dans n’importe quelle fonction : un échec y est un panic, V panic: Assertion failed.... Avec -prod, les asserts sont supprimés, comme le dit la documentation : sur ma machine, v -prod run d’un programme dont le assert 1 + 1 == 3 échoue sans -prod affiche la ligne suivante et sort avec 0. Debug.Assert en C# disparaît de la même façon d’un build Release.
v fmt est le formateur, l’équivalent de dotnet format ; Java n’en a pas dans le JDK. Il n’a aucune option de style : tabulations, espacement et retours à la ligne sont ceux de V. vet/v08_vet.v est mal formaté exprès :
module main
// Compte les références d'un projetpub fn count(refs []string) int { return refs.len}
fn main() { println(count( ['GA.Core','GA.Domain.Core'] ))}v fmt -verify vérifie sans écrire, pour la CI, comme dotnet format --verify-no-changes :
v08_vet.v is not vfmt'edEncountered a total of: 1 formatting errors.Le code de sortie est 1, et le vrai message commence par le chemin absolu du fichier. v fmt v08_vet.v affiche le fichier formaté, et v fmt -w le réécrit :
module main
// Compte les références d'un projetpub fn count(refs []string) int { return refs.len}
fn main() { println(count(['GA.Core', 'GA.Domain.Core']))}Le check.sh du cours exécute v fmt -verify sur tous ses dossiers sauf les snippets rejetés, que v fmt vérifie au niveau des types et refuse (journal).
v vet signale le code suspect, un petit équivalent des analyseurs .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 automaticallyLe code de sortie est 1, à cause de l’erreur ; -W transforme aussi les avertissements en erreurs. L’avertissement concerne le commentaire : V attend que la documentation d’une fonction publique commence par son nom, // count renvoie …, comme le fait Go. v fmt -w corrige l’indentation, pas le commentaire.
v doc construit la documentation d’un module à partir des commentaires placés au-dessus de ses déclarations publiques, comme javadoc ou les commentaires de documentation XML de C#, sans balises. version.v et graph.v suivent la règle de v vet. Depuis l08-testing, v doc -comments deps affiche :
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.Les fonctions privées, comme parse_release, sont omises, mais le champ privé refs de Graph est affiché. -f html ou -f md écrivent d’autres formats.
À retenir
Section intitulée « À retenir »- Un test est une fonction
test_dans un fichier_test.v;module xle rend interne,import xexterne.v test .compile chaque fichier comme un programme séparé et les exécute en parallèle. assertaffiche les deux côtés d’une comparaison qui échoue, plus un message facultatif ; il termine le test sauf avec@[assert_continues], et disparaît avec-prod.- Les fonctions de test peuvent renvoyer
!; une erreur attendue se vérifie avecor. -statset les résumés comptent des asserts, et rapportent commeOKun test dont les échecs ont continué : lis les lignes d’échec, et le code de sortie.- En CI,
v testmasque les fichiers qui réussissent, sauf avecVTEST_HIDE_OK=0. v fmt -verify,v vetetv docsont livrés avec le compilateur ;v vetveut des commentaires de documentation qui commencent par le nom.
Exercices
Section intitulée « Exercices »Graph.reachablene doit pas boucler sur un cycle de références. Écris un test pourA → B → A: que doit renvoyerreachable('A')?
Solution
l08-testing/deps/s08_cycle_test.v :
// Leçon 8, exercice 1 : un cycle de référencesmodule 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 n’affiche rien et sort avec 0. L’ensemble seen arrête le parcours. A fait partie de son propre résultat, parce que B le référence : reachable renvoie tous les projets atteints depuis A, et A en fait partie. Savoir si c’est juste est une décision que le test enregistre désormais. v test . trouve le fichier avec les autres : le résumé plus haut compte 4 fichiers.
- Dans
failing_test.v,test_messages’arrête à9.5et ne vérifie jamais1.0.0-beta.1, qui a 4 parties une fois découpé sur les points. Comment voir les deux échecs sans modifier la boucle ?
Solution
Mets @[assert_continues] au-dessus de fn test_message(), ou exécute v -assert continues failing_test.v pour toutes les fonctions du fichier. Le rapport a alors un bloc pour 9.5, avec Left value (len: 1): `2`, et un pour 1.0.0-beta.1, avec `4` : '1.0.0-beta.1'.split('.') donne ['1', '0', '0-beta', '1']. Avec @[assert_continues], -stats rapporte alors test_message comme OK, comme test_continues.
- Corrige
vet/v08_vet.vpour quev vetne signale rien.
Solution
v fmt -w v08_vet.v corrige l’indentation. L’avertissement demande un commentaire qui commence par le nom de la fonction :
// count renvoie le nombre de références d'un projet.pub fn count(refs []string) int { return refs.len}Les fichiers de l08-testing suivent cette règle : v vet . n’y signale rien.
- Documentation V — Testing : Asserts, Asserts that do not abort your program, Test files, Running tests ; Tools — v fmt ; Writing documentation ; la documentation de la 0.5.2 :
doc/docs.md v help test,v help vet,v help docetv help fmt, affichés par V 0.5.2- xUnit,
dotnet test,dotnet format, Code analysis in .NET - JUnit 5 User Guide