Hero background

Building Quality By Testing

I'm Jakub and testing helps me build better software.
Practical articles, and open-source tools for developers who want to build software they trust.

My Open Source

cucumber logo

Cucumber

Write behavior specifications in plain English, implement them as R functions, and run them as tests. Requirements stay in sync with the code because they are the code.

βœ—comments as specs

Comments encode intent but not procedure. They don't separate precondition from action from outcome. They can't be run, so they drift β€” and a stale comment is worse than no comment.

test_that("sales trend works", {
  # sales data is available
  # check the plot for Electronics
  # make sure it looks right

  result <- get_sales_trend("Electronics")
  expect_s3_class(result, "ggplot")
})

βœ“the specification

Gherkin forces you to think in procedure: what state is required, what action is taken, what outcome is observable. Vague intent doesn't survive the structure.

Feature: Sales Trends

  Scenario: User views trend for a category
    Given the sales data is loaded
    When the user views the trend for "Electronics"
    Then the sales trend plot for "Electronics" is shown

βœ“the implementation

Each line of the spec maps to one R function. The English phrase becomes the function signature β€” the same words, now executable.

given("the sales data is loaded", function(context) {
  context$data <- load_sales_data()
})

when("the user views the trend for {string}", function(category, context) {
  context$plot <- get_sales_trend(context$data, category)
})

then("the sales trend plot for {string} is shown", function(category, context) {
  expect_s3_class(context$plot, "ggplot")
  expect_equal(context$plot$labels$title, category)
})

βœ“verification

> cucumber::test()
#> βœ” | F W  S  OK | Context
#> βœ” |          1 | Feature: Sales Trends
#>
#> ══ Results ═══════════════════════════════════════════════════
#> [ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ]
muttest logo

Muttest

Mutation testing for R. Introduces small changes to your source code and checks whether your tests catch them. Reveals gaps that code coverage misses.

the code

A simple boundary check. Two tests cover adults and minors β€” but never the edge.

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

mutation score

50%

The > 18 mutant survived. Boundary value 18 is never tested β€” your suite can't tell >= from >.

βœ—the tests

test_that("is_adult returns TRUE for adults", {
  expect_true(is_adult(25))  # passes even with age > 18
})

test_that("is_adult returns FALSE for minors", {
  expect_false(is_adult(10))  # passes even with age > 18
})

run mutation testing

> muttest::muttest(plan)
#> β„Ή Mutation Testing
#>   |   K |   S |   E |   T |   % | Mutator  | File
#> βœ” |   1 |   0 |   0 |   1 | 100 | >= β†’ <=  | is_adult.R
#> x |   1 |   1 |   0 |   2 |  50 | >= β†’ >   | is_adult.R
#>
#> ── Survived Mutants ─────────────────────────────────────────────────────────
#> is_adult.R  >= β†’ >
#>   2-   age >= 18
#>   2+   age > 18
#>
#> ── Results ──────────────────────────────────────────────────────────────────
#> [ KILLED 1 | SURVIVED 1 | ERRORS 0 | TOTAL 2 | SCORE 50.0% ]

after the fix

100%

Every mutation triggers a failure. Adding the boundary test kills the survivor.

βœ“the fix

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))
})

test_that("is_adult is TRUE at the boundary", { 
  expect_true(is_adult(18))  # kills >= β†’ > #
}) 

Tutorials

Shiny Acceptance TDD

Build Shiny apps from the outside in. Write acceptance tests first, then let them drive every design decision down to the module level.

βœ—vague requirements

Stories written in prose stay prose. They can't be run, so nobody knows when the app actually satisfies them. Requirements drift the moment code ships.

Budget tracking
  As a user I want to see my net balance
  so that I can understand my financial situation.

  Acceptance: shows income, expenses, and net.
  // ← lives in a doc, never executed

βœ“executable specification

The same scenario becomes a test. Given-When-Then forces you to name preconditions, actions, and outcomes. When it passes, the feature is done.

# tests/acceptance/test-budget.R
test_that("Scenario: I can inspect my net balance", {
  # Given
  dsl$record_income(2000)
  dsl$record_expense(500)
  # When
  dsl$inspect_finances()
  # Then
  dsl$verify_total_income(2000)
  dsl$verify_total_expenses(500)
  dsl$verify_net_balance(1500)
  dsl$teardown()
})

what you'll learn

  • βœ“Transform user stories into runnable tests
  • βœ“Build a DSL that hides UI details from specs
  • βœ“Keep tests green as the UI evolves
  • βœ“Structure Shiny modules for testability

Shiny Test-Driven Development

ShinyConf 2024. A structured approach to testing Shiny apps: inside-out unit tests, outside-in acceptance tests, and the loop that connects them.

what you'll learn

  • βœ“Inside-out vs. outside-in strategies
  • βœ“Automate acceptance criteria
  • βœ“Isolate and test Shiny modules
  • βœ“Inject fake dependencies

Behavior-Driven Development

useR! 2025. From vague wish to working code: how to cooperate with stakeholders, write Gherkin scenarios, and execute them with Cucumber for R.

what you'll learn

  • βœ“BDD fundamentals and why they work
  • βœ“Given-When-Then scenario structure
  • βœ“Run specs with Cucumber for R
  • βœ“Align code with business language
Hero background
Jakub Sobolewski

I'm Jakub Sobolewski

I'm a software engineer specializing in R with 5+ years of experience.

I believe automated testing is the key to building quality software.

My journey into R testing began with a project where to develop code, you had to be connected to the production environment. Turns out, it was a terrible developer experience.

I'm particularly passionate about knowledge sharing, which is why I maintain an active blog and R Tests Gallery. I believe that when we share our testing experiencesβ€”both successes and failuresβ€”we all become better developers.

I approach testing with a practical mindset: tests should make development faster and more confident, not slower and more burdensome. My goal is to help teams find testing strategies that actually enhance their workflow.