Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: testing private functions in packages #1017

Open
djbpitt opened this issue Jun 23, 2020 · 14 comments
Open

Feature request: testing private functions in packages #1017

djbpitt opened this issue Jun 23, 2020 · 14 comments

Comments

@djbpitt
Copy link

djbpitt commented Jun 23, 2020

First, a big thank you to the developers for implementing support for testing packages.

#762 (comment) observes that private functions within packages cannot be tested, and discussed why this is both desirable and not desirable. Specifically:

A question stays un-answered : is a private function in a package should be unit-tested ? On one hand, I'd be inclined to test everything ; on the other hand, a private function is not an API, and does not offer service to package end-users ; so it is a package implementation detail, and there is no imperial reason to unit-test it.

Insofar as private functions are called by public or otherwise exposed components within a package, the private functions may be the lowest level of functionality, and therefore, it would seem, be sensible candidates for unit testing. For example, if public A calls private X and private Y and A fails a test, the locus of the failure could be in A itself or in B or C. Without being able to test B and C independently, we would seem to sacrifice one of the benefits of unit testing, that is, isolating a small unit of functionality. I understand why testing private components is challenging, but I don’t understand why it wouldn’t be beneficial for the developer of the package.

We should clearly avoid changing visibility of functions only for unit-tests, as we sometime do in Java.

I’m not a Java developer, and perhaps that’s why the reason for this is not clear to me. I would agree that we should not treat public and private functions equivalently, since the developer of the package distinguished between them for a reason, and the inaccessibility of a private function outside its package is part of what it is. But if, for example, we decide to require an attribute on the scenario for testing a private function that identifies it as private (so that private functions would continue to be hidden unless the test acknowledged explicitly that they were private, but needed nonetheless to be tested), why would it be a Bad Thing if the testing process would create accessible shadows of private functions for testing purposes?

@AirQuick
Copy link
Member

AirQuick commented Jun 23, 2020

Right now, you would need to write a small wrapper stylesheet like this where you expose some components in the included package. In that example, you can test the private my:square function in tested.xsl by specifying the wrapper external_tested.xsl in /x:description/@stylesheet.

@AirQuick
Copy link
Member

@jcronk
Just curious. Have you tested private functions using XSpec? If so, how?

@jcronk
Copy link

jcronk commented Jun 30, 2020

@AirQuick I haven't - in general, if a package has substantial enough private components that they need their own tests, that's a signal to me that I should separate that out into its own package with its own testable public API. A while back you mentioned maybe adding something like an x:include-helper tag, though, and I wonder if that could be used in a sideways manner to expose private components. I was going to follow up on that and create a feature request, but I hadn't gotten around to doing that yet.

@djbpitt
Copy link
Author

djbpitt commented Jun 30, 2020

I’m working with a package that exposes 3 public (final) functions (same name, different arity), which rely on 16 private "helper" functions. The private functions are not used or needed anywhere except as helpers for these three public functions, so putting them in the same package as the public ones seemed optimally legible and maintainable. Each of the helper functions provides a small, specific type of functionality, which made it easy to develop the code incrementally, and to refactor individual helper functions without having to touch anything else in the pipeline. It isn’t so much that the package has “has substantial enough private components that they need their own tests” as that small, specialized pipeline components, which in this case are private because they are only used as helper functions for the broader, public ones, seem like natural candidates for unit testing. As I wrote when I opened this issue (with apologies for garbling my Xs and Yx and As and Bs), if a public function breaks because of an error in one of the helper functions, unit testing at the level of the private, helper functions could make it easier to find and repair the error.

Perhaps this is wrong-headed. It feels natural to me, but I'm not a software engineer, and my understanding of Best Practice may well be misguided.

I also don’t understand what it would mean to put the private functions into their own package with their own testable public AP (see above). If the issue is that it isn’t currently possible to test private functions using XSpec, what might the testable public API look like, and why would that be preferable to being able to run regular XSpec tests over private functions?

I don’t have a strong opinion about whether it would be preferable to declare that private functions should be tested for the test suite as a whole (at the <x:description> level) or for the individual scenarios (on <x:scenario> or <x:call>), except that doing it at the scenario level, while requiring fussier set-up, would keep the developer’s attention focused on which functions are public and which are private. I recently refactored a package to add a public wrapper function around other functions that had previously been public, and I then switched the newly wrapped functions to private, so that they would be accessible only from within the wrapper function. This type of refactoring creates an opportunity for careless error on the part of the developer, which might be caught if private functions had to be flagged as such at the scenario level.

@jcronk
Copy link

jcronk commented Jul 10, 2020

I don't know, I think we're all still figuring this out, since the package concept is new to XSLT 3.0. That said, I think that there are some good reasons to only test the public API of a package - this way you're testing the visible behavior and not any of the implementation details, and that means that your tests don't necessarily have to change if the underlying implementation of your visible behavior changes. Wherever possible, I want to avoid spending any more time than I need to on writing or maintaining tests, and so I try to avoid situations where refactoring the parts of the code that I'm using will require rewriting the tests.

For me, what I've been doing is looking at it in terms of what I want to be able to test. If there's a component that should be private to my package, but I think its behavior is important enough that it needs testing, then to me that suggests that the component is a public part of an API that my package depends on, making it into its own package. So far, for my use cases, this has worked, but if I run into a situation where this is overly arduous or it doesn't make sense, then I'm not opposed to looking for a different solution.

@djbpitt
Copy link
Author

djbpitt commented Jul 10, 2020

@jcronk Thanks, Jeremy, for the response. As I wrote earlier, I'm not a software engineer, so perhaps I've simply misunderstood the purpose of testing, but I'll try to summarize my reasoning below, and if it is misguided, I'd be grateful for an explanation of why I shouldn't want to be able to do what I think I want to be able to do.

  1. Make it possible, but not obligatory, to test everything. This meets the needs of the greatest number of users because it supports both those who perceive a need to test private functions and those who don’t. Making it possible to test private functions by simply writing tests for them, as for public functions (that is, without having to create a wrapper to work around a limitation in the test architecture), does not require the developer to test private functions if the developer does not want to test them. We would have to think about how to report coverage, but that’s a complication that can be resolved in many ways, including ways that will not require those who prefer not to test private functions to alter their workflow.
  2. If I have understood correctly that unit testing is supposed to isolate and test a small unit of functionality because that helps the developer find where bugs are located, and if a large public function draws on many small private ones, the fact that the private ones are “implementation details”, and not “visible behavior”, seems beside the point. Specifically, if I change the implementation details, I won’t have to change the tests for the public function, since its behavior should not be affected (if it is, I’ve unwittingly changed more than the implementation details). But if I change implementation details in a few private functions and the only report I get of a bug is that the test for the public function that imports the private ones fails, I don’t know which private function introduced the bug that broke the public one. Isn’t finding the location of bugs that have higher-level consequences the main purpose of unit testing?

I agree, then, that we “want to avoid spending any more time than [we] need to on writing or maintaining tests, and so [we] try to avoid situations where refactoring the parts of the code that [we’re] using will require rewriting the tests”. But making it possible, but not required, to write tests for private functions meets that need because a developer who doesn’t want to test private functions 1) will not have to rewrite tests for the private functions because there aren’t any such tests, and 2) that same developer will not have to rewrite tests for the public functions that use the private ones because the visible behavior should not be impacted by implementation details.

Perhaps my rationale for deciding when to divide functionality over multiple packages and when to keep it in a single package is wrong-headed, and in the interest of learning whether I have misunderstood how such decisions are made, I can share that I am guided by two considerations:

  1. DRY situations: If a small function is used in more than one package that do not import each other, the shared function belongs in a separate library that both higher-level packages can import.
  2. Non-DRY: If a small function is used only in one higher-level function, it is most easily managed as a private function in the same package as the public function that calls it. Among other things, I sometimes find myself refactoring between inlining a bit of code and breaking it out into a separate function that I then call, depending on the organization is most legible and easiest to maintain. Sometimes inline is easier initially, and then I realize that the inline operation needs to do more, at which point breaking it out into a separate function makes it easier to understand. If the private function is in the same module as the public one that uses it, I know immediately that it is not imported elsewhere, which means that I can refactor it without having to leave that package. Splitting tightly-coupled functionality over multiple packages only because the testing environment refuses to test all of the functions otherwise seems to amount to letting the testing architecture dictate the organization of the code, instead of testing the organization that the developer chooses.

I am aware that I am the only participant in this conversation who is not a trained software developer, so if my reasoning is fallacious, I would appreciate learning what I have misunderstood. As I reread the discussion, though, I don’t understand how my proposal that it be possible to test private functions requires writing or rewriting extra tests, since the ability to test private functions does not confer a requirement to do so.

@AirQuick
Copy link
Member

AirQuick commented Jul 10, 2020

@djbpitt

Does this method work for you in some cases, or not work at all?

