Testing private methods in Raku
Asked Answered
P

3

10

Is there a way to test private methods in Raku?

I understand that one should ideally define their tests targeting the public methods, but is there a way to do it "the wrong way"? :)

I initially thought about defining a subclass for the Testing that inherited from the class I wanted to test and do the tests there, but it seems that private methods are not inherited.

Then I saw the 'trusts' routine, but I wouldn't want to reference a Testing class on any of the classes of the code.

Is there something like changing the 'private' property of a method via introspection?

What would be the best way to call/test a private method?

Phenyl answered 18/7, 2020 at 22:33 Comment(3)
A search for "[raku] test private". I'm going to hang fire on marking your question as a dupe because I'm hopeful someone can provide a short version of @Kaiepi's answer -- hopefully just a couple lines of code -- and then refer to their SO as a detailed explanation of how one might have independently come to that solution. Perhaps they will answer your question; perhaps you can work out the short answer by reading their SO and answer your own question; perhaps someone else can. (I'm about to go to sleep. :))Lipophilic
Thanks @raiph!, that's what I finally did. I created a shorter version and added a link to the more detailed explanation.Phenyl
.oO ( Three answers. Sometimes (apparent) dupes are really useful. )Lipophilic
L
5

A fresh cup of tea and @Julio's and @JJ's answers inspired the following:

class SomeClass { method !private ($foo) { say $foo } }

use MONKEY-TYPING; augment class SomeClass { trusts GLOBAL }

my SomeClass $some-class = SomeClass.new;

$some-class!SomeClass::private(42); # 42

My solution tweaks the class using monkey typing. Monkey typing is a generally dodgy thing to do (hence the LOUD pragma). But it seems tailor made for a case just like this. Augment the class with a trusts GLOBAL and Bob's your Uncle.

Raku requires the SomeClass:: qualification for this to work. (Perhaps when RakuAST macros arrive there'll be a tidy way to get around that.) My inclination is to think that having to write a class qualification is OK, and the above solution is much better than the following, but YMMV...

Perhaps, instead:

use MONKEY-TYPING;
augment class SomeClass {
  multi method FALLBACK ($name where .starts-with('!!!'), |args) {
    .(self, |args) with $?CLASS.^find_private_method: $name.substr: 3
  }
}

and then:

$some-class.'!!!private'(42); # 42

I've used:

  • A multi for the FALLBACK, and have required that the method name string starts with !!!;

  • A regular method call (. not !);

  • Calling the method by a string version of its name.

The multi and !!! is in case the class being tested already has one or more FALLBACK methods declared.

A convention of prepending !!! seems more or less guaranteed to ensure that the testing code will never interfere with how the class is supposed to work. (In particular, if there were some call to a private method that didn't exist, and there was existing FALLBACK handling, it would handle that case without this monkey FALLBACK getting involved.)

It should also alert anyone reading the test code that something odd is going on, in the incredibly unlikely case that something weird did start happening, either because I'm missing something that I just can't see, or because some FALLBACK code within a class just so happened to use the same convention.

Lipophilic answered 19/7, 2020 at 18:9 Comment(4)
Great answer! I think the first method you propose is much cleaner than the second. Even with the MONKEY pragma, I still see it as a better solution than using introspection for this particular case (testing). Also, my answer about introspection was already covered in a more verbose way in the other SO question. So I'm accepting your answer, which covers the problem in a different way and let me learn cool things. Thanks!Phenyl
"I think the first method you propose is much cleaner than the second. " Me too. I tried four approaches: custom op was a bust but led me to the idea of a mixin; that was a bust too but led me to the idea of augment + FALLBACK; that was kind of a bust too but led me to the ultimate "aha!" of augment + trusts GLOBAL. (That led to the "requires qualification" wrinkle, which was enough to lead me to include the FALLBACK as a fallback.) Anyhoo, I'm very glad you asked the Q, pleased with (the first part of) my answer, and delighted it shows just how valuable TIMTOWTDI thinking can be. :)Lipophilic
I didn't even know you could trust scopes, besides classes. I don't think it's in the documentation, and I'm baffled to see you come up with this stuff. Hats off (again)Commercialize
@Commercialize I nurture my inner mini Socrates (I know I know nothing, except I'll die drinking others' judgment of a wisecrack). I never trust anything I think, privately or otherwise. Especially not due to reading something. Double especially a single source. Triple my interpretation of things. But I do let it soak in. I read ["trusts is a keyword that can be used from within packages to permit another package to call its private methods". (Thank you .@kaiepi.) It worked for GLOBAL. \o/Lipophilic
P
8

This can be done using introspection.

Consider this is the class you want to test:

class SomeClass {
    has Int $!attribute;

    method set-value(Int $value) returns Nil {
        $!attribute = $value;
        return;
    }

    method get-value returns Int {
        return $!attribute;
    }

    # Private method
    method !increase-value-by(Int $extra) returns Nil {
        $!attribute += $extra;
        return;
    }
}

You may create a test like this:

use Test;
use SomeClass;

plan 3;

my SomeClass $some-class = SomeClass.new;
my Method:D $increase-value = $some-class.^find_private_method: 'increase-value-by';

$some-class.set-value: 1;
$increase-value($some-class, 4);
is $some-class.get-value, 5, '1+4 = 5';

$increase-value($some-class, 5);
is $some-class.get-value, 10, '5+5 = 10';

my SomeClass $a-new-class = SomeClass.new;
$a-new-class.set-value: 0;
$increase-value($a-new-class, -1);
is $a-new-class.get-value, -1, '0+(-1) = -1; The method can be used on a new class';

done-testing;

You first create an instance of the class and the use ^find_private_method to get its private Method. Then you can call that Method by passing an instance of a class as the first parameter.

There's a more complete explanation on this answer:

How do you access private methods or attributes from outside the type they belong to?

Phenyl answered 19/7, 2020 at 11:59 Comment(1)
You can also call it as a method. $some-class.$increase-value(4)Sideway
L
5

A fresh cup of tea and @Julio's and @JJ's answers inspired the following:

class SomeClass { method !private ($foo) { say $foo } }

use MONKEY-TYPING; augment class SomeClass { trusts GLOBAL }

my SomeClass $some-class = SomeClass.new;

$some-class!SomeClass::private(42); # 42

My solution tweaks the class using monkey typing. Monkey typing is a generally dodgy thing to do (hence the LOUD pragma). But it seems tailor made for a case just like this. Augment the class with a trusts GLOBAL and Bob's your Uncle.

Raku requires the SomeClass:: qualification for this to work. (Perhaps when RakuAST macros arrive there'll be a tidy way to get around that.) My inclination is to think that having to write a class qualification is OK, and the above solution is much better than the following, but YMMV...

Perhaps, instead:

use MONKEY-TYPING;
augment class SomeClass {
  multi method FALLBACK ($name where .starts-with('!!!'), |args) {
    .(self, |args) with $?CLASS.^find_private_method: $name.substr: 3
  }
}

and then:

$some-class.'!!!private'(42); # 42

I've used:

  • A multi for the FALLBACK, and have required that the method name string starts with !!!;

  • A regular method call (. not !);

  • Calling the method by a string version of its name.

The multi and !!! is in case the class being tested already has one or more FALLBACK methods declared.

A convention of prepending !!! seems more or less guaranteed to ensure that the testing code will never interfere with how the class is supposed to work. (In particular, if there were some call to a private method that didn't exist, and there was existing FALLBACK handling, it would handle that case without this monkey FALLBACK getting involved.)

It should also alert anyone reading the test code that something odd is going on, in the incredibly unlikely case that something weird did start happening, either because I'm missing something that I just can't see, or because some FALLBACK code within a class just so happened to use the same convention.

Lipophilic answered 19/7, 2020 at 18:9 Comment(4)
Great answer! I think the first method you propose is much cleaner than the second. Even with the MONKEY pragma, I still see it as a better solution than using introspection for this particular case (testing). Also, my answer about introspection was already covered in a more verbose way in the other SO question. So I'm accepting your answer, which covers the problem in a different way and let me learn cool things. Thanks!Phenyl
"I think the first method you propose is much cleaner than the second. " Me too. I tried four approaches: custom op was a bust but led me to the idea of a mixin; that was a bust too but led me to the idea of augment + FALLBACK; that was kind of a bust too but led me to the ultimate "aha!" of augment + trusts GLOBAL. (That led to the "requires qualification" wrinkle, which was enough to lead me to include the FALLBACK as a fallback.) Anyhoo, I'm very glad you asked the Q, pleased with (the first part of) my answer, and delighted it shows just how valuable TIMTOWTDI thinking can be. :)Lipophilic
I didn't even know you could trust scopes, besides classes. I don't think it's in the documentation, and I'm baffled to see you come up with this stuff. Hats off (again)Commercialize
@Commercialize I nurture my inner mini Socrates (I know I know nothing, except I'll die drinking others' judgment of a wisecrack). I never trust anything I think, privately or otherwise. Especially not due to reading something. Double especially a single source. Triple my interpretation of things. But I do let it soak in. I read ["trusts is a keyword that can be used from within packages to permit another package to call its private methods". (Thank you .@kaiepi.) It worked for GLOBAL. \o/Lipophilic
C
3

Besides using introspection, you can try and use a external helper role to access all private methods and call them directly. For instance:

role Privateer {
    method test-private-method ( $method-name, |c  ) {
    self!"$method-name"(|c);
    }
}

class Privateed does Privateer {
    method !private() { return "⌣"  }
}

my $obj = Privateed.new;
say $obj.test-private-method( "private" );

The key here is to call a method by name, which you can do with public and private methods, although for private methods you need to use their special syntax self!.

Commercialize answered 19/7, 2020 at 16:12 Comment(3)
.oO ( IWBNI downvoters said why they downvote. ) Your solution requires modifying the class's construction, and @Phenyl had written "I wouldn't want to reference a Testing class on any of the classes of the code." I interpreted that as being against a solution such as the one you suggest. But I agreed with the spirit of your answer, as I saw it, namely to think anew. Add a cup of tea and voila. So I'm upvoting your answer, because without your answer, and the friendly ways of looking at things you inspire, I doubt I would have come up with mine.Lipophilic
That's an interesting approach. I think it is cleaner than using the trusts directly. But it still has the drawback of having to modify the original class. Anyways, not a reason to downvote. Thanks!Phenyl
@Lipophilic That's right, but "trusts" also needs a change. I have checked "but" and "does", which wouldn't need modification, but they don't seem to be able to access privae stuff.Commercialize

© 2022 - 2024 — McMap. All rights reserved.