Page 1 of 1

Setup for CRUD testing

Posted: Sun Dec 10, 2006 10:13 am
by matthijs
I'm currently starting with TDD, the first tests shown below. It's for a CRUD-like app, in which I have users, routes and climbs. Below is the start of the test for the model routeclimbs. With the setup method I'm building a fresh db table with each test run (for each test method as I understood).

However, I was wondering what the most efficient way is to do this kind of testing. First thing: should I use a helper method to insert dummy data (just as I have done with the table setup). I don't feel like repeating what I wrote in testInsertMultipleClimbs() again and again for all methods that follow.
Second, are there other ways to write these tests? I know setting up a clean db with each test (method) is the best, but it does seem a bit cumbersome.

Code: Select all

<?php
include('../MC/models/RouteClimbModel.php');
include('../lib/DB.php');

define('ROUTECLIMBS_TABLE_DDL', <<<EOS
CREATE TABLE `mc_routeclimbs` (
	  `climb_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
	  `route_id` INT UNSIGNED NOT NULL,
	  `user_id` INT UNSIGNED NOT NULL,
	  `climb_date` DATE NOT NULL,
	  `climb_style` ENUM('OS','FL','RP') NOT NULL,
	  `climb_tries` SMALLINT UNSIGNED NOT NULL DEFAULT '1',
	  `climb_value` TINYINT UNSIGNED,
	  `climb_notes` TEXT NOT NULL,
	  `climb_dateposted` DATETIME NOT NULL,
	PRIMARY KEY (`climb_id`)
	)
EOS
);

class TestOfRouteClimbModel extends UnitTestCase {

	protected $conn;
	function __construct($name=''){
		$this->UnitTestCase($name);
		$this->conn = DB::conn();
	}
	function setup(){
		$this->conn->query('drop table mc_routeclimbs');
		$this->conn->query(ROUTECLIMBS_TABLE_DDL);
	}
	function testInsertNewRouteClimb(){
		$climb = new RouteClimbModel;
		$climb->route_id = '4';
		$climb->user_id  = '3';
		$climb->climb_date = '2006-10-22';
		$climb->climb_style = 'RP';
		$climb->climb_tries = '4';
		$climb->climb_value = '3';
		$climb->climb_notes = 'Nice route!!';
		$climb->save();
		$this->assertEqual(1, $climb->getId());
		$rs = $this->conn->query("select * from mc_routeclimbs");
		$this->assertEqual(1, count($rs), 'returned 1 row');
	}
	function testInsertMultipleClimbs(){
		$climb = new RouteClimbModel;
		$climb->route_id = '4';
		$climb->user_id  = '3';
		$climb->climb_date = '2006-10-22';
		$climb->climb_style = 'RP';
		$climb->climb_tries = '4';
		$climb->climb_value = '3';
		$climb->climb_notes = 'Nice route!!';
		$climb->save();
		
		$climb->route_id = '8';
		$climb->user_id  = '2';
		$climb->climb_date = '2006-03-12';
		$climb->climb_style = 'OS';
		$climb->climb_tries = '1';
		$climb->climb_value = '1';
		$climb->climb_notes = 'very hard';
		$climb->save();
		$this->assertEqual(2, $climb->getId());
		$rs = $this->conn->query("select * from mc_routeclimbs");
		$this->assertEqual(1, count($rs), 'returned 1 row');
	}
	function testGetClimbsByUser(){
		
	}
	
}

Posted: Mon Dec 11, 2006 4:32 am
by Maugrim_The_Reaper
One of the important things in testing is to keep tests isolated. If one test can effect the completion of a second test, then you've created a link between the two. There's the possibility that if one of the tests fail, so will the other. If multiple tests are in the same Test Case (different methods using the same Database) you need to make sure they never refer to the same data. Although you could create/destoy tables, it's generally easier just to manage the data from test to test yourself. Between Test Cases however, you should have a setup() method for creating a test table, and a teardown() method for removing it.

Back to your tests.

You're using the setup() method to destroy a table - in an ideal world, the previous test using the same table should have a tearDown() method for cleaning up after itself. Same goes for the current test case - add a tearDown() method to remove the table.

I can't go into huge detail since I can't see the source code, but consider if you even need a database. Does $climb object's methods perform database writes/reads, or is it delegating to an intermediary like a separate database abstraction library/class? If it's delegating then you can simply Mock the second class/API and check that $climb' methods use the Mock correctly (i.e. call all the correct methods on the second class, in the correct order, with the correct parameters, the correct number of times.

Remember you are testing the RouteClimbModel class. Any other classes should be minimised/mocked so the test is not reliant on a possible third party untested class that can create a failure you can't trace to RouteClimbModel. e.g.:
http://quantumstar.svn.sourceforge.net/ ... iew=markup

Keep an eye out for areas where the tests become a little too tedious. For example, can you add a method to your model which allows setting data by passing an associative array (keys as field names, values as data) to the constructor? Note - there's nothing wrong with what you've done here, just noting a possible shortcut. You could then define the initial data as a test class property (make it reuseable).

Don't be afraid to refactor the tests. It's just like any PHP source - if you can make the tests leaner without changing what is tested and how, then fire away. I usually end up with private methods for repetitive stuff that all tests end up doing. So long as it's not directly tested, it reduces the amount of code I need to write for tests. ;)

Posted: Mon Dec 11, 2006 4:53 am
by matthijs
Thanks for your reply Maugrim.

Not sure about the teardown method, since the setup already drops the relevant table. Should I just move the drop table rule to the teardown method then? (i'll research this question myself as well)

In my situation, the Model classes I'm testing now interact directly with the database. I use PDO for that. So my class Db looks like:

Code: Select all

<?php
class DB {
	private function __construct(){}
	
	public static function conn(){
		static $conn;
		
		if(!$conn){
			$user = '***';
			$pass = '***';
			$conn = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
		}
		
		return $conn;
	}
}

?>
and (one of) the model class(es) looks like:

Code: Select all

<?php
/**
 * RouteClimb class. Model for climbed routes
 *
 */
class RouteClimbModel {
	public $climb_id;
	public $route_id;
	public $user_id;
	public $climb_date;
	public $climb_style;
	public $climb_tries;
	public $climb_value;
	public $climb_notes;
	public $climb_dateposted;
	
	public function __construct(){
		$this->conn = DB::conn();
	}
	
	public function getId() {
		return $this->id;
	}
	
	public function save(){
		$rs = $this->conn->query("INSERT INTO mc_routeclimbs (climb_id, route_id, user_id, climb_date, 
			climb_style, climb_tries, climb_value, climb_notes, climb_dateposted) 
			VALUES (
				'', 
				'{$this->route_id}',
				'{$this->user_id}',
				'{$this->climb_date}',
				'{$this->climb_style}',
				'{$this->climb_tries}',
				'{$this->climb_value}',
				'{$this->climb_notes}',
				now())"
				);
		if ($rs){
			$this->id = (int)$this->conn->lastInsertId();
		}
		else
		{
			trigger_error('DB error: ' . $this->conn->errorMsg());
		}
	}

// .. other methods
}
?>
As you see very basic. That's just because I want to start simple (as you have advised in the past as well). I'll refactor to more layers if needed and when I'm ready.
i understand that I should keep test classes as independent as possible. I'll look at mocks.

Your idea for setting data as an associative array might be an idea as well. A side benefit of that could be that the code is more flexible for possible future changes.

Note that I will change the class to use prepared statements as well. Maybe I should do that right away.