Categories Uncategorized

muttest 0.2.0 : plus de mutateurs, de meilleurs rapports et une exécution parallèle

[This article was first published on jakub::sobolewski, and kindly contributed to R-bloggers]. (Vous pouvez signaler un problème concernant le contenu de cette page ici)


Vous souhaitez partager votre contenu sur R-bloggers ? cliquez ici si vous avez un blog, ou ici si vous n’en avez pas.

Vos tests réussissent. La couverture est élevée. Tout semble bien – jusqu’à ce que quelqu’un trouve un bug en production que vous n’avez pas détecté – tout cela à cause d’une mauvaise assertion.

La couverture du code vous indique quelles lignes ont été exécutées. Cela ne dit rien quant à savoir si ces lignes sont réellement testées. Vous pouvez supprimer chaque assertion de votre suite de tests, exécuter covret je vois toujours 100 %. La couverture est une mesure d’exécution et non d’exactitude. Cet écart est exactement ce {muttest} a été conçu pour fermer – et la version 0.2.0 le rend beaucoup plus performant que la version précédente.

📝 Consultez le journal des modifications complet ici.

Qu’est-ce que le test de mutation ?

Les tests de mutation posent une question plus difficile que la couverture : si ce code était subtilement faux, vos tests le remarqueraient-ils ?

Cela fonctionne en apportant de petites modifications délibérées à votre code source – en échangeant > pour >=retournement TRUE à FALSEremplaçant && avec || – puis exécutez votre suite de tests sur chaque version modifiée. Chaque version modifiée est appelée un mutant. Si vos tests échouent, le mutant est tué: vos tests ont remarqué le changement. Si vos tests réussissent, le mutant survécu: vos tests sont aveugles à ce genre de bug.

Le résultat est un score de mutation:

Score de mutation=Mutants tuésTotal des mutants×100%\text{Score de mutation} = \frac{\text{Mutants tués}}{\text{Total des mutants}} \times 100\%

  • 0% — Vos tests réussissent quoi que fasse le code. Les assertions sont manquantes ou triviales.
  • 100% — Chaque mutation déclenche un échec de test. Vos tests sont serrés.

Contrairement à la couverture, ce score reflète qualité des assertionspas seulement l’exécution. Une suite de tests pleine de expect_true(is.numeric(x)) les contrôles atteindront une couverture de 100 % tout en manquant chaque échec significatif. Les tests de mutation le révèlent.

Pourquoi vous devriez vous en soucier

Voici l’exemple canonique. La fonction is_adult a une condition aux limites :

# R/is_adult.R
is_adult <- function(age) {
  age >= 18
}

Et ces tests donnent une couverture à 100 % :

# tests/testthat/test-is_adult.R
test_that("is_adult returns TRUE for adults", {
  expect_true(is_adult(25))
})

test_that("is_adult returns FALSE for minors", {
  expect_false(is_adult(10))
})

Les deux tests réussissent. Les deux réussiraient quand même si >= ont été accidentellement remplacés par >. La valeur limite 18 n’est jamais testé, donc aucun des mutants n’est tué :

#' R/is_adult.R — mutant 1: ">=" → ">"
is_adult <- function(age) {
  age > 18
}

Imaginez que ce bug arrive en production. Un utilisateur de 18 ans tente de s’inscrire et le système le rejette. Le bug est réel, mais vos tests ne l’ont jamais vu venir.

En cours d’exécution muttest expose cela immédiatement:

library(muttest)

plan <- muttest_plan(
  mutators = comparison_operators()
)
muttest(plan)

Le tableau de progression montre un survivant. Le correctif est un test unique :

test_that("is_adult returns TRUE at the boundary age", {
  expect_true(is_adult(18))  # kills the >= → > mutant
})

Ce mutant survivant n’est pas un problème à résoudre, c’est une spécification que vous avez oublié d’écrire.

Le problème du test LLM

De nombreux développeurs utilisent désormais les LLM pour générer des tests. De toute façon, qui aime écrire des tests lui-même ?

Les LLM sont rapides et produisent du code syntaxiquement correct, mais ils peuvent produire des cas évidents, manquer des limites ou simplement tester les propriétés du code. Le is_adult La suite de tests ci-dessus est ce qu’un modèle de langage pourrait produire : structurellement fin, sémantiquement incomplet.

Les tests de mutation vous donnent un signal objectif sur la force réelle des tests, que vous les ayez écrits vous-même ou qu’ils aient été générés par un LLM. Un faible score de mutation ne signifie pas que le LLM a fait un mauvais travail : cela signifie que vous savez maintenant exactement où renforcer les affirmations. Les tests générés par LLM nécessitent une validation externe tout autant que les tests écrits par des humains.

muttest fournit des outils pour aider à cette validation.


Quoi de neuf dans la version 0.2.0

Bibliothèque de mutations étendue

Le plus gros ajout de cette version est une liste complète de nouveaux mutateurs, organisés en mutateurs individuels et en collections prédéfinies prêtes à l’emploi.

Nouveaux mutants individuels :

  • boolean_literal("TRUE", "FALSE") — retourne les constantes booléennes : TRUE → FALSE
  • na_literal("NA", "NULL") — échange les variantes NA et NULL : NA → NULL
  • negate_condition() — enveloppements if conditions dans !(...): if (x > 0)if (!(x > 0))
  • remove_condition_negation() — bandes menant ! à partir de conditions : if (!done)if (done)
  • numeric_increment() / numeric_decrement() — décale les constantes numériques d’une unité : 5 → 6, 5 → 4
  • index_increment() / index_decrement() — décale les indices d’indice : x[i]x[i + 1L]
  • string_empty() — remplace les chaînes non vides par "": "hello" → ""
  • string_fill() — remplace les chaînes vides par "mutant": "" → "mutant"
  • call_name("any", "all") — échange les noms de fonctions : any(x) → all(x)
  • remove_negation() – supprime ! n’importe où: !is.na(x) → is.na(x)
  • replace_return_value() — remplace les valeurs de retour explicites par NULL: return(x) → return(NULL)
  • delete_statement() – supprime les affectations et les appels autonomes un par un, détectant les effets secondaires non testés et les affectations mortes

Nouvelles collections prédéfinies – passez un seul appel et obtenez l’ensemble complet des mutateurs pertinents :

  • boolean_literals()TRUE ↔ FALSE, T ↔ F
  • na_literals()NA ↔ NULL, NA ↔ NA_real_, NA ↔ NA_integer_, NA ↔ NA_character_
  • numeric_literals() — combine numeric_increment() et numeric_decrement()
  • index_mutations() — combine index_increment() et index_decrement()
  • string_literals() — combine string_empty() et string_fill()
  • condition_mutations() — combine negate_condition() et remove_condition_negation()

Les trois préréglages opérateur de la version 0.1.0 sont toujours là — arithmetic_operators(), comparison_operators(), logical_operators() – et maintenant ils ont de la compagnie.

Une configuration de départ pratique couvre la plupart de ce que vous voudriez comprendre en logique métier :

plan <- muttest_plan(
  source_files = "R/my_file.R",
  mutators = c(
    arithmetic_operators(),
    comparison_operators(),
    logical_operators(),
    condition_mutations(),
    numeric_literals(),
    list(remove_negation())
  )
)

Superposer boolean_literals(), na_literals(), string_literals()ou index_mutations() en fonction de ce que fait réellement votre code.

Les mutations sont désormais paramétrées

Les mutants individuels acceptent les arguments de configuration. operator("+", "-") et boolean_literal("TRUE", "FALSE") vous permettent de définir exactement quel jeton remplacer et par quoi – afin que vous puissiez exprimer les mutations importantes pour votre domaine sans écrire un mutateur personnalisé à partir de zéro. Le Mutator La classe de base est également désormais exportée pour les cas où vous souhaitez aller plus loin et créer un mutateur entièrement personnalisé.

Les mutants survivants sont désormais signalés

Le ProgressMutationReporter précédemment, vous aviez montré uniquement le nombre total de mutants tués et le nombre total de mutants. Dans la version 0.2.0, il signale désormais mutants survivants – ceux que vos tests ont manqués.

C’est le signal qui compte. Les survivants ne sont pas du bruit ; chacun représente une véritable lacune dans votre suite de tests. Les voir apparaître directement dans la sortie de progression rend la boucle de rétroaction plus serrée : exécutez muttestlisez les survivants, ajoutez un test, répétez.

i Mutation Testing
  |   K |   S |   E |   T |   % | Mutator  | File
v |   1 |   0 |   0 |   1 | 100 | > → <    | shipping.R
x |   1 |   1 |   0 |   2 |  50 | > → >=   | shipping.R
-- Survived Mutants -----------------------------------------------
shipping.R  > → >=
2-   if (weight_kg > 5) 15.00 else 5.00
2+   if (weight_kg >= 5) 15.00 else 5.00
-- Results --------------------------------------------------------
[ KILLED 1 | SURVIVED 1 | ERRORS 0 | TOTAL 2 | SCORE 50.0% ]

Délais d’attente et gestion améliorée des erreurs

Les tests de mutation fonctionnent en exécutant votre suite de tests une fois par mutant. Certaines mutations produisent du code qui se bloque : une boucle infinie, un appel bloquant, un calcul qui ne se termine jamais. Dans la version 0.1.0, cela bloquerait toute votre exécution.

Dans la version 0.2.0, muttest() prend en charge délais d’attente par mutant. Définissez un délai d’attente et tout mutant dont le test le dépasse est marqué comme erroné. Le reste de la course continue sans être affecté.

La gestion des erreurs en général a été améliorée. Lorsque l’exécution d’un test échoue de manière inattendue, les erreurs sont désormais capturées et signalées proprement plutôt que d’apparaître comme des conditions non gérées qui arrêtent l’exécution entière. Cela rend les tests de mutation plus robustes dans les projets réels où les environnements de test ne sont pas toujours parfaitement contrôlés.

Exécution parallèle

La version 0.1.0 exécutait les mutants de manière séquentielle. Dans les gros fichiers contenant de nombreux mutants, cela s’additionne. muttest() prend désormais en charge exécution parallèle avec {mirai} sous le capot : les mutants peuvent être exécutés simultanément sur plusieurs travailleurs, réduisant ainsi le temps d’exécution sur des référentiels plus grands.


Commencer

Installer depuis CRAN :

install.packages("muttest")

Choisissez un fichier avec une logique significative : branchement, comparaisons, arithmétique. Définir un plan :

library(muttest)

plan <- muttest_plan(
  source_files = "R/your_file.R",
  mutators = comparison_operators()
)

muttest(plan)

Lisez le résultat. Trouvez les survivants. Ajoutez les tests qu’ils impliquent. Répéter.

Commencez avec un fichier et un préréglage de mutateur. Visez une amélioration significative du score à chaque itération plutôt que de rechercher 100 % immédiatement. Un score de plus de 80 % sur la logique métier critique constitue un objectif de départ solide.

Essayez-le sur un fichier pour lequel vous pensez que les tests sont faibles. Les survivants vous diront exactement quoi ajouter.


J’aimerais avoir de vos nouvelles

{muttest} est encore frais et ses fonctionnalités et son interface peuvent changer. La nouvelle bibliothèque de mutations couvre un large éventail de modèles, mais il existe certainement des mutations spécifiques à votre domaine qui ne sont pas encore couvertes. Si vous rencontrez un cas où la bonne mutation est manquante, si un mutateur existant se comporte de manière inattendue ou si quelque chose dans le résultat est difficile à interpréter, veuillez ouvrir un ticket sur GitHub.

Les demandes de fonctionnalités sont également les bienvenues. S’il y a un type de changement de code que vous souhaitez tester et qu’il n’existe pas encore de bon moyen de l’exprimer, veuillez déposer un problème dans le référentiel.


PakarPBN

A Private Blog Network (PBN) is a collection of websites that are controlled by a single individual or organization and used primarily to build backlinks to a “money site” in order to influence its ranking in search engines such as Google. The core idea behind a PBN is based on the importance of backlinks in Google’s ranking algorithm. Since Google views backlinks as signals of authority and trust, some website owners attempt to artificially create these signals through a controlled network of sites.

In a typical PBN setup, the owner acquires expired or aged domains that already have existing authority, backlinks, and history. These domains are rebuilt with new content and hosted separately, often using different IP addresses, hosting providers, themes, and ownership details to make them appear unrelated. Within the content published on these sites, links are strategically placed that point to the main website the owner wants to rank higher. By doing this, the owner attempts to pass link equity (also known as “link juice”) from the PBN sites to the target website.

The purpose of a PBN is to give the impression that the target website is naturally earning links from multiple independent sources. If done effectively, this can temporarily improve keyword rankings, increase organic visibility, and drive more traffic from search results.

Jasa Backlink

Download Anime Batch

Leave a Reply

Your email address will not be published. Required fields are marked *