11

Consider a script local function in a plugin/myplugin.vim file. For example

myplugin.vim:

func! s:someFunction()
    return 4
endfunc

I would like to unit test this function. In the simplest example I would like to execute something like the following in ANOTHER script. Without modifying the original script.

test_myplugin.vim:

if s:someFunction() != 4
    throw "Error"
endif

This obviously does not work because s:someFunction is not visible from outside of the myplugin.vim.

Is there a way to unit test these script local functions?


Some options that come to mind:

  1. Move the function to be unit tested to autoload directory and rename it myplugin#someFunction(). Inspiration is from :help write-library-script. I do not prefer this approach. The function(s) are not reused they are not supposed to be a library. I also do not own the plugin/myplugin.vim.
  2. Apparently vim prepends a <SNR>123_ like string to script-local-functions. (123 can be any number). I could get the list of all functions with the :function command. Then find the full function name that matches <SNR>\d+_someFunction and call that function by name. Looking at this question it looks like calling functions is possible once you know their string name.

Update:

If anyone is interested in an implementation of the hacky approach in @IngoKarkat's answer, you can take a look at my take on it here

2 Answers 2

8

TL;DR: Yes, but you probably shouldn't (in general)

Other answers

Christian's answer offers two approaches that modify myplugin.vim in order to expose the script-local function (either as a Funcref or just the SID that allows you to obtain a Funcref).

Having to extend a plugin just for testing purposes is not nice, and as I understand, you're reluctant to change myplugin.vim, too.

The hacky way

What I have done occasionally for testing is parsing the output of :scriptnames, matching plugin/myplugin.vim in order to obtain the SID. With this, you can build a Funcref and invoke any script-local function:

:let sid = ... " Parse :scriptnames output
:let MyFuncref = function("<SNR>" . sid . "_someFunction")
:echo call(MyFuncref, [])

The purist's answer

Script-local functions are equivalent to private methods in other languages (e.g. Java). There, there are similar discussions about lifting access restrictions (e.g. to protected) for unit testing, and many people think that this is a bad idea, that unit tests should only rely on the object's public API, and that testing private implementation details leads to brittle tests and impedes refactoring.

Therefore, my first impulse would be to expose the function as an autoload function. (In fact, I almost never put functions in the plugin script; everything is (script-local or public) in an autoload script.)

I don't agree with your apprehension of turning these into "library functions". Most Vim plugins don't expose a Vimscript API at all (just mappings and custom commands). Even if you have a public API, you can still differentiate between official public functions and autoload functions exposed for unit testing via other means:

  • Only have API documentation for the official functions, or mention the intended "visibility" (public / private) in the attached function documentation.
  • Segregate into different autoload scripts, e.g. autoload/myplugin.vim for the public API and autoload/myplugin/impl.vim for the functions to be unit-tested.
3
  • can you do this for variables? (as opposed to functions)
    – Mass
    Commented Mar 4, 2019 at 18:06
  • @Mass No, variables are truly private. You'd have to define an accessor function for them. Commented Mar 4, 2019 at 20:16
  • 1
    @IngoKarkat I've been ending up on your SO answers about advanced stuff in Vim a awful lot of times recently, and I just wanna say: thank you so deeply for all your detailed, thoughtful and welcoming answers. The Vi community is extremely lucky to have you. (I know it's not really the place for this, I'll delete the comment if you want)
    – adamency
    Commented Oct 20, 2023 at 0:57
4

I think there are two different possibilities to achieve what you want.

  1. You can create a global funcref to your script local function and then call that funcref. Something like this:

    :let g:MyCustomFuncref=funcref("<sid>MyScriptLocalFunction")
    

(Note, a funcref variable must start with a capital letter).

  1. Parse the script number inside your s:Function and make it available so other functions can call it, even when not defined inside your script-local file.

      :fu! <sid>GetSID() "{{{1
           return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_GetSID$')
      endfu
      let g:mysid = <sid>GetSID()
    

I have done this in my changes Plugin. You can then dynamically construct the function name and call it. That is because script-local functions are not really script-local, they are just namespaced.

In my csv plugin I used to set the foldexpr to a script local function.

Now either possibility will possibly create a new global variable which you might want to avoid. What I have been doing in the past is to only allow access to those variable, if the plugin has been run in debug mode. (So set a configuration variable to enable debug mode, after which you have access to those special variable and can dynamically call all needed script-local functions).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.