tests/testthat/helper-test.R

# Comprehensive helper file for birdnetR tests
# Contains mocks, fixtures, and shared resources for testing

#' SECTION 1: MOCK OBJECTS FOR UNIT TESTS
#' These functions create mock objects that don't require Python

#' Create a mock Python object for testing
#'
#' This helper creates a mock that satisfies is_py_object() checks.
#' To use this in tests, you need to mock the is_py_object function.
#'
#' @param attributes List of attributes to attach to the mock object
#' @return A mock object that behaves like a Python object
create_mock_py_object <- function(attributes = list()) {
  obj <- structure(
    attributes,
    class = c("mock_py_object")
  )
  return(obj)
}

#' Creates a function to check if an object is a mock Python object for testing
#'
#' @return Function that returns TRUE for mock objects and real Python objects
mock_is_py_object <- function() {
  function(object) {
    inherits(object, "mock_py_object") || 
      inherits(object, "birdnet_py_mock") ||
      reticulate::py_is_null_xptr(object) == FALSE
  }
}

#' Create a mock BirdNET model for unit testing
#'
#' Creates a mock model that doesn't require Python dependencies or
#' actual model initialization. Use this in unit tests.
#'
#' @param version Version string
#' @param language Language code
#' @param model_type Type of model (tflite, protobuf, meta, etc.)
#' @return A mock model object that can be used for testing
create_mock_model <- function(version = "v2.4", language = "en_us", model_type = "tflite") {
  # Create a mock Python object
  mock_py_obj <- create_mock_py_object(list(
    language = language,
    predict = function(...) {
      # Return a mock prediction structure that matches what the real model would return
      list(
        "(0.0, 3.0)" = list(
          "Cyanocitta cristata_Blue Jay" = 0.85,
          "Zenaida macroura_Mourning Dove" = 0.65
        ),
        "(3.0, 6.0)" = list(
          "Poecile atricapillus_Black-capped Chickadee" = 0.75
        )
      )
    }
  ))

  # Temporarily override reticulate::is_py_object to recognize our mock objects
  old_is_py_object <- reticulate::is_py_object

  tryCatch({
    # Assign the mocked function
    assignInNamespace("is_py_object", mock_is_py_object(), ns = "reticulate")

    # Now create the model using the package function
    model <- new_birdnet_model(
      mock_py_obj,
      model_version = version,
      language = language,
      subclass = model_type
    )

    return(model)
  }, finally = {
    # Always restore the original function to prevent side effects
    assignInNamespace("is_py_object", old_is_py_object, ns = "reticulate")
  })
}

#' SECTION 2: SHARED TEST RESOURCES
#' These functions provide cached access to real models for integration tests

# Lazy-loaded models - only initialized when needed
.test_models <- new.env()

#' Get a TFLite model for integration testing
#'
#' This function returns a cached model or initializes a new one
#' @param skip_if_not_available If TRUE, skips the test if model can't be initialized
#' @return A TFLite model or skips the test
get_test_tflite_model <- function(skip_if_not_available = TRUE) {
  if (!exists("tflite", envir = .test_models)) {
    # First check if we should even attempt model initialization
    if (is_full_test_env()) {
      tryCatch({
        message("Initializing TFLite model for tests...")
        model <- birdnet_model_tflite(version = "v2.4", language = "en_us")
        assign("tflite", model, envir = .test_models)
      }, error = function(e) {
        if (skip_if_not_available) {
          skip(paste("Failed to initialize TFLite model:", e$message))
        } else {
          NULL
        }
      })
    } else {
      if (skip_if_not_available) {
        skip("Not in a full test environment - TFLite model not available")
      }
      return(NULL)
    }
  }

  return(get("tflite", envir = .test_models))
}

#' Get a Meta model for integration testing
#' 
#' This function returns a cached model or initializes a new one
#' @param skip_if_not_available If TRUE, skips the test if model can't be initialized
#' @return A Meta model or skips the test
get_test_meta_model <- function(skip_if_not_available = TRUE) {
  if (!exists("meta", envir = .test_models)) {
    # First check if we should even attempt model initialization
    if (is_full_test_env()) {
      tryCatch({
        message("Initializing Meta model for tests...")
        model <- birdnet_model_meta(version = "v2.4", language = "en_us")
        assign("meta", model, envir = .test_models)
      }, error = function(e) {
        if (skip_if_not_available) {
          skip(paste("Failed to initialize Meta model:", e$message))
        } else {
          NULL
        }
      })
    } else {
      if (skip_if_not_available) {
        skip("Not in a full test environment - Meta model not available")
      }
      return(NULL)
    }
  }

  return(get("meta", envir = .test_models))
}

#' Get standard test audio file path
#'
#' @return Path to the test audio file or skips the test if not found
test_audio_file <- function() {
  audio_file <- system.file("extdata", "soundscape.mp3", package = "birdnetR")
  if (audio_file == "") {
    skip("Test audio file not found")
  }
  return(audio_file)
}

Try the birdnetR package in your browser

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

birdnetR documentation built on June 8, 2025, 10:29 a.m.