Tests using the unittest.mock
framework have a lot of test_ prefixes: methods are prefixed with test_
, classes are prefixed with Test_
and they are stored in files with filenames prefixed with test_
.
Finding the Bug
The unit test below shows a simple test of a TemperaturePlotSource
object created in Kelvin mode
and tested with values starting at 20
degrees Celsius which would equate to 293.15
K.
# an excerpt from Test_TemperaturePlotSource class SENSOR_DATA = (20, 21.3, 22.0, 0.0, -40, 85) def test_kelvin(self): """Create the source in Kelvin mode and test with some values.""" # Emulate the clue's temperature sensor by # returning a temperature from a small tuple # of test data mocked_clue = Mock() expected_data = (293.15, 294.45, 295.15, 273.15, 233.15, 358.15) type(mocked_clue).temperature = PropertyMock(side_effect=self.SENSOR_DATA) source = TemperaturePlotSource(mocked_clue, mode="Kelvin") for expected_value in expected_data: data = source.data() # self.assertEqual(data, # expected_value, # msg="An inappropriate check for floating-point") self.assertAlmostEqual(data, expected_value, msg="Checking converted temperature is correct")
There are three tests in a Test_TemperaturePlotSource
class. Python on a desktop computer (or Raspberry Pi) can execute these. One run is shown below.
$ python tests/test_PlotSource.py test_celsius (__main__.Test_TemperaturePlotSource) Create the source in Celsius mode and test with some values. ... ok test_fahrenheit (__main__.Test_TemperaturePlotSource) Create the source in Fahrenheit mode and test with some values. ... ok test_kelvin (__main__.Test_TemperaturePlotSource) Create the source in Kelvin mode and test with some values. ... FAIL ====================================================================== FAIL: test_kelvin (__main__.Test_TemperaturePlotSource) Create the source in Kelvin mode and test with some values. ---------------------------------------------------------------------- Traceback (most recent call last): File "tests/test_PlotSource.py", line 101, in test_kelvin msg="Checking converted temperature is correct") AssertionError: 20.0 != 293.15 within 7 places : Checking converted temperature is correct ---------------------------------------------------------------------- Ran 3 tests in 0.002s FAILED (failures=1)
The Celsius and Fahrenheit tests are fine (ok
output) but the Kelvin one is failing. The failure is reported with the value from the first comparison. For some reason, the value 20.0
is being returned when we expect 293.15
, no conversion is taking place and the value has been left in Celsius for some reason.
A previous code inspection showed an anomaly with the use of lower()
vs lower
. CircuitPython's REPL allows us to explore this interactively on the command line, using the CLUE board.
The absence of the round brackets on the end of the lower
method call introduces a critical flaw into the code. CircuitPython is somehow comparing a method call with a string. This "chalk and cheese" comparison is very unfortunate here!
Adafruit CircuitPython 5.0.0 on 2020-03-02; Adafruit CLUE nRF52840 Express with nRF52840 >>> mode="Kelvin" >>> mode[0].lower == "k" False >>> mode[0].lower == "K" False >>> mode[0] 'K' >>> mode[0].lower <bound_method> >>> mode[0].lower() == "k" True
Python is often referred to as strongly and dynamically-typed but an equality test using ==
will compare anything against anything - if they are not equivalent in some way then it will evaluate as False
.
For comparison, this is the approximate equivalent in C++ using a different member function (C++ terminology for a method) called front
.
std::string Family("Coronaviridae"); if (Family.front() == 'N') { std::cout << "Starts with N" << std::endl; } else if (Family.front == 'C') { std::cout << "Starts with C" << std::endl; }
The compilation aborts with an error because the types cannot be legitimately compared using the C++ type system. The relative strength of type systems is a subjective topic and can vary in practical terms based on how a language is used.
$ g++ -std=c++11 -o type-demonstration type-demonstration.cpp type-demonstration.cpp: In function ‘int main(int, char**)’: type-demonstration.cpp:15:30: error: invalid operands of types ‘<unresolved overloaded function type>’ and ‘char’ to binary ‘operator==’ } else if (Family.front == 'C') { ^
A language which checks types at compile-time is referred to as statically-typed.
Sometimes other software tools can help find likely bugs. In this case pylint has some ability to find dubious comparisons but it needs to be able to determine the type accurately and this prevents it from finding this particular bug. The simpler case is shown below where pylint presents a warning (W
in W0143
) about a possible mistake by the programmer.
STRING = "HELLO" if STRING[0].lower == "h": # not what programmer intended! print("Found an h or an H") # won't even be executed
$ pylint check_pylint_comparison_with_callable.py ************* Module check_pylint_comparison_with_callable check_pylint_comparison_with_callable.py:4:3: W0143: Comparing against a callable, did you omit the parenthesis? (comparison-with-callable)
Fixing the Bug
Adding the missing ()
is all that's required but the test still fails. The error message has changed.
AssertionError: -253.14999999999998 != 293.15 within 7 places : Checking converted temperature is correct
Text editor powered by tinymce.