spinlock99

spinlock99

Testing Private Functions

I always wind up not having private functions in my modules because you can’t test them. I really have a strong preference for TDD and - at this point - I can barely write a function without having a test first. I recently had a function that is stupid simple:

def char_value(char), do: char - ?@

The point is to convert an uppercase letter to a numerical value (i.e. A == 1, B == 2, etc…). This is just a helper function that I’d never want to expose outside of the module, it’s logic is very simple, but It was tricky (for me) to figure out that ?@ == 64 and gave me the value I want. I suppose I could have just made the function char - ?A + 1 but this is just an example of when a function (which should be private IMO) benefits from being tested.

Am I just thinking about private functions wrong? To me, a private function is a helper for simplifying the complexity of public functions in the module. Should I just think of private functions as functions that are themselves simple enough to not need a test?

First 10 of 30 Posts! Switch mode

sodapopcan

sodapopcan

You could start a holy war with questions like this! :upside_down_face:

I’m a TDDer myself and I do not see value in testing private functions in that they mostly appear later as I’m refactoring. However, sometimes you can find yourself with a single function that does a lot of stuff. In those cases, you can always move these function to a sub “private” module (@moduledoc false) and write tests for them there (though personally when I do this I just end up deleting the tests when I’m done as I just want tests for my public interface).

In the case of your example, that is a candidate to just be moved to an external helper module. It’s one of those functions that does one small thing very well, so there’s no harm in other people using it, especially since it has tests! However, if you really want to keep it private in your current module, then the tests for whatever public function you are using this for will certainly cover it. If you really want to start with just a test for that one line, then I’d do what I suggested above (and sanctioned by Sandi Metz herself ;)), make it public, write a test for it and when you’re done, or at least when you have public functions that consume it, delete the test and make it private.

Otherwise, keeping tests for things at this level of granularity makes reafactorting a massive pain. It’s also exhausting for readers (although I guess no one reads code anymore and LLMs don’t get exhausted /s)

LostKobrakai

LostKobrakai

In isolation it surely makes sense to have tests for the behaviour. But given finite time it makes more sense to test from the perspective of what the software brings value to instead of the details. Like this char_value function will be used somewhere. Depending on how important it is to that user testing will include what char_value does.

Then there are layers in software. Just like with third party dependencies you can have first party dependencies by having modules in your codebase sitting at lower levels of abstraction tested in isolation to the higher levels - e.g. to keep test complexity down. That‘ll be different modules though, which can have public functions. If you‘re looking for that I‘d strongly consider not leaving them inline with the higher level abstraction code.

Vidar

Vidar

For a concrete test example:

defmodule MyApp.CharConverter do
    def uppercase_to_numerical(char) when char in ?A..?Z do
      char_value(char)
    end

    defp char_value(char), do: char - ?@
  end

  defmodule MyApp.CharConverterTest do
    use ExUnit.Case, async: true

    alias MyApp.CharConverter

    describe "uppercase_to_numerical/1" do
      test "converts A to 1" do
        assert CharConverter.uppercase_to_numerical(?A) == 1
      end

      test "converts Z to 26" do
        assert CharConverter.uppercase_to_numerical(?Z) == 26
      end
    end
  end

As for private function thinking I also think of them in terms of keeping implementation details private for safer refactoring if needed later.

katafrakt

katafrakt

TW: controversial statements ahead :stuck_out_tongue:

In languages like Elixir, where you don’t get truly private modules (another example of that would be Ruby), private functions don’t matter as much as people give them meaning. Say, you write a code like this:

defmodule Note do
  def normalize_title(title)
    title
    |> normalize_case()
    |> replace_illegal_characters()
  end

  def normalize_case(string), do: [something]
  def replace_illegal_characters(string), do: [something]
end

In my experience there is a 95% chance someone will call that out during the code review, because normalize_case and replace_illegal_charecters should be private. But do it like that, and nobody will bat an eye :wink:

defmodule Note do
  def normalize_title(title)
    title
    |> StringHelper.normalize_case()
    |> StringHelper.replace_illegal_characters()
  end
end

Now it’s okay. You can test the StringHelper in isolation, because its functions are “legally” public. This is encountered in codebases more often than people think.

This matters if you are writing a library, i.e. you don’t control how and by whom your code is used. In this scenario it makes sense to try to keep public API surface as small as possible. Make the function public and you never know if someone uses it, so even renaming it is a breaking change.

But in a classic “work project” it’s pretty much a cargo cult. You control the “supply” and “demand” side, both definition and usage. You can simply grep a project for usages before making a change - and it will be safe. Private vs public remains mostly a guideline.

And if someone needs your private function outside of the module, they will simply make it public. Another things I’ve witnessed dozens of times.

The only thing maybe keeping the sense of having private functions id adhering to another cargo cult - that every public function needs to be unit testing. Then having some function private serves the purpose of avoiding too many, too brittle tests.

Vidar

Vidar

I know both future Vidar and Claude are both forgetful and clueless, so providing a private vs public even as a guideline do provide value for me. Sure keeping the public API consistent can be checked otherwise, but for me using private is a straight forward solution. As for Claude I have a hard rule to respect the intent of private functions which is a clean way to do it. Trying to explain Claude which functions are actually public API, and which ones can be changed for every single module in a refactoring, does not seem very attractive to me.

Private is a clear signal not to use the function outside of the module. If someone decide to use it anyway then that is on them, and any later refactoring that breaks their code will be their mess to clean up.

tfwright

tfwright

I don’t think there is any real tension here. TDD is a method for writing code. Private functions are a convention for maintaining code. If TDD helps you build out working implementations, then write it that way and then make the functions private later and remove the tests.

But in my view the value of TDD is primarily as a tool to help me think about the API (since writing the test requires defining the inputs outputs and name). dbg is way more useful for sanity checks that I’ve implemented something correctly, as you describe in your example.

spinlock99

spinlock99

I think this is the answer. If I want to use a private function, I have to test it through the interface I’m building. That’s a really good constraint for the type of private functions I’m thinking about.

Asd

Asd

It is completely reasonable to want to test private functions. Making function invisible for other modules doesn’t mean that it doesn’t need testing. It is possible to test private functions with Repatch.

For example, you want to test to_number/1 from this module

defmodule Calculator do
  def add(left, right) do
    to_number(left) + to_number(right)
  end

  defp to_number(x) when is_binary(x) do
    String.to_float(x)
  end

  defp to_number(x) when is_number(x) do
    x
  end
end

First, initialize Repatch in test/test_helper.exs

ExUnit.start()
Repatch.setup()

Second, in test do something like

defmodule CalculatorTest do
  use ExUnit.Case, async: true
  use Repatch.ExUnit
  import Repatch, only: [private: 1]
  
  setup do
     Repatch.spy(Calculator)
  end

  test "to_number/1" do
    assert 1234.0 == private Calculator.to_number("1234")
    assert 1234.5 == private Calculator.to_number("1234.5")
    assert 1234 == private Calculator.to_number(1234)
    assert 1234.5 == private Calculator.to_number(1234.5)
  end
end
tfwright

tfwright

I don’t think this is unquestionably true. Maybe debatable, but I’d strongly argue that one of the underrated benefits of unit tests is that they document what a module does. If you maintain tests for private functions you are now also documenting how it does it. Not terrible, but why do that when you can write a test for the caller than exercises the private function?

Asd

Asd

I understand your point and I find it completely valid and right. However, I personally don’t write unit tests as documentation, and I also use ExUnit as a generic testing framework for property testing, behavior testing, performance testing, stress-testing, fuzzing etc. Even for functionality tests, I find it useful to be able to test private functions

For example, I have a module

defmodule Statistics do
  def top_users(source, amount) do
    source
    |> download()
    |> parse_stream()
    |> extract_users()
    |> calculate_rating()
    |> take_top(amount)
  end
  
  ...
end

All functions except top_users are private, because I don’t want other modules to rely on this logic. But I want to be able to test them to make sure that each step of this pipeline is free of errors and covers every corner case I come up with.

Other options are to

  1. Make these functions public (or move them to separate module). Then some other module might be able to call them, creating a dependency I don’t want
  2. Test only top_users. Then I will have to create an input for every possible corner-case, having a lot of work to do

Again, writing code in a way that you never have to test a private function, or writing a code in a way that every ExUnit test is more than just a test, but it is a documentation of the feature, is good and I accept these approaches. However, I prefer to write the code the other way and I prefer to set up the limits of the system myself

Where Next?

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2976 91332 914
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
zachdaniel
Introducing AshStorage! Attachment and file management that slots directly into your resources :smiling_face_with_sunglasses: I had hope...
New

We're in Beta

About us Mission Statement