(Acest articol a fost publicat pentru prima dată pe jakub::sobolewskiși cu amabilitate a contribuit la R-bloggeri). (Puteți raporta problema legată de conținutul acestei pagini aici)
Doriți să vă distribuiți conținutul pe R-bloggeri? dați clic aici dacă aveți un blog, sau aici dacă nu aveți.
Ai stabilit toate condițiile prealabile.
Ați declanșat acțiunea.
Acum, specificația trebuie să răspundă la o întrebare: a făcut sistemul corect?
Pentru asta sunt atunci pașii. Cât de precis răspundeți determină dacă specificațiile dvs. acționează ca o plasă de siguranță fiabilă sau produc o încredere falsă.
Acest articol este a treia parte a unei serii despre scrierea specificațiilor BDD pentru aplicațiile Shiny. Am creat un formular de trimitere a datelor, am gestionat precondiții cu pașii Dați și am modelat interacțiunile utilizatorilor cu pașii Când.
Citiți articolele anterioare pentru a obține contextul complet sau continuați aici pentru a vă concentra pe scrierea Then steps.
- Dezvoltare bazată pe comportament în R Shiny: un exemplu pas cu pas
- Dezvoltare bazată pe comportament în R Shiny: Configurarea condițiilor preliminare de testare cu pași dați
- Dezvoltare bazată pe comportament în R Shiny: Scrierea când pașii care modelează comportamentul utilizatorului
Crește-ți jocul de testare! Luați o copie a foii de parcurs de testare R.
Scopul Atunci
Apoi pașii răspund la o întrebare: Ce s-a schimbat ca urmare a acțiunii utilizatorului?
Nu cum s-a schimbat în interior. Ceea ce utilizatorul poate observa acum – sau ceea ce a făcut sistemul acum – nu era adevărat înainte.
Această distincție modelează fiecare pas Apoi pe care îl scrieți. Afirmația trăiește la nivelul rezultatului, nu la nivelul mecanismului care a produs-o.
# Outcome — what the user cares about
then_there_are_entries <- function(context, n) {
expect_equal(context$storage$size(), n)
context
}
# Mechanism — how the app stored it internally
then_the_database_table_has_n_rows <- function(context, n) {
expect_equal(nrow(DBI::dbReadTable(context$conn, "entries")), n)
context
}
Ambele afirmații verifică același fapt subiacent – că intrarea a fost salvată – dar prima supraviețuiește trecerii de la memoria cache a discului la baza de date la API. Al doilea se rupe în momentul în care schimbi tehnologia de stocare.
Afirmați ce garantează utilizatorul sau sistemul. Lăsați modul în care se realizează testele de implementare.
Ce să afirmăm
Apoi, pașii verifică rezultatele prin trei tipuri de dovezi observabile. Aceste categorii descriu ce puteți verifica — ei nu prescriu câți pași să folosiți. Cum să grupați afirmațiile în pași este o întrebare separată, tratată în secțiunea următoare.
Stare vizibilă de utilizator
Interfața de utilizare a schimbat modul în care un utilizator s-ar aștepta?
then_i_am_prompted_to_provide_required_fields <- function(context) {
context$driver$expect_validation_feedback()
context
}
Aserțiunile folosesc metode driver – același strat de traducere pe care îl folosesc When steps – astfel încât sunt protejate de detaliile de implementare a UI.
Starea sistemului
Uneori, cea mai semnificativă dovadă nu este ceea ce vede utilizatorul, ci ceea ce deține acum sistemul.
then_the_entry_has_title <- function(context, expected_title) {
entry <- context$storage$get_first()
expect_equal(entry$title, expected_title)
context
}
Aceste afirmații ajung direct în obiectul de stocare pe care pașii Given l-au configurat. Sunt mai rapide decât afirmațiile UI și mai precise: verifică starea exactă a sistemului fără a aștepta ca browserul să redea ceva.
Efecte secundare
Unele comportamente nu produc modificări vizibile ale interfeței de utilizare și nici date stocate – declanșează efecte secundare: e-mailuri, apeluri API, intrări în jurnal. Acestea mai au nevoie de verificare.
then_email_notification_is_sent <- function(context) {
context$email_service$expect_sent()
context
}
then_i_am_informed_email_was_not_sent <- function(context) {
context$driver$expect_visible("email_failure_message")
context
}
Prima afirmație funcționează deoarece testul serviciului de e-mail dublu (configurat în Given) înregistrează dacă a fost apelat. Al doilea verifică dacă aplicația a comunicat eșecul utilizatorului – care este comportamentul despre care este de fapt specificația.
Implementarea apoi pașii în driver
Apoi, pașii care verifică starea UI aparțin driverului, la fel ca pașii Când. Acest lucru păstrează detaliile afirmației ascunse din specificație:
#' tests/testthat/setup-driver.R
MyAppDriver <- R6::R6Class(
classname = "MyAppDriver",
inherit = shinytest2::AppDriver,
public = list(
expect_visible = function(output_id) {
# Find if HTML element is visible
invisible(self)
},
expect_entry_count = function(n) {
# Find number of HTML elements
invisible(self)
},
expect_validation_feedback = function() {
# Check if HTML element is visible
invisible(self)
}
)
)
Fiecare metodă de driver este o afirmație numită, reutilizabilă. Dacă structura HTML se modifică, o remediați într-un singur loc.
Gruparea Apoi Pașii
Întrebarea corectă nu este „câte afirmații pe pas?” dar „ce afirmații sunt împreună?”
Grupați afirmații care descriu același comportament observabil. Împărțiți când rezultatele pot diverge în mod independent.
Luați în considerare ce se întâmplă atunci când un utilizator trimite cu succes o înregistrare: spațiul de stocare crește cu unu și apare un mesaj de confirmare. Aceste două rezultate sunt două fețe ale aceleiași monede – dacă una este adevărată, ar trebui să fie și cealaltă. Împărțirea lor în pași separați implică că pot diverge, ceea ce invită cititorul să se întrebe ce ar însemna ca stocarea să reușească, dar nicio confirmare să apară, sau invers. Gruparea lor într-un singur pas denumește comportamentul direct:
then_entry_is_submitted <- function(context) {
expect_equal(context$storage$size(), 1)
context$driver$expect_visible("confirmation_message")
context
}
Notificarea prin e-mail este diferită. Trimiterea poate avea succes chiar și atunci când serviciul de e-mail eșuează – cele două rezultate pot diverge cu adevărat. Exact atunci un pas separat este apelul potrivit:
it("should submit entry and send notification", {
given_no_content() |>
given_an_authenticated_user(email = "(email protected)") |>
given_email_service_is_available() |>
when_i_submit_entry_with_all_required_fields() |>
then_entry_is_submitted() |>
then_notification_was_sent_to_the_authenticated_user()
})
Doi pași, nu trei – pentru că specificațiile reflectă acum structura reală a comportamentului. Dacă then_entry_is_submitted eșuează, știți că prezentarea de bază s-a rupt. Dacă then_notification_was_sent_to_the_authenticated_user nu reușește, știi că efectul secundar s-a spart. Diviziunea transportă informații deoarece se mapează la un punct de divergență real.
Atunci ar trebui să fie și pașii numai pentru citire. Ei inspectează statul; ei nu o schimbă. A Apoi, pasul care modifică stocarea sau declanșează efecte secundare este să faci o treabă greșită. Păstrați curgerea curată: setări date, când acționează, apoi observă.
A face mesajele de eșec utile
Un test eșuat cu un mesaj bun economisește minute. Un test eșuat cu un mesaj prost irosește ore întregi.
Cea mai frecventă greșeală este lăsarea eșecurilor de afirmații de nivel scăzut să apară direct. Când expect_equal(nrow(df), 2) eșuează cu "actual 0, expected 2"care nu vă spune nimic despre scenariul care a eșuat sau cum au arătat datele.
The label argumentul setează numele testcare îl folosește pentru obiect (primul argument) în mesajul de eșec. Păstrați-l scurt și descriptiv – test care adaugă în sine valorile reale și așteptate:
then_there_are_entries <- function(context, n) {
testthat::expect_equal(
context$storage$size(), n,
label = "number of entries in storage"
)
context
}
Un eșec spune acum: "Expected number of entries in storage to equal n" — ștergeți imediat fără nicio formatare manuală.
Pentru afirmațiile UI, etichetați elementul care este verificat:
expect_visible = function(output_id) {
val <- self$get_value(output = output_id)
testthat::expect_true(
!is.null(val) && nchar(val) > 0,
label = sprintf("output '%s'", output_id)
)
invisible(self)
}
Un eșec spune: "Expected output 'confirmation_message' to be TRUE" — care vă spune imediat ce element să vă uitați.
Ceea ce nu aparține apoi pașii
Nu împingeți testele de implementare până la pașii Then.
Dacă afirmați textul exact al unui mesaj de eroare, clasa CSS aplicată unei intrări nevalide sau SQL-ul exact care a fost executat – acestea sunt detalii de implementare. Ei nu aparțin aici.
Nivelul de acceptare Apoi pașii răspund: s-a întâmplat lucrul potrivit din perspectiva utilizatorului?
Poza completă
Punând totul împreună, o specificație bine formată are o structură clară la fiecare nivel:
describe("data submission", {
it("should submit entry and send notification", {
given_no_content() |>
given_an_authenticated_user(email = "(email protected)") |>
given_email_service_is_available() |>
when_i_submit_entry_with_all_required_fields() |>
then_entry_is_submitted() |>
then_notification_was_sent_to_the_authenticated_user()
})
it("should handle email service failure gracefully", {
given_no_content() |>
given_an_authenticated_user() |>
given_email_service_is_unavailable() |>
when_i_submit_entry_with_all_required_fields() |>
then_entry_is_submitted() |>
then_i_am_informed_email_was_not_sent()
})
it("should require all required fields", {
given_no_content() |>
given_an_authenticated_user() |>
when_i_submit_entry_with_missing_required_fields() |>
then_i_am_prompted_to_provide_required_fields()
})
})
Fiecare specificație se citește ca o propoziție. Niciunul dintre ei nu menționează shinytest2ID-uri de intrare sau interogări de baze de date. Implementarea se află în spatele DSL-ului, unde se poate schimba fără a atinge specificațiile.
Încheierea
Apoi, pașii sunt cei în care o specificație își câștigă credibilitatea.
O specificație care testează doar rezultate ușoare produce o încredere falsă. Unul care afirmă starea vizibilă de utilizator, starea sistemului și efectele secundare prinde de fapt probleme reale. Afirmați rezultatele, nu implementările. Grupați afirmațiile după un comportament observabil și împărțiți-le numai acolo unde pot diverge cu adevărat. Scrieți mesaje de eșec care economisesc timp. Împingeți detaliile de implementare până la testele unitare și module.
Cu pașii Given, When, and Then implementați, specificațiile citesc ca cerințe și rulează pe fiecare versiune. Acesta valorează mai mult decât orice cadru de testare în sine.
