Using regression tests
Overview
Teaching: 40 min
Exercises: 10 minQuestions
Test-driven development
Objectives
Be able to create and run test files
Understand how test discrepancies and runtime regressions could be identified and interpreted
Learn the ‘Make it right, then make it fast’ concept
The code of AvgOrdOfGroup
is very simple and we expect it to be correct. It avoids problems with
running out of memory as it iterates over the group instead of creating a list of its elements
(calling AsList(SymmetricGroup(11))
already results in exceeding the permitted
memory). However, it takes several minutes to calculate the average order of an
element of SymmetricGroup(11)
and thus needs improvement. This chapter deals with improving AvgOrdOfGroup
using some facts from Group Theory and particularly with designing automated tests to check the correctness
of these changes.
At this point we should think about putting avgorg.g
under version control to be able to revert changes if needed instead
of creating a new function to keep the old one around. Additionally, we should start testing our function
methodically as we move along. This can be done efficiently by using regression testing, i.e. testing based on rerunning previously completed tests to check that new changes do not impact their correctness or worsen their performance.
First we need to create a test file. The test file looks exactly like a GAP session. Thus, it is easy to create a test file simply by copying and pasting the GAP session with all GAP prompts, inputs and outputs into a text file. Let us have a closer look at testing files.
GAP test files are just text files but it is common practice to name them with the extension .tst
. Now create the file avgord.tst
(for simplicity in the current directory) with the following content:
# tests for average order of a group element
# permutation group
gap> n := 9;;
gap> S := SymmetricGroup(n);;
gap> A := AlternatingGroup(n);;
gap> D := DihedralGroup(2*n);;
gap> AvgOrdOfGroup(S);
3291487/362880
gap> AvgOrdOfGroup(A);
1516831/181440
gap> AvgOrdOfGroup(D);
79/18
As you see, the test file may include comments with certain rules specifying
where they may be placed, because one should be able to distinguish comments
in the test file from GAP output started with #
. For that purpose
lines at the beginning of the test file starting with #
and one empty line
together with one or more lines starting with #
are considered to be comments.
All other lines are considered GAP output from the preceding GAP input. Please be careful:
By default, the Test
function considers blank spaces and additional empty lines as a difference in the output.
To run the test one should use the function Test
, as documented here.
For example (assuming that the function AvgOrdOfGroup
is already loaded):
Test("avgord.tst");
true
In this case Test
reported no discrepancies and returned true
, indicating that AvgOrdOfGroup
has passed the test.
Floats vs. Ints in AvgOrdOfGroup
- What would the return value of
AvgOrdOfGroup
be if the variablesum
would be a float (e.g.sum := 0.0
) instead of an integer?- What would this mean for the tests?
Solution
- The return value would be a float.
- Floating point errors could force the test to fail even though the code is correct.
Creating your own test file.
Create your own test file and run a test on
AvgOrdOfGroup
for the following groups:SymmetricGroup(7)
,DihedralGroup(14)
andAlternatingGroup(7)
.Solution
S := SymmetricGroup(7);; D := DihedralGroup(14);; A := AlternatingGroup(7);; gap> AvgOrdOfGroup(S); 31333/5040 gap> AvgOrdOfGroup(D); 57/14 gap> AvgOrdOfGroup(A); 12601/2520
Writing good test functions is an art in itself. We only cover the very basics in this course.
We will now add more groups to avgord.tst
to demonstrate that the
code also works with other kinds of groups and to show various ways of
combining commands in the test file:
# tests for average order of a group element
# permutation group
gap> n := 9;;
gap> S := SymmetricGroup(n);;
gap> A := AlternatingGroup(n);;
gap> D := DihedralGroup(2*n);;
gap> AvgOrdOfGroup(S);
3291487/362880
gap> AvgOrdOfGroup(A);
1516831/181440
gap> AvgOrdOfGroup(D);
79/18
# permutation group
gap> S:=SymmetricGroup(9);
Sym( [ 1 .. 9 ] )
gap> AvgOrdOfGroup(S);
3291487/362880
# pc group
gap> D:=DihedralGroup(512);
<pc group of size 512 with 9 generators>
gap> AvgOrdOfGroup(D);
44203/512
gap> G:=TrivialGroup();; # suppress output
gap> AvgOrdOfGroup(G);
1
# matrix group over a finite field
gap> AvgOrdOfGroup(SL(2,5));
221/40
Let us check the extended version of the test again and see if it works:
Test("avgord.tst");
true
We have not used the underlying group structure in our AvgOrdOfGroup
function yet. It works just as fine
on a set of permutations not forming a group as the following example shows:
gap> coll := [(1,2,3),(2,3,4),(8,9,3),(1,2)];
[ (1,2,3), (2,3,4), (3,8,9), (1,2) ]
gap> AvgOrdOfGroup(coll);
11/4
gap> IsInt(last);
false
We now want to work on a better implementation of AvgOrdOfGroup
using the group structure of
the argument thus improving the runtime of our algorithm. For this purpose, we rename our original function
to AvgOrdOfCollection
and define a new AvgOrdOfGroup
function. Using the fact that the order of an element is invariant
under conjugation we compute the conjugacy classes of elements and one representative of each class.
This yields the following improved version of AvgOrdOfGroup
that should be added into avgord.g
.
AvgOrdOfGroup := function(G)
local cc, sum, c;
cc:=ConjugacyClasses(G);
sum:=0;
for c in cc do
sum := sum + Order( Representative(c) ) * Size(cc);
od;
return sum/Size(G);
end;
But when we run the test, here comes a surprise!
Read("avgord.g");
Test("avgord.tst");
########> Diff in avgord.tst, line 6:
# Input is:
AvgOrdOfGroup(S);
# Expected output:
3291487/362880
# But found:
11/672
########
########> Diff in avgord.tst, line 12:
# Input is:
AvgOrdOfGroup(D);
# Expected output:
44203/512
# But found:
2862481/512
########
########> Diff in avgord.tst, line 21:
# Input is:
AvgOrdOfGroup(SL(2,5));
# Expected output:
221/40
# But found:
69/20
########
false
Indeed, we deliberately made a typo and replaced Size(c)
by Size(cc)
. The correct version should look as follows:
AvgOrdOfGroup := function(G)
local cc, sum, c;
cc:=ConjugacyClasses(G);
sum:=0;
for c in cc do
sum := sum + Order( Representative(c) ) * Size(c);
od;
return sum/Size(G);
end;
Fixing this error in avgord.g
reading the file again and running the test one more time yields:
Read("avgord.g");
Test("avgord.tst");
true
Thus, the approach “Make it right, then make it fast” helps to detect bugs immediately after they have been introduced.
Another way of creating a test file is by using the LogTo
command to record a GAP session.
During the test GAP will run all inputs from the test, compare the outputs with those in the test
file and report any differences. Let us first have a look at the LogTo()
function before concerning ourselves with testing the code.
If you want to record what you did in a GAP session to have a look at it later, you can enable logging with the LogTo
function:
LogTo("gap-intro.log");
This will create a file gap-intro.log
in the current directory which
contains all subsequent input and output appearing in your terminal.
To stop logging, you can call LogTo
without arguments:
gap> LogTo();
or leave GAP. Note that LogTo
blanks the file before starting, if it
already exists!
It can be useful to leave some comments in the log file in case you return to it in the future. You can enter the following after the GAP prompt:
# GAP Software Carpentry Lesson
then after pressing the Return key, GAP will display a new prompt and the comment will be written to the log file.
The log file records all interaction with GAP happening after the call of LogTo
but nothing that happened
before the function call.
Now we know two ways of creating test files: Copying the relevant lines out of your shell into a document or
saving the session with LogTo
.
Creating test-files with the
LogTo
-commandCreate your own test file using the
LogTo
-command and testAvgOrdOfGroup
for the following groups:SymmetricGroup(9)
,AlternatingGroup(9)
,DihedralGroup(18)
andSL(2,5)
.Solution
gap> S := SymmetricGroup(9);; A := AlternatingGroup(9);; D := DihedralGroup(18);; M := SL(2,5);; gap> AvgOrdOfGroup(S); 3291487/362880 gap> AvgOrdOfGroup(A); 1516831/181440 gap> AvgOrdOfGroup(D); 79/18 gap> AvgOrdOfGroup(M); 221/40
Continuous integration
Key Points
It is easy to create a test file by copying and pasting GAP session or use the LogTo-command.
Writing a good test suite is an art in itself.
Make it right, then make it fast!