diff --git a/src/application/config/hooks.php b/src/application/config/hooks.php index 6c6fb6cf..dfbb0be8 100644 --- a/src/application/config/hooks.php +++ b/src/application/config/hooks.php @@ -9,14 +9,6 @@ | http://codeigniter.com/user_guide/general/hooks.html | */ -// This hook is necessary to make the phpunit work with -// the codeigniter framework. -$hook['display_override'] = array( - 'class' => 'DisplayHook', - 'function' => 'captureOutput', - 'filename' => 'DisplayHook.php', - 'filepath' => 'hooks' -); /* End of file hooks.php */ /* Location: ./application/config/hooks.php */ \ No newline at end of file diff --git a/src/application/controllers/test.php b/src/application/controllers/test.php new file mode 100644 index 00000000..fc3f61cd --- /dev/null +++ b/src/application/controllers/test.php @@ -0,0 +1,38 @@ +load->driver('Unit_tests'); + } + + /** + * Run all available unit tests. + */ + public function index() { + $this->load->view('general/test'); + $this->unit_tests->run_all_tests(); + } + + /** + * Test only the app models. + */ + public function models() { + $this->load->view('general/test'); + $this->unit_tests->run_model_tests(); + } + + /** + * Test only the app libraries. + */ + public function libraries() { + $this->load->view('general/test'); + $this->unit_tests->run_library_tests(); + } +} + +/* End of file test.php */ +/* Location: ./application/controllers/test.php */ \ No newline at end of file diff --git a/src/application/helpers/custom_exceptions_helper.php b/src/application/helpers/custom_exceptions_helper.php new file mode 100644 index 00000000..482f13a2 --- /dev/null +++ b/src/application/helpers/custom_exceptions_helper.php @@ -0,0 +1,14 @@ +CI =& get_instance(); - $output = $this->CI->output->get_output(); - if (ENVIRONMENT != 'testing') { - echo $output; - } - } -} \ No newline at end of file diff --git a/src/application/libraries/Unit_tests/Unit_tests.php b/src/application/libraries/Unit_tests/Unit_tests.php new file mode 100644 index 00000000..7848429e --- /dev/null +++ b/src/application/libraries/Unit_tests/Unit_tests.php @@ -0,0 +1,54 @@ +CI =& get_instance(); + // Add more subclasses to the following array to expand + // the unit testing classes. + $this->valid_drivers = array( + 'Unit_tests_appointments_model' + ); + } + + /** + * Run all the available tests of the system. + * + * If a new group of tests is added, it should be also added in + * this method, in order to be executed when all the tests need to + * be run. + */ + public function run_all_tests() { + $this->run_model_tests(); + $this->run_library_tests(); + } + + /////////////////////////////////////////////////////////////////////////// + // UNIT TEST GROUPS + /////////////////////////////////////////////////////////////////////////// + /** + * Run all the models tests. + */ + public function run_model_tests() { + $this->appointments_model->run_all(); + } + + /** + * Run all the library tests. + */ + public function run_library_tests() { + + } +} \ No newline at end of file diff --git a/src/application/libraries/Unit_tests/drivers/Unit_tests_appointments_model.php b/src/application/libraries/Unit_tests/drivers/Unit_tests_appointments_model.php new file mode 100644 index 00000000..b46fd542 --- /dev/null +++ b/src/application/libraries/Unit_tests/drivers/Unit_tests_appointments_model.php @@ -0,0 +1,200 @@ +CI =& get_instance(); + + $this->CI->load->library('Unit_test'); + $this->CI->load->model('Appointments_Model'); + + // Get some sample data from the database (will be needed in the + // testing methods). + $this->provider_id = $this->CI->db + ->select('ea_users.id') + ->from('ea_users') + ->join('ea_roles', 'ea_roles.id = ea_users.id_roles', 'inner') + ->where('ea_roles.slug', DB_SLUG_PROVIDER) + ->get()->row()->id; + $this->service_id = $this->CI->db + ->select('ea_services.id') + ->from('ea_services') + ->join('ea_services_providers', 'ea_services_providers.id_services = ea_services.id', 'inner') + ->where('ea_services_providers.id_users', $this->provider_id) + ->get()->row()->id; + $this->customer_id = $this->CI->db + ->select('ea_users.id') + ->from('ea_users') + ->join('ea_roles', 'ea_roles.id = ea_users.id_roles', 'inner') + ->where('ea_roles.slug', DB_SLUG_CUSTOMER) + ->get()->row()->id; + } + + /** + * Run all the available tests + */ + public function run_all() { + // All the methods whose names start with "test" are going to be + // executed. If you want a method to not be executed remove the + // "test" keyword from the beginning. + $class_methods = get_class_methods('Unit_tests_appointments_model'); + foreach ($class_methods as $method_name) { + if (strpos($method_name, 'test_') !== FALSE) { + call_user_func(array($this, $method_name)); + } + } + + // Create a report on the browser. + $this->CI->output->append_output($this->CI->unit->report()); + } + + ///////////////////////////////////////////////////////////////////////// + // UNIT TESTS + ///////////////////////////////////////////////////////////////////////// + /** + * Test the appointment add method - insert new record. + */ + private function test_add_appointment_insert() { + // Add - insert new appointment record to the database. + $appointment_data = array( + 'start_datetime' => '2013-05-01 12:30:00', + 'end_datetime' => '2013-05-01 13:00:00', + 'notes' => 'Some notes right here...', + 'id_users_provider' => $this->provider_id, + 'id_users_customer' => $this->customer_id, + 'id_services' => $this->service_id + ); + $appointment_data['id'] = $this->CI->Appointments_Model->add($appointment_data); + $this->CI->unit->run($appointment_data['id'], 'is_int', 'Test if add() appointment (insert operation) returned the db row id.'); + + // Check if the record is the one that was inserted. + $db_data = $this->CI->db->get_where('ea_appointments', array('id' => $appointment_data['id']))->row_array(); + $this->CI->unit->run($appointment_data, $db_data, 'Test if add() appointment (insert operation) has successfully inserted a record.'); + + // Delete inserted record. + $this->CI->db->delete('ea_appointments', array('id' => $appointment_data['id'])); + } + + /** + * Test the appointment add method - update existing record. + */ + private function test_add_appointment_update() { + // Insert new appointment (this row will be updated later). + $appointment_data = array( + 'start_datetime' => '2013-05-01 12:30:00', + 'end_datetime' => '2013-05-01 13:00:00', + 'notes' => 'Some notes right here...', + 'id_users_provider' => $this->provider_id, + 'id_users_customer' => $this->customer_id, + 'id_services' => $this->service_id + ); + $this->CI->db->insert('ea_appointments', $appointment_data); + $appointment_data['id'] = $this->CI->db->insert_id(); + + // Perform the update operation and check if the record is update. + $changed_notes = 'Some CHANGED notes right here ...'; + $appointment_data['notes'] = $changed_notes; + + $update_result = $this->CI->Appointments_Model->add($appointment_data); + $this->CI->unit->run($update_result, 'is_int', 'Test if add() appointment (update operation) has returned the row id.'); + + $db_notes = $this->CI->db->get_where('ea_appointments', array('id' => $update_result))->row()->notes; + $this->CI->unit->run($changed_notes, $db_notes, 'Test add() appointment (update operation) has successfully updated record.'); + + // Delete inserted record. + $this->CI->db->delete('ea_appointments', array('id' => $appointment_data['id'])); + } + + /** + * Test appointment data insertion with wrong date + * format for the corresponding datetime db fields. + */ + private function test_add_appointment_wrong_date_format() { + // Insert new appointment with no foreign keys. + $appointment_data = array( + 'start_datetime' => '2013-0WRONG5-01 12:30WRONG:00', + 'end_datetime' => '2013-0WRONG5-01WRONG 13:00WRONG:00', + 'notes' => 'Some notes right here...', + 'id_users_provider' => $this->provider_id, + 'id_users_customer' => $this->customer_id, + 'id_services' => $this->service_id + ); + + $hasThrownException = FALSE; // This method must throw a validation exception. + try { + $this->CI->Appointments_Model->add($appointment_data); + } catch(ValidationException $valExc) { + $hasThrownException = TRUE; + } + + $this->CI->unit->run($hasThrownException, TRUE, 'Test add() appointment with wrong date format.', 'A validation exception must be thrown.'); + } + + /** + * Test the exists() method. + * + * Insert a new appointment and test if it exists. + */ + private function test_appointment_exists() { + // Insert new appointment (this row will be checked later). + $appointment_data = array( + 'start_datetime' => '2013-05-01 12:30:00', + 'end_datetime' => '2013-05-01 13:00:00', + 'notes' => 'Some notes right here...', + 'id_users_provider' => $this->provider_id, + 'id_users_customer' => $this->customer_id, + 'id_services' => $this->service_id + ); + $this->CI->db->insert('ea_appointments', $appointment_data); + $appointment_data['id'] = $this->CI->db->insert_id(); + + // Test the exists() method + $this->CI->unit->run($this->CI->Appointments_Model->exists($appointment_data), TRUE, 'Test exists() method with an inserted record.'); + + // Delete inserted record. + $this->CI->db->delete('ea_appointments', array('id' => $appointment_data['id'])); + } + + private function test_appointment_does_not_exist() { + // Create random appointmnet data that doesn't exist in the database. + $appointment_data = array( + 'start_datetime' => '2013-05-01 08:33:45', + 'end_datetime' => '2013-05-02 13:13:13', + 'notes' => 'This is totally random!', + 'id_users_provider' => '198678', + 'id_users_customer' => '194702', + 'id_services' => '8766293' + ); + + $this->CI->unit->run($this->CI->Appointments_Model->exists($appointment_data), FALSE, 'Test exists() method with an appointment that does not exist'); + } + + private function test_appointment_exists_wrong_data() { + // Create random appointmnet data that doesn't exist in the database. + $appointment_data = array( + 'start_datetime' => '2WRONG013-05-01 0WRONG8:33:45', + 'end_datetime' => '2013-0WRONG5-02 13:13:WRONG13', + 'notes' => 'This is totally random!', + 'id_users_provider' => '1986WRONG78', + 'id_users_customer' => '1WRONG94702', + 'id_services' => '876WRONG6293' + ); + + $this->CI->unit->run($this->CI->Appointments_Model->exists($appointment_data), FALSE, 'Test exists() method with wrong appointment data.'); + } + + // @task Test find_record_id + // @task Test delete + // @task Test get_batch + // @task Test get_row + // @task Test get_value + // @task Test validate_data +} \ No newline at end of file diff --git a/src/application/models/appointments_model.php b/src/application/models/appointments_model.php index 4050df2e..fe82de27 100644 --- a/src/application/models/appointments_model.php +++ b/src/application/models/appointments_model.php @@ -6,6 +6,7 @@ class Appointments_Model extends CI_Model { */ public function __construct() { parent::__construct(); + $this->load->helper('custom_exceptions'); } /** @@ -15,26 +16,27 @@ class Appointments_Model extends CI_Model { * appointment doesn't exists it is going to be inserted, otherwise * the record is going to be updated. * + * @expectedException ValidationException + * @expectedException DatabaseException + * * @param array $appointment_data Associative array with the appointmet's * data. Each key has the same name with the database fields. * @return int Returns the appointments id. */ public function add($appointment_data) { - try { - $appointment_id = $this->exists($appointment_data); - - if (!$appointment_id) { - $appointment_id = $this->insert($appointment_data); - } else { - $appointment_data['id'] = $appointment_id; - $this->update($appointment_data); - } - - return $appointment_id; - - } catch (Exception $exc) { - echo $exc->getMessage() . '
' . $exc->getTraceAsString() . '
'; + // Validate the appointment data before doing anything. + if (!$this->validate_data($appointment_data)) { + throw new ValidationException('Appointment data are not valid'); } + + // Insert or update the appointment data. + if (!$this->exists($appointment_data)) { + $appointment_data['id'] = $this->insert($appointment_data); + } else { + $appointment_data['id'] = $this->update($appointment_data); + } + + return $appointment_data['id']; } /** @@ -44,36 +46,35 @@ class Appointments_Model extends CI_Model { * the database. This method does not search with the id, but with a * combination of the appointments field values. * + * @uses find_record_id() + * * @param array $appointment_data Associative array with the appointment's * data. Each key has the same name with the database fields. - * @return int|bool Returns the record id or FALSE if it doesn't exist. + * @return bool Returns wether the record exists or not. */ - public function exists($appointment_data) { - $this->db->where(array( - 'start_datetime' => $appointment_data['start_datetime'], - 'end_datetime' => $appointment_data['end_datetime'], - 'id_users_provider' => $appointment_data['id_users_provider'], - 'id_users_customer' => $appointment_data['id_users_customer'], - 'id_services' => $appointment_data['id_services'] - )); - - $result = $this->db->get('ea_appointments'); - - return ($result->num_rows() > 0) ? $result->row()->id : FALSE; + public function exists($appointment_data) { + try { + $this->find_record_id($appointment_data); + return TRUE; + } catch(DatabaseException $dbExc) { + return FALSE; + } } /** * Insert a new appointment record to the database. * + * @expectedException DatabaseException + * * @param array $appointment_data Associative array with the appointment's * data. Each key has the same name with the database fields. * @return int Returns the id of the new record. */ private function insert($appointment_data) { if (!$this->db->insert('ea_appointments', $appointment_data)) { - throw new Exception('Could not insert new appointment record.'); + throw new DatabaseException('Could not insert appointment record.'); } - return $this->db->insert_id(); + return intval($this->db->insert_id()); } /** @@ -84,12 +85,77 @@ class Appointments_Model extends CI_Model { * * @param array $appointment_data Associative array with the appointment's * data. Each key has the same name with the database fields. - * @return @return int Returns the id of the updated record. + * @return int Returns the id of the updated record. */ private function update($appointment_data) { + if (!isset($appointment_data['id'])) { + $appointment_data['id'] = $this->find_record_id($appointment_data); + } + $this->db->where('id', $appointment_data['id']); if (!$this->db->update('ea_appointments', $appointment_data)) { - throw new Exception('Could not update appointment record.'); + throw new DatabaseException('Could not update appointment record.'); + } + + return $appointment_data['id']; + } + + /** + * Find the database id of an appointment record. + * + * The appointment data should include the following fields in order to + * get the unique id from the database: start_datetime, end_datetime, + * id_users_provider, id_users_customer, id_services. + * + * @expectedException DatabaseException + * + * @param array $appointment_data Array with the appointment data. The + * keys of the array should have the same names as the db fields. + * @return int Returns the id. + */ + public function find_record_id($appointment_data) { + $this->db->where(array( + 'start_datetime' => $appointment_data['start_datetime'], + 'end_datetime' => $appointment_data['end_datetime'], + 'id_users_provider' => $appointment_data['id_users_provider'], + 'id_users_customer' => $appointment_data['id_users_customer'], + 'id_services' => $appointment_data['id_services'] + )); + + $result = $this->db->get('ea_appointments'); + + if ($result->num_rows() == 0) { + throw new DatabaseException('Could not find appointment record id.'); + } + + return $result->row()->id; + } + + /** + * Validate appointment data before the insert or + * update operation is executed. + * + * @param array $appointment_data Contains the appointment data. + * @return boolean Returns the validation result. + */ + public function validate_data($appointment_data) { + $this->load->helper('data_validation'); + + try { + // Check if appointment dates are valid. + if (!validate_mysql_datetime($appointment_data['start_datetime'])) { + throw new Exception('Appointment start datetime is invalid.'); + } + + if (!validate_mysql_datetime($appointment_data['end_datetime'])) { + throw new Exception('Appointment end datetime is invalid.'); + } + + // @task Check if appointment foreign keys are valid. + + return TRUE; + } catch (Exception $exc) { + return FALSE; } } diff --git a/src/application/views/general/test.php b/src/application/views/general/test.php new file mode 100644 index 00000000..bbc706a5 --- /dev/null +++ b/src/application/views/general/test.php @@ -0,0 +1,45 @@ + + + + + +
+ Easy!Appointments Unit Testing + +
\ No newline at end of file diff --git a/src/configuration.php b/src/configuration.php index 4fae61ab..7ebebd4e 100644 --- a/src/configuration.php +++ b/src/configuration.php @@ -1,7 +1,7 @@