Is it possible to retrieve all test names in a *_test.exs(s) file(s) or directory inside a test folder?

This is more of curiosity than something utterly needed but would be great if there was a not overly hacky way of accessing them?

Basically I have a bunch of entities in json files that are parsed on application startup and represent a bunch of elements in a game.

{
   "a_slug_1": {....},
   "a_slug_2": {....},
   ....
}

Each json file represents a given “collection” of elements that share a trait, but some elements appear in more than a single json file.

Now I’ve decided to add specific tests that cover the contents of each of these files, in way of interactions when used in through a game engine, so each test is specifically named with the “slug” that is the same key by which the element is accessed in the json file, and after startup parsing is accessible application wide.

I would like to have a way of with N *_test.exs files, getting the test names included in those files so that I could assert that each element in all of the json files is covered.

Let’s say I have a test file, example_test.exs with two tests, described as:

test("a_slug_1", ....) do .... end
test("a_slug_2", ....) do .... end

I would like to be able to extract from a filepath ...../test/examples_test.exs a list of

["a_slug_1", "a_slug_2"]

Is this possible? Thanks

Just place this after the closing “end” of your test module. Replace NameOfYourModule with the name of your module.

IO.inspect(
  NameOfYourModule.__info__(:functions)
  |> Enum.reduce([], fn {name, _arity}, acc ->
    case Atom.to_string(name) do
      <<"test " <> rest>> ->
        [rest | acc]

      _ ->
        acc
    end
  end)
)
1 Like

Thank you!
I ended up doing something like this:

defmodule Scroll.AllScrolls.Test do
  use ExUnit.Case, async: true

  
  @modules ["air_test.exs", "earth_test.exs"]
  @all_tested Enum.reduce(@modules, %{}, fn(module, acc) ->
    [{mod, _}] = Code.compile_file(module, "./test/scrolls/")
    Enum.reduce(mod.__info__(:functions), acc, fn({name, _arity}, acc_i) ->
      case Atom.to_string(name) do
        <<"test " <> rest>> -> Map.put(acc_i, rest, true)
        _ -> acc_i
      end
    end)
  end)

  test("all_scrolls_are_tested") do

    scrolls = Enum.each([:air, :earth, :fire, :light, :void, :water], fn(domain) ->
      :ets.select(domain, [{{:_, :_, :"$1"}, [], [:"$1"]}])
      |> Enum.each(fn(%Scroll{slug: slug}) ->
        assert @all_tested[slug]
      end)
    end)
  end

end

I would like to use @before_compile but that wouldn’t work when running just this particular test file (haven’t tried when running the whole suite, but anyway I’m happy with this solution)

Actually this gives me the opposite error, when trying to run the whole suite it will complain that those modules are already compiled.

Using instead Code.require_file works in both cases, but I need to use both the file (for requiring) and the module name (for applying the __info__ fun) as otherwise require_file returns nil when running the whole test suite, but returns [{mod, bytecode}] when executing only that single test file… The final version:

defmodule Scroll.AllScrolls.Test do
  use ExUnit.Case, async: true

  
  @modules [
    {Elixir.Scrolls.Air.Test, "air_test.exs"},
    {Elixir.Scrolls.Earth.Test, "earth_test.exs"}
  ]
  @all_tested Enum.reduce(@modules, %{}, fn({module, file}, acc) ->

    Code.require_file(file, "./test/scrolls/")
    Enum.reduce(module.__info__(:functions), acc, fn({name, _arity}, acc_i) ->
      case Atom.to_string(name) do
        <<"test " <> rest>> -> Map.put(acc_i, rest, true)
        _ -> acc_i
      end
    end)
  end)

  test("all_scrolls_are_tested") do

    scrolls = Enum.each([:air, :earth, :fire, :light, :void, :water], fn(domain) ->
      :ets.select(domain, [{{:_, :_, :"$1"}, [], [:"$1"]}])
      |> Enum.each(fn(%Scroll{slug: slug}) ->
        assert @all_tested[slug]
      end)
    end)
  end

end