Chapter 19. Debugging and Testing

Trying hard to speak and Fighting with my weak hand Driven to distraction So part of the plan When something is broken And you try to fix it Trying to repair it Any way you can I’m diving off the deep end You become my best friend I wanna love you But I don’t know if I can I know something is broken And I’m trying to fix it Trying to repair it Any way I can

Debugging and testing are not as romantic as solving a difficult partial differential equation, creating a breathtaking plot, or achieving a compelling interactive demonstration of a complicated mathematical concept. But, to loosely paraphrase Edison, Mathematica creation is often 10% coding and 90% debugging and testing. Mathematica’s interactive development paradigm encourages incremental development, so often you proceed to solve a complex problem by writing little pieces, trying them, tweaking them, and repeating. In time, you will find yourself with quite a bit of code. Then, quite satisfied with yourself, you begin to feed your code real-world data and—bam!—something goes awry. Now what? 19.1 Printing as the First Recourse to Debugging through 19.6 Debugging Built-In Functions with Evaluation and Step Monitors demonstrate various debugging techniques that you can use from within the traditional Mathematica frontend. 19.7 Visual Debugging with Wolfram Workbench shows you how to use the powerful symbolic debugger provided by Wolfram Workbench.

Debugging skills are essential, but here frustration can begin to creep in. Mathematica code can often be difficult to debug, and if you’ve written a lot of it in a haphazard fashion, you might have your work cut out for you. There are two complementary techniques for maintaining your sanity when working with Mathematica on large projects. The first is knowing how to isolate the problem through debugging techniques, and the second is not getting into the problem in the first place. Clearly, the second is preferable, but how is it achieved? As convenient as interactive development inside a notebook can be, it is often a trap. How thoroughly can you test a complex function by feeding it a few values? Not very thoroughly. The solution is to write repeatable unit tests. Why is that better? First, test-drive development (part of what software developers call an agile development methodology) encourages breaking problems into small, easily testable pieces. In its purest form, developers are encouraged to actually write the test before the code! Having a test suite acts as documentation for the use cases of your function and is a godsend if you decide to enhance your implementation, because you can quickly see if you have broken existing functionality. 19.8 Writing Unit Tests to Help Ensure Correctness of Your Code through 19.10 Organizing and Controlling MUnit Tests and Test Suites show how to develop unit tests within Wolfram Workbench. 19.11 Integrating Wolfram Workbench’s MUnit Package into the Frontend shows how to adapt the underlying MUnit framework that is integrated with Wolfram Workbench for use in the frontend.

This chapter’s workhorse function for illustrating debugging techniques is the Ackermann function. This infamous function has a simple recursive definition, but its pathological behavior makes it convenient for illustrating various real-world debugging problems (like stack overflows).

Anyone who has spent even a day programming has come across this obvious debugging technique, so it may seem hardly worth a whole recipe, but please read on. Sometimes, injecting Print into code is very inconvenient, especially if you code in tight function style with few intermediate values appearing in variables. The problem is that you can’t inject Print into functional code because Print does not return a value. Consider if the code for the value y did not exist because it was in-lined.

In[7]:= x = -1;
        If[ Power[x, 1/3] == -1, "expected", "not expected"]
Out[8]= not expected

You can’t wrap the call to Power in a Print because it would change the behavior of the expression, which is not what you want to do when you are already contending with bugs. For these situations, it is handy to whip up a functional version of Print, which I call fPrint. This saves you the trouble of introducing temporary variables for purposes of debugging, thus leaving less mess to clean up after you have diagnosed the problem.

Discussion

A possible problem that can lead to lost or gray hairs when debugging with Print is when it seems to print nothing. This can take you down the road to hell by misleading you into thinking your code must be taking a different branch. For example, it is easy to miss the empty print cell created by executing this code.

Discussion

This is not as contrived as it may seem: there are bugs that arise from failure to consider the fact that a sequence might be null, for example, when you use Apply (@@) on an empty list.

Discussion

Here an error was generated, and the output was "something completely different" because the expression in the If was neither True nor False. Pretend it was not immediately obvious to you what was going on (after all, you clearly see that you called Total with one argument x). You decide to use Print to get to the bottom of it. Notice that introducing Print into this code requires the whole thing to be wrapped in parentheses (another common debugging pitfall).

Discussion

If you were confused before, you are now totally befuddled! Here is where your own little functional fPrint can help, but you need to tweak it slightly to expose two common ghosts you might encounter in the wild.

In[18]:= Clear[fPrint];
         fPrint[] := (Print["NullSequence!!"]; Unevaluated[Sequence[]])
         fPrint[""] := (Print["NullString!!"]; "")
         fPrint[x__] := (Print[x]; x)

Now the problem is revealed, and you also side-stepped the parenthesis mistake.

Discussion

19.6 Debugging Built-In Functions with Evaluation and Step Monitors shows another common application of Reap-Sow in the debugging of built-in numerical algorithms or plotting functions.

19.3 Stack Tracing to Debug Recursive Functions shows how to use Reap-Sow to take Stack snapshots.

Again, I use the Ackermann function to illustrate the issue, although this problem is not particular to recursive functions. Ackermann is convenient because it creates a large number of nested function calls and intermediate expressions. In addition, I purposefully throw a monkey wrench into this function to simulate a bug: "bug". Real-world bugs don’t come so nicely labeled (if only!) but the point here is that in a real-world debugging situation you are looking for a particular subexpression that looks fishy based on your knowledge of the intended computation.

In[42]:= A[0, n_] := n + 1
         A[m_, 0] := A[m - 1, 1]
         A[m_, 2] := ("bug"; A[m - 1, A[m, 1]])
         A[m_, n_] := A[m - 1, A[m, n - 1]]

If you attempt to trace this buggy Ackermann on even relatively tame inputs, you will quickly generate a lot of output that anyone but the most seasoned Mathematica developer would have trouble deciphering. In essence, what you are seeing is an expansion of the call tree, and thus, the problem is not only the amount of output but the deeply nested structure of the output. You could easily miss the "bug" in this data, and even if you spot it, you might still have trouble understanding what led up to its occurrence.

In[46]:= trace = Trace[A[2, 3]]
Out[46]= {A[2, 3], A[2 - 1, A[2, 3 - 1]], {2 - 1,1},
          {{3 - 1, 2}, A[2, 2], bug; A[2 - 1, A[2, 1]],
           {{2 - 1, 1}, {A[2, 1], A[2 - 1, A[2, 1 - 1]], {2 - 1,1}, {{1 - 1, 0}, A[2, 0],
              A[2 - 1, 1], {2 - 1, 1}, A[1, 1], A[1 - 1, A[1, 1 - 1]], {1 - 1, 0},
              {{1 - 1, 0}, A[1, 0], A[1 - 1, 1], {1 - 1, 0}, A[0, 1], 1 + 1, 2},
              A[0, 2], 2 + 1, 3}, A[1, 3], A[1 - 1, A[1, 3 - 1]],
             {1 - 1, 0}, {{3 - 1, 2}, A[1, 2], bug; A[1 - 1, A[1, 1]],
              {{1 - 1, 0}, {A[1, 1], A[1 - 1, A[1, 1 - 1]], {1 - 1, 0},
                {{1 - 1, 0}, A[1, 0], A[1 - 1, 1], {1 - 1, 0}, A[0, 1], 1 + 1, 2},
                A[0, 2], 2 + 1, 3}, A[0, 3], 3 + 1, 4}, 4}, A[0, 4], 4 + 1, 5}, A[1, 5],
            A[1 - 1, A[1, 5 - 1]], {1 - 1, 0}, {{5 - 1,4}, A[1,4], A[1 - 1, A[1, 4 - 1]],
             {1 - 1, 0}, {{4 - 1, 3}, A[1, 3], A[1 - 1, A[1, 3 - 1]],
              {1 - 1, 0}, {{3 - 1, 2}, A[1, 2], bug; A[1 - 1, A[1, 1]],
               {{1 - 1, 0}, {A[1, 1], A[1 - 1, A[1, 1 - 1]], {1 - 1, 0},
                 {{1 - 1, 0}, A[1, 0], A[1 - 1, 1], {1 - 1, 0}, A[0, 1], 1 + 1, 2},
                 A[0, 2], 2 + 1, 3}, A[0, 3], 3 + 1, 4}, 4}, A[0, 4], 4 + 1, 5},
             A[0, 5], 5 + 1, 6}, A[0, 6], 6 + 1, 7}, 7}, A[1, 7],
          A[1 - 1, A[1,7 - 1]], {1 - 1, 0}, {{7 - 1,6},
           A[1, 6],
           A[1 - 1, A[1, 6 - 1]],
           {1 - 1, 0},
           {{6 - 1, 5}, A[1, 5], A[1 - 1, A[1, 5 - 1]], {1 - 1, 0},
            {{5 - 1,4}, A[1,4], A[1 - 1, A[1,4 - 1]], {1 - 1, 0},
             {{4 - 1, 3}, A[1, 3], A[1 - 1, A[1, 3 - 1]], {1 - 1, 0},
              {{3 - 1, 2}, A[1, 2], bug; A[1 - 1, A[1, 1]], {{1 - 1, 0},
                {A[1, 1], A[1 - 1, A[1, 1 - 1]], {1 - 1, 0}, {{1 - 1, 0}, A[1, 0],
                  A[1 - 1, 1], {1 - 1, 0}, A[0, 1], 1 + 1, 2}, A[0, 2], 2 + 1, 3},
                A[0, 3], 3 + 1, 4}, 4}, A[0, 4], 4 + 1, 5}, A[0, 5], 5 + 1, 6},
           A[0, 6], 6 + 1, 7}, A[0, 7], 7 + 1, 8}, A[0, 8], 8 + 1, 9}

Using Depth, you can see that there are 13 levels in the expression output by Trace (although this is inflated by the existence of HoldForm, as I explain later). In a real-world use of Trace, you could easily encounter output with depth an order of magnitude larger and an overall output several orders of magnitude larger still.

In[47]:= Depth[trace]
Out[47]= 13

To understand this solution, be aware that all the intermediate expressions output by Trace are wrapped in HoldForm to prevent their evaluation (which would of course defeat the purpose of Trace). You can see this by using InputForm. I use Short to suppress repeating the mess of output from above.

   In[48]:=  trace // InputForm // Short
Out[48]//Short=
              {HoldForm[A[2, 3]], HoldForm[A[2
                - 1, A[2, <<1>>]]], <<8>>, HoldForm[9]}

One way to get a handle on the output of Trace is to linearize it so you get a flat structure that presents the sequence of operations as they occur in time. This can be done by using what amounts to a preorder tree traversal.

In[49]:= Clear[traverseTrace, traverseTrace1];
         traverseTrace[x_] := Flatten[Reap[traverseTrace1[x]]]
         traverseTrace1[{}] := Sequence[]
         traverseTrace1[x_List] :=
          (traverseTrace1[First[x]]; traverseTrace1[Rest[x]])
         traverseTrace1[HoldForm[x_]] := Sow[HoldForm[x]]

This still produces as much raw data, but its linear nature makes it easier to visualize and manipulate.

