Функции модульного тестирования с глобальными переменными в R

Преамбула: структура пакета

У меня есть пакет R, который содержит файл R/globals.R со следующим содержимым (упрощенно):

utils::globalVariables("COUNTS")

Затем у меня есть функция, которая просто использует эту переменную. Например, R/addx.R содержит функцию, которая добавляет число к COUNTS.

addx <- function(x) {
    COUNTS + x
}

Это все нормально, когда я делаю devtools::check() на моем пакете, я не жалуюсь на то, что COUNTS выходит за рамки addx().

Проблема: написание юнит-теста

Однако, скажем, у меня также есть файл tests/testthtat/test-addx.R со следующим содержимым:

test_that("addition works", expect_gte(fun(1), 1))

Содержание теста здесь не имеет особого значения, потому что при запуске devtools::test() я получаю ошибку «объект 'COUNTS' not found».

Что мне не хватает? Как я могу правильно написать этот тест (или настроить свой пакет).

Что я пытался решить проблему

  1. Добавление utils::globalVariables("COUNTS") к R/addx.R до, внутри или после определения функции.
  2. Добавление utils::globalVariables("COUNTS") к tests/testthtat/test-addx.R во всех местах, которые я мог придумать.
  3. Ручная инициализация COUNTS (например, с помощью COUNTS <- 0 или <<- 0) во всех местах tests/testthtat/test-addx.R, которые я мог придумать.
  4. Читаю несколько примеров из других пакетов на GitHub, которые используют аналогичный синтаксис (источник).

