tests/testthat/test-conditional_effects.R

# Tests for conditional_effects.bmmfit() and its internal helpers
# Tier 1: Unit tests (always run, no fitted model)
# Tier 2: Fixture-based integration tests (skip on CRAN)
# Tier 3: Model-fitting integration tests (skip on CRAN)

load_sdm_fit <- function() {
  path <- test_path("assets/bmmfit_example1.rds")
  skip_if_not(file.exists(path), "SDM fixture not available (excluded by .Rbuildignore)")
  readRDS(path)
}

# ===========================================================================
# Tier 1: Unit tests — .extract_re_grouping_vars()
# ===========================================================================

test_that(".extract_re_grouping_vars extracts single-bar grouping var", {
  f <- y ~ x + (1 | id)
  expect_equal(.extract_re_grouping_vars(f), "id")
})

test_that(".extract_re_grouping_vars extracts double-bar grouping var", {
  f <- y ~ x + (1 || id)
  expect_equal(.extract_re_grouping_vars(f), "id")
})

test_that(".extract_re_grouping_vars extracts correlation-ID and grouping var", {
  f <- y ~ x + (1 |ID1| id)
  result <- .extract_re_grouping_vars(f)
  expect_true("id" %in% result)
  expect_true("ID1" %in% result)
})

test_that(".extract_re_grouping_vars extracts gr() grouping var", {
  f <- y ~ x + (1 | gr(id, by = exp))
  expect_equal(.extract_re_grouping_vars(f), "id")
})

test_that(".extract_re_grouping_vars extracts gr() with cor arg", {
  f <- y ~ x + (1 | gr(id, cor = FALSE))
  expect_equal(.extract_re_grouping_vars(f), "id")
})

test_that(".extract_re_grouping_vars extracts mm() grouping vars", {
  f <- y ~ x + (1 | mm(g1, g2))
  result <- .extract_re_grouping_vars(f)
  expect_true("g1" %in% result)
  expect_true("g2" %in% result)
  expect_length(result, 2)
})

test_that(".extract_re_grouping_vars extracts crossed grouping vars", {
  f <- y ~ x + (1 | id:group)
  result <- .extract_re_grouping_vars(f)
  expect_true("id" %in% result)
  expect_true("group" %in% result)
})

test_that(".extract_re_grouping_vars handles multiple RE terms", {
  f <- y ~ x + (1 | id) + (1 | group)
  result <- .extract_re_grouping_vars(f)
  expect_true("id" %in% result)
  expect_true("group" %in% result)
})

test_that(".extract_re_grouping_vars returns empty for no RE", {
  f <- y ~ x
  expect_equal(.extract_re_grouping_vars(f), character(0))
})

test_that(".extract_re_grouping_vars returns empty for intercept only", {
  f <- y ~ 1
  expect_equal(.extract_re_grouping_vars(f), character(0))
})


# ===========================================================================
# Tier 1: Unit tests — .ce_summarize_draws()
# ===========================================================================

test_that(".ce_summarize_draws computes mean/SD summary", {
  set.seed(42)
  draws <- matrix(rnorm(1000 * 3), nrow = 1000, ncol = 3)
  result <- .ce_summarize_draws(draws)

  expect_named(result, c("estimate", "lower", "upper", "se"))
  expect_length(result$estimate, 3)
  expect_length(result$lower, 3)
  expect_length(result$upper, 3)
  expect_length(result$se, 3)

  # Estimates should be close to column means
  expect_equal(result$estimate, colMeans(draws), tolerance = 1e-10)
  # SE should be close to column SDs
  expect_equal(result$se, apply(draws, 2, sd), tolerance = 1e-10)
})

test_that(".ce_summarize_draws uses median/MAD when robust = TRUE", {
  set.seed(42)
  draws <- matrix(rnorm(1000 * 2), nrow = 1000, ncol = 2)
  result <- .ce_summarize_draws(draws, robust = TRUE)

  expect_equal(result$estimate, apply(draws, 2, median), tolerance = 1e-10)
  expect_equal(result$se, apply(draws, 2, mad), tolerance = 1e-10)
})

test_that(".ce_summarize_draws handles single-row draws", {
  draws <- matrix(c(1, 2, 3), nrow = 1, ncol = 3)
  result <- .ce_summarize_draws(draws)

  expect_equal(result$estimate, c(1, 2, 3))
  expect_length(result$lower, 3)
  expect_length(result$upper, 3)
})

test_that(".ce_summarize_draws prob argument controls CI width", {
  set.seed(42)
  draws <- matrix(rnorm(5000 * 2), nrow = 5000, ncol = 2)

  wide <- .ce_summarize_draws(draws, prob = 0.95)
  narrow <- .ce_summarize_draws(draws, prob = 0.50)

  # Wider prob → wider interval
  expect_true(all(wide$upper - wide$lower > narrow$upper - narrow$lower))
})


# ===========================================================================
# Tier 1: Unit tests — .apply_link_transform()
# ===========================================================================

# Helper to create mock brms_conditional_effects objects
mock_ce <- function(...) {
  dfs <- list(...)
  class(dfs) <- c("brms_conditional_effects", "list")
  dfs
}

mock_ce_df <- function(estimate, lower, upper) {
  data.frame(
    x = seq_along(estimate),
    estimate__ = estimate,
    lower__ = lower,
    upper__ = upper
  )
}

test_that(".apply_link_transform is no-op for identity link", {
  ce <- mock_ce(
    eff1 = mock_ce_df(c(1, 2, 3), c(0.5, 1.5, 2.5), c(1.5, 2.5, 3.5))
  )
  result <- .apply_link_transform(ce, "identity", inverse = TRUE)
  expect_equal(result[[1]]$estimate__, c(1, 2, 3))
  expect_equal(result[[1]]$lower__, c(0.5, 1.5, 2.5))
  expect_equal(result[[1]]$upper__, c(1.5, 2.5, 3.5))
})

test_that(".apply_link_transform applies inverse log (exp)", {
  ce <- mock_ce(
    eff1 = mock_ce_df(c(0, 1, 2), c(-0.5, 0.5, 1.5), c(0.5, 1.5, 2.5))
  )
  result <- .apply_link_transform(ce, "log", inverse = TRUE)
  expect_equal(result[[1]]$estimate__, exp(c(0, 1, 2)), tolerance = 1e-10)
  expect_equal(result[[1]]$lower__, exp(c(-0.5, 0.5, 1.5)), tolerance = 1e-10)
  expect_equal(result[[1]]$upper__, exp(c(0.5, 1.5, 2.5)), tolerance = 1e-10)
})

test_that(".apply_link_transform applies forward log", {
  ce <- mock_ce(
    eff1 = mock_ce_df(c(1, 2, 3), c(0.5, 1.5, 2.5), c(1.5, 2.5, 3.5))
  )
  result <- .apply_link_transform(ce, "log", inverse = FALSE)
  expect_equal(result[[1]]$estimate__, log(c(1, 2, 3)), tolerance = 1e-10)
  expect_equal(result[[1]]$lower__, log(c(0.5, 1.5, 2.5)), tolerance = 1e-10)
  expect_equal(result[[1]]$upper__, log(c(1.5, 2.5, 3.5)), tolerance = 1e-10)
})

test_that(".apply_link_transform applies inverse logit (plogis)", {
  ce <- mock_ce(
    eff1 = mock_ce_df(c(-1, 0, 1), c(-2, -1, 0), c(0, 1, 2))
  )
  result <- .apply_link_transform(ce, "logit", inverse = TRUE)
  expect_equal(result[[1]]$estimate__, plogis(c(-1, 0, 1)), tolerance = 1e-10)
  expect_equal(result[[1]]$lower__, plogis(c(-2, -1, 0)), tolerance = 1e-10)
})

test_that(".apply_link_transform preserves class and names", {
  ce <- mock_ce(
    set_size = mock_ce_df(c(1, 2), c(0.5, 1.5), c(1.5, 2.5)),
    condition = mock_ce_df(c(3, 4), c(2.5, 3.5), c(3.5, 4.5))
  )
  result <- .apply_link_transform(ce, "log", inverse = TRUE)
  expect_s3_class(result, "brms_conditional_effects")
  expect_named(result, c("set_size", "condition"))
})

test_that(".apply_link_transform transforms all elements in list", {
  ce <- mock_ce(
    eff1 = mock_ce_df(c(0, 1), c(-0.5, 0.5), c(0.5, 1.5)),
    eff2 = mock_ce_df(c(2, 3), c(1.5, 2.5), c(2.5, 3.5))
  )
  result <- .apply_link_transform(ce, "log", inverse = TRUE)
  expect_equal(result[[1]]$estimate__, exp(c(0, 1)), tolerance = 1e-10)
  expect_equal(result[[2]]$estimate__, exp(c(2, 3)), tolerance = 1e-10)
})


# ===========================================================================
# Tier 1: Unit tests — .filter_internal_effects()
# ===========================================================================

test_that(".filter_internal_effects removes internal variables", {
  # Build a mock bmmfit with minimal structure
  mock_bmmfit <- list(
    bmm = list(
      model = structure(
        list(other_vars = list()),
        class = c("sdm", "bmmodel")
      )
    )
  )

  ce <- mock_ce(
    set_size = mock_ce_df(1:3, 0:2, 2:4),
    LureIdx1 = mock_ce_df(1:3, 0:2, 2:4),
    Idx_corr = mock_ce_df(1:3, 0:2, 2:4),
    inv_ss = mock_ce_df(1:3, 0:2, 2:4),
    Item1_Col_rad = mock_ce_df(1:3, 0:2, 2:4),
    expS = mock_ce_df(1:3, 0:2, 2:4)
  )

  result <- .filter_internal_effects(ce, mock_bmmfit)
  expect_named(result, "set_size")
  expect_s3_class(result, "brms_conditional_effects")
})

test_that(".filter_internal_effects keeps all user vars", {
  mock_bmmfit <- list(
    bmm = list(
      model = structure(
        list(other_vars = list()),
        class = c("sdm", "bmmodel")
      )
    )
  )

  ce <- mock_ce(
    set_size = mock_ce_df(1:3, 0:2, 2:4),
    condition = mock_ce_df(1:3, 0:2, 2:4)
  )

  result <- .filter_internal_effects(ce, mock_bmmfit)
  expect_named(result, c("set_size", "condition"))
})


# ===========================================================================
# Tier 2: Fixture-based integration tests
# ===========================================================================

test_that("conditional_effects returns correct class for par = 'c'", {
  skip_on_cran()
  fit <- load_sdm_fit()

  ce <- conditional_effects(fit, par = "c")
  expect_s3_class(ce, "brms_conditional_effects")
  expect_true(length(ce) > 0)
})

test_that("conditional_effects works for intercept-only par = 'kappa'", {
  skip_on_cran()
  fit <- load_sdm_fit()

  ce <- conditional_effects(fit, par = "kappa")
  expect_s3_class(ce, "brms_conditional_effects")
})

test_that("conditional_effects with par = NULL returns all estimated params", {
  skip_on_cran()
  fit <- load_sdm_fit()

  ce <- conditional_effects(fit)
  expect_s3_class(ce, "brms_conditional_effects")
  # SDM fixture has estimated params: c and kappa
  # Effect names are prefixed with par name: "c.set_size", "kappa.1"
  effect_names <- names(ce)
  expect_true(any(grepl("^c\\.", effect_names)))
  expect_true(any(grepl("^kappa\\.", effect_names)))
})

test_that("conditional_effects errors for invalid par name", {
  skip_on_cran()
  fit <- load_sdm_fit()

  expect_error(
    conditional_effects(fit, par = "nonexistent"),
    "not found in model"
  )
})

test_that("conditional_effects errors for non-character par", {
  skip_on_cran()
  fit <- load_sdm_fit()

  expect_error(
    conditional_effects(fit, par = 42),
    "must be a single character string"
  )
})

test_that("scale = 'native' gives positive values for log-linked par", {
  skip_on_cran()
  fit <- load_sdm_fit()

  ce <- conditional_effects(fit, par = "c", scale = "native")
  # c has log link, so native scale = exp(sampling) → all positive
  estimates <- ce[[1]]$estimate__
  expect_true(all(estimates > 0))
})

test_that("scale = 'sampling' can give negative values for log-linked par", {
  skip_on_cran()
  fit <- load_sdm_fit()

  ce <- conditional_effects(fit, par = "c", scale = "sampling")
  # On log scale, values can be any real number
  # Just verify it returns successfully and has different values from native
  ce_native <- conditional_effects(fit, par = "c", scale = "native")
  expect_false(
    isTRUE(all.equal(ce[[1]]$estimate__, ce_native[[1]]$estimate__))
  )
})

test_that("effects argument limits output to specified effect", {
  skip_on_cran()
  fit <- load_sdm_fit()

  ce <- conditional_effects(fit, par = "c", effects = "set_size")
  expect_length(ce, 1)
  expect_true("set_size" %in% names(ce))
})

test_that("plotting conditional_effects works", {
  skip_on_cran()
  fit <- load_sdm_fit()

  ce <- conditional_effects(fit, par = "c")
  p <- plot(ce, plot = FALSE)
  expect_true(length(p) > 0)
})

Try the bmm package in your browser

Any scripts or data that you put into this service are public.

bmm documentation built on March 30, 2026, 5:08 p.m.