In[54]:= timeSequence = traverseTrace[trace]
Out[54]= {A[2, 3], A[2 - 1, A[2, 3 - 1]], 2 - 1, 1, 3 - 1, 2, A[2, 2], bug;
          A[2 - 1, A[2, 1]], 2 - 1, 1, A[2, 1], A[2 - 1, A[2, 1 - 1]], 2 - 1, 1, 1 - 1,
          0, A[2, 0], A[2 - 1, 1], 2 - 1, 1, A[1, 1], A[1 - 1, A[1, 1 - 1]], 1 - 1,
          0, 1 - 1, 0, A[1, 0], A[1 - 1, 1], 1 - 1, 0, A[0, 1], 1 + 1, 2, A[0, 2],
          2 + 1, 3, A[1, 3], A[1 - 1, A[1, 3 - 1]], 1 - 1, 0, 3 - 1, 2, A[1, 2], bug;
          A[1 - 1, A[1, 1]], 1 - 1, 0, A[1, 1], A[1 - 1, A[1, 1 - 1]], 1 - 1, 0,
          1 - 1, 0, A[1, 0], A[1 - 1, 1], 1 - 1, 0, A[0, 1], 1 + 1, 2, A[0, 2], 2 + 1,
          3, A[0, 3], 3 + 1, 4, 4, A[0,4], 4 + 1, 5, A[1, 5], A[1 - 1, A[1, 5 - 1]],
          1 - 1, 0, 5 - 1, 4, A[1, 4], A[1 - 1, A[1, 4 - 1]], 1 - 1, 0, 4 - 1, 3, A[1,3],
          A[1 - 1, A[1, 3 - 1]], 1 - 1, 0, 3 - 1, 2, A[1, 2], bug; A[1 - 1, A[1, 1]],
          1 - 1, 0, A[1, 1], A[1 - 1, A[1, 1 - 1]], 1 - 1, 0, 1 - 1, 0, A[1, 0],
          A[1 - 1, 1], 1 - 1, 0, A[0, 1], 1 + 1, 2, A[0, 2], 2 + 1, 3, A[0, 3], 3 + 1,
          4, 4, A[0, 4], 4 + 1, 5, A[0, 5], 5 + 1, 6, A[0, 6], 6 + 1, 7, 7, A[1, 7],
          A[1 - 1, A[1, 7 - 1]], 1 - 1, 0, 7 - 1, 6, A[1, 6], A[1 - 1, A[1, 6 - 1]],
          1 - 1, 0, 6 - 1, 5, A[1, 5], A[1 - 1, A[1, 5 - 1]], 1 - 1, 0, 5 - 1, 4, A[1, 4]
          A[1 - 1, A[1, 4 - 1]], 1 - 1, 0, 4 - 1, 3, A[1, 3], A[1 - 1, A[1, 3 - 1]],
          1 - 1, 0, 3 - 1, 2, A[1, 2], bug; A[1 - 1, A[1, 1]], 1 - 1, 0, A[1, 1],
          A[1 - 1, A[1, 1 - 1]], 1 - 1, 0, 1 - 1, 0, A[1, 0], A[1 - 1, 1], 1 - 1, 0,
          A[0, 1], 1 + 1, 2, A[0, 2], 2 + 1, 3, A[0, 3], 3 + 1, 4, 4, A[0, 4], 4 + 1, 5,
          A[0, 5], 5 + 1, 6, A[0, 6], 6 + 1, 7, A[0, 7], 7 + 1, 8, A[0, 8], 8 + 1, 9}

Once you have linearized the output of Trace, you can easily extract segments of the execution history or use patterns to extract specific segments of interest.

   In[55]:=  timeSequence[[5 ;; 18]] // InputForm
Out[55]//InputForm=
             {HoldForm[3 - 1], HoldForm[2], HoldForm[A[2, 2]],
              HoldForm[bug; A[2 - 1, A[2, 1]]], HoldForm[2 - 1], HoldForm[1],
              HoldForm[A[2, 1]], HoldForm[A[2 - 1, A[2, 1 - 1]]], HoldForm[2 - 1],
              HoldForm[1], HoldForm[1 - 1], HoldForm[0], HoldForm[A[2, 0]],
              HoldForm[A[2 - 1, 1]]}

Here I use ReplaceList to find every occurrence of a call to A where the first argument was 0, and then output the expression computed immediately before and immediately after.

