I’m sure we all know the formula to convert Celsius to Fahrenheit, or convert Fahrenheit to Celsius.
It goes something like this:
celsius2fahrenheit = (temperature * 9/5) – 32
fahrenheit2celsius = (temperature – 32) * 5/9
I always forget and have to experiment a bit before I get the pluses, fives, and nines all in the right places — not to mention the correct spelling of f-a-h-r-e-n-h-e-i-t.
Luckily, I know 4 temperatures that I can test on scratch paper with this formula. They are:
- The boiling point of water (212F or 100C)
- The freezing point of water (32F or 0C)
- Standard temperature (59F or 17C)
- Forty below (-40F and -40C)
Today, I decided to codify it in a PHP script when I saw this request on PeoplePerHour.com
PHP Code for a Celsius/Fahrenheit convertor |
Now, I like to think I’m above doing someone’s homework for them (just barely), but I thought I’d take up the challenge. I also decided I would use PHPUnit to write the tests and came up with this:
TempConverter.php
<?php class TempConverter { public static function c2f($temp) { return $temp * 1.0 * 9/5 + 32; } public static function f2c($temp) { return ($temp -32) * 1.0 * 5/9; } }
TestTempConverter.php
<?php require_once('TempConverter.php'); class TempConverterTest extends PHPUnit_Framework_Testcase { public function testInstantiation() { $obj = new TempConverter(); $this->assertTrue($obj instanceof TempConverter); } public function testStandardTemp() { $standardF = 59; $standardC = 15; $this->assertEquals($F, TempConverter::c2f($standardC)); $this->assertEquals($C, TempConverter::f2c($standardF)); } public function testFreezing() { $freezingF = 32; $freezingC = 0; $this->assertEquals($freezingF, TempConverter::c2f($freezingC)); $this->assertEquals($freezingC, TempConverter::f2c($freezingF)); } public function testBoiling() { $boilingF = 212; $boilingC = 100; $this->assertEquals($boilingF, TempConverter::c2f($boilingC)); $this->assertEquals($boilingC, TempConverter::f2c($boilingF)); } public function testFortyBelow() { $fortyBelow = -40; $this->assertEquals($fortyBelow, TempConverter::c2f($fortyBelow)); $this->assertEquals($fortyBelow, TempConverter::f2c($fortyBelow)); } }
After a bit of shuffling I figured out the righrt formulas, and got all my tests passing.
As luck would have it, I had pretty good coverage with my test data. Not just a wide range of temperatures, but a wide variety of inputs as well. I’ve got positive numbers, negative numbers, zero, and even a result with identical numbers. But what could I do to improve?
My first thought was that I knew one other number for comparison, absolute zero, the temperature at which atoms stop moving: zero Kelvin or -273C. A quick google search gave the Fahrenheit number -459.67F So I added this test case:
public function testAbsoluteZero() { $absoluteZeroF = -459.67; $absoluteZeroC = -273; $this->assertEquals($absoluteZeroF, TempConverter::c2f($absoluteZeroC)); $this->assertEquals($absoluteZeroC, TempConverter::c2f($absoluteZeroF)); }
When I ran again, I got this message:
There was 1 failure: 1) TempConverterTest::testAbsoluteZero Failed asserting that <double:-459.4> matches expected <double:-459.67>.
After a little head scratching (and a bit more googling) I learned that they’ve changed the bar, and 0 Kelvin is now precisely -273.15C. I fixed the test and ran it again, only to get this puzzling answer:
There was 1 failure: 1) TempConverterTest::testAbsoluteZero Failed asserting that <double:-459.67> matches expected <double:-459.67>.
Now that was some odd behavior. Obviously my code was working, and I was getting the right result, but something was amiss. I tried this, which passed:
$this->assertEquals($absoluteZeroF, round(TempConverter::c2f($absoluteZeroC), 2));
Clearly it was either a rounding in PHP or something wrong with float comparisons in PHPUnit.
But that’s not what I wanted to talk about. I wanted to discuss how to structure tests. Let’s comment that out for now.
A common way of writing unit tests is to test each method in the system under test with one test method. I could have written something like this:
public function testC2F() { $standardF = 59; $standardC = 15; $freezingF = 32; $freezingC = 0; $boilingF = 212; $boilingC = 100; $fortyBelow = -40; $absoluteZeroF = -459.67; $absoluteZeroC = -273.15; $this->assertEquals($standardF, TempConverter::c2f($standardC)); $this->assertEquals($freezingF, TempConverter::c2f($freezingC)); $this->assertEquals($boilingF, TempConverter::c2f($boilingC)); $this->assertEquals($fortyBelow, TempConverter::c2f($fortyBelow)); } public function testF2C() { $standardF = 59; $standardC = 15; $freezingF = 32; $freezingC = 0; $boilingF = 212; $boilingC = 100; $fortyBelow = -40; $absoluteZeroF = -459.67; $absoluteZeroC = -273.15; $this->assertEquals($standardC, TempConverter::f2c($standardF)); $this->assertEquals($freezingC, TempConverter::f2c($freezingF)); $this->assertEquals($boilingC, TempConverter::f2c($boilingF)); $this->assertEquals($fortyBelow, TempConverter::f2c($fortyBelow)); }
This works reasonably well besides the duplication of test data (which could be solved with class constants), but the point I’m trying to make is that you can test a scenario better the other way. Then your test function describes your test scenario.
It could easily be refactored into a data driven test with a comment denoting the scenario, and an allowance for rounding errors:
/** * @dataProvider knownTemperatures */ public function testDataDrivenConversion($f, $c, $scenario) { $digits = 7; $this->assertEquals(round($c, $digits), round(TempConverter::f2c($f), $digits), $scenario); $this->assertEquals(round($f, $digits), round(TempConverter::c2f($c), $digits), $scenario); } public function knownTemperatures() { $tempuratures = array( array(59, 15, 'compare standard temperatures at standard pressure'), array(32, 0, 'compare the freezing point of water'), array(212, 100, 'compare the boiling point of water'), array(-40, -40, 'compare forty below zero (should be the same for both)'), array(-459.67, -273.15, 'compare absolute zero'), array(6, -14.44444444, 'compare a random number'), ); return $tempuratures; }
The source code is available at: