ExUnit log_level macro
June 27, 2025
Recently I was working on an Elixir code path that is expected to emit specific info
logs for short term auditing purposes. It was nearly impossible to test because my test environment’s log level is set to :warning
and above. While this is a fairly sane default to prevent noisy log output on errors, I needed a way to temporarily change the log level for a couple specific tests.
Example of the problem
Here is a self contained fabricated example that leverages with_log/2
to capture the log stream along with the result. The assertion at line 10 will fail due to the info log being dropped.
defmodule MyApp.SomeTest do
test "something with info logs" do
{result, log} =
with_log(fn ->
Logger.info("log msg")
2 + 2
end)
assert result == 4
assert log =~ "log msg" # 💥💥💥
end
end
Macros to the rescue
This felt like the perfect use case for macros, so I defined a log_level
macro to wrap test blocks that need alternative Logger
level configuration.
The macro does the following:
- Wraps the block in a
describe
mentioning how it’s changing the log level - Creates a
setup
block that remembers the original log level - Sets the new temporary log level
- Sets up an
on_exit
callback to restore the original log level after the test runs - Calls the original test block
defmodule MyApp.Case do
# Add this macro to your base Case module
defmacro log_level(level, description \\ nil, do: block) do
description =
if is_binary(description),
do: description,
else: "with log level #{level}"
quote do
describe unquote(description) do
setup do
old_level = Logger.level()
Logger.configure(level: unquote(level))
on_exit(fn -> Logger.configure(level: old_level) end)
end
unquote(block)
end
end
end
end
Example usage
Once you’ve added the macro to your application Case module, you can wrap a test like this:
defmodule MyApp.SomeTest do
log_level :info do
test "something with info logs" do
{result, log} =
with_log(fn ->
Logger.info("log msg")
2 + 2
end)
assert result == 4
assert log =~ "log msg"
end
end
end
Limitation
This solution does come with a limitation in that it wraps the tests in a describe
block. ExUnit
does not allow nesting of describe
calls, so this macro will need to replace any existing describe
you have. We slightly make up for this limitation by allowing you to override the message passed to describe
as the second argument.
This is just the way I solved the problem for myself (as a novice Elixir dev). There are probably better or cleaner ways to handle this, so feel free to reach out with your ideas.