Introduction to Combinatorial Testing
In this section we will explain the basic concepts regarding Combinatorial Testing.
System Under Test (SUT)
Imagine that we are working on an online service that offers a web page and a mobile application, both parts sharing the same code base.
We are interested on finding potential bugs on these platforms before releasing them. To achieve that, we want to run several tests in our application to catch as many failures as possible, and from now on we will refer to the application that we are testing as System Under Test (SUT).
Let’s define the set of parameters that our SUT exposes:
Notice that there are a set of combinations that do not make sense to be applied in our SUT:
Whenever the OS is Linux, Windows or Mac, the Orientation must be Landscape and the Platform cannot be App.
When the Platform is Safari, the OS must be Mac or iOS.
For iOS and Android the Resolution cannot be 4K.
We can specify those constraints using Propositional Logic:
Note
We define a System Under Test (SUT) as a system that exposes a finite set of parameters p of finite domain (called SUT parameters), and a set of constraints \(\varphi\) that implicitly represent the parameterizations that the system accepts (called SUT constraints).
Test cases, parameter tuples and \(t\)-tuples
Now that we have our SUT perfectly defined, we can start thinking on how to test it.
A Test case for our SUT will be a complete assignment of values for each one of the parameters of our SUT. For example, \(\{(OS, W), (Pl, F), (Re, K), (Or, L)\}\) would be a possible test case.
Taking into account the definition of our SUT, we could generate a Test suite of 160 test cases that will exhaustively test all the combinations of our four parameters and their values. From these 160 test cases, we must remove the ones that are not consistent with our SUT constraints (for example, all the test cases that have \((OS, A)\) and \(Re, K\)). After applying this filter we will reduce our test suite to 70 tests.
Note
Notice that, although in this simple example we could try to apply all these test cases, it is usually impractical to exhaustively test our SUT. For example, for the gcc compiler (which has 189 parameters of domain 2 and 10 of domain 3), we would need more than \(4.6 \cdot 10^{61}\) tests (if we consider that a test needs 1s to be executed, we would need \(1.5 \cdot 10^{54}\) years).
We can try to reduce the size of our test suite by relaxing our problem. Now, instead of exhaustively testing all the possible combinations in our SUT, we will test all the combinations of just \(t\) parameters.
Here we are making the reasonable assumption that failures are produced by the interaction of few parameters, so they can be detected by just testing the interaction of small subsets of parameters.
For our example we will consider interactions of 2 parameters. We refer to the size of the interactions that we are considering as the strength (also refered as \(t\)). In particular, we will consider the following combinations of parameters (what we call parameter tuples):
For each one of these parameter tuples, we have to test all the valid combinations of its values (what we call t-tuples). For example, for the \((Pl, Or)\) parameter tuple, we have the following \(t\)-tuples for strength \(t=2\):
By applying the same principle to the remaining parameter tuples we find that there are 82 \(t\)-tuples for \(t=2\) in our SUT. From these 82 tuples, there are 69 allowed tuples (i.e. they are consistent with the SUT constraints) and 13 forbidden tuples (i.e. they are not consistent with the SUT constraints).
Covering Arrays
As we have seen in the previous section, we needed 70 tests to exhaustively check the parameters of our SUT, and we had 69 \(2\)-tuples.
The first approach that we could take to reduce the number of test cases is to just test the \(2\)-tuples by creating a test case for each tuple, what will save use one test.
Note
If we apply a test case for each \(t\)-tuple in the gcc compiler we will reduce from \(4.6 \cdot 10^{61}\) to \(82,809\). It is a great improvement, but we can do it better!
However, notice that the rest of the parameters must have a value assigned (even if it is its default value). So we can reduce the number of tests by fitting several \(t\)-tuples in one test. For example, consider the example test case \(\{(OS, W), (Pl, F), (Re, K), (Or, L)\}\), where we can find the following \(2\)-tuples:
Then, we can improve the number of required test cases to test all the \(2\)-tuples by applying this principle through a matematical object that is called Covering Array (CA).
Note
We define a Covering Array (CA) (denoted by \(CA(N;t,S)\)) as a set of \(n\) test cases for a SUT model \(S\) such that all \(t\)-tuples are at least covered by one test case.
In particular, we focus on Mixed Covering Arrays with Constraints (MCAC), where Mixed means that parameters domains can have different cardinalities, and with Constraints that we support SUT constraints.
Building a Covering Array for a given SUT is a complex problem that we can solve using any of the state-of-the-art algorithms implemented in ctlog
. In particular, here we present a Covering Array with just 22 test cases for our SUT:
OS |
Pl |
Re |
Or |
---|---|---|---|
W |
F |
F |
L |
L |
C |
H |
L |
M |
F |
H |
L |
A |
C |
W |
P |
i |
S |
F |
P |
L |
F |
K |
L |
A |
A |
F |
L |
M |
C |
K |
L |
W |
C |
H |
L |
W |
C |
W |
L |
L |
C |
F |
L |
i |
F |
W |
P |
i |
A |
H |
P |
i |
C |
H |
L |
M |
S |
W |
L |
A |
F |
H |
L |
L |
C |
W |
L |
M |
S |
K |
L |
M |
S |
H |
L |
W |
F |
K |
L |
i |
A |
W |
L |
M |
F |
F |
L |
Note
For the gcc compiler, we can reduce the test suite to check all the valid interactions of 2 parameters from \(82809\) test cases to just \(15\) by building a CA.
Optimal Covering Arrays
In general, we will be interested on Covering Arrays as small as possible 1. Then, we might ask about which is the optimal CA for a given SUT and strength (i.e. the CA with the smallest number of test cases). This is what the Covering Array Number problem tries to solve:
Note
The Covering Array Number (CAN) (denoted by \(CAN(t,S)\)) is the minimum \(N\) for which there exists a \(CA(N;t,S)\).
The Covering Array Number problem is to find a CA of size \(CAN(t,S)\).
For our example, we can use the maxsat-mcac
algorithm in ctlog
to find an optimal CA, which will reduce from 22 test case to just 21:
OS |
Pl |
Re |
Or |
---|---|---|---|
L |
C |
K |
L |
L |
F |
F |
L |
L |
C |
H |
L |
L |
C |
W |
L |
W |
F |
K |
L |
W |
C |
F |
L |
W |
F |
H |
L |
W |
F |
W |
L |
M |
S |
K |
L |
M |
S |
F |
L |
M |
C |
H |
L |
M |
F |
W |
L |
i |
C |
F |
P |
i |
S |
H |
P |
i |
S |
W |
P |
A |
A |
F |
L |
A |
A |
H |
P |
A |
F |
W |
P |
i |
A |
W |
L |
A |
C |
H |
L |
i |
F |
H |
L |
This has been a quick summary on the most important concepts on Combinatorial Testing that might be required to use ctlog
. You can check the Hands-on CTLog section to see a hands-on example on how to practically apply ctlog
to generate the CAs that we showed in this section.
Footnotes
- 1
If the execution of the test is quick and we want to scale to higher strength or SUTs with high number of parameters and domains, we might not care much about smaller CAs.