Blog

Format Express

Collection of data files for automated tests

-
Published on - By

Automated testing for a formatter like Format Express is as straightforward as you'd expect : given a JSON or XML extract, check the formatter returns the expected formatted output. Have as many tests as possible to build a robust application. In this article, I show you how to create a nice collection of tests by putting each test in its own data file.

The issue

A ruby test for the following JSON {"single-quote": "'", "double-quote":"\""} would look like this :

test "Test JSON with single and double quotes in values" do input = '{"single-quote":"\'","double-quote":"\""}' # ' must be escaped expected = "{\n \"single-quote\": \"'\",\n \"double-quote\": \"\\\"\"\n}" # " and \ are escaped assert_equal expected, FormatExpressFormatter.new.format(input) end

I'm sure it bothers you too ! The input and ouput strings are hard to read and understand.
Let's see how it can be better...

First try: improve Ruby strings

The 2 main issues to tackle are :

  • Special characters like ', " or \ must be escaped;
  • Use multi-line strings instead of \n in a single line string

Ruby offers several notations for string literals, let's take advantage of them.

One improvement is to use the %q(...) notation for Ruby strings. With this notation, I can choose another delimiter instead of quotes. This way I don't have to escape quotes. Here an example using | :

input = %q|{"single-quote":"'","double-quote":"\""}| # Neither ' nor " have to be escaped

For multi-line string, I use a heredoc << with the following attributes :

  • single quotes to disable interpolation, so I don't have to escape ', " or \
  • the "squiggly" heredoc <<~ that keeps a nice indentation
  • the .chomp to remove the final \n
expected = <<~'EOS'.chomp { "single-quote": "'", "double-quote": "\"" } EOS

The test now like this

test "Test JSON with single and double quotes in values" do input = %q|{"single-quote":"'","double-quote":"\""}| expected = <<~'EOS'.chomp { "single-quote": "'", "double-quote": "\"" } EOS assert_equal expected, FormatExpressFormatter.new.format(input) end

That's undeniably better, yet it's not as readable as I'd like, and it will be even harder with larger JSON/XML extracts.
That's why I switched to a different direction : put each test in an isolated test data file.

Better solution: using text files as test data

For each test I create a file with the 3 following sections :

  • a description of the test
  • the input to give to the formatter
  • the expected formatted output
# Test JSON with single and double quotes in values ====INPUT==== {"single-quote":"'","double-quote":"\""} ====EXPECTED==== { "single-quote": "'", "double-quote": "\"" }

The file is easy to understand by itself, and there is no special character to escape. I can create as many files that I want, group them into directories, ...

Next step is an automated test to look for all the test data files, read the input and expected output, apply the input to the formatter and check the returned formatted string matches the expected output.

The naive implementation would be a single test which loops on each test data file. That's not good because it would stop on the first error found, and I would not know how many tests are broken (did my last change break only one file ? or 80% of files ? That's 2 totally different situations).
I want every file checked on each test run, so instead I dynamically create a test for each file, using instance_eval

class TestDataSuiteTest < ActionDispatch::IntegrationTest # Test data files delimiters INPUT_DELIMITER = "====INPUT====\n" EXPECTED_DELIMITER = "\n====EXPECTED====\n" setup do @formatter = FormatExpressFormatter.new end # Generate a test for each file in the test_data directory Dir['test/test_data/*.testdata.txt'].each do |filename| instance_eval do test "Should format #{filename}" do execute_test_data(filename) end end end private def execute_test_data(filename) # Extract input and expected result from the test data file text = File.read(filename) comment, _, content = text.partition(INPUT_DELIMITER) input, _, expected = content.partition(EXPECTED_DELIMITER) # Test assert_equal expected, @formatter.format(input), "Not the expected result for #{filename} #{comment}; The input was #{input}" end end

With this, no excuse to not apply TDD : when a new feature is implemented, or someone points out an unexpected formatting, I create a new test data file (or several) with the new case, and work until all tests are green.