ntd23
Private, a lib to test private functions in 2022
Hi again ppl,
I really like the idea of test my private functions, mainly after read this article of @pragdave, but the library that he create is a little old and the article too, the last modification on code was 2 years ago . So, I have two questions:
- I get much risk using this lib, even it being “stable”?
- Is really worth test many private functions as I can or the article already get a strong oposit response?
Marked As Solved
pragdave
FWIW, my personal belief is that no one knows enough about software development to be able to make rules: it’s just too young a discipline.
But, I also believe that:
-
defining helper functions as private sets a demarcation layer in my code: if the code is to be used by a third party, then making a function private means that I’m free to change it without worrying about unintended consequences.
-
tests can be helpful for some functions. Which ones? The ones where there’s doubt; the ones which are pivotal to the rest of the code; the ones where the tests help you explore the problem.
These two things do not necessarily correlate. I often find myself writing internal (private) functions which need tests.
“But, Dave” say those in the never-test-a-private-function camp. “Why not just test them indirectly via the public functions that use them?”
Because, I say (outside of quotes), that is often a lot of work. The higher level the function, the more state it assumes, and the harder the test is to write. And, typically, when you invoke these higher level functions, you’re calling a number of lower-level ones. The test loses focus.
Let’s look at this a different way. Let’s think about the demarcation of code in GenServers.
When I write GenServers, I typically split them into two files. One file contains the server stuff: start_link, init, and all the handle_xxx calls, the other contains the implementation.
Here’s part of a typical server:
defmodule ExMdns.Server.ReceiveDNSPackets do
alias ExMdns.Impl.PubSub
alias ExMdns.Impl.ReceiveDNSPackets, as: Impl
use GenServer
require Logger
@me __MODULE__
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: @me)
end
def init(_) do
PubSub.join()
{ :ok, Impl.initial_state }
end
def handle_info({ :pubsub, msg=%PubSub.PacketReceived{}}, state) do
state = Impl.handle_incoming_packet(state, msg.data)
{ :noreply, state }
end
def handle_info({ :pubsub, _ }, state), do: { :noreply, state }
end
None of these functions contain any logic (apart from pattern matching on their parameters. Instead, they delegate to a second module that contains the actual logic.
The functions in this second module contain the implementation of the logic behind the server. The server module calls an implementation function with the server state as a parameter, along with any additional parameters from the handle_xxx. The implementation function returns an updated state.
Here’s the implementation corresponding to that server:
defmodule ExMdns.Impl.ReceiveDNSPackets do
alias ExMdns.Impl.PubSub
require Logger
def initial_state() do
[]
end
def handle_incoming_packet(state, raw_packet) do
try do
DnsPackets.decode(raw_packet.data)
|> publish_replies()
rescue
e ->
Logger.error("Ignoring invalid DNS packet: #{inspect raw_packet}")
Logger.error(inspect e)
end
state
end
defp publish_replies(packet) when packet.header.qr == :reply do
for answer <- packet.answers do
PubSub.publish(%PubSub.AnswerReceived{ answer: answer })
end
end
defp publish_replies(packet) do
Logger.info("ignoring query\n#{inspect packet, pretty: true}")
end
end
Why is this relevant to testing? Because I can test the implementation functions standalone, without firing up a GenServer. And it’s easy to do, because my tests just create the state to pass in, and then verify the state that comes back. In fact, I rarely run any genservers in test: My application.ex looks like this:
def start(_type, _args) do
children = case Mix.env do
:test ->
[ ]
_ ->
[
Server.Cache,
Server.ReceiveDNSPackets,
Server.UdpInterface,
# Registration,
]
end
opts = [strategy: :one_for_one, name: ExMdns.Supervisor]
Supervisor.start_link(children, opts)
end
What has this to do with private functions?
It’s an illustration of how I believe that testing convenience trumps following rules.
Typically, Elixir developers would put the implementation functions in the same module as the server stuff. They’d then struggle with whether they should be private. Purity insists that they should: they are accessed via the GenServer.call interface, and so shouldn’t be exposed externally. Make 'em private, and then test via GenServer calls. Or, make 'em public, test them directly, and feel guilty.
Instead, this two-module approach sidesteps the dogma. In my apps, nothing in a subdirectory of lib/ should be accessed externally. Instead, I have a top-level api.ex that delegates each API function, calling .Server files if the implementation of the function uses a GenServer, and calling the implementation files directly for stateless functions.
It then becomes immediately obvious to any external user of the code what they’re allowed to use: anything with more than one module name in the path is internal to my application. It may be public, but that’s for my convenience and not for your use.
Do I do this consistently? No: to be honest I’m experimenting with each app I write. This is where I am now.
TLDR; forget what people tell you is right. Do what you feel makes your code easier to work with.
Also Liked
dorgan
I have been using Patch — patch v0.16.0 quite a lot recently. It’s very well documented and behaves as advertised. It’s a powerful alternative to Mox though it can also be used alongside it.
Private functions frequently go untested because they are difficult to test. Developers are faced with a few options when they have a private function.
- Don’t test the private function.
- Test the private function circuitously by calling some public functions.
- Make a public wrapper for the private function and test that.
- Change the visibility to public and put a comment with some form of, “This is public just for testing, this function should be treated as though it’s private.”
Patch provides a new mechanism for testing private functions,
expose/2.
dorgan
Also, code doesn’t need to be complex to be hard to test.
The behaviour of a genserver with no public api that all it does is periodically call itself to perform some side effect is built on entirely private behavior that should not be used by any other module. I’ve had to deal with a bunch of them and they are always nasty to test.
Whatever approach you take, you’re ripping the module open and testing its internals, not any “observed behavior through public api”. Theres no public api.
The process calling itself every X time is not something you can test via public api, so giving up on idealistic notions of how code should be tested in favor of pragmatism is best. This point is also raised in the Testing Elixir book by Andrea Leopardi, in the Testing OTP chapter.
You can decide to move some functions to a different module and test that because now it’s no longer “private” and you can test it “normally”, but imho that’s just lying to yourself and if theres no other reason for that module to exist(to split complex logic into smaller files, to name an example), it’s just an indirection that brings very little value.
At that point, just test the behaviour callbacks directly by providing some state. This may sound controversial, but if theres no client api then no solution will appeal to everyone’s taste. FWIW this would be just like testing a custom Ecto type by calling the callbacks directly instead of creating a schema and persisting it to indirectly test dump and cast.
1000 times this
tcoopman
Tests are also code that you need to maintain. Delete tests that don’t have any value anymore. Nothing wasteful about that.
Last Post!
waseigo
I don’t mean to “necrobump” this thread, but I just want to express how thankful I am to everyone who posted in this thread. It’s a gold mine of opinions and nuance.
I was recently faced with the dilemma of whether I should be testing private functions, and this thread was part of my exploration.
I wrote a blog post that summarizes my finding, though I don’t know if it’s 100% correct, or somewhat opinionated: Things I've realized about testing Elixir code
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex










