AndyL

AndyL

ExUnit / Data-Driven Tests / Macros

Elixir newbie here working on a stemmer for a document indexing engine. The stemming algorithm has dozens of steps with many tests per step. I’ve got a data-driven approach that is helpful, but I think it must be possible to be even more efficient with a little meta-programming. Here is an example of one of my test modules:

defmodule StemEx.StepsTest do

  use ExUnit.Case, async: true

  # ----- step1a -----
  
  step1a_vals = [ 
     ["caresses" , "caress"],
     ["ponies"   , "poni"  ],
     ["ties"     , "ti"    ],
     ["caress"   , "caress"],
     ["cats"     , "cat"   ],
  ]

  for [input, output] <- step1a_vals do
    @input  input
    @output output
    test "step1a: '#{input}' has output of '#{output}'" do
      assert StemEx.Steps.step1a(@input) == @output
    end
  end

  # ----- step1b -----

  step1b_vals = [
     ["feed"     , "feed"    ],
     ["agreed"   , "agree"   ],
     ["plastered", "plaster" ],
     ["bled"     , "bled"    ],
     ["motoring" , "motoring"],
     ["sing"     , "sing"    ],
  ]

  for [input, output] <- step1b_vals do
    @input  input
    @output output
    test "step1b: '#{input}' has output of '#{output}'" do
      assert StemEx.Steps.step1b(@input) == @output
    end
  end
end

This works great but I don’t like to repetitive boilerplate in the for blocks. Ideally I could reduce the blocks to a single call - something like test_loop_for("step1b", step1b_vals).

Would it be possible to use a defmacro to auto-magically generate the loop code? Can someone post an example?? Thanks in advance.

Most Liked

axelson

axelson

Scenic Core Team

For anyone else that comes across this as an example of data-driven tests and wants to improve their test reporting output you can do something like the following:

test "stem transformations" do
  input_list  = String.split(File.read!("test/data/voc.txt")   , "\n")
  output_list = String.split(File.read!("test/data/output.txt"), "\n")
  io_list     = List.zip([input_list, output_list])
  for {input, output} <- io_list do
    actual = StemEx.stem(input)

    message = """
    StemEx.stem(
      input = #{inspect input}
    )
    was expected to output #{output} but instead was #{actual}
    """

    assert actual == output, message
  end
end

This will give you pretty output like:

1) test stem transformations (StemExTest)
   test/stem_ex_test.exs:10
   StemExTest.stem(
     input = "some input"
   )
   was expected to equal false but instead was true

Whereas the default output show something more like:

Assertion with == failed
code:  StemExTest.stem(input) == output
left:  true
right: false

Which isn’t that helpful since you can’t tell what the input is without adding some IO.puts into your test code. There’s probably more elegant ways to accomplish what I’m outlining but I think this is at least a good start!

AndyL

AndyL

I ended up with a solution that looks much like your example. Simpler than metaprogramming (maybe this is metaprogramming?!) Not sure exactly what is going on under the covers - but it seems to give a lot of flexibility for data-driven tests. The tests run very fast and the failure messages are easy to decipher. Looks like you can load datasets from the filesystem - I’ve got a files with ~22K test examples. Just saved a lot of typing! :grinning:

defmodule StemEx.StepsTest do

  use ExUnit.Case, async: true

  functions = %{
    step1a: &StemEx.Steps.step1a/1  ,
    step1b: &StemEx.Steps.step1b/1  ,
  }
  
  values = [
     [:step1a  ,  "caresses"  , "caress"  ],
     [:step1a  ,  "ponies"    , "poni"    ],
     [:step1a  ,  "ties"      , "ti"      ],
     [:step1a  ,  "caress"    , "caress"  ],
     [:step1a  ,  "cats"      , "cat"     ],

     [:step1b  ,  "feed"      , "feed"    ],
     [:step1b  ,  "agreed"    , "agree"   ],
     [:step1b  ,  "plastered" , "plaster" ],
     [:step1b  ,  "bled"      , "bled"    ],
     [:step1b  ,  "motoring"  , "motoring"],
     [:step1b  ,  "sing"      , "sing"    ],
  ]

  for [label, input, output] <- values do
    @label  label
    @input  input
    @output output
    @func   functions[@label]
    test "#{label}: '#{input}' has output of '#{output}'" do
      assert @func.(@input) == @output
    end
  end
end

sheharyarn

sheharyarn

How about Enum.each/2? I do something like this myself. Here’s how it would look:

defmodule StemEx.StepsTest do
  use ExUnit.Case, async: true

  tests = [
    step1a: [
      ["caresses" , "caress"],
      ["ponies"   , "poni"  ],
      ["ties"     , "ti"    ],
      ["caress"   , "caress"],
      ["cats"     , "cat"   ],
    ],

    step1b: [
      ["feed"     , "feed"    ],
      ["agreed"   , "agree"   ],
      ["plastered", "plaster" ],
      ["bled"     , "bled"    ],
      ["motoring" , "motoring"],
      ["sing"     , "sing"    ],
    ]
  ]

  Enum.each tests, fn {name, values} ->
    @name name

    for [input, output] <- values do
      @input  input
      @output output

      test "#{@name}: '#{input}' has output of '#{output}'" do
        result = apply(StemEx.Steps, @name, [@input])
        assert result == @output
      end
    end
  end

end

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New

Other popular topics Top

bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement