Options:
a. Obliterate protected and private during testing with a concise macro (is this possible?)
# visibility_macros.cr
macro _private do
{% if !flag?(:test) && !env("TEST") %}
private
{% end %}
end
macro _protected do
{% if !flag?(:test) && !env("TEST") %}
protected
{% end %}
end
# foo.cr
require "./visibility_macros"
class Foo
_private def self.foo # or a variation thereof
end
_protected def bar
end
end
b. Break up methods into include and extend “concern” modules. Downside is more layers of abstraction.
c. (Your idea here)
Another option could be to make use of Macro Hooks to do some dirty tricks. By dirty i mean what i’m doing with arg expansion. I’m sure there are much better and safe approaches on dealing with that, so take this below code as an experimental or inspration only
module TestHelper
macro included
{% if flag?(:test) && env("TEST") %}
{% verbatim do %}
macro method_added(method)
{% if method.visibility == :private %}
def {{method.name.id}}({{method.args.stringify[1...-1].id}})
{{method.body}}
end
{% end %}
end
{% end %}
{% end %}
end
end
class Foo
include TestHelper
def foo(a : Int, b : String)
p! a, b
end
private def bar(s : String, v = true)
p! s,v
end
end
f = Foo.new
f.bar("voila") # this would compile only when conditions specified in TestHelper are met
IMO it depends on the context of what your project is doing/is intended for. Regardless tho, having to have special logic within the non-test code itself is a bit of a code smell.
I personally think testing private/protected methods is a bit of a bad practice most of the time. The main reason being these methods are usually used as implementation details within a type while the public methods are the ones you actually interact with, which should be the main focus of your testing.
By only testing the public methods, you still are asserting the type functions as it should, since the private/protected methods will be used internally, but without tightly coupling your tests to the internals of the type. E.g. in an ideal world you could go thru and refactor the internal implementation and use your tests of the public API to validate your changes.
However, context also plays a role in this topic. If your code is for private use only, and won’t be used by others outside of you/your team or whatever, then you have a bit more freedom in that you could just make all your methods public and use doc comments or something to denote they’re internal. In a similar vein, the :nodoc: doc comment can be useful for this, even when your code may be used by others, in that it can allow a method to be public, but not part of the public API.
In the end it’s your decision, and there is no “right” answer, but might be worth taking a step back and ask yourself if it’s actually worth trying to hack something in to do this, or just take a different approach/refactor the code to avoid the issue entirely.
I second that. If a method is private, it’s typically not a good target for tests. If it was, it would perhaps rather be public.
Anyway, I’d most certainly recommend about any special casing for test mode with compile time flags and such. That’s just adding complexity and can lead to weird behaviour when your code is different in a test scenario.
If you must test a private method, the best course of action IMO is to reopen the type in the test code and add a method of the same name but with _public suffix which calls the private method.
Trust me, if someone write tests often enough, a lot of times, there will be test private method(it probably the most easy/simple way to ensure logic correct), but, still have reason to keep this method private.
This. I’ve used subclassing the class under test in languages that doesn’t allow reopening in order to get at protected methods, but in that world private might be a major problem in testing. Being able to reopen is one of the things I love about Crystal.