12

Here's a basic example of what my unit test needs to be, using qunit:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>

<link rel="stylesheet" href="qunit/qunit-1.13.0.css">
<script src = "qunit/qunit-1.13.0.js"></script>
<script src = "../js/fuzzQuery.js"></script>

<script>

test("Fuzz Query Basics", function()
        {
            equal(fuzzQuery("name:(John Smith)"), "name:(John~ Smith~)");
            equal(fuzzQuery("name:Jon~0.1"), "name:Jon~0.1");
            equal(fuzzQuery("Jon"), "Jon~");
            //etc

        }
    );

</script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Now I was thinking this is a bit repetitive.

Could put all the inputs/outputs into an array, and loop through it.

test("Fuzz Query Basics", function()
        {
            var equals = [
                           ["name:(John Smith)", "name:(John~ Smith~)"],
                           ["name:Jon~0.1", "name:Jon~0.1"],
                           ["Jon", "Jon~"]
                           ];

            for (var i = 0; i<equals.length; i++)
                {
                    equal(fuzzQuery(equals[i][0]), equals[i][1]);               
                }

        }
    );

And this works fine.

The single advantage I can think of for this second method, is that if it turns out that you don't actually want to use equal it's easier to make that change in one spot.

In terms of readability, I don't think it's conclusive either way, though I probably prefer the second.

Abstracting it further, you could put the input/output cases into a separate CSV file, which might make it easier to modify.

Question is - what are the general conventions around writing these kinds of unit tests?

Is there a reason you shouldn't put them into arrays?

2
  • Will either of these tell you which value failed?
    – JeffO
    Jan 23, 2014 at 0:16
  • 1
    @JeffO - Yes - With QUnit - If a test fails, the output will show expected value and actual value.
    – dwjohnston
    Jan 23, 2014 at 1:21

4 Answers 4

8

Your refactored tests have a smell: Conditional Test Logic.

The reasons you should avoid writing conditional logic in your tests are two-fold. The first is that it undermines your ability to be confident that your test code is correct, as described in the linked xUnit Patterns article.

The second is that it obscures the meaning of the tests. We write Test Methods because they put the logic for testing a given behaviour in one place, and allow us to give it a descriptive name (see Dan North's original BDD article for an exploration of the value of good names for tests). When your tests are hidden inside a single function with a for loop, it obscures the meaning of the code for the reader. Not only does the reader have to comprehend the loop, they also have to mentally unravel all the different behaviours being tested within the loop.

The solution, as always, is to move up a level of abstraction. Use a test framework which gives you parametrised tests, like xUnit.NET or Contexts do (disclaimer: I wrote Contexts). This allows you to group triangulating tests for the same behaviour together in a natural way, while keeping tests for separate behaviours separate.

4
  • Good question, by the way Jan 23, 2014 at 0:16
  • 1
    1) If you move up a level of abstraction, aren't you hiding those very same details that you said are being obscured by the for loop? 2) not sure parameterized tests are applicable here. It does seem like there are parallels here somewhere, but I've had plenty of situations similar to OPs where I had a data set of 10-20 values and just wanted to run all of them through SUT. Yes each value is different and potentially tests different boudaries but seems to actually come "invent" test names for every single value would be an overkill. I found optimal value/code-size ratio in using similar...
    – DXM
    Jan 23, 2014 at 2:42
  • ... loops. As long as when the test fails, the assert prints exactly what failed, developer has enough feedback to precisely pinpoint the problem.
    – DXM
    Jan 23, 2014 at 2:43
  • @DXM 1) the test framework provides the parametrised test functionality. We trust the test framework implicitly so we don't write tests for it. 2) parametrised tests are for exactly this purpose: you are doing exactly the same steps each time but with different input/output values. The test framework saves you the need to write names for each one by running the different inputs through the same test method. Jan 23, 2014 at 8:03
5

It seems like you really want a Data Driven Unit Test. Since you mentioned using QUnit, I found a plugin that enables parameterized tests:

https://github.com/AStepaniuk/qunit-parameterize

There's nothing ideologically wrong with a data driven test, as long as the test code itself is not conditional. Looking at your test code, it appears to be a very good candidate for a Data Driven Test.

Example code for the GitHub README:

QUnit
    .cases([
        { a : 2, b : 2, expectedSum : 4 },
        { a : 5, b : 5, expectedSum : 10 },
        { a : 40, b : 2, expectedSum : 42 }
    ])
    .test("Sum test", function(params) {
        var actualSum = sum(params.a, params.b);
        equal(actualSum, params.expectedSum);
    });
2
  • 1
    Agreed, it looks like a data-driven test. But it seems like that's what he already has in his second code example. Feb 6, 2014 at 18:25
  • 1
    @RobertHarvey - Correct. There is an accepted term for what he is trying to accomplish, and a plugin exists for the testing framework being used to make it easier to write these kinds of tests. I thought it was worth noting in an answer for the future, that's all. Feb 6, 2014 at 18:30
1

You are repeating yourself less by using the array which is more maintainable. One approach I like to use is to have a separate method which arranges, acts and asserts the tests, but accepts the input parameters I'm testing with so I have 1 test method per input set.

This allows me to instantly tell which test / inputs are failing.

0

I like your second approach, but I would add 2 Points

  • do not use arrays to store tested data, as working with indexes is a not clean way
  • do not use for loops

`

[
    {
        process: "name:(John Smith)",
        result: "name:(John~ Smith~)"
    },
    {
        process: "name:Jon~0.1", 
        result: "name:Jon~0.1"
    },
    {
        process: "Jon", 
        result: "Jon~"
    }
]
.forEach(function(data){

    var result = fuzzQuery(data.process);
    equal(result, data.result);
});

I'm not sure about qunit, but a good test runner will show you what input string failed, and what the expected result was

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.