tests/testthat/test-API.R

####
## Making use of the shared OpenTree testing architecture
####
## The R, Python and Ruby wrappers for the Open Tree share a very similar design,
## allowing them to make use of a single test suite for the low-level functions
## (thus, the tests both checkvan individual library works as expected, and that
## the different libraries stay in line with each other).
##
## This file pulls the current version of the test from a github repo
## (https://github.com/OpenTreeOfLife/shared-api-tests) and translates the json
## files into tests that run in testthat. This takes a considerable amount of
## infrastructure so I'll briefly described the rational here.
##
## The JSON test-specificaton is defined at the github repo linked above, to
## translate these tests I have created custom testthat expectation-functionals
## (contains(), (key_has_value()... ). Because many of the test blocks in the
## JSON files have multiple expectiatoins (i.e. many key-value pairs for
## test_equals) there are functions starting with `test_` that run an entire
## test block for a given expectation. Since many of these tests require
## translation between R-objects and JSON encoded strings there is a set of
## convienence functions to automate that step and a function "test_map" that
## returns the appropriate test_* function for r given JSON test block.
##
## Finally, testthat_json_test uses the above functions to runs an entire test
## from a JSON object, and run_shared_tests() runs every tests in a JSON file.




# functionals that start with a response
contains <- function(key_name) {
  function(x) {
    expectation(key_name %in% names(x), sprintf("Missing key name: %s", key_name))
  }
}

key_has_value <- function(key, value) {
  function(x) {
    if (length(value) == 0) {
      expectation(
        length(x[[key]]) == 0,
        paste("Key", key, "is not empty")
      )
    }
    else if (length(value) == 1) {
      expectation(
        x[[key]] == value,
        paste("Key", key, "doesn't have value", value)
      )
    }
    else {
      expectation(
        all(x[[key]] %in% value),
        paste("Key", key, "doesn't contain all of", value)
      )
    }
  }
}

value_is_longer_than <- function(key, len) {
  function(x) {
    expectation(
      length(x[[key]]) > len,
      paste("Value for key", key, "is shorter than", len)
    )
  }
}

value_is_error <- function(key_name) {
  function(x) {
    expectation(
      x[[key_name]] == "error",
      sprintf("Key %s is not 'error'", key_name)
    )
  }
}

## Functions to test entire test blocks with the above expectations

test_contains <- function(response, test_block) {
  key_names <- test_block[, 1]
  sapply(key_names, function(k) expect_that(response, contains(k)))
}

test_equals <- function(response, test_block) {
  kv_pairs <- sapply(test_block, "[[", 1)
  for (i in 1:length(kv_pairs)) {
    expect_that(response, key_has_value(kv_pairs[[1]], kv_pairs[[2]]))
  }
}

test_of_type <- function(response, test_block) {
  rtype <- type_map(test_block[[1]])
  expect_that(response, is_a(rtype))
}

test_deep_equals <- function(response, test_block) {
  cat("*")
  expect_true(TRUE)
}


test_length_greater_than <- function(response, test_block) {
  vl_pairs <- sapply(test_block, "[[", 1)
  apply(vl_pairs, 2, function(v) {
    expect_that(response, value_is_longer_than(v[[1]], v[[2]]))
  })
}

test_contains_error <- function(response, test_block) {
  errs <- test_block[, 1]
  sapply(errs, function(e) expect_that(reponse, contains_error(e)))
}

## convience functions
obj_map <- function(input) {
  if (is.character(input) & length(input) == 1) {
    switch(tolower(input),
      "true" = TRUE,
      "false" = FALSE,
      "null"  = NULL,
      input
    )
  }
  else {
    input
  }
}

json_to_r <- function(test_input) {
  if (length(test_input) == 0) {
    return(test_input)
  }
  return(lapply(test_input, obj_map))
}

type_map <- function(json_type) {
  switch(json_type,
    "dict" = "list",
    stop(sprintf("unknown json type in testing file: %s", json_type))
  )
}


test_map <- function(test_type) {
  switch(test_type,
    "contains"    = test_contains,
    "equals"      = test_equals,
    "deep_equals" = test_deep_equals,
    "error"       = stop("Error tests should be handled first"),
    "length_greater_than" = test_length_greater_than,
    "of_type"     = test_of_type,
    stop(sprintf("Unkown error type in JSON test: %s", test_type))
  )
}

make_request <- function(json_test) {
  test_fxn <- paste0(".", json_test$test_function)
  do.call(what = test_fxn, args = json_to_r(json_test$test_input))
}


testthat_json_test <- function(test_obj, test_name) {
  tests_to_run <- names(test_obj[[test_name]]$tests)
  if (length(tests_to_run) == 1) {
    if (grepl("error", tests_to_run)) {
      expect_error(make_request(test_obj[[test_name]]))
    }
  }
  else {
    response <- make_request(test_obj[[test_name]])
    for (i in 1:length(tests_to_run)) {
      test_block <- test_obj[[test_name]]$tests[[tests_to_run[i]]]
      test_fxn <- test_map(tests_to_run[i])
      test_fxn(response, test_block)
    }
  }
}

run_shared_test <- function(json_obj) {
  all_tests <- names(json_obj)
  for (i in 1:length(all_tests)) {
    test_that(all_tests[i], {
      skip_on_cran()
      testthat_json_test(json_obj, all_tests[i])
    })
  }
}


## if (identical(Sys.getenv("NOT_CRAN"), "true")) {
##     base_url <- "https://raw.githubusercontent.com/OpenTreeOfLife/shared-api-tests/master/"
##     apis <- c("graph_of_life",
##               "studies",
##               "taxonomy",
##               "tree_of_life",
##               "tnrs"
##               )
##     for(i in 1:length(apis)){
##         context( paste(apis[i], "API") )
##         test_text <- httr::GET(paste0(base_url, apis[i], ".json"))
##         test_description <- jsonlite::fromJSON(httr::content(test_text))
##         run_shared_test(test_description)
##     }
## }
ropensci/rotl documentation built on June 27, 2023, 4:55 p.m.