Nothing
# Legacy Contract Tests - Output Invariants
# WS2.2: Lock output schemas so refactors cannot silently change user-visible behavior
# These tests verify behavioral contracts that must hold across all releases.
# They serve as guardrails to prevent accidental breaking changes.
# Column Name Invariants -------------------------------------------------
# Fail if columns are renamed or removed from legacy API output
test_that("FitCurves column names are stable (HS equation)", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id == 19, ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2)
))
# Core parameter columns must exist
core_params <- c("id", "Q0d", "Alpha", "K", "R2")
expect_true(all(core_params %in% names(result)),
info = paste(
"Core parameter columns missing:",
paste(setdiff(core_params, names(result)), collapse = ", ")
)
)
# Standard error columns must exist
se_cols <- c("Q0se", "Alphase")
expect_true(all(se_cols %in% names(result)),
info = paste(
"Standard error columns missing:",
paste(setdiff(se_cols, names(result)), collapse = ", ")
)
)
# Derived metrics columns must exist
derived_cols <- c("EV", "Pmaxd", "Omaxd", "Pmaxa", "Omaxa", "Pmaxe", "Omaxe")
expect_true(all(derived_cols %in% names(result)),
info = paste(
"Derived metrics columns missing:",
paste(setdiff(derived_cols, names(result)), collapse = ", ")
)
)
# Confidence interval columns must exist
ci_cols <- c("Q0Low", "Q0High", "AlphaLow", "AlphaHigh")
expect_true(all(ci_cols %in% names(result)),
info = paste(
"Confidence interval columns missing:",
paste(setdiff(ci_cols, names(result)), collapse = ", ")
)
)
})
test_that("FitCurves column names are stable (Koff equation)", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id == 19, ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "koff", k = 2)
))
# Same columns as HS
core_params <- c("id", "Q0d", "Alpha", "K", "R2")
expect_true(all(core_params %in% names(result)))
})
# Parameter Bound Constraints --------------------------------------------
# Q0 > 0, alpha > 0 for converged models
test_that("Converged models have positive Q0", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30, 38), ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2)
))
converged <- result[result$Notes == "converged", ]
# All converged Q0 values should be positive
expect_true(all(converged$Q0d > 0, na.rm = TRUE),
info = "All converged Q0d values should be positive"
)
})
test_that("Converged models have positive alpha", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30, 38), ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2)
))
converged <- result[result$Notes == "converged", ]
# All converged Alpha values should be positive
expect_true(all(converged$Alpha > 0, na.rm = TRUE),
info = "All converged Alpha values should be positive"
)
})
test_that("Converged models have R2 in [0, 1] range", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30, 38), ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2)
))
converged <- result[result$Notes == "converged", ]
# R2 should be between 0 and 1 for well-fitting models
# Note: technically R2 can be negative for very poor fits, but converged
# models should typically have R2 >= 0
expect_true(all(converged$R2 <= 1, na.rm = TRUE),
info = "R2 should not exceed 1"
)
})
# Monotonicity Check -----------------------------------------------------
# Predicted consumption should be non-increasing in price for demand models
test_that("Predicted demand is monotonically non-increasing in price", {
skip_on_cran()
data(apt, package = "beezdemand")
apt_test <- apt[apt$id == 19, ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2, detailed = TRUE)
))
# Get fitted model for subject 19
fit_obj <- result[[2]][[1]]
df <- result[[3]][[1]]
# Generate predictions at increasing prices
test_prices <- seq(0.1, 30, by = 0.5)
# Predict using model formula
k_val <- result[[1]]$K[1]
q0_val <- result[[1]]$Q0d[1]
alpha_val <- result[[1]]$Alpha[1]
# HS equation: y = q0 * 10^(k * (exp(-alpha * q0 * x) - 1))
predicted <- q0_val * 10^(k_val * (exp(-alpha_val * q0_val * test_prices) - 1))
# Check monotonicity: each prediction should be >= next prediction
diffs <- diff(predicted)
expect_true(all(diffs <= 1e-10),
info = paste(
"Demand should be monotonically decreasing.",
"Found", sum(diffs > 1e-10), "violations."
)
)
})
test_that("Predicted demand (Koff) is monotonically non-increasing", {
skip_on_cran()
data(apt, package = "beezdemand")
apt_test <- apt[apt$id == 19, ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "koff", k = 2)
))
# Get parameter estimates
k_val <- result$K[1]
q0_val <- result$Q0d[1]
alpha_val <- result$Alpha[1]
# Generate predictions at increasing prices
test_prices <- seq(0.1, 30, by = 0.5)
# Koff equation: y = q0 * 10^(k * (exp(-alpha * q0 * x) - 1))
# Note: Same functional form for predicted values
predicted <- q0_val * 10^(k_val * (exp(-alpha_val * q0_val * test_prices) - 1))
# Check monotonicity
diffs <- diff(predicted)
expect_true(all(diffs <= 1e-10),
info = "Koff demand should be monotonically decreasing."
)
})
# Convergence Status Format Stability ------------------------------------
test_that("Notes column uses consistent convergence format", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30, 38), ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2)
))
# Notes should contain recognizable status strings
valid_statuses <- c("converged", "maxiter", "singular", "failed", "")
# All notes should be character
expect_type(result$Notes, "character")
# Check that converged subjects have "converged" status
converged_rows <- which(!is.na(result$Q0d) & !is.na(result$Alpha))
if (length(converged_rows) > 0) {
expect_true(
any(grepl("converged", result$Notes[converged_rows], ignore.case = TRUE)),
info = "At least some successful fits should have 'converged' in Notes"
)
}
})
# EV Column Invariant -----------------------------------------------------
test_that("EV (Essential Value) is calculated for converged models", {
skip_on_cran()
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30, 38), ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2)
))
converged <- result[result$Notes == "converged", ]
# EV should be present and positive for converged models
expect_true(all(!is.na(converged$EV)),
info = "EV should be calculated for converged models"
)
expect_true(all(converged$EV > 0, na.rm = TRUE),
info = "EV should be positive for converged models"
)
})
# Row Count Invariants ---------------------------------------------------
test_that("FitCurves returns one row per subject", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30, 38), ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2)
))
expect_equal(nrow(result), 3)
expect_equal(sort(as.character(unique(result$id))), sort(c("19", "30", "38")))
})
test_that("FitMeanCurves returns exactly one row", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30, 38), ]
result_mean <- suppressMessages(suppressWarnings(
FitMeanCurves(apt_test, equation = "hs", k = 2, method = "Mean")
))
result_pooled <- suppressMessages(suppressWarnings(
FitMeanCurves(apt_test, equation = "hs", k = 2, method = "Pooled")
))
expect_equal(nrow(result_mean), 1)
expect_equal(nrow(result_pooled), 1)
})
# Detailed Output Contract -----------------------------------------------
test_that("FitCurves detailed=TRUE returns named list with expected elements", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id %in% c(19, 30), ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "hs", k = 2, detailed = TRUE)
))
expect_type(result, "list")
# detailed=TRUE returns list with: dfres, fits, newdats, adfs
expect_true(length(result) >= 3,
info = "detailed=TRUE should return at least 3 elements"
)
# Check named elements exist
expect_true("dfres" %in% names(result) || is.data.frame(result[[1]]),
info = "First element should be results data frame"
)
expect_true("fits" %in% names(result) || is.list(result[[2]]),
info = "Second element should be list of model fits"
)
# First element: data frame of results
expect_s3_class(result$dfres %||% result[[1]], "data.frame")
# Second element: list of model objects (one per subject)
fits_element <- result$fits %||% result[[2]]
expect_type(fits_element, "list")
expect_equal(length(fits_element), 2) # One per subject
})
# N Column Contract ------------------------------------------------------
test_that("N column reflects number of data points per subject", {
data(apt, package = "beezdemand")
apt_test <- apt[apt$id == 19, ]
result <- suppressMessages(suppressWarnings(
FitCurves(apt_test, equation = "koff", k = 2)
))
# N should be the number of non-NA observations
expected_n <- sum(!is.na(apt_test$y))
expect_equal(result$N[1], expected_n)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.