Определен ли COUNTS буквально в одном из yourpackage/R/*.R вне определений функций?

r2evans 09.12.2020 17:38

Альтернативная ситуация, о которой я могу думать, заключается в том, что вы ожидаете, что COUNTS будет определено в вызывающей среде. Если это так, то... Я думаю, что ответ "не делай этого".

r2evans 09.12.2020 17:45

@r2evans, есть пара функций, которые выполняют присваивания COUNTS. clearGlobal() в основном инициализирует его как COUNTS <- vector(), а updateGlobal() пересчитывает COUNTS как функцию других переданных аргументов. Насколько я знаю, нет случаев, когда COUNTS определяется вне определения функции (это огромный проект, с которым я не знаком на 100%). Я думаю, что случай, который у нас есть, - это случай из вашего второго комментария, который, я согласен, не идеален, но я боюсь, что что-то сломаю, если изменю реализованную динамику.

Waldir Leoncio 11.12.2020 12:13
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
723
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я думаю, вы неправильно понимаете, что делает utils::globalVariables("COUNTS"). Он просто объявляет, что COUNTS является глобальной переменной, поэтому, когда анализ кода увидит

addx <- function(x) {
    COUNTS + x
}

он не будет жаловаться на использование неопределенной переменной. Тем не менее, вы должны фактически создать переменную, например, с помощью явного

COUNTS <- 0

где-то в вашем источнике. Я думаю, что если вы это сделаете, вам даже не понадобится вызов utils::globalVariables("COUNTS"), потому что анализ кода увидит глобальное определение.

Там, где вам это нужно, это когда вы делаете какую-то нестандартную оценку, так что не очевидно, откуда берется переменная. Затем вы объявляете его глобальным, и анализ кода не будет об этом беспокоиться. Например, вы можете получить предупреждение о

subset(df, Col1 < 0)

потому что кажется, что она использует глобальную переменную с именем Col1, но, конечно, это нормально, потому что функция subset() вычисляет нестандартным образом, позволяя вам включать имена столбцов без записи df$Col.

Спасибо, добавление COUNTS <- vector() к тому же сценарию, что и globalVariables() (для удобства), решило проблему. Я могу поклясться, что пробовал это (см. пункт 3 OP), но я просто рад, что это работает. Скрестим пальцы, это не сломает что-то ниже по течению. Ваше здоровье!

Waldir Leoncio 11.12.2020 12:27

Ответ @ user2554330 отлично подходит для многих вещей.

Если я правильно понимаю, у вас есть COUNTS, который нужно обновлять, поэтому его размещение в среде пакета может быть проблемой.

Один из методов, который вы можете использовать, — это использование локальных сред.

Две альтернативы:

  1. Если на него всегда будут ссылаться в одной функции, проще всего будет изменить функцию с

    myfunc <- function(...) {
      # do something
      COUNTS <- COUNTS + 1
    }
    

    к

    myfunc <- local({
      COUNTS <- NA
      function(...) {
        # do something
        COUNTS <<- COUNTS + 1
      }
    })
    

    Что это делает, так это создает локальную среду «вокруг» myfunc, поэтому, когда он ищет COUNTS, он будет немедленно найден. Обратите внимание, что он переназначается с использованием <<- вместо <-, поскольку последний не будет обновлять версию переменной для другой среды.

    На самом деле вы можете получить доступ к этому COUNTS из другой функции в пакете:

    otherfunc <- function(...) {
      COUNTScopy <- get("COUNTS", envir = environment(myfunc))
      COUNTScopy <- COUNTScopy + 1
      assign("COUNTS", COUNTScopy, envir = environment(myfunc))
    }
    

    (Не стесняйтесь называть его COUNTS здесь, я использовал другое имя, чтобы подчеркнуть, что это не имеет значения.)

    Хотя использование get и assign немного неудобно, оно должно требоваться только дважды для каждой функции, которая должна это делать.

    Обратите внимание, что пользователь может добраться до этого, если это необходимо, но ему нужно будет использовать аналогичные механизмы. Возможно, это проблема; в моих пакетах, где мне нужна какая-то форма постоянства, подобная этой, я использовал удобные функции получения/установки.

  2. Вы можете поместить среду в свой пакет, а затем использовать ее как именованный список в функциях вашего пакета:

    E <- new.env(parent = emptyenv())
    myfunc <- function(...) {
      # do something
      E$COUNTS <- E$COUNTS + 1
    }
    otherfunc <- function(...) {
      E$COUNTS <- E$COUNTS + 1
    }
    

    Нам не нужна пара функций get/assign, так как E (ужасное имя, выбранное из-за его краткости) должно быть видно всем функциям в вашем пакете. Если вам не нужен доступ пользователя, оставьте его неэкспортированным. Если вы хотите, чтобы пользователи могли получить к нему доступ, экспортируйте его через обычные механизмы пакетов.

Обратите внимание, что в обоих случаях, если пользователь выгружает и перезагружает пакет, значение COUNTS будет потеряно/сброшено.

Я приведу третий вариант на случай, если пользователь хочет/нуждается в прямом доступе или вы не хотите использовать этот тип управления значениями в своем пакете.

  1. Заставьте пользователя предоставить его в любое время. Для этого добавьте аргумент к каждой функции, которая в нем нуждается, и попросите пользователя передать среду. Я рекомендую это, потому что большинство аргументов передаются по значению, но среды позволяют ссылочную семантику (передавать по ссылке).

    Например, в вашем пакете:

    myfunc <- function(..., countenv) {
      stopifnot(is.environment(countenv))
      # do something
      countenv$COUNT <- countenv$COUNT + 1
    }
    otherfunc <- function(..., countenv) {
      countenv$COUNT <- countenv$COUNT + 1
    }
    new_countenv <- function(init = 0) {
      E <- new.env(parent = emptyenv())
      E$COUNT <- init
      E
    }
    

    где new_countenv на самом деле просто функция удобства.

    Затем пользователь будет использовать ваш пакет как:

    mycount <- new_countenv()
    myfunc(..., countenv = mycount)
    otherfunc(..., countenv = mycount)
    

Спасибо, что поделились несколькими решениями. Это будет очень полезно, поскольку я продвигаюсь по своему пакету. Ваше здоровье!

Waldir Leoncio 15.01.2021 09:46

Другие вопросы по теме