Testing a temperature converter

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:

  1. The boiling point of water (212F or 100C)
  2. The freezing point of water (32F or 0C)
  3. Standard temperature (59F or 17C)
  4. 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:

http://one-shore.com/aaron/TempConverter.zip

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s