[debugging] add section about unit testing in matlab

This commit is contained in:
Jan Grewe 2017-11-03 18:05:15 +01:00
parent 6da927c601
commit 78f6b352a2

View File

@ -112,8 +112,8 @@ to a number and uses this number to address the element in
thus the 65th element of \varcode{my\_array} is returned. thus the 65th element of \varcode{my\_array} is returned.
\subsection{\codeterm{Assignment error}} \subsection{\codeterm{Assignment error}}
Related to the Indexing error this error occurs when we want to write Related to the Indexing error, an assignment error occurs when we want
data into a variable, that does not fit into it. Listing to write data into a variable, that does not fit into it. Listing
\ref{assignmenterror} shows the simple case for 1-d data but, of \ref{assignmenterror} shows the simple case for 1-d data but, of
course, it extents to n-dimensional data. The data that is to be course, it extents to n-dimensional data. The data that is to be
filled into a matrix hat to fit in all dimensions. The command in line filled into a matrix hat to fit in all dimensions. The command in line
@ -172,11 +172,12 @@ expected elementwise multiplication.
\section{Logical error} \section{Logical error}
Sometimes a program runs smoothly and terminates without any Sometimes a program runs smoothly and terminates without any
complaint. This, however, does not necessarily mean that the program is complaint. This, however, does not necessarily mean that the program
correct. We may have made a \codeterm{logical error}. Logical errors is correct. We may have made a \codeterm{logical error}. Logical
are hard to find, \matlab{} has no chance to find such an error and can errors are hard to find, \matlab{} has no chance to detect such errors
not help us fixing bugs origination from these. We are on our own but since they do not violate the syntax or cause the throwing of an
there are a few strategies that should help us. error. Thus, we are on our own to find and fix the bug. There are a
few strategies that should we can employ to solve the task.
\begin{enumerate} \begin{enumerate}
\item Be sceptical: especially when a program executes without any \item Be sceptical: especially when a program executes without any
@ -185,21 +186,25 @@ there are a few strategies that should help us.
it. Comment, but only where necessary. Correctly indent your it. Comment, but only where necessary. Correctly indent your
code. Use descriptive variable and function names. code. Use descriptive variable and function names.
\item Keep it simple. \item Keep it simple.
\item Test your code by writing \codeterm{unit tests} that test every
aspect of your program (\ref{unittests}).
\item Use scripts and functions and call them from the command \item Use scripts and functions and call them from the command
line. \matlab{} can then provide you with more information. It will line. \matlab{} can then provide you with more information. It will
then point to the line where the error happens. then point to the line where the error happens.
\item If you still find yourself in trouble: Apply debugging \item If you still find yourself in trouble: Apply debugging
strategies to find and fix bugs (below). strategies to find and fix bugs (\ref{debugging}).
\end{enumerate} \end{enumerate}
\subsection{Avoiding errors --- Keep it small and simple} \section{Avoiding errors}
It would be great if we could just sit down, write a program, run it, It would be great if we could just sit down, write a program, run it,
and be done with the task. Most likely this will not happen. Rather, and be done with the task. Most likely this will not happen. Rather,
we will make mistakes and have to bebug the code. There are a few we will make mistakes and have to bebug the code. There are a few
guidelines that help to reduce the number of errors. guidelines that help to reduce the number of errors.
\subsection{Keep it small and simple}
\shortquote{Debugging time increases as a square of the program's \shortquote{Debugging time increases as a square of the program's
size.}{Chris Wenham} size.}{Chris Wenham}
@ -247,10 +252,173 @@ into the development of the most elegant solution relative to its
importance in the project? The decision is yours. importance in the project? The decision is yours.
\section{Debugging strategies} \subsection{Unit tests}\label{unittests}
The idea of unit tests to write small programs that test \emph{all}
functions of a program by testing the program's results against
expectations. The pure lore of test-driven development requires that
the tests are written \textbf{before} the actual program is
written. In parts the tests put the \codeterm{functional
specification}, the agreement between customer and programmer, into
code. This helps to guarantee that the delivered program works as
specified. In the scientific context, we tend to be a little bit more
relaxed and write unit tests, where we think them helpful and often
test only the obvious things. To write \emph{complete} test suits that
lead to full \codeterm{test coverage} is a lot of work and is often
considered a waste of time. The first claim is true, the second,
however, may be doubted. Consider that you change a tiny bit of a
standing program to adjust it to the current needs, how will you be
able to tell that it is still valid for the previous purpose? Of
course you could try it out and be satisfied, if it terminates without
an error, but, remember, there may be logical errors hiding behind the
facade of a working program.
Writing unit tests costs time, but provides the means to guarantee
validity.
\subsubsection{Unit testing in \matlab{}}
Matlab offers a unit testing framework in which small scripts are
written that test the features of the program. We will follow the
example given in the \matlab{} help and assume that there is a
function \code{rightTriangle} (listing\,\ref{trianglelisting}).
\begin{lstlisting}[label=trianglelisting, caption={Slightly more readable version of the example given in the \matlab{} help system. Note: The variable name for the angles have been capitalized in order to not override the matlab defined functions \code{alpha, beta,} and \code{gamma}.}]
function angles = rightTriangle(length_a, length_b)
ALPHA = atand(length_a / length_b);
BETA = atand(length_a / length_b);
hypotenuse = length_a / sind(ALPHA);
GAMMA = asind(hypotenuse * sind(ALPHA) / length_a);
angles = [ALPHA BETA GAMMA];
end
\end{lstlisting}
This function expects two input arguments that are the length of the
sides $a$ and $b$ and assumes a right angle between them. From this
information it calculates and returns the angles $\alpha, \beta,$ and
$\gamma$.
Let's test this function: To do so, create a script in the current
folder that follows the following rules.
\begin{enumerate}
\item The name of the script file must start or end with the word
'test', which is case-insensitive.
\item Each unit test should be placed in a separate section/cell of the script.
\item After the \code{\%\%} that defines the cell, a name for the
particular unit test may be given.
\end{enumerate}
Further there are a few things that are different in tests compared to normal scripts.
\begin{enumerate}
\item The code that appears before the first section is the in the so
called \emph{shared variables section} and the variables are available to
all tests within this script.
\item In the \emph{shared variables section}, one can define
preconditions necessary for your tests. If these preconditions are
not met, the remaining tests will not be run and the test will be
considered failed and incomplete.
\item When a script is run as a test, all variables that need to be
accessible in all test have to be defined in the \emph{shared
variables section}.
\item Variables defined in other workspaces are not accessible to the
tests.
\end{enumerate}
The test script for the \code{rightTrianlge} function
(listing\,\ref{trianglelisting}) may look like in
listing\,\ref{testscript}.
\begin{lstlisting}[label=testscript, caption={Unit test for the \code{rightTriangle} function stored in an m-file testRightTriangle.m}]
tolerance = 1e-10;
% preconditions
angles = rightTriangle(7, 9);
assert(angles(3) == 90, 'Fundamental problem: rightTriangle is not producing a right triangle')
%% Test 1: sum of angles
angles = rightTriangle(7, 7);
assert((sum(angles) - 180) <= tolerance)
angles = rightTriangle(7, 7);
assert((sum(angles) - 180) <= tolerance)
angles = rightTriangle(2, 2 * sqrt(3));
assert((sum(angles) - 180) <= tolerance)
angles = rightTriangle(1, 150);
assert((sum(angles) - 180) <= tolerance)
%% Test: isosceles triangles
angles = rightTriangle(4, 4);
assert(abs(angles(1) - 45) <= tolerance)
assert(angles(1) == angles(2))
%% Test: 30-60-90 triangle
angles = rightTriangle(2, 2 * sqrt(3));
assert(abs(angles(1) - 30) <= tolerance)
assert(abs(angles(2) - 60) <= tolerance)
assert(abs(angles(3) - 90) <= tolerance)
%% Test: Small angle approx
angles = rightTriangle(1, 1500);
smallAngle = (pi / 180) * angles(1); % radians
approx = sin(smallAngle);
assert(abs(approx - smallAngle) <= tolerance, 'Problem with small angle approximation')
\end{lstlisting}
In a test script we can execute any code. The actual test whether or
not the results match our predictions is done using the
\code{assert()}{assert} function. This function basically expects a
boolean value and if this is not true, it raises an error that, in the
context of the test does not lead to a termination of the program. In
the tests above, the argument to assert is always a boolean expression
which evaluates to \code{true} or \code{false}. Before the first unit
test (``Test 1: sum of angles'', that starts in line 5,
listing\,\ref{testscript}) a precondition is defined. The test assumes
that the $\gamma$ angle must always be 90$^\circ$ since we aim for a
right triangle. If this is not true, the further tests, will not be
executed. We further define a \varcode{tolerance} variable that is
used when comparing double values (Why might the test on equality of
double values be tricky?).
\begin{lstlisting}[label=runtestlisting, caption={Run the test!}]
result = runtests('testRightTriangle')
\end{lstlisting}
During the run, \matlab{} will put out error messages onto the command
line and a summary of the test results is then stored within the
\varcode{result} variable. These can be displayed using the function
\code{table(result)}
\begin{lstlisting}[label=testresults, caption={The test results.}, basicstyle=\ttfamily\scriptsize]
table(result)
ans =
4x6 table
Name Passed Failed Incomplete Duration Details
_________________________________ ______ ______ ___________ ________ ____________
'testR.../Test_SumOfAngles' true false false 0.011566 [1x1 struct]
'testR.../Test_IsoscelesTriangles' true false false 0.004893 [1x1 struct]
'testR.../Test_30_60_90Triangle' true false false 0.005057 [1x1 struct]
'testR.../Test_SmallAngleApprox' true false false 0.0049 [1x1 struct]
\end{lstlisting}
So far so good, all tests pass and our function appears to do what it
is supposed to do. But tests are only as good as the programmer who
designed them. The attentive reader may have noticed that the tests
only check a few conditions. But what if we passed something else than
a numeric value as the length of the sides $a$ and $b$? Or a negative
number, or zero?
\section{Debugging strategies}\label{debugging}
If you find yourself in trouble you can apply a few strategies to If you still find yourself in trouble you can apply a few strategies
solve the problem. that help to solve the problem.
\begin{enumerate} \begin{enumerate}
\item Lean back and take a breath. \item Lean back and take a breath.