API Project - Part 2 - Model Objects and Testing Them

Model Objects

Some thoughts and questions before we dig in:

Look at the Model class (Model.inc.php in the includes/models folder). All of the models that we create in this project will extend (inherit from) this class. Note the following:

Now take a look at the tests I created for this class by opening this page in the browser: localhost/api/tests/ModelTests.php. Note that I could have (and should have) written many more tests for this class. Don't worry about the code in ModelTests.php write now, it's complicated and we'll get into writing our own tests later.

These are the steps you need to do in order to create your own model class:

  1. Create a class that extends Model (make sure to include Model.inc.php)
  2. Add a constructor that takes an associative array (from the database) and sets the properties of the model. The constructor will be used to create and populate the model.
  3. Implement the isValid() method

Now we'll go ahead and create a model class that represents a row from the user_roles table. Open Role.inc.php (in the models folder) and add this code to get started:

include_once("Model.inc.php");

class Role extends Model{

    public $id;
    public $name;
    public $description;

}

Note:

  1. The class extends the Model class (and must therefore include/import it on the first line)
  2. There are 3 properties (instance variables) added to the class and each one will map to a column from the user_roles table.

Also note that the property names in the class are not the same as the column names in the database. This is a common problem when working with a database. When we work with Role objects we don't want to refer to their properties as user_role_id,user_role_name, and user_role_desc. Instead we would rather use id, name, and description. Later when we start working with the database, we'll see how you convert the column names to property names. But if you are interested to learn more now, you can look up ORM (object relational mapping).

Now go ahead and add this constructor function to the class (be sure to put it under the properties, but before the curly brace that closes the class:

function __construct($args=[]){
    $this->id = $args["id"] ?? 0;
    $this->name = $args["name"] ?? "";
    $this->description = $args["description"] ?? "";
}

Note:

  1. There is an optional parameter that will default to an empty array
  2. If the parameter IS passed in, then it should be an associative array that has the following keys:
    • id
    • name
    • description
  3. The keys from the associative array are used to set the properties of the Role object.
  4. The null coalescing operator (??) is used to set the properties to default values if the proper key does not exist.

Now add the isValid() method to the class (remember that we are required to implement this method because it is abstract in the super class):

public function isValid(){
    
    $valid = true;
    $this->validationErrors = [];

    // validate the id (it must be a number greater than or equal to 0)
    if(!is_numeric($this->id) || $this->id < 0){
        $valid = false;
        $this->validationErrors["id"] = "ID is not valid";
    }

    // name should be less than 30 characters
    // name should  not be empty
    if(empty($this->name)){
        $valid = false;
        $this->validationErrors["name"] = "Role name cannot be empty";
    }else if(strlen($this->name) > 30){
        $valid = false;
        $this->validationErrors["name"] = "Role name cannot be more than 30 characters";
    }

    return $valid;
}

Note:

  1. The method returns true if the role id and name are valid and returns false otherwise (we are not validating the description, although maybe we should verify that it is 200 characters or less)
  2. If we find something that is not valid, we add a message to the validationErrors property which is inherited from the Model class. The validationErrors property is an associative array and when we add a message to it, we set it's key to the name of the property that is not valid.

Now let's test the Role class. Open RoleTests.php which is in the tests folder. Then add this code to it:

include_once("../includes/models/Role.inc.php");

// we need an associative array to pass into the constructor
$options = [
    "id" => 1,
    "name" => "Test Role",
    "description" => "This role is for testing" 
];

// When each test completes, the result message will get added to this array:
$testResults = [];

// This function will run tests on the constructor in the Role class:
testConstructor();

// This function will run tests on the isValid() method: 
testIsValid();

// This will echo the test results:
echo(implode("<br>", $testResults));

function testConstructor(){
    global $testResults, $options;
    $testResults[] = "<b>Testing Constructor</b>";

    // We'll add test code here
}

function testIsValid(){
    global $testResults, $options;
    $testResults[] = "<b>Testing isValid()</b>";

    // We'll add test code here
}

Note:

Go ahead and run this page in the browser. It doesn't actually run any tests yet, but it's important for you to understand how the code flows and populates the $testResults array. You may even want to step through the code line by line with the debugger.

Now we'll add test code to the testConstructor() function, add this code to the body:

// Test 1 - Make sure we can instantiate a Role object
$r = new Role();
if($r){
    $testResults[] = "PASS - created instance";
}else{
    $testResults[] = "Fail - did not instance";
}

// Test 2 - Make sure that the id property gets set properly
$r = new Role($options);
if($r->id === 1){
    $testResults[] = "PASS - id set properly";
}else{
    $testResults[] = "Fail - id NOT set properly";
}

// Test 3 - Make sure that the name property gets set properly
$r = new Role($options);
if($r->name === "Test Role"){
    $testResults[] = "PASS - name set properly";
}else{
    $testResults[] = "Fail - name NOT set properly";
}

// Test 4 - Make sure that the description property gets set properly
$r = new Role($options);
if($r->description === "This role is for testing"){
    $testResults[] = "PASS - description set properly";
}else{
    $testResults[] = "Fail - description NOT set properly";
}

Go ahead and reload the page in the browser and you should see the results of our tests. What we are doing with each test is calling the constructor and then verifying that each property in the Role instance is set how we expect it.

This is how unit testing works, you call functions and verify that they work the way you expect them to. If they don't, then a bug has surfaced. In class we should add a bug to the Role class and run the tests to see if they catch it (remind me if you want to do this).

Now we'll go ahead and add the tests for the isValid() method, add this code to the body of the testIsValid() method:

//isValid() should return false if ID is not numeric
$r = new Role($options);
$r->id = ""; // set the id to an INVALID value

if($r->isValid() === false){
    $testResults[] = "PASS - isValid() returns false when ID is not numeric";
}else{
    $testResults[] = "FAIL - isValid() DOES NOT return false when ID is not numeric";
}

//isValid() should return false if ID is a negative number
$r = new Role($options);
$r->id = -1;

if($r->isValid() === false){
    $testResults[] = "PASS - isValid() returns false when ID is a negative number";
}else{
    $testResults[] = "FAIL - isValid() DOES NOT return false when ID is a negative number";
}

// if the ID is not valid, the validationErrors should contain an ID key
$r = new Role($options);
$r->id = -1;
$r->isValid();
$errors = $r->getValidationErrors();
//var_dump($errors);

if(isset($errors["id"])){
    $testResults[] = "PASS - ID key exists in the validation errors";
}else{
    $testResults[] = "FAIL - ID key DOES NOT exist in the validation errors";
}


// The role name should not be empty
$r = new Role($options);
$r->name = "";

if($r->isValid() === false){
    $testResults[] = "PASS - isValid() returned false when the name is empty";
}else{
    $testResults[] = "FAIL - isValid() DID NOT return false when the name is empty";
}

// The role name should not be more than 30 characters
$r = new Role($options);
$r->name = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

if($r->isValid() === false){
    $testResults[] = "PASS - isValid() returned false when the name is too long";
}else{
    $testResults[] = "FAIL - isValid() DID NOT return false when the name is too long";
}

// If the role name is empty there should be an error messsage that says 'Role name cannot be empty'
$r = new Role($options);
$r->name = "";
$r->isValid();
$errors = $r->getValidationErrors();

if(isset($errors["name"]) && $errors['name'] === "Role name cannot be empty"){
    $testResults[] = "PASS - the error message for the name was correct when name was empty";
}else{
    $testResults[] = "FAIL - the error message for the name was NOT correct when name was empty";
}

// If the role name is more than 30 characters there should be an error messsage that says 'Role cannot be more than 30 characters'
$r = new Role($options);
$r->name = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
$r->isValid();
$errors = $r->getValidationErrors();

if(isset($errors["name"]) && $errors['name'] === "Role name cannot be more than 30 characters"){
    $testResults[] = "PASS - the error message for the name was correct when the name was too long";
}else{
    $testResults[] = "FAIL - the error message for the name was NOT correct when the name was too long";
}

Go ahead and reload the page in the browser to see the test results.

Let me know if you want to discuss any of the tests, they can be extremely tedious! You may also want to step through each one in the debugger (in class remind to step through this code together).

Hopefully you are beginning to understand how the isValid() method works. It includes a series of IF statements that checks each property in the Role object. If a property is not valid, then it adds a key to the $validationErrors array (which is declared in the super class). The key is set to the name of the property that is invalid, and the value is a string/message that indicates why the property is not valid.

Now that we have tested the Role model class, we can have some confidence that it is working properly before we incorporate it into our project (actually, we could have written many, many more tests but at least we have some confidence that it's working).

Finally, open this page in the browser: http://localhost/api/tests/UserTests.php. And note that (most of) the tests are failing.

Soon, if I haven't already done so, I'll assign you to finish off the code in the User model object, and make sure that all the tests pass.

You can begin working on model objects for the tables that are in your final project (and testing them too).