Combinatorial Testing: The Introduction

According to the US National Institute of Standards and Technology’s (NIST) page on Combinatorial Testing, combinatorial testing can reduce costs for software testing, and its applications in software engineering can be significant. This is not surprising, as combinatorial testing is the cornerstone of any type of software testing. In its simplest form, one can think of it as finding the correct combination for a padlock. But instead of looking for a combination that opens the lock, the quality assurance team looks for combinations that lead to a violation of one or more requirements—the bugs. The technique is simple and universal, and in theory, it provides the most sought-out feature in testing, which is complete test coverage.

Combinatorial methods can reduce costs for software testing and have significant applications in software engineering.

What is combinatorial testing?

Combinatorial testing is a technique that checks all, the exhaustive case, or just some combinations of system inputs. The most common application of combinatorial testing is to check combinations of input parameters or configuration options. Checking combinations of the system’s input parameters is the simplest form of application. A system can either be complex or be as simple as a function.

Applying combinatorial testing

For example, here is a function that defines a pressure switch. This function has bugs for some specific combinations of values for the pressure and volume input parameters. Combinatorial testing would check the pressure switch function against all possible combinations of pressure and volume.

1
2
3
4
5
6
7
8
9
def pressure_switch(pressure, volume):
"""Pressure switch."""
if (pressure < 10 or pressure > 30):
if (volume > 250):
assert False, "boom!"
else:
note("ok, no problem")
else:
note("ok")

Let’s assume that the pressure parameter could take values {0, 10, 20, 30, 40}, and the volume parameter could have values {0, 50, 100, 150, 200, 250, 300}. Given that the pressure has 5 possible values and the volume has 7 possible values, the total number of all combinations is 35.

pressuresvolumes=57=35pressures * volumes = 5 * 7 = 35

We can write a simple test scenario to check all of these combinations using a simple nested for-loop.

1
2
3
4
5
6
7
8
9
10
11
@TestScenario
def check_pressure_switch(self):
"""Check all combinations of pressure and volume.
"""
pressures = [0, 10, 20, 30, 40]
volumes = [0, 50, 100, 150, 200, 250, 300]

for pressure in pressures:
for volume in volumes:
with Check(f"pressure={pressure},volume={volume}", flags=TE):
pressure_switch(pressure=pressure, volume=volume)

In general, the total number of combinations, NN, is simply the product of number of values for each parameter.

N=v0v1...vnN = v_0 * v_1 v_n

The complete TestFlows.com Open-Source Testing Framework test program would be the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from testflows.core import *

def pressure_switch(pressure, volume):
"""Pressure switch."""
if (pressure < 10 or pressure > 30):
if (volume > 250):
assert False, "boom!"
else:
note("ok, no problem")
else:
note("ok")

@TestScenario
def check_pressure_switch(self):
"""Check all combinations of pressure and volume.
"""
pressures = [0, 10, 20, 30, 40]
volumes = [0, 50, 100, 150, 200, 250, 300]

for pressure in pressures:
for volume in volumes:
with Check(f"pressure={pressure},volume={volume}", flags=TE):
pressure_switch(pressure=pressure, volume=volume)

if main():
check_pressure_switch()

