Jakub Sobolewski
July 10, 2024
🏛️ A design technique.
📈 A guided evolution.
🧪 Tests first.
❌ Red
✅ Green
🔁 Refactor
❌ Red
Write a failing test first. ⛔️
✅ Green
🔁 Refactor
❌ Red
✅ Green
Write the production code.
Only as much as needed to make tests pass. ✋
🔁 Refactor
❌ Red
✅ Green
🔁 Refactor
Don’t worry about breaking things.
Make code more readable and maintainable. 💄
test-sum.R
── Error: my_sum: should calculate the sum for multiple numbers ────────────────
Error in `my_sum(numbers)`: could not find function "my_sum"
Error:
! Test failed
test-sum.R
── Failure: my_sum: should calculate the sum for multiple numbers ──────────────
`result` not equal to 3.
target is NULL, current is numeric
Error:
! Test failed
test-sum.R
describe("my_sum", {
it("should calculate the sum for multiple numbers", {
# Arrange
numbers <- c(1, 2)
# Act
result <- my_sum(numbers)
# Assert
expect_equal(result, 3)
})
it("should calculate the sum for one number", {
# Arrange
numbers <- c(1)
# Act
result <- my_sum(numbers)
# Assert
expect_equal(result, 1)
})
})
Test passed 🎊
── Failure: my_sum: should calculate the sum for one number ────────────────────
`result` not equal to 1.
1/1 mismatches
[1] NA - 1 == NA
Error:
! Test failed
test-sum.R
describe("my_sum", {
it("should calculate the sum for multiple numbers", {
# Arrange
numbers <- c(1, 2)
# Act
result <- my_sum(numbers)
# Assert
expect_equal(result, 3)
})
it("should calculate the sum for one number", {
# Arrange
numbers <- c(1)
# Act
result <- my_sum(numbers)
# Assert
expect_equal(result, 1)
})
})
Test passed 🎉
Test passed 🥳
test-sum.R
describe("my_sum", {
it("should calculate the sum for multiple numbers", {
# Arrange
numbers <- c(1, 2)
# Act
result <- my_sum(numbers)
# Assert
expect_equal(result, 3)
})
it("should calculate the sum for one number", {
# Arrange
numbers <- c(1)
# Act
result <- my_sum(numbers)
# Assert
expect_equal(result, 1)
})
})
Test passed 🎉
Test passed 🥳
How do we use it for more complex problems?
📈 Bottom-up
📉 Top-down
📈 Bottom-up
Start small. 🧱
A design emerges. 🏛️
📉 Top-down
📈 Bottom-up
📉 Top-down
List high-level objectives first. 🎯
Describe behavior from user perspective. 🙌
Fill in the gaps until tests pass. 🧩
They tell us if the code does what we think it does.
🙋♀️ 💬
🙋♂️ 💬
➡️
✅ Pass
✅ Pass
A list of verifiable statements.
🎯 Specific for low-level (unit) tests.
💬 Abstract for high-level (acceptance) tests.
🎯 Specific for low-level (unit) tests.
☑️ It should work correctly.
☑️ It should throw an error when the table has 1 row.
💬 Abstract for high-level (acceptance) tests.
🎯 Specific for low-level (unit) tests.
💬 Abstract for high-level (acceptance) tests.
☑️ It should render the table after clicking the “Go” button.
☑️ It should show the summary.
📝 And document what we solved.
📊 Via executable examples on real data.
📦 The testable unit of an app is a Shiny module.
✨ It represents a feature.
🖥️ It can be run as a standalone app.
test-dataset_summary.R
── Error: dataset summary: should show the summary ─────────────────────────────
Error in `dataset_summary_ui(NULL)`: could not find function "dataset_summary_ui"
Backtrace:
▆
1. └─shiny::shinyApp(...)
2. └─shiny:::uiHttpHandler(ui, uiPattern)
3. └─base::force(ui)
Error:
! Test failed
test-dataset_summary.R
── Error: dataset summary: should show the summary ─────────────────────────────
Error in `eval(code, test_env)`: object 'DatasetSummary' not found
Error:
! Test failed
test-dataset_summary.R
DatasetSummary <- R6::R6Class(
classname = "DatasetSummary",
public = list(
driver = NULL,
initialize = function(app) {
self$driver <- AppDriver$new(app)
},
expect_summary_visible = function() {
}
)
)
describe("dataset summary", {
it("should show the summary", {
# Arrange
app <- shinyApp(
ui = dataset_summary_ui(NULL),
server = function(input, output) dataset_summary_server(NULL)
)
page <- DatasetSummary$new(app)
# Act
# Assert
page$expect_summary_visible()
fail("Not implemented")
})
})
── Failure: dataset summary: should show the summary ───────────────────────────
Not implemented
Error:
! Test failed
test-dataset_summary.R
DatasetSummary <- R6::R6Class(
classname = "DatasetSummary",
public = list(
driver = NULL,
initialize = function(app) {
self$driver <- AppDriver$new(app)
},
expect_summary_visible = function() {
}
)
)
describe("dataset summary", {
it("should show the summary", {
# Arrange
app <- shinyApp(
ui = dataset_summary_ui(NULL),
server = function(input, output) {
data_provider <- list(get_summary = \() {
iris
})
dataset_summary_server(NULL, data_provider)
}
)
page <- DatasetSummary$new(app)
# Act
# Assert
page$expect_summary_visible()
fail("Not implemented")
})
})
── Failure: dataset summary: should show the summary ───────────────────────────
Not implemented
Error:
! Test failed
test-dataset_summary.R
DatasetSummary <- R6::R6Class(
classname = "DatasetSummary",
public = list(
driver = NULL,
initialize = function(app) {
self$driver <- AppDriver$new(app)
},
expect_summary_visible = function() {
}
)
)
describe("dataset summary", {
it("should show the summary", {
# Arrange
app <- shinyApp(
ui = dataset_summary_ui(NULL),
server = function(input, output) {
data_provider <- list(get_summary = \() {
iris
})
dataset_summary_server(NULL, data_provider)
}
)
page <- DatasetSummary$new(app)
# Act
# Assert
page$expect_summary_visible()
fail("Not implemented")
})
})
── Failure: dataset summary: should show the summary ───────────────────────────
Not implemented
Error:
! Test failed
🎯 {selenider}
test-dataset_summary.R
DatasetSummary <- R6::R6Class(
classname = "DatasetSummary",
public = list(
driver = NULL,
selenider = NULL,
initialize = function(app) {
self$driver <- AppDriver$new(app)
self$selenider <- selenider_session(
driver = self$driver,
local = FALSE
)
},
expect_summary_visible = function() {
find_element(self$selenider, css = "#summary > table") |>
elem_expect(is_visible) |>
elem_expect(\(x) has_at_least(elem_children(x), 1))
}
)
)
describe("dataset summary", {
it("should show the summary", {
# Arrange
app <- shinyApp(
ui = dataset_summary_ui(NULL),
server = function(input, output) {
data_provider <- list(
get_summary = function() {
iris
}
)
dataset_summary_server(NULL, data_provider)
}
)
page <- DatasetSummary$new(app)
# Act
# Assert
page$expect_summary_visible()
})
})
── Failure: dataset summary: should show the summary ───────────────────────────
Condition failed:
`is_visible`
ℹ `x` is not visible.
Caused by error in `is_visible()`:
! `x` does not exist in the DOM.
Where `x` is:
A selenider element selecting:
The first element with css selector "#summary > table".
Backtrace:
▆
1. ├─page$expect_summary_visible()
2. │ └─selenider::elem_expect(...)
3. │ └─selenider:::eval_conditions(x, dots, timeout)
4. │ └─selenider:::eval_condition(x)
5. │ ├─rlang::try_fetch(...)
6. │ │ └─base::withCallingHandlers(...)
7. │ ├─selenider:::with_timeout(0, eval_tidy(x, data = data_mask))
8. │ │ └─base::force(code)
9. │ └─rlang::eval_tidy(x, data = data_mask)
10. └─selenider::elem_expect(...)
── Failure: dataset summary: should show the summary ───────────────────────────
Condition failed after waiting for 4 seconds:
`function(x) has_at_least(elem_children(x), 1)`
ℹ `x` does not exist, which may have caused the condition to fail.
Caused by error in `value[[3L]]()`:
! `x`'s parent element does not exist in the DOM.
Where `x` is:
A selenider element selecting:
The first element with css selector "#summary > table".
Backtrace:
▆
1. └─page$expect_summary_visible()
2. └─selenider::elem_expect(...)
Error:
! Test failed
test-dataset_summary.R
DatasetSummary <- R6::R6Class(
classname = "DatasetSummary",
public = list(
driver = NULL,
selenider = NULL,
initialize = function(app) {
self$driver <- AppDriver$new(app)
self$selenider <- selenider_session(
driver = self$driver,
local = FALSE
)
},
expect_summary_visible = function() {
find_element(self$selenider, css = "#summary > table") |>
elem_expect(is_visible) |>
elem_expect(\(x) has_at_least(elem_children(x), 1))
}
)
)
describe("dataset summary", {
it("should show the summary", {
# Arrange
app <- shinyApp(
ui = dataset_summary_ui(NULL),
server = function(input, output) {
data_provider <- list(
get_summary = function() {
iris
}
)
dataset_summary_server(NULL, data_provider)
}
)
page <- DatasetSummary$new(app)
# Act
# Assert
page$expect_summary_visible()
})
})
Test passed 😸
test-dataset_summary.R
if (interactive()) {
shinyApp(
ui = dataset_summary_ui(NULL),
server = function(input, output) {
data_provider <- list(
get_summary = function() {
iris
}
)
dataset_summary_server(NULL, data_provider)
}
)
}
DatasetSummary <- R6::R6Class(
classname = "DatasetSummary",
public = list(
driver = NULL,
selenider = NULL,
initialize = function(app) {
self$driver <- AppDriver$new(app)
self$selenider <- selenider_session(
driver = self$driver,
local = FALSE
)
},
expect_summary_visible = function() {
find_element(self$selenider, css = "#summary > table") |>
elem_expect(is_visible) |>
elem_expect(\(x) has_at_least(elem_children(x), 1))
}
)
)
describe("dataset summary", {
it("should show the summary", {
# Arrange
app <- shinyApp(
ui = dataset_summary_ui(NULL),
server = function(input, output) {
data_provider <- list(
get_summary = function() {
iris
}
)
dataset_summary_server(NULL, data_provider)
}
)
page <- DatasetSummary$new(app)
# Act
# Assert
page$expect_summary_visible()
})
})
Test passed 🌈
Feature 👔
Feature 👔
Feature 👔
🧪 Unit tests
🏎️ Faster development cycles.
🧠 Understanding the problem.
🛡️ Confidence that it works.