Discussion

More to the point, here I do the same with the pattern that is the proxy for the buggy behavior. This shows the expressions that preceded and followed the bug.

Discussion

Clearly, linearizing loses some information that was in the original output of Trace. What you lose is the information that says a certain bunch of subexpressions were triggered by some parent expression. But, the act of debugging (or indeed understanding any complex data set) is the act of suppressing extraneous information until you can identify the area where there was a problem. Then some strategically placed debug code or Print functions can often get you the rest of the way to the fix.

A very similar result to this solution can be obtained using a variation of Trace called TraceScan along with Reap-Sow. The difference is that this expression will include a bit more extraneous detail because it shows the evaluation of every symbol and constant. Here is an excerpt using Short.

   In[58]:=  Reap[TraceScan[Sow, A[2, 3]]][[2, 1]] // Short
Out[58]//Short=
             {A[2, 3], A, 2, 3, A[2 - 1, A[2, 3 - 1]], A, 2 - 1,
              Plus, 2, <<450>>, 7, 1, 8, A[0, 8], 8 + 1, Plus, 8, 1, 9}

Mathematica has an alternative print command called PrintTemporary that inspired me to create a sort of interactive debugger. PrintTemporary works just like Print except after the evaluation is complete the output is automatically removed. Further, PrintTemporary returns a value that can be passed to the function NotebookDelete to delete the output at any time. You can get an idea of what PrintTemporary does by evaluating the following line:

In[59]:=  PrintTemporary["test"]; Pause[2]

If you could inject debug code into your ill-behaved programs that used PrintTemporary and then paused until you took some action (like pressing a button), you could effectively step though the code with the embedded prints acting like breakpoints in a real debugger. This can be done using a relatively small amount of code.

In[60]:= pmDebuggerInit[] :=
          Module[{}, $pmStep = False; $pmStop = False; CellPrint[Dynamic[Row[
               {Button["Step", $pmStep = True], Button["Stop", $pmStop = True]}]]]]
         pmWait[x__, t_] := (While[$pmStep == False && $pmStop == False,
            Pause[$TimeUnit]]; If[$pmStop, Abort[]]; NotebookDelete[t]; x)
         pmPrint[] := Module[{t}, $pmStep = False;
           t = PrintTemporary["NullSequence!!"];
           pmWait[Unevaluated[Sequence[]], t]]
         pmPrint[""] := Module[{t}, $pmStep = False;
           t = PrintTemporary["NullString!!"]; pmWait["", t]]
         pmPrint[x__] := Module[{t}, $pmStep = False;
           t = PrintTemporary[x]; pmWait[x, t]]

I explain this code further in the following "Discussion" section. For now, let’s just try it out. Here I use an instrumented version of the Ackermann function as a test example.

In[65]:=
         A[0, n_] := pmPrint[n + 1];
         A[m_, 0] := A[m - 1, 1];
         A[m_, n_] := A[m - 1, A[m, n - 1]];
         test[] := Module[{}, pmDebuggerInit[]; A[4, 1]]

Executing test[] creates the debugging controls.

Solution

The code in the solution contains two user functions, pmPrint and pmDebuggerInit. Function pmPrint has the same features as fPrint from 19.1 Printing as the First Recourse to Debugging, but it uses PrintTemporary rather than Print. Further, it calls a function pmWait, which loops and pauses until a Boolean variable becomes true. These variables are initialized in pmDebuggerInit and associated with buttons that are used to control progress of the debugging session.

Often when creating little utilities like this, it’s fun to see how far you can extend them without going too far over the top. There are a few deficiencies in the solution’s debugging techniques. First, if you insert multiple print statements, there is no way to know which one created output. Second, it would be nice if you did not always have to step one print at a time. Third, it might be nice if you can also dump the stack while the program is paused. It turns out that using a bit of cleverness can get you all this new functionality using roughly the same amount of code.

Discussion

