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

Vous pourriez les qualifier de « moqueurs ».
Moquez-vous de la base de données. Moquez-vous de l’API. Moquez-vous de la fonction. Le mot devient un fourre-tout pour tout double de test, tout objet que vous remplacez par une dépendance réelle dans un test. Les regrouper rend plus difficile le choix du bon outil, et un mauvais choix conduit à des tests fragiles et trompeurs.
Il existe cinq types distincts, chacun ayant un travail spécifique. Savoir ce qui est quoi, c’est ainsi que vous arrêtez d’écrire des tests qui font la mauvaise chose.
Le code en test
Les cinq exemples utilisent une seule fonction : process_payment. Il débite une carte, enregistre la tentative et informe éventuellement le client.
process_payment <- function(order, payment_gateway, logger, notifier = NULL) {
logger$log(paste("Processing order", order$id))
result <- payment_gateway$charge(order$amount, order$card_token)
if (!result$success) stop("Payment failed: ", result$error)
if (!is.null(notifier)) {
notifier$send(order$customer_id, result$transaction_id)
}
result$transaction_id
}
Il a trois dépendances : payment_gateway, loggeret notifier. Chacun sera remplacé par un type de double différent en fonction de ce que nous essayons de tester.
1. Factice
💡 Définition : un objet transmis pour satisfaire un paramètre requis mais jamais réellement utilisé par le test.
process_payment appelle toujours logger$log. L’enregistreur est requis. Mais pour un test qui vérifie uniquement si l’ID de transaction correct est renvoyé, peu nous importe ce qui est enregistré. Nous avons juste besoin de quelque chose qui n’explosera pas lorsqu’on l’appellera.
test_that("returns the transaction ID on successful payment", {
# Arrange
order <- list(
id = "ord-1",
amount = 100,
card_token = "tok_visa",
customer_id = "cust-42"
)
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(
charge = function(amount, token) {
list(success = TRUE, transaction_id = "txn-abc")
}
)
# Act
result <- process_payment(
order,
payment_gateway = stub_gateway,
logger = dummy_logger
)
# Assert
expect_equal(result, "txn-abc")
})
Test passed with 1 success 🥇.
dummy_logger accepte n’importe quel appel et ne fait rien. Le test ne l’affirme pas du tout. Son seul travail est de satisfaire la signature de la fonction.
Un mannequin devrait être la chose la plus simple à compiler. Enregistrer des appels ou définir des attentes en ferait autre chose. Si vous vous retrouvez à écrire un mannequin qui plante ou fait quelque chose d’inattendu lorsqu’il est appelé, le chemin de code que vous testez utilise en fait la dépendance.
Ça vaut le coup de le savoir.
2. Talon
💡 Définition : un remplacement qui renvoie des réponses préprogrammées, utilisées pour contrôler ce que reçoit le code testé.
Un stub vous permet de mettre le système dans un état spécifique sans impliquer une véritable infrastructure. Si vous voulez tester quoi process_payment fait lorsqu’une carte est refusée, vous n’avez pas besoin d’une véritable API de paiement. Vous renvoyez simplement la réponse souhaitée.
test_that("throws an error when payment is declined", {
# Arrange
order <- list(
id = "ord-2",
amount = 200,
card_token = "tok_declined",
customer_id = "cust-7"
)
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(
charge = function(amount, token) {
list(success = FALSE, error = "insufficient funds")
}
)
# Act & Assert
expect_error(
process_payment(
order,
payment_gateway = stub_gateway,
logger = dummy_logger
),
"insufficient funds"
)
})
Test passed with 1 success 🥇.
Le stub fournit des entrées à le système testé. Vous affirmez ce que le code a fait avec ces entrées (dans ce cas, qu’il a généré la bonne erreur).
Notez que process_payment accepte payment_gateway comme argument. C’est de l’injection de dépendances : la fonction ne crée ni n’importe sa propre passerelle, le test peut donc réussir n’importe quoi avec la même interface. Sans cela, vous auriez besoin d’une bibliothèque de correctifs pour intercepter la véritable dépendance en cours d’appel. Avec lui, une simple liste avec un charge la fonction est suffisante. Les stubs fonctionnent mieux lorsque le code est conçu de cette façon : les dépendances sont acceptées comme arguments, et non câblées à l’intérieur.
Si vous pratiquez le développement axé sur les tests, vous remarquerez que vous utilisez ce modèle tout le temps. Vous ne pouvez pas passer le test sans cela. Vous ne savez pas quoi patcher dans une fonction qui n’existe pas encore ! Il est tout à fait naturel d’injecter toutes les dépendances lorsque vous écrivez l’interface de votre code.
Quand la dépendance n’est-ce pas déclaré dans l’interface, lorsque la fonction appelle une autre fonction directement par son nom, mockery::stub() peut le patcher le temps d’un test :
# A function that calls charge_card() internally, with no way to inject it
process_payment_legacy <- function(order) {
result <- charge_card(order$amount, order$card_token)
if (!result$success) {
stop("Payment failed: ", result$error)
}
result$transaction_id
}
charge_card <- function(amount, token) {
stop("would call real payment API")
}
test_that("returns transaction ID when charge succeeds", {
# Arrange
order <- list(amount = 100, card_token = "tok_visa")
mockery::stub(
process_payment_legacy,
"charge_card",
function(amount, token) {
list(success = TRUE, transaction_id = "txn-stub")
}
)
# Act
result <- process_payment_legacy(order)
# Assert
expect_equal(result, "txn-stub")
})
Test passed with 1 success 🥳.
mockery::stub() remplace charge_card dans le cadre de process_payment_legacy pour cet appel test, sans toucher à la vraie fonction ailleurs.
mockery::stub() a un piège. Le stub est ciblé par le nom de la fonction sous forme de chaîne, donc si vous renommez charge_cardle stub cesse de fonctionner silencieusement et le test réussit avec la fonction réelle sans avertissement. Le test est également couplé à un détail d’implémentation : si vous refactorisez process_payment_legacy appeler payment_gateway$charge() au lieu de cela, le stub se brise même si le comportement reste inchangé. C’est l’odeur de surspécification.
Utiliser mockery::stub() lorsque vous travaillez avec du code existant qui n’a pas été conçu dans un souci de testabilité et que vous ne pouvez pas refactoriser l’interface pour le moment. Il vous permet de mettre en place des tests rapidement. Traitez-le comme un tremplin : une fois que les tests de caractérisation sont verts, refactorisez vers l’injection de dépendances et remplacez le patch par un simple stub passé en argument.
Pour résumer : lorsque vous avez besoin de contrôler ce qu’une dépendance renvoie et que vous ne vous souciez pas de la façon dont elle a été appelée, recherchez un stub.
3. Espion
💡 Définition : un stub qui enregistre également les appels qui lui sont adressés, afin que vous puissiez les affirmer par la suite.
Parfois, le comportement que vous testez est un effet secondaire. Une notification qui aurait dû être envoyée, un message qui aurait dû être enregistré. Le code ne renvoie pas de valeur sur laquelle vous pouvez affirmer. Cela appelle quelque chose. Un espion capte ces appels.
make_notifier_spy <- function() {
calls <- list()
list(
send = function(customer_id, transaction_id) {
calls[[length(calls) + 1]] <<- list(
customer_id = customer_id,
transaction_id = transaction_id
)
},
calls = function() calls
)
}
test_that("notifies the customer after successful payment", {
# Arrange
order <- list(
id = "ord-3",
amount = 50,
card_token = "tok_visa",
customer_id = "cust-99"
)
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(
charge = function(amount, token) {
list(success = TRUE, transaction_id = "txn-xyz")
}
)
spy_notifier <- make_notifier_spy()
# Act
process_payment(
order,
payment_gateway = stub_gateway,
logger = dummy_logger,
notifier = spy_notifier
)
# Assert
expect_length(spy_notifier$calls(), 1)
expect_equal(spy_notifier$calls()[[1]]$customer_id, "cust-99")
expect_equal(spy_notifier$calls()[[1]]$transaction_id, "txn-xyz")
})
Test passed with 3 successes 🌈.
L’espion est un bout de mémoire. Vous appelez le code, puis interrogez l’espion pour voir ce qui s’est passé.
Vous n’avez pas toujours besoin de construire un espion à la main. mockery::mock() collecte également les appels, il peut donc servir d’espion lorsque vous souhaitez le comportement d’enregistrement sans écrire vous-même la fermeture :
test_that("notifies the customer after successful payment (mockery spy)", {
# Arrange
order <- list(
id = "ord-3b",
amount = 50,
card_token = "tok_visa",
customer_id = "cust-99"
)
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(
charge = function(amount, token) {
list(success = TRUE, transaction_id = "txn-xyz")
}
)
spy_send <- mockery::mock()
# Act
process_payment(
order,
payment_gateway = stub_gateway,
logger = dummy_logger,
notifier = list(send = spy_send)
)
# Assert
mockery::expect_called(spy_send, 1)
expect_equal(mockery::mock_args(spy_send)[[1]][[1]], "cust-99")
expect_equal(mockery::mock_args(spy_send)[[1]][[2]], "txn-xyz")
})
Test passed with 3 successes 😀.
La version manuscrite est plus claire lorsque l’on souhaite que le mécanisme d’enregistrement soit visible aux lecteurs, utile dans une base de code où tout le monde ne connaît pas mockery. mockery::mock() est plus concis une fois que l’équipe est familiarisée avec la bibliothèque.
La différence avec une simulation se résume aux valeurs de retour. Un espion enregistre les appels et rien d’autre. Une simulation enregistre les appels et peut également renvoyer des valeurs préprogrammées, ce qui la rend utile lorsque vous avez besoin que la dépendance se comporte d’une manière spécifique et que vous souhaitez affirmer comment elle a été utilisée.
4. Se moquer
💡 Définition : “un double préprogrammé avec des attentes qui forment une spécification des appels qu’il doit recevoir. Un vrai simulacre peut lancer s’il reçoit un appel auquel il ne s’attend pas, et est vérifié lors de la vérification pour confirmer qu’il a reçu tous les appels qu’il attendait.”[1, Fowler]
mockery::mock() est plus vague que cette définition. Il accepte n’importe quel appel sans se plaindre et n’impose pas les attentes dès le départ. Il enregistre chaque appel qu’il reçoit (les arguments, l’ordre, le décompte) et renvoie les valeurs préprogrammées que vous fournissez. La vérification relève de votre responsabilité lors de l’étape d’affirmation.
test_that("sends exactly one notification with correct arguments", {
# Arrange
order <- list(
id = "ord-4",
amount = 75,
card_token = "tok_visa",
customer_id = "cust-11"
)
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(
charge = function(amount, token) {
list(success = TRUE, transaction_id = "txn-def")
}
)
mock_notifier <- list(send = mockery::mock())
# Act
process_payment(
order,
payment_gateway = stub_gateway,
logger = dummy_logger,
notifier = mock_notifier
)
# Assert
mockery::expect_called(mock_notifier$send, 1)
mockery::expect_args(mock_notifier$send, 1, "cust-11", "txn-def")
})
Test passed with 5 successes 🥳.
Utilisez une simulation lorsque le l’interaction elle-même c’est ce que vous testez : si le code a appelé la dépendance de la bonne manière, avec les bons arguments.
C’est également le double le plus facile à abuser. Affirmez chaque appel à chaque dépendance et vous avez écrit un test surspécifié, qui s’interrompt chaque fois que l’implémentation change, même lorsque le comportement reste le même.
Préférez un espion lorsque vous n’avez besoin que d’enregistrer des appels. Une simple liste avec une fonction qui s’ajoute à un vecteur suffit souvent. Recherchez une simulation lorsque vous devez également contrôler ce que renvoie la dépendance. Le risque est le même avec toute assertion basée sur l’interaction : vérifiez chaque appel à chaque dépendance et vous vous retrouvez avec un test qui reflète l’implémentation plutôt que le comportement, s’interrompant chaque fois que les éléments internes changent, même si le résultat ne change pas.
5. Faux
💡 Définition : une implémentation fonctionnelle plus simple que la réalité, adaptée aux tests mais pas à la production.
Un faux n’est pas seulement une réponse préprogrammée. Il a un vrai comportement. Une base de données en mémoire est une fausse : elle stocke et récupère des données comme les vraies, sans persistance ni surcharge du réseau. Il se comporte correctement lors de plusieurs appels, ce qu’un stub ne peut pas faire.
make_fake_payment_gateway <- function() {
transactions <- list()
list(
charge = function(amount, token) {
if (amount <= 0) {
return(list(success = FALSE, error = "invalid amount"))
}
if (token == "tok_declined") {
return(list(success = FALSE, error = "card declined"))
}
id <- paste0("txn-", length(transactions) + 1)
transactions[[id]] <<- list(
amount = amount,
token = token
)
list(success = TRUE, transaction_id = id)
},
find = function(transaction_id) {
transactions[[transaction_id]]
}
)
}
test_that("successful charges are recorded in the gateway", {
# Arrange
order <- list(
id = "ord-5",
amount = 120,
card_token = "tok_visa",
customer_id = "cust-3"
)
dummy_logger <- list(log = function(...) invisible(NULL))
fake_gateway <- make_fake_payment_gateway()
# Act
txn_id <- process_payment(
order,
payment_gateway = fake_gateway,
logger = dummy_logger
)
# Assert
recorded <- fake_gateway$find(txn_id)
expect_equal(recorded$amount, 120)
expect_equal(recorded$token, "tok_visa")
})
Test passed with 2 successes 🎊.
Les contrefaçons fonctionnent bien lorsque vous devez tester le comportement sur plusieurs opérations : passer une commande, interroger son statut, la rembourser. Un stub devrait être reprogrammé pour chaque appel. Un faux s’en occupe.
Ils conviennent également parfaitement aux tests d’acceptation et à l’inspection manuelle. Un test d’acceptation exerce un comportement complet face à l’utilisateur de bout en bout, plusieurs couches de l’application travaillant ensemble. À ce niveau, vous ne voulez pas que les talons soient reprogrammés pour des appels individuels ; vous voulez une dépendance qui se comporte de manière réaliste tout au long du flux. Une fausse passerelle de paiement, un faux expéditeur d’e-mails, un faux magasin de fichiers : ceux-ci permettent à votre suite de tests d’acceptation de s’exécuter dans CI sans vous connecter à des services externes, sans avoir besoin d’informations d’identification ou sans laisser d’effets secondaires. Vous pouvez également connecter les mêmes contrefaçons dans un mode de développement de l’application. Lancez l’application Shiny en pointant vers la passerelle en mémoire et vous pourrez cliquer sur chaque scénario de paiement sans toucher à une véritable API.
Le coût est que les contrefaçons prennent du temps à construire et à entretenir. Ils doivent rester synchronisés avec la véritable interface qu’ils remplacent. Pour une interface petite et stable qui est largement utilisée dans votre suite de tests et dans les flux de travail manuels, l’investissement est payant. Pour une dépendance que vous n’utilisez que dans un seul test unitaire, un stub est plus simple.
Quand atteindre chacun
| Double | A un comportement | Enregistre les appels | Renvoie les valeurs programmées | Rejette les appels inattendus | Quand utiliser |
|---|---|---|---|---|---|
| Factice | ❌ | ❌ | ❌ | ❌ | Remplissez un paramètre obligatoire auquel vous ne toucherez pas |
| Bout | Préprogrammé uniquement | ❌ | ✅ | ❌ | Contrôler ce que le code reçoit |
| Espionner | Préprogrammé uniquement | ✅ | ❌ | ❌ | Affirmer les effets secondaires après coup |
Se moquer (mockery) |
Préprogrammé uniquement | ✅ | ✅ | ❌ | Affirmez les appels et contrôlez ce que le code reçoit |
| Se moquer | Préprogrammé uniquement | ✅ | ✅ | ✅ | Épinglez une interaction exacte sous forme de contrat ferme |
| Faux | ✅ | ❌ | ❌ | ❌ | Remplacer les dépendances avec état ou multi-appels |
La principale différence réside entre le stub et le mock. Un stub renvoie des valeurs. Vous vous affirmez sur le résultat. Une simulation enregistre les appels et peut revenir préprogrammée valeurs. L’utilisation d’une simulation là où un stub ferait l’affaire associe votre test aux détails d’implémentation. Utiliser un stub lorsqu’une simulation est nécessaire signifie manquer l’interaction que vous essayiez de vérifier.
En cas de doute : si vous faites une déclaration sur une valeur de retour ou un changement d’état, utilisez un stub. Si vous affirmez qu’un appel spécifique a été passé, utilisez un espion ou une simulation. Si la dépendance a un état réel qui doit survivre aux appels, créez un faux.
Annexe : implémenter une simulation impatiente à la main
mockery::mock() est suffisant pour un usage quotidien. Ignorez ceci si vous n’êtes pas curieux de connaître les simulations qui génèrent des échecs lors de l’exécution du code testé.
Voici à quoi ressemble une simulation correspondant à la définition de Fowler dans R. Elle prend une liste d’appels attendus à l’étape Arrange, échoue immédiatement en cas d’inattendu et expose un verify() fonction pour confirmer que chaque appel attendu a été effectué.
make_mock_notifier <- function(expected_calls) {
received <- list()
list(
send = function(customer_id, transaction_id) {
call <- list(
customer_id = customer_id,
transaction_id = transaction_id
)
match <- any(sapply(expected_calls, identical, call))
if (!match) {
testthat::fail(sprintf(
"Unexpected call: send('%s', '%s')",
customer_id,
transaction_id
))
}
received[[length(received) + 1]] <<- call
},
verify = function() {
for (exp in expected_calls) {
found <- any(sapply(received, identical, exp))
if (!found) {
testthat::fail(sprintf(
"Expected call never made: send('%s', '%s')",
exp$customer_id, exp$transaction_id
))
}
}
testthat::succeed()
}
)
}
Le moqueur rejette sur-le-champ les appels inattendus :
test_that("throws immediately when called with unexpected arguments", {
# Arrange
order <- list(
id = "ord-4b",
amount = 75,
card_token = "tok_visa",
customer_id = "cust-11"
)
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(
charge = function(amount, token) {
list(success = TRUE, transaction_id = "txn-def")
}
)
mock_notifier <- make_mock_notifier(
expected_calls = list(list(
customer_id = "cust-WRONG",
transaction_id = "txn-def"
))
)
# Act — throws before we even reach Assert
process_payment(
order,
payment_gateway = stub_gateway,
logger = dummy_logger,
notifier = mock_notifier
)
})
── Failure: throws immediately when called with unexpected arguments ───────────
Unexpected call: send('cust-11', 'txn-def')
Backtrace:
▆
1. └─global process_payment(...)
2. └─notifier$send(order$customer_id, result$transaction_id)
Error:
! Test failed with 1 failure and 0 successes.
Et verify() intercepte les appels attendus qui n’ont jamais été émis :
test_that("fails verification when an expected call was never made", {
# Arrange
order <- list(id = "ord-4c", amount = 75, card_token = "tok_visa", customer_id = "cust-11")
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(charge = function(amount, token) list(success = TRUE, transaction_id = "txn-def"))
mock_notifier <- make_mock_notifier(
expected_calls = list(
list(customer_id = "cust-11", transaction_id = "txn-def"),
list(customer_id = "cust-99", transaction_id = "txn-xyz") # will never be called
)
)
# Act
process_payment(order, payment_gateway = stub_gateway, logger = dummy_logger, notifier = mock_notifier)
# Assert
mock_notifier$verify()
})
── Failure: fails verification when an expected call was never made ────────────
Expected call never made: send('cust-99', 'txn-xyz')
Error:
! Test failed with 1 failure and 1 success.
Le chemin heureux passe les deux contrôles :
test_that("passes when all expected calls are made and no unexpected ones occur", {
# Arrange
order <- list(
id = "ord-4d",
amount = 75,
card_token = "tok_visa",
customer_id = "cust-11"
)
dummy_logger <- list(log = function(...) invisible(NULL))
stub_gateway <- list(
charge = function(amount, token) {
list(success = TRUE, transaction_id = "txn-def")
}
)
mock_notifier <- make_mock_notifier(
expected_calls = list(list(
customer_id = "cust-11",
transaction_id = "txn-def"
))
)
# Act
process_payment(
order,
payment_gateway = stub_gateway,
logger = dummy_logger,
notifier = mock_notifier
)
# Assert
mock_notifier$verify()
})
Test passed with 1 success 🥇.
Références
- Martin Fowler — TestDouble
- Gérard Meszaros — Tester les modèles doubles
En rapport
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.