Right now, you would need to write a small wrapper stylesheet like this where you expose some components in the included package. In that example, you can test the private my:square function in tested.xsl by specifying the wrapper external_tested.xsl in /x:description/@stylesheet.

@AirQuick AirQuick reopened this Jul 10, 2020
@djbpitt
Copy link
Author

djbpitt commented Jul 10, 2020

@AirQuick The wrapper does not seem to work. Here is my version (filename xspec-wrapper.xsl):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:package exclude-result-prefixes="#all" version="3.0" xmlns:djb="http://www.obdurodon.org"
    xmlns:f="http://www.obdurodon.org/function-variables"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:include href="plot-lib.xsl"/>
    <xsl:expose component="*" names="djb:*" visibility="final"/>
</xsl:package>

plot-lib.xsl is a package (root element <xsl:package>), and I can run XSpec against it directly, testing its public (final) functions. To test the wrapper I created xspec-wrapper.xspec and copied the tests that work for plot-lib.xsl directly into a root element that begins:

<?xml version="1.0" encoding="UTF-8"?>
<x:description xmlns:x="http://www.jenitennison.com/xslt/xspec"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:djb="http://www.obdurodon.org"
    xmlns:f="http://www.obdurodon.org/function-variables" stylesheet="xspec-wrapper.xsl"
    run-as="external">

I did not yet write any tests for private functions. When I try to run XSpec against xspec-wrapper.xspec , it apparently fails to recognize the included XSLT file as XSLT:

djb@koala-4 lib % xspec xspec-wrapper.xspec
Transformation failed: Unknown configuration feature help
Saxon script not found, invoking JVM directly instead.

Creating Test Stylesheet...


Running Tests...
Testing with SAXON EE 10.0
Scenarios for testing function validate-points
..with valid input (at least three, no white-space, monotonic X)
Error at xsl:include on line 5 column 39 of xspec-wrapper.xsl:
  XTSE0165  Included document plot-lib.xsl is not a stylesheet
Error at xsl:include on line 5 column 39 of xspec-wrapper.xsl:
  XTSE0165  Included document plot-lib.xsl is not a stylesheet
at template scenario1-scenario1 on line 56 of xspec-wrapper-compiled.xsl:
     Focus: absent
     Local variables
        (All variables are null)
     invoked by xsl:call-template at file:/Users/djb/repos/plot/lib/./xspec/xspec-wrapper-compiled.xsl#50
at template scenario1 on line 42 of xspec-wrapper-compiled.xsl:
     invoked by xsl:call-template at file:/Users/djb/repos/plot/lib/./xspec/xspec-wrapper-compiled.xsl#29
at template main on line 15 of xspec-wrapper-compiled.xsl:
Included document plot-lib.xsl is not a stylesheet

*** Error running the test suite

@AirQuick
Copy link
Member

@djbpitt
Sorry to hear it didn't help. Somehow I had been thinking about only implicit packages. You're right, the wrapper doesn't seem to work with explicit packages.

The current XSpec implementation does not support testing private functions. But that's not because it's a bad practice. The lack of support is purely due to technical limitations. If there is a clean lightweight way to forcibly access private functions, I would like to utilize it for testing them. Unfortunately, I'm not aware of it.

@djbpitt
Copy link
Author

djbpitt commented Jul 21, 2020

Thank you, @AirQuick, for the follow-up. I’m not sure what qualifies as clean or lightweight, and I don’t know much about how XSpec works under the hood, but since you mention being at a loss to identify a way to test private functions, I wonder whether the following might suggest a way forward: Could the testing process create shadow copies of packages, designate all functions in the copy as public, and then test the no-longer-private functions by testing the copy? Private functions might call on other private functions in the same package, but treating them all as public won’t interfere with those calls, and private functions cannot be called from outside their own package, so the only changes needed to test a package thoroughly would be to the private functions in the package being tested at the moment (and not in other packages it might use). I could, I suppose, transforming the package explicitly and then test the transformed version, but doesn’t seem either clean or lightweight, and an alternative that lets the user test the package directly, with the copying happening in the background, would maintain consistency in the testing process by not requiring the user to pre-process packages explicitly and externally before testing them.

I fear that I probably don’t have the developer skills to implement this improvement myself, but if it seems like a constructive way forward, I would be eager to help in whatever way I can, whether by testing, or by helping, though these conversations, to frame the problem and identify a path toward a solution.

@AirQuick
Copy link
Member

Thanks @djbpitt for sharing the idea. I'm afraid that creating a temporary copy of the package would be costly and should generally be considered as the last resort.