The trick here is to convert $pmStep to a counter instead of a Boolean and $pmStop to a function that can be changed by the buttons to either Abort or Print the Stack. I also introduce a new variable to collect multiple temporary print cells and move their cleanup to the button press for Step or Step 10. Finally, the pmPrint is refactored to take an optional tag to display so you can distinguish one debug output from another.

Use Wolfram Workbench, a Mathematica-specific extension to the Eclipse platform. When you launch Wolfram Workbench, you must first create a project. Use menu File, New, New Project. Give the project a name. I used the name Debugging for this example. Workbench automatically creates two files named after your project. In this example, I got a Debugging.m and a Debugging.nb. The .m file is where you would enter code that you want to debug. The Debugging.nb is a normal frontend notebook file. Here you would typically set up your test calls.

Once you have these files set up, you can place a breakpoint by double-clicking on the left margin of the line of code you want the debugger to stop. In Figure 19-4 you see a dot appear in the margin to indicate the successful placement of the breakpoint. You can place as many breakpoints as necessary.

Now right-click on the Debugging.nb file in the Package Explorer and select Debug As... Mathematica. You will be prompted to switch to the Debug perspective, which is recommended. Figure 19-5 shows what this perspective looks like. It will also launch the frontend with Debugging.nb active. Here you can use normal Shift-Enter evaluation to execute an expression. When a breakpoint is hit, you can switch back to the Workbench to continue debugging. Here you can inspect the call stack, see the value of variables, and set further breakpoints. You can step over or into further functions using F5 (set), F6 (step over) and F7 (step return). In short, you can perform all the operations you’d expect from a modern symbolic debugger.

Many old-time Mathematica users feel that it is sacrilegious (or perhaps just frustrating) to leave the comfortable Mathematica frontend just to debug. If you don’t have such a prejudice, your willingness will be rewarded. There is nothing like debugging within a real debugging environment! If you are a Java or C programmer who is used to such luxuries, the Eclipse-based Workbench environment is a must-have. Eclipse is an open source framework for building integrated software development environments (IDEs) that first gained popularity with Java developers. Wolfram used Eclipse to build an alternative development environment for Mathematica as an alternative to the traditional frontend. However, you don’t need to abandon the traditional Mathematica interface to use Workbench to debug. In this section, I refer to Eclipse when speaking about generic features that are true about all Eclipse IDEs and Workbench when speaking about features of Workbench in particular.

If you have never used more traditional languages, such as Java, C, C++ and C#, then you are likely to find working in Workbench somewhat foreign. To avoid being frustrated, you should keep a few ideas in mind. First, because Workbench is built on top of Eclipse and Eclipse was built outside of Wolfram, you should not expect Workbench to have the same look and feel as the traditional frontend. You should approach it as you would approach any new piece of software—with an open mind and no preconceptions. For example, you should not expect to debug code that is written using all the fancy mathematical typesetting features available in a notebook. If you developed code solely using the .nb format, you should save your code as a .m, which is a pure text format. This is not to say you can’t launch notebooks from Eclipse (the solution shows this is possible) but rather you should make all code that you wish to debug available in text format.

Another important concept of Eclipse is that it wants to manage all the source code under a project. Projects in Eclipse typically correspond to directories under a specific root directory you choose when Eclipse is installed. It is possible to specify other directories outside this hierarchy, but you will not automatically pick up files that happen to be in an existing location. You can use File, Import for that purpose.

In addition to source code-level breakpoints, Workbench supports message breakpoints that break when a function emits any error message and symbol breakpoints that provide a convenient way to place a breakpoint on an overloaded function name. For example, a symbol breakpoint can be used to put a break on all three variants of the Ackermann function A. The three types of breakpoints are accessible from the Breakpoints tab shown in Figure 19-6. The message break is set using , and is used for symbol breakpoints. There are also buttons for clearing selected breakpoints, , or all breakpoints, , and you can uncheck a breakpoint in the list to temporarily disable it.

You can execute your unit tests at any time by saving the test file, right-clicking on it in the package explorer, and selecting Run As, Mathematica Test. This will generate a Test Report, as shown in Figure 19-7. The report shows which tests passed and which failed. Unique TestIDs are essential to this function, and Workbench has a feature that will help fix and duplicate IDs. Simply right-click on the file, select the Source menu, and then select Fix Test IDs.

Functions like Ackermann that return scalar values are easy to inspect in the failed tests section to investigate the difference between the expected and actual output. In Figure 19-7, you can see that the expected output is 6, but the actual output is 4. In this case, it is the test function that is wrong, because the correct output is 4. The more typical circumstance is that the function is wrong, but in either case you can quickly see that something is awry. With more complex outputs, it can be difficult to find the difference. A useful feature of Workbench is Failure Compare. Simply right-click on the failure test ID and select Failure Compare. This will open a dialog with a side-by-side tree view of the expected and actual expression (see Figure 19-8). You can expand the tree to inspect the branches that indicate differences (the X).

You need a test driver to run the tests. This mimics the basic functionality of Workbench.

In[88]:= Needs["MUnit`"];
         TestDriver[tests__] :=
          Module[{testList = {tests}, numTests, failedTests},
           numTests = Length[testList];
           failedTests = Select[{tests}, (FailureMode[#] =!= "Success") &];
           Print["Passed Tests: ", numTests - Length[failedTests]];
           Print["Failed Tests: ", Length[failedTests]];
           Print["Failed Test Id: ", TestID[#], "\nExpected: ",
              ExpectedOutput[#], " Actual: ", ActualOutput[#]] & /@ failedTests;
          ]
In[90]:= AppendTo[$Path, FileNameJoin[{"C:", "Program Files",
            "Wolfram Research", "WolframWorkbench", "1.1", "plug-ins",
            "com.wolfram.eclipse.testing_1.1.0", "MathematicaSource"}]]

Out[90]= {C:\Program Files\Wolfram
            Research\Mathematica\7.0\SystemFiles\Links,
          C:\Users\Sal Mangano\AppData\Roaming\Mathematica\Kernel,
          C:\Users\Sal Mangano\AppData\Roaming\Mathematica\Autoload,
          C:\Users\Sal Mangano\AppData\Roaming\Mathematica\Applications,
          C:\ProgramData\Mathematica\Kernel,
          C:\ProgramData\Mathematica\Autoload,
          C:\ProgramData\Mathematica\Applications, .,
          C:\Users\Sal Mangano, C:\Program Files\Wolfram
            Research\Mathematica\7.0\AddOns\Packages,
          C:\Program Files\Wolfram
            Research\Mathematica\7.0\AddOns\LegacyPackages,
          C:\Program Files\Wolfram
            Research\Mathematica\7.0\SystemFiles\Autoload, C:\Program
            Files\Wolfram Research\Mathematica\7.0\AddOns\Autoload,
          C:\Program Files\Wolfram
            Research\Mathematica\7.0\AddOns\Applications, C:\Program
            Files\Wolfram Research\Mathematica\7.0\AddOns\ExtraPackages,
          C:\Program Files\Wolfram
            Research\Mathematica\7.0\SystemFiles\Kernel\Packages,
          C:\Program Files\Wolfram
            Research\Mathematica\7.0\Documentation\English\System,
          C:\Program Files\Wolfram
            Research\WolframWorkbench\1.1\plug-ins\com.wolfram.eclipse.
            testing_1.1.0\MathematicaSource}

You can add this to init.m if you intend to use MUnit frequently. Alternatively, you can also copy the MUnit package into one of the locations in $Path.

Here is a simple example of using the driver. I purposefully made tests with ID2 and ID4 fail.

Solution

The test driver used in the preceding "Solution" section is very basic and does not support all the features available when you build unit tests in Workbench. If you are ambitious, you can build a more sophisticated driver—even one that has more features than Workbench. It really depends on your needs. The main requirement is to become familiar with the MUnit API. Although documentation on MUnit is sparse at the time I am writing this, well-written Mathematica packages are self-describing. For example, you can find all the public functions in the package by using ?"MUnit`*". For the sake of space, I’ll only list the functions that begin with the letter T. By clicking on the output, you can see what the function or option does. The most important functions are selectors, like TestID, because these allow you to extract information from a TestResultObject, which is the output produced by functions like Test, TestMatch, and so on.

Discussion

By inspecting MUnit's functions, I was inspired to create a test driver that supports the idea of test sections (see 19.10 Organizing and Controlling MUnit Tests and Test Suites). However, instead of a BeginTestSectionEndTestSection pair, I use a single TestSection function. The TestDriver will work with multiple TestSections or multiple Tests but not mixtures of both. For this driver to handle skipping and aborting, it must be careful to evaluate a test lazily, hence, it uses Hold and the HoldAll attribute judiciously. It also uses Catch and Throw combinations. This is a feature of Mathematica I have largely avoided in the book, but it sometimes comes in handy as a way to terminate an iteration without cumbersome conditional logic. In this case, the function RunTest causes a test to evaluate and tests for failure. If the test does not succeed, it defers further decisions to OnFailedTest based on the test’s FailureMode. OnFailedTest will either Throw or return, depending on the mode. Further, it uses the mode as a tag in the Throw, so the appropriate Catch handler can intercept the failure.

In[93]:= ClearAll[TestDriver, TestSection, RunTest, OnFailedTest];
         SetAttributes[{TestDriver, TestDriver2, TestSection, RunTest}, HoldAll];

         (*OnFailedTest simply returns the test if mode is Continue,
         otherwise it throws using mode as a tag.*)
         OnFailedTest[test_, "Continue"] := test
         OnFailedTest[test_, mode_] := Throw[test, mode]

         (*RunTest tests the failure mode and updates
          counters. It defers failure action to OnFailedTest.*)
         RunTest[testTestResultObject] :=
          If[FailureMode[test] =!= "Success", failedTests++;
           OnFailedTest[test, TestFailureAction[test]], passedTests++; test]

         (*A TestSection has one or more tests, a name,
         and Boolean for enabling or disabling the section.*)
         TestSection[tests__, section_String, False] := {}
         TestSection[tests__, section_String, _ : True] :=
          Module[{},
           Catch[ReleaseHold[RunTest[#] & /@ Hold[tests]], "SkipSection"]]

         (*TestDriver2 valuates the results of tests.*)
         TestDriver2 [tests__] := Module [{testList = {tests}, numTests, failed},
           failed =Select[{tests}, (FailureMode [#] =!= "Success") &];
           Print["Passed Tests: ", passedTests];
           Print["Failed Tests: ", failedTests];
           Print["Failed Test Id: ", TestID[#], "\nExpected: ",
              ExpectedOutput[#], " Actual: ", ActualOutput [#]]& /@ failed;
         ]

        (*This instance of TestDriver executes sections.*)
        TestDriver[secs__TestSection] :=
         Block[{ passedTests = 0, failedTests = 0},
          TestDriver2 @@ Flatten[{Catch[ {secs}, "Abort"]}]]

        (*This instance of TestDriver executes tests.*)
        TestDriver[tests__] := Block[{passedTests = 0, failedTests = 0},
          TestDriver2 @@ Flatten[{Catch[RunTest /@ {tests}, "Abort"]}]]

Here I put the driver through its paces demonstrating different failure scenarios.

In this scenario, the second test in sectl fails with an Abort; hence, tests with test IDs "Sect1ID3" and "Sect2ID1" are not run.

Discussion

In this scenario, the second test in sectl fails with a "SkipSection"; hence, the test with test ID "Sect1ID3" is skipped, but a "Sect2ID1" runs.

Discussion

Here sections are not used, but a TestFailureAction of "Abort" is still handled appropriately.

Discussion