defmodule DescribeFunction do @moduledoc """ Inspired by a [conversation on Twitter][twitter], `DescribeFunction` adds an additional layer of TDD to your Elixir application. The `describe_function/2` macro validates that the function you are testing is defined. This will catch untested changes to your application's API at a high level in your test suite, resulting in a single, straightforward error, rather than many errors that might occur at a more removed level of your API. ## Usage defmodule ExampleTest do use ExUnit.Case, asnyc: true import DescribeFunction describe_function &Example.hello_world/0 do test "hello_world functions as expected" do assert Example.hello_world() == "hello world" end end end Assuming your `Example` module implements `hello_world/0`, your test will pass or fail, based on its implementation. On the other hand, if `Example.hello_world/0` is undefined, running your tests will raise ** (DescribeFunction.UndefinedFunctionError) function not defined: &Example.hello_world/0 [twitter]: https://twitter.com/zorn/status/1572788455507374082 """ @doc """ This macro expands on the functionality of `ExUnit.Case.describe/2` by validating that the function passed as its first argument exists. This will catch untested changes to your application's API at a higher level in your test suite, resulting in a single, straightforward error, rather than many errors that might occur at a more removed level in your API. ## Usage defmodule ExampleTest do use ExUnit.Case, asnyc: true import DescribeFunction describe_function &Example.hello_world/0 do test "hello_world functions as expected" do assert Example.hello_world() == "hello world" end end end Assuming your `Example` module implements `hello_world/0`, your test will pass or fail, based on its implementation. On the other hand, if `Example.hello_world/0` is undefined, running your tests will raise ** (DescribeFunction.UndefinedFunctionError) function not defined: &Example.hello_world/0 """ defmacro describe_function(f, do: describe_block) do quote location: :keep do describe(func(unquote(f)), do: unquote(describe_block)) end end @doc false def func(f) when is_function(f) do [module: module, name: name, arity: arity, env: _, type: _] = Function.info(f) if function_exported?(module, name, arity), do: inspect(f), else: raise(DescribeFunction.UndefinedFunctionError.for_function(f)) end end