What if you define the private visibility attributes as shadow attributes and what if the static parameters are supported by XSpec?
I mean something like this:

<xsl:package ...>
  <xsl:param name="internal:internal-component-visibility"
             as="xs:string"
             select="'private'"
             static="yes" />
  <xsl:function name="f:complex-number" 
                _visibility="{$internal:internal-component-visibility}">
<x:description run-as="external"
               stylesheet=".../package.xsl"
               ...>
  <x:param name="internal:internal-component-visibility"
           select="'public'"
           static="yes" />
  <x:scenario label="Calling a private function in the package for testing purposes">
    <x:call function="f:complex-number">

This seems to work on my end. (Note that x:param/@static is not supported by the current version of XSpec, but it will be easy to implement it.)

This way, the private functions are no longer private in its strict sense, but I thought it might possibly be an acceptable compromise.

@djbpitt
Copy link
Author

djbpitt commented Jul 26, 2020

@AirQuick Thank you for this creative idea. If you’d like to implement support of x:param/@static, I’d be happy to give it a try. It sounds like a not very burdensome way to support testing private functions.

Looking forward, though, I would normally want to weigh the costs and benefits of competing solutions, and not identify any of them as a last resort because of a cost until I had assessed the costs of the alternatives. In the interest of making that comparison explicit, it seems as if using a static parameter might entail the following costs, although they are costs in developer effort, and not in computation:

  1. The static parameter would be there only to accommodate XSpec. Ideally, I think, XSpec would test whatever the developer writes, and any accommodation required by XSpec would be constrained to the test file, and not to the package being tested. I understand that perhaps we are not in an ideal situation, but I think having to add extra code to the package that will be used only by XSpec is nonetheless a cost, even if a small and one-time one (that is, once per package).
  2. An overriding value for the static parameter has to be supplied every time the tests are run on any package where the developer wants to test private functions. The extra effort is small, but it is nonetheless something that a human has to do repeatedly. (The cost in efficiency of creating a shadow copy is also, to be sure, paid every time the tests are run, although perhaps that inefficiency might also be small.) I could write a shell script or other wrapper to set the value of the static parameter when I run tests, but that would come at the expense of having to introducing a (one-time) complication into configuring the testing scenario—another small complication, but nonetheless a cost.
  3. Of possible relevance is that the cost of shadowing (copying) would come into play only when the developer wants to test private functions in a package. It would not impinge on the performance for other users, whether those who don’t use packages or those who don’t believe in testing private functions.

I appreciate that the static parameter might, after weighing the costs and benefits of all of the options (those above and those I may not have thought of), turn out to be the best compromise, and of course I’ll trust your judgment about the best way to meet the need for testing private functions. That the static parameter approach can be met with one-time costs (in each package, and in a wrapper to set the overriding value when running tests) does seem, when I consider the issues above, superior to an efficiency cost that is paid on every run, and that cannot be converted to a one-time cost. But I thought I should mention that the fact that copying comes with a cost in efficiency does not mean that the static parameter approach does not also come with a cost, and that it might be useful to compare those costs explicitly, as I’ve tried to do here.

In any case, as I write above, I’ll be happy to test the static parameter approach once it has been implemented, and if it works as expected and you consider it the best available solution, we could close the issue.

@AirQuick
Copy link
Member

AirQuick commented Jul 29, 2020

@djbpitt
You're right, the static parameters will still cost something. But at least it is based on the standard spec, so you will be able to rely on it even when you switch from XSpec to another testing framework.

I opened #1120. Can you try it and let us know what you find out in it? Your possible use case is demonstrated in test/external_xslt-package_arith_private.xspec.


Another thought...

I guess the most transparent way in your case would be to have a custom XML parser and plug it into Saxon. The parser would set @visibility as you wish and would fool every player into thinking that the package really consists of that @visibility. I'm not aware of such parser readily available, though. If you find such one, you will probably be able to plug it into Saxon on XSpec via SAXON_CUSTOM_OPTIONS (command line) or saxon.custom.options (Ant).

@djbpitt
Copy link
Author

djbpitt commented Jul 29, 2020

@AirQuick Thank you! I’m at (that is, “at”) Balisage intensively this week, but I’ll check #1120 out over the weekend and report back. Plugging a custom XML parser into Saxon is beyond my ability, but along those same lines I could, in a shell script, transform my real library (using XSLT) to one that changes the visibility of the functions, creating my own shadow copy, and then run the tests against that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants