(Acest articol a fost publicat pentru prima dată pe Jakub :: Sobolewskiși a contribuit cu drag la R-Bloggers). (Puteți raporta problema despre conținutul de pe această pagină aici)
Doriți să vă împărtășiți conținutul pe R-Bloggers? Faceți clic aici dacă aveți un blog sau aici dacă nu.
Funcțiile de testare care se bazează pe dependențe externe este greu.
Testele dvs. devin lente, fragile și nesigure atunci când depind de API -uri externe, sisteme de fișiere sau servicii. Mai rău încă, unele dependențe le place Sys.time() valorile de returnare care se schimbă constant, ceea ce face ca testarea constantă să fie aproape imposibilă.
Soluția este simplă: înfășurați dependențele externe în propriile funcții și înțepeniți -le cu testthat::local_mocked_bindings.
De ce să înfășurați dependențe externe?
Dependențele externe fac testarea dureroasă în trei moduri:
- În primul rând, ele pot fi imprevizibile. API -urile coboară. Schimbarea sistemelor de fișiere. Rețeaua solicită expirare.
- În al doilea rând, pot fi incontrolabile. Nu poți forța
Sys.time()Pentru a returna o valoare specifică sau a face previzibil un răspuns API aleatoriu. - În al treilea rând, pot fi lente. Întrebări reale de baze de date și solicitări HTTP adaugă secunde la apartamentele de testare care ar trebui să funcționeze în milisecunde.
Înfășurarea dependențelor rezolvă toate cele trei probleme simultan.
Modelul: înveliți, ciot, test
Iată cum să construiți funcții testabile cu dependențe externe:
Pasul 1: Înveliți apeluri externe
În loc să sune Sys.time() direct, creați o funcție de înveliș:
get_current_time <- function() {
Sys.time()
}
calculate_elapsed_time <- function(start_time) {
current <- get_current_time()
difftime(current, start_time, units = "secs")
}
Pasul 2: Testați cu cioturi
Utilizare local_mocked_bindings Pentru a înlocui învelișul cu un ciot previzibil:
test_that("calculate_elapsed_time returns time difference", {
# Arrange
local_mocked_bindings(
get_current_time = function() as.POSIXct("2023-01-01 12:30:00")
)
start_time <- as.POSIXct("2023-01-01 12:00:00")
# Act
result <- calculate_elapsed_time(start_time)
# Assert
expect_equal(as.numeric(result), 1800) # 30 minutes = 1800 seconds
})
Uită -te cât de curat este acel test. Fără configurare. Fără rupere. Nu există probleme de sincronizare.
De ce local_mocked_bindings Încurajează un design bun
Multă vreme am folosit doar mockery pentru că se înnebunește, dar local_mocked_bindings începe să crească asupra mea.
Insight -ul cheie: obțineți la maxim local_mocked_bindings Când nu utilizați .package argument.
Pariez că acesta a fost un design intenționat al acestei interfețe: obțineți cel mai curat cod de testare, atunci când nu utilizați argumente suplimentare. Fără .package Puteți doar funcții care sunt definite în spațiul de nume curent. Acest lucru vă obligă să aplicați principii bune de proiectare: înfășurați dependențe externe în propriile funcții.
Încearcă să -ți bâlbâie Sys.time direct și veți avea nevoie:
# Messy - requires .package argument
local_mocked_bindings(
Sys.time = function() as.POSIXct("2023-01-01 12:30:00"),
.package = "base"
)
Dar înfășurați -l mai întâi:
# Clean - no .package needed
local_mocked_bindings(
get_current_time = function() as.POSIXct("2023-01-01 12:30:00")
)
Funcția vă învață un design mai bun (dacă acordați atenție).
Trei beneficii ale ambalajului
Înfășurarea dependențelor externe vă oferă trei capacități puternice:
- STUPTING PENTRU TESTARE: Înlocuiți apelurile externe imprevizibile cu dubla test controlată folosind
local_mocked_bindings. - Injecție de dependență: Deschide uși pentru injectarea diferitelor implementări pentru diferite medii. Poate că producția folosește
Sys.time()Dar mediul dvs. de stadializare citește de la un server de timp batjocoritor. Apoi, în teste, putem folosi injecția de dependență a unui fals sau încă folosim un ciot culocal_mocked_bindings. - Migrație ușoară: Schimbați implementările fără a atinge codul de apel. Astăzi ați citit timp de la ceasul sistemului, mâine de la o cameră de ceas solar, săptămâna viitoare de la o API atomică.
Sys.time() Problemă
Sys.time() exemplifică de ce ambalarea problemelor.
Spre deosebire de numerele aleatorii (controlate de set.seed()), timpul se schimbă întotdeauna. Fiecare test de test primește valori diferite. Nu puteți face funcții dependente de timp deterministe fără a înlocui sursa de timp.
Luați în considerare o funcție care calculează programul de lucru:
is_business_hour <- function() {
current_hour <- hour(Sys.time())
current_hour >= 9 && current_hour <= 17
}
Cum testezi asta? Nu puteți controla când testele dvs. se execută.
Înfășurați dependența de timp:
get_current_time <- function() {
Sys.time()
}
is_business_hour <- function() {
current_hour <- hour(get_current_time())
current_hour >= 9 && current_hour <= 17
}
Acum testarea devine banală:
test_that("is_business_hour returns TRUE during business hours", {
# Arrange
local_mocked_bindings(
get_current_time = function() as.POSIXct("2023-01-01 14:00:00") # 2 PM
)
# Act
result <- is_business_hour()
# Act
expect_true(result)
})
test_that("is_business_hour returns FALSE outside business hours", {
# Arrange
local_mocked_bindings(
get_current_time = function() as.POSIXct("2023-01-01 22:00:00") # 10 PM
)
# Act
result <- is_business_hour()
# Assert
expect_false(result)
})
Control perfect. Fiabilitate perfectă.
Exemplu din lumea reală
Iată cum funcționează modelul cu dependențe mai complexe:
# Wrapper functions for external dependencies
get_system_info <- function() {
Sys.info()
}
get_package_versions <- function(path) {
if (!rlang::is_installed("yaml")) {
stop("Packages "yaml" not installed", call. = FALSE)
}
if (!rlang::is_installed("here")) {
stop("Package "here" not installed", call. = FALSE)
}
yaml::read_yaml(here::here(path, "renv.lock"))
}
get_test_results <- function(...) {
testthat::test_local(..., stop_on_failure = FALSE)
}
# Function that uses wrapped dependencies
generate_system_report <- function(project_path = ".") {
system_info <- get_system_info()
packages <- get_package_versions(project_path)
tests <- get_test_results(project_path)
list(
os = system_info(("sysname")),
r_version = system_info(("version")),
package_count = length(packages$Packages),
test_status = all(tests$passed)
)
}
Testarea devine simplă:
test_that("generate_system_report creates complete report", {
# Arrange
local_mocked_bindings(
get_system_info = function() c(sysname = "Linux", version = "4.0.0"),
get_package_versions = function(path) list(Packages = list(a = 1, b = 2)),
get_test_results = function(...) data.frame(passed = c(TRUE, TRUE))
)
# Act
report <- generate_system_report()
# Assert
expect_equal(report$os, "Linux")
expect_equal(report$package_count, 2)
expect_true(report$test_status)
})
Trei dependențe externe controlate cu trei cioturi simple. Fără acces real al sistemului de fișiere. Fără execuție reală a testului. Fără introspecție a sistemului.
Interfață peste implementare
Modelul de înveliș creează o interfață între codul dvs. și dependențele externe.
Interfețele sunt puternice pentru că separă „ce” de „cum”. Codul dvs. știe ce are nevoie (timpul curent, informațiile despre sistem, rezultatele testelor), dar nu le pasă cum sunt îndeplinite aceste nevoi.
În producție, get_current_time() apeluri Sys.time(). În teste, acesta returnează un timestamp fix. Într -un mediu specializat, s -ar putea citi de pe un server de protocol de timp de rețea sau chiar de acea cameră de ceas solar.
Modificați implementarea fără a schimba o singură linie de cod de apelare.
Testele curate câștigă
Comparați aceste două abordări:
Fără înfășurare:
# Brittle, slow, unpredictable
test_that("time calculation works", {
start <- Sys.time()
Sys.sleep(0.1)
result <- calculate_duration(start)
expect_gt(result, 0.1) # Flaky assertion
})
Cu ambalare:
# Reliable, fast, predictable
test_that("calculate_duration returns time difference between start and current time", {
# Arrange
local_mocked_bindings(
get_current_time = function() as.POSIXct("2023-01-01 12:30:00")
)
start <- as.POSIXct("2023-01-01 12:00:00")
# Act
result <- calculate_duration(start)
# Assert
expect_equal(result, 1800)
})
Al doilea test rulează în microsecunde, nu eșuează niciodată la întâmplare și își exprimă clar intenția.
Începeți să înfășurați astăzi
Data viitoare când scrieți o funcție care atinge lumea exterioară, înfășurați apelul extern. Viitorul tău testare vă va mulțumi.
Modelul este simplu: înfășurați dependențe externe, ciot în teste, bucurați -vă de apartamente de testare curate și fiabile care rulează rapid și trec în mod constant.