Executing the program above would produce the following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
Oct 10,2023 16:38:44   ⟥  Scenario check pressure switch
Check all combinations of pressure and volume.
Oct 10,2023 16:38:44 ⟥ Check pressure=0,volume=0, flags:TE
262us ⟥ [note] ok, no problem
328us ⟥⟤ OK pressure=0,volume=0, /check pressure switch/pressure=0,volume=0
Oct 10,2023 16:38:44 ⟥ Check pressure=0,volume=50, flags:TE
474us ⟥ [note] ok, no problem
578us ⟥⟤ OK pressure=0,volume=50, /check pressure switch/pressure=0,volume=50
Oct 10,2023 16:38:44 ⟥ Check pressure=0,volume=100, flags:TE
357us ⟥ [note] ok, no problem
450us ⟥⟤ OK pressure=0,volume=100, /check pressure switch/pressure=0,volume=100
Oct 10,2023 16:38:44 ⟥ Check pressure=0,volume=150, flags:TE
161us ⟥ [note] ok, no problem
203us ⟥⟤ OK pressure=0,volume=150, /check pressure switch/pressure=0,volume=150
Oct 10,2023 16:38:44 ⟥ Check pressure=0,volume=200, flags:TE
161us ⟥ [note] ok, no problem
204us ⟥⟤ OK pressure=0,volume=200, /check pressure switch/pressure=0,volume=200
Oct 10,2023 16:38:44 ⟥ Check pressure=0,volume=250, flags:TE
156us ⟥ [note] ok, no problem
197us ⟥⟤ OK pressure=0,volume=250, /check pressure switch/pressure=0,volume=250
Oct 10,2023 16:38:44 ⟥ Check pressure=0,volume=300, flags:TE
665us ⟥ Exception: Traceback (most recent call last):
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 26, in <module>
check_pressure_switch()
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 23, in check_pressure_switch
pressure_switch(pressure=pressure, volume=volume)
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 7, in pressure_switch
assert False, "boom!"
AssertionError: boom!
776us ⟥⟤ Fail pressure=0,volume=300, /check pressure switch/pressure=0,volume=300, AssertionError
Oct 10,2023 16:38:44 ⟥ Check pressure=10,volume=0, flags:TE
196us ⟥ [note] ok
247us ⟥⟤ OK pressure=10,volume=0, /check pressure switch/pressure=10,volume=0
Oct 10,2023 16:38:44 ⟥ Check pressure=10,volume=50, flags:TE
167us ⟥ [note] ok
215us ⟥⟤ OK pressure=10,volume=50, /check pressure switch/pressure=10,volume=50
Oct 10,2023 16:38:44 ⟥ Check pressure=10,volume=100, flags:TE
161us ⟥ [note] ok
204us ⟥⟤ OK pressure=10,volume=100, /check pressure switch/pressure=10,volume=100
Oct 10,2023 16:38:44 ⟥ Check pressure=10,volume=150, flags:TE
162us ⟥ [note] ok
205us ⟥⟤ OK pressure=10,volume=150, /check pressure switch/pressure=10,volume=150
Oct 10,2023 16:38:44 ⟥ Check pressure=10,volume=200, flags:TE
161us ⟥ [note] ok
208us ⟥⟤ OK pressure=10,volume=200, /check pressure switch/pressure=10,volume=200
Oct 10,2023 16:38:44 ⟥ Check pressure=10,volume=250, flags:TE
161us ⟥ [note] ok
203us ⟥⟤ OK pressure=10,volume=250, /check pressure switch/pressure=10,volume=250
Oct 10,2023 16:38:44 ⟥ Check pressure=10,volume=300, flags:TE
229us ⟥ [note] ok
270us ⟥⟤ OK pressure=10,volume=300, /check pressure switch/pressure=10,volume=300
Oct 10,2023 16:38:44 ⟥ Check pressure=20,volume=0, flags:TE
149us ⟥ [note] ok
190us ⟥⟤ OK pressure=20,volume=0, /check pressure switch/pressure=20,volume=0
Oct 10,2023 16:38:44 ⟥ Check pressure=20,volume=50, flags:TE
146us ⟥ [note] ok
187us ⟥⟤ OK pressure=20,volume=50, /check pressure switch/pressure=20,volume=50
Oct 10,2023 16:38:44 ⟥ Check pressure=20,volume=100, flags:TE
155us ⟥ [note] ok
196us ⟥⟤ OK pressure=20,volume=100, /check pressure switch/pressure=20,volume=100
Oct 10,2023 16:38:44 ⟥ Check pressure=20,volume=150, flags:TE
147us ⟥ [note] ok
193us ⟥⟤ OK pressure=20,volume=150, /check pressure switch/pressure=20,volume=150
Oct 10,2023 16:38:44 ⟥ Check pressure=20,volume=200, flags:TE
154us ⟥ [note] ok
196us ⟥⟤ OK pressure=20,volume=200, /check pressure switch/pressure=20,volume=200
Oct 10,2023 16:38:44 ⟥ Check pressure=20,volume=250, flags:TE
155us ⟥ [note] ok
197us ⟥⟤ OK pressure=20,volume=250, /check pressure switch/pressure=20,volume=250
Oct 10,2023 16:38:44 ⟥ Check pressure=20,volume=300, flags:TE
219us ⟥ [note] ok
263us ⟥⟤ OK pressure=20,volume=300, /check pressure switch/pressure=20,volume=300
Oct 10,2023 16:38:44 ⟥ Check pressure=30,volume=0, flags:TE
147us ⟥ [note] ok
189us ⟥⟤ OK pressure=30,volume=0, /check pressure switch/pressure=30,volume=0
Oct 10,2023 16:38:44 ⟥ Check pressure=30,volume=50, flags:TE
145us ⟥ [note] ok
187us ⟥⟤ OK pressure=30,volume=50, /check pressure switch/pressure=30,volume=50
Oct 10,2023 16:38:44 ⟥ Check pressure=30,volume=100, flags:TE
145us ⟥ [note] ok
187us ⟥⟤ OK pressure=30,volume=100, /check pressure switch/pressure=30,volume=100
Oct 10,2023 16:38:44 ⟥ Check pressure=30,volume=150, flags:TE
150us ⟥ [note] ok
193us ⟥⟤ OK pressure=30,volume=150, /check pressure switch/pressure=30,volume=150
Oct 10,2023 16:38:44 ⟥ Check pressure=30,volume=200, flags:TE
146us ⟥ [note] ok
188us ⟥⟤ OK pressure=30,volume=200, /check pressure switch/pressure=30,volume=200
Oct 10,2023 16:38:44 ⟥ Check pressure=30,volume=250, flags:TE
156us ⟥ [note] ok
201us ⟥⟤ OK pressure=30,volume=250, /check pressure switch/pressure=30,volume=250
Oct 10,2023 16:38:44 ⟥ Check pressure=30,volume=300, flags:TE
154us ⟥ [note] ok
196us ⟥⟤ OK pressure=30,volume=300, /check pressure switch/pressure=30,volume=300
Oct 10,2023 16:38:44 ⟥ Check pressure=40,volume=0, flags:TE
216us ⟥ [note] ok, no problem
257us ⟥⟤ OK pressure=40,volume=0, /check pressure switch/pressure=40,volume=0
Oct 10,2023 16:38:44 ⟥ Check pressure=40,volume=50, flags:TE
177us ⟥ [note] ok, no problem
220us ⟥⟤ OK pressure=40,volume=50, /check pressure switch/pressure=40,volume=50
Oct 10,2023 16:38:44 ⟥ Check pressure=40,volume=100, flags:TE
149us ⟥ [note] ok, no problem
190us ⟥⟤ OK pressure=40,volume=100, /check pressure switch/pressure=40,volume=100
Oct 10,2023 16:38:44 ⟥ Check pressure=40,volume=150, flags:TE
156us ⟥ [note] ok, no problem
201us ⟥⟤ OK pressure=40,volume=150, /check pressure switch/pressure=40,volume=150
Oct 10,2023 16:38:44 ⟥ Check pressure=40,volume=200, flags:TE
147us ⟥ [note] ok, no problem
189us ⟥⟤ OK pressure=40,volume=200, /check pressure switch/pressure=40,volume=200
Oct 10,2023 16:38:44 ⟥ Check pressure=40,volume=250, flags:TE
146us ⟥ [note] ok, no problem
187us ⟥⟤ OK pressure=40,volume=250, /check pressure switch/pressure=40,volume=250
Oct 10,2023 16:38:44 ⟥ Check pressure=40,volume=300, flags:TE
317us ⟥ Exception: Traceback (most recent call last):
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 26, in <module>
check_pressure_switch()
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 23, in check_pressure_switch
pressure_switch(pressure=pressure, volume=volume)
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 7, in pressure_switch
assert False, "boom!"
AssertionError: boom!
393us ⟥⟤ Fail pressure=40,volume=300, /check pressure switch/pressure=40,volume=300, AssertionError
14ms ⟥⟤ Fail check pressure switch, /check pressure switch, AssertionError
Traceback (most recent call last):
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 26, in <module>
check_pressure_switch()
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 23, in check_pressure_switch
pressure_switch(pressure=pressure, volume=volume)
File "/home/user/Projects/TestFlows/WebSite/py/cintro.py", line 7, in pressure_switch
assert False, "boom!"
AssertionError: boom!

Failing

✘ [ Fail ] /check pressure switch/pressure=0,volume=300 (776us)
✘ [ Fail ] /check pressure switch/pressure=40,volume=300 (393us)
✘ [ Fail ] /check pressure switch (14ms)

1 scenario (1 failed)
35 checks (33 ok, 2 failed)

Total time 14ms

Executed on Oct 10,2023 16:38
TestFlows.com Open-Source Software Testing Framework v2.0.231001.1175523

Computing combinations using the Cartesian product

A simple nested for-loop solution only works well when the number of combination variables is small. However, it does not scale if the number of parameters is large. In this case, using a Cartesian product function comes in handy.

The Python standard library provides itertools.product function that calculates the Cartesian product of iterables, and we can use it to remove nested for-loops. The testflows.combinatorics module conveniently provides it.

itertools.product(*iterables, repeat=1)
Cartesian product of input iterables.

Using the product function, we can rewrite the scenario to check all input parameter combinations of the pressure switch function as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
from testflows.combinatorics import product

@TestScenario
def check_pressure_switch(self):
"""Check all combinations of pressure and volume
using the Cartesian product."""
pressures = [0, 10, 20, 30, 40]
volumes = [0, 50, 100, 150, 200, 250, 300]

for combination in product(pressures, volumes):
pressure, volume = combination
with Check(f"pressure={pressure},volume={volume}", flags=TE):
pressure_switch(pressure=pressure, volume=volume)

As we can see, now we have only one for-loop instead of two, and if the number of combination variables was greater than two, then we would not need to add any more nested loops but instead would just pass more variables into the Cartesian product function.

Exhaustive testing and the combinatorial explosion problem

Even though we could pass as many iterables as we want into the Cartesian product function, one for each of our input parameters, the number of combinations will quickly grow, and exhaustive testing would become infeasible due to the combinatorial explosion problem.

The combinatorial explosion arises from the fact that the total number of combinations, NN, grows very rapidly, exponentially, as the number of parameters increases and becomes even worse when the number of values for each parameter is large.

N=v0v1...vnN = v_0 * v_1 v_n

For example, if we have 10 parameters (n = 10) that each have 10 possible values (v = 10), the number of all combinations is vn=1010=10billionv^n=10^{10} = {10}_{billion}, thus requiring 10 billion test combinations for complete test coverage.

If we take the case where each parameter has the same number of values, then we will get:

N=vnN = v^n

If we graph this formula for the v=2v=2 case, with the x-axis being the number of parameters, nn, and the y-axis being the number of combinations, NN, then it will look as follows:

To make it even worse, the line gets even steeper as we increase the value of vv!

So, can we conclude that combinatorial testing is a lost cause? Actually, not really, and far from it! However, this is a topic for another blog article. So please stay tuned.

If we take the case where each parameter has the same number of values, then we will get that N=vnN = v^n .

Conclusion

This was a brief introduction to combinatorial testing. We’ve seen a simple example of applying the combinatorial testing technique to a simple pressure switch function and explored two solutions that checked all possible combinations of its input parameters. The first naive solution using a nested for-loop suffered from scalability issues if the number of combination variables increased. The second solution using the Cartesian product function solved the scalability issue but still ran into problems with combinatorial explosion when the number of input parameters in the system increased and exhaustive testing of all possible combinations became infeasible. Nonetheless, the principles of combinatorial testing were applied successfully. Working around the combinatorial explosion problem is not trivial, but techniques such as using covering arrays can help us make combinatorial testing practical.

If you want to know more, read our introductory article on covering arrays titled Get Your Software Covered Using Covering Arrays. For more in-depth overview of combinatorial testing, please read an excellent article provided by NIST titled Practical Combinatorial Testing. Also, read the Combinatorial Tests section in the Handbook to find out more about how TestFlows.com Open-Source Testing Framework supports combinatorial tests.