Testing and refactoring boolean expressions

When dealing with non-trivial boolean expressions mutation testing often helps put things into perspective. It causes you to rethink the expression which often leads to refactoring and killing mutants.

Example

$ pip install nose
$ pip install https://github.com/sixty-north/cosmic-ray/zipball/master

Initially we start with the example in boolops1.py and test1.py. Although the test appears to be correct, all possible values for list_a and list_b are tested, there are still surviving mutants.

$ cosmic-ray run --test-runner nose --baseline=10 example.json boolops1.py -- test1.py:
$ cosmic-ray report example.json

job ID 5:Outcome.SURVIVED:boolops
command: cosmic-ray worker boolops replace_Eq_with_LtE 0 nose -- -v test1.py
--- mutation diff ---
--- a/example_10/boolops1.py
+++ b/example_10/boolops1.py
@@ -1,6 +1,6 @@


 def xnor_raise(list_a, list_b):
-    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
+    if (((len(list_a) <= 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
         raise Exception('TEST')


job ID 6:Outcome.SURVIVED:boolops
command: cosmic-ray worker boolops replace_Eq_with_LtE 1 nose -- -v test1.py
--- mutation diff ---
--- a/example_10/boolops1.py
+++ b/example_10/boolops1.py
@@ -1,6 +1,6 @@


 def xnor_raise(list_a, list_b):
-    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
+    if (((len(list_a) == 0) and (len(list_b) <= 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
         raise Exception('TEST')


job ID 13:Outcome.SURVIVED:boolops
command: cosmic-ray worker boolops replace_Gt_with_NotEq 0 nose -- -v test1.py
--- mutation diff ---
--- a/example_10/boolops1.py
+++ b/example_10/boolops1.py
@@ -1,6 +1,6 @@


 def xnor_raise(list_a, list_b):
-    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
+    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) != 0) and (len(list_b) > 0))):
         raise Exception('TEST')


job ID 14:Outcome.SURVIVED:boolops
command: cosmic-ray worker boolops replace_Gt_with_NotEq 1 nose -- -v test1.py
--- mutation diff ---
--- a/example_10/boolops1.py
+++ b/example_10/boolops1.py
@@ -1,6 +1,6 @@


 def xnor_raise(list_a, list_b):
-    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
+    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) != 0))):
         raise Exception('TEST')


job ID 23:Outcome.SURVIVED:boolops
command: cosmic-ray worker boolops replace_Gt_with_IsNot 0 nose -- -v test1.py
--- mutation diff ---
--- a/example_10/boolops1.py
+++ b/example_10/boolops1.py
@@ -1,6 +1,6 @@


 def xnor_raise(list_a, list_b):
-    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
+    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) is not 0) and (len(list_b) > 0))):
         raise Exception('TEST')


job ID 24:Outcome.SURVIVED:boolops
command: cosmic-ray worker boolops replace_Gt_with_IsNot 1 nose -- -v test1.py
--- mutation diff ---
--- a/example_10/boolops1.py
+++ b/example_10/boolops1.py
@@ -1,6 +1,6 @@


 def xnor_raise(list_a, list_b):
-    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) > 0))):
+    if (((len(list_a) == 0) and (len(list_b) == 0)) or ((len(list_a) > 0) and (len(list_b) is not 0))):
         raise Exception('TEST')


total jobs: 38
complete: 38 (100.00%)
survival rate: 15.79%

If we proceed to refactor len(list) comparisons as shown previously it is easier to figure out that the boolean function is XNOR (if and only if), also called logical equality. In boolops2.py Cosmic-Ray doesn’t detect any possible mutations because at the moment of writing it doesn’t support mutating boolean operators.

$ cosmic-ray run --test-runner nose --baseline=10 example.json boolops2.py -- test2.py:
$ cosmic-ray report example.json
total jobs: 0
no jobs completed

Another possible refactoring is boolops3.py where valid parameters are enumerated before the condition is checked.

$ cosmic-ray run --test-runner nose --baseline=10 example.json boolops3.py -- test3.py:
$ cosmic-ray report example.json
total jobs: 12
complete: 12 (100.00%)
survival rate: 0.00%

Yet another possible refactoring is boolops4.py where the condition is expressed using the built-ins any and all. Unfortunately Cosmic-Ray doesn’t recognize these as possible mutations either.

$ cosmic-ray run --test-runner nose --baseline=10 example.json boolops4.py -- test4.py:
$ cosmic-ray report example.json
total jobs: 0
no jobs completed

Source code

boolops1.py
def xnor_raise(list_a, list_b):
    if (len(list_a) == 0 and len(list_b) == 0) or (len(list_a) > 0 and (len(list_b) > 0)):
        raise Exception('TEST')
test1.py
import boolops1 as boolops
import unittest

class TestBoolOps(unittest.TestCase):
    def test_xnor_raise_a_empty_b_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([], [])

    def test_xnor_raise_a_not_empty_b_not_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([1], [2])

    def test_xnor_raise_a_empty_b_not_empty(self):
        # doesn'r raise exception
        boolops.xnor_raise([], [2])

    def test_xnor_raise_a_not_empty_b_empty(self):
        # doesn't raise exception
        boolops.xnor_raise([1], [])


if __name__ == "__main__":
    unittest.main()
boolops2.py
def xnor_raise(list_a, list_b):
    if (not list_a and not list_b) or (list_a and list_b):
        raise Exception('TEST')
test2.py
import boolops2 as boolops
import unittest

class TestBoolOps(unittest.TestCase):
    def test_xnor_raise_a_empty_b_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([], [])

    def test_xnor_raise_a_not_empty_b_not_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([1], [2])

    def test_xnor_raise_a_empty_b_not_empty(self):
        # doesn'r raise exception
        boolops.xnor_raise([], [2])

    def test_xnor_raise_a_not_empty_b_empty(self):
        # doesn't raise exception
        boolops.xnor_raise([1], [])


if __name__ == "__main__":
    unittest.main()
boolops3.py
def xnor_raise(list_a, list_b):
    valid_lists = 0
    if list_a:
        valid_lists += 1

    if list_b:
        valid_lists += 1

    if valid_lists != 1:
        raise Exception('TEST')
test3.py
import boolops3 as boolops
import unittest

class TestBoolOps(unittest.TestCase):
    def test_xnor_raise_a_empty_b_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([], [])

    def test_xnor_raise_a_not_empty_b_not_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([1], [2])

    def test_xnor_raise_a_empty_b_not_empty(self):
        # doesn'r raise exception
        boolops.xnor_raise([], [2])

    def test_xnor_raise_a_not_empty_b_empty(self):
        # doesn't raise exception
        boolops.xnor_raise([1], [])


if __name__ == "__main__":
    unittest.main()
boolops4.py
def xnor_raise(list_a, list_b):
    if not any([list_a, list_b]) or all([list_a, list_b]):
        raise Exception('TEST')
test4.py
import boolops4 as boolops
import unittest

class TestBoolOps(unittest.TestCase):
    def test_xnor_raise_a_empty_b_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([], [])

    def test_xnor_raise_a_not_empty_b_not_empty(self):
        with self.assertRaises(Exception):
            boolops.xnor_raise([1], [2])

    def test_xnor_raise_a_empty_b_not_empty(self):
        # doesn'r raise exception
        boolops.xnor_raise([], [2])

    def test_xnor_raise_a_not_empty_b_empty(self):
        # doesn't raise exception
        boolops.xnor_raise([1], [])


if __name__ == "__main__":
    unittest.main()