Are script local functions (s:funcName()) unit testable?











up vote
5
down vote

favorite












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 hackish approach in @IngoKarkat's answer, you can take a look at my take on it here










share|improve this question









New contributor




Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
























    up vote
    5
    down vote

    favorite












    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 hackish approach in @IngoKarkat's answer, you can take a look at my take on it here










    share|improve this question









    New contributor




    Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
    Check out our Code of Conduct.






















      up vote
      5
      down vote

      favorite









      up vote
      5
      down vote

      favorite











      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 hackish approach in @IngoKarkat's answer, you can take a look at my take on it here










      share|improve this question









      New contributor




      Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      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 hackish approach in @IngoKarkat's answer, you can take a look at my take on it here







      vimscript functions






      share|improve this question









      New contributor




      Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      share|improve this question









      New contributor




      Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      share|improve this question




      share|improve this question








      edited 33 secs ago





















      New contributor




      Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      asked Nov 8 at 6:32









      Hakan Baba

      1283




      1283




      New contributor




      Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.





      New contributor





      Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






      Hakan Baba is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






















          2 Answers
          2






          active

          oldest

          votes

















          up vote
          5
          down vote



          accepted










          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.






          share|improve this answer






























            up vote
            3
            down vote













            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>zsd+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).






            share|improve this answer





















              Your Answer








              StackExchange.ready(function() {
              var channelOptions = {
              tags: "".split(" "),
              id: "599"
              };
              initTagRenderer("".split(" "), "".split(" "), channelOptions);

              StackExchange.using("externalEditor", function() {
              // Have to fire editor after snippets, if snippets enabled
              if (StackExchange.settings.snippets.snippetsEnabled) {
              StackExchange.using("snippets", function() {
              createEditor();
              });
              }
              else {
              createEditor();
              }
              });

              function createEditor() {
              StackExchange.prepareEditor({
              heartbeatType: 'answer',
              convertImagesToLinks: false,
              noModals: true,
              showLowRepImageUploadWarning: true,
              reputationToPostImages: null,
              bindNavPrevention: true,
              postfix: "",
              imageUploader: {
              brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
              contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
              allowUrls: true
              },
              onDemand: true,
              discardSelector: ".discard-answer"
              ,immediatelyShowMarkdownHelp:true
              });


              }
              });






              Hakan Baba is a new contributor. Be nice, and check out our Code of Conduct.










               

              draft saved


              draft discarded


















              StackExchange.ready(
              function () {
              StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fvi.stackexchange.com%2fquestions%2f17866%2fare-script-local-functions-sfuncname-unit-testable%23new-answer', 'question_page');
              }
              );

              Post as a guest
































              2 Answers
              2






              active

              oldest

              votes








              2 Answers
              2






              active

              oldest

              votes









              active

              oldest

              votes






              active

              oldest

              votes








              up vote
              5
              down vote



              accepted










              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.






              share|improve this answer



























                up vote
                5
                down vote



                accepted










                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.






                share|improve this answer

























                  up vote
                  5
                  down vote



                  accepted







                  up vote
                  5
                  down vote



                  accepted






                  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.






                  share|improve this answer














                  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.







                  share|improve this answer














                  share|improve this answer



                  share|improve this answer








                  edited 2 days ago

























                  answered 2 days ago









                  Ingo Karkat

                  11.3k2538




                  11.3k2538






















                      up vote
                      3
                      down vote













                      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>zsd+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).






                      share|improve this answer

























                        up vote
                        3
                        down vote













                        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>zsd+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).






                        share|improve this answer























                          up vote
                          3
                          down vote










                          up vote
                          3
                          down vote









                          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>zsd+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).






                          share|improve this answer












                          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>zsd+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).







                          share|improve this answer












                          share|improve this answer



                          share|improve this answer










                          answered 2 days ago









                          Christian Brabandt

                          15k2445




                          15k2445






















                              Hakan Baba is a new contributor. Be nice, and check out our Code of Conduct.










                               

                              draft saved


                              draft discarded


















                              Hakan Baba is a new contributor. Be nice, and check out our Code of Conduct.













                              Hakan Baba is a new contributor. Be nice, and check out our Code of Conduct.












                              Hakan Baba is a new contributor. Be nice, and check out our Code of Conduct.















                               


                              draft saved


                              draft discarded














                              StackExchange.ready(
                              function () {
                              StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fvi.stackexchange.com%2fquestions%2f17866%2fare-script-local-functions-sfuncname-unit-testable%23new-answer', 'question_page');
                              }
                              );

                              Post as a guest




















































































                              Popular posts from this blog

                              Guess what letter conforming each word

                              Port of Spain

                              Run scheduled task as local user group (not BUILTIN)