0

I am trying to work with PHPUnit on some legacy code. The idea is I need to find out if using PHPUnit in the future is viable or feasible for the company. One problem I'm having is that we use a lot of define() functions to set up our database information. Specifically, we use 'DB_HOST' to substitute over our database's name. I'm noticing that PHPUnit isn't liking this method of getting everything uniform. Here is the relevant code + error.

require_once("./includes/class.casestatusprovider.php");
require_once("./config/config_db.php");
require_once("./config/config_tables.php");

class TestCaseStatusProvider extends TestCase
{

    public function setUp()
    {
        $csp = new CaseStatusProvider();
    }

    /**
      * @covers CaseStatusProvider::insert
      */
    public function testInsert_csrIdActive()
    {
        require_once("./config/config_db.php");
        require_once("./config/config_tables.php");
        $this->case_id=10;
        $this->csr_id=1;
        $this->assertNotFalse(insert($this->case_id, $this->csr_id));
    }
}

CODE TO BE TESTED

abstract class CaseStatusProvider
 {
    public static function insert($case_id, $csr_id)
    {
        global $DBH;
        global $session;

        if($session->isLoggedIn())
        {
            try{

                $query = "INSERT into ".DB_NAME.".".TBL_CASE_STATUS." (case_id, csr_id, created_by, effective_date, isPast) Values (?, ?, ?, ?, ?) ";
                $data = array($case_id, $csr_id, $session->user_id, time(), 0);
                $STH = $DBH->prepare($query);
                $STH->execute($data);
                $fetched = $STH->fetch();

                return $fetched;

            }catch(PDOException $e) { 
                echo $e->getMessage();
                return false;   
            }
        } 

        return false;           
    }

ERROR

 Could not connect: Unknown MySQL server host 'DB_HOST' 

So, what should we be doing to make this work? As an aside that's slightly relevant, we also can't figure out how to get global variables to work properly for objects (we have a session object and a database object, I don't know why - I'm an intern and this was coded long before I was here).

dmcoding
  • 332
  • 4
  • 18
  • 1
    The global connection is bad when testing because you should mock the behavior, you should use dependency injection (add DBH in CaseStatusProvider constuctor) because you would be able to say : if method 'fetch' is called within insert then it should return a given value and test the class behaviour – ka_lin Jul 08 '16 at 21:41
  • 1
    The idea is do you want unit tests (test single responsibility of a class) or behavior tests (test a class and all it's children to work together okay) ? – ka_lin Jul 08 '16 at 21:45
  • That's a good question. As I'm an intern and my boss isn't currently in today, I have no way to answer that question. From what I understand, though, is that he wants unit tests just judging off of the very, very basic information he gave me. – dmcoding Jul 11 '16 at 15:03
  • 1
    For unit tests all classes must have dependency injection set up (the basic answer here seems preaty easy to understand http://stackoverflow.com/questions/130794/what-is-dependency-injection#answer-16328631 ). When making unit tests (any kind) you mock objects (sounds fancy but basically it means you have a well established test) and test preconditions (mocking) and post-conditions (what the output should be) – ka_lin Jul 11 '16 at 15:48

1 Answers1

1

I thought I would answer this because it's easier.

First of all DI, you should know this by now (comments) - the class need to be modified like below (in order to mock behavior) - or by using setters but in construct seems better because it's state depends on them:

abstract class CaseStatusProvider
{

    private $database;

    private $session;

    public funtion __construct($database, $session) {
        $this->database = $database;
        $this->session = $session;
    }

    public static function insert($case_id, $csr_id)
    {
        if($session->isLoggedIn())
        {
            try{

                $query = "INSERT into ".DB_NAME.".".TBL_CASE_STATUS." (case_id, csr_id, created_by, effective_date, isPast) Values (?, ?, ?, ?, ?) ";
                $data = array($case_id, $csr_id, $this->session->user_id, time(), 0);
                $STH = $this->database->prepare($query);
                $STH->execute($data);
                $fetched = $STH->fetch();
                return $fetched;
            }catch(PDOException $e) { 
                echo $e->getMessage();
                return false;   
            }
        } 
        return false;           
    }
}

class classToBeTested extends CaseStatusProvider
{

}

And our test case should look something like this: Please note that when using DI we can force the bevavior of the given classes.

class TestCaseStatusProvider extends TestCase
{

    private $session;

    private $database;
    //we need to mock the behavior of the statement in order to retrieve different data sets
    //according to our test cases
    private $pdoStatement;

    private $databaseClass;

    public function setUp()
    {
        //we start by mocking the database
        $this->database = $this->getMock('mysqli'); // I'm guessing mysqli

        //mock the statement in order to controll the fetch method later
        $this->pdoStatement = this->getMock('PDOStatement');
        $this->pdoStatement->method('execute')
            ->willReturn(null); // we'll just mock the fetch method in our test cases

        $this->database->method('prepare')
            ->willReturn($pdoStatement); // we mock the retrieval of a PDOStatement

        //then we mock the session
        $this->session = $this->getMock('YourSessionClass');
        //since you are accessing user_id from the class you should mock it
        $this->session->user_id = 20;

        $this->databaseClass = new classToBeTested( $this->session);
    }


    public function testInsertOk()
    {
        //We mock that the user is logged in
        $this->session->method('isLoggedIn')
             ->willReturn(true);
        $this->pdoStatement->method('fetch')
            ->willReturn(array()); // we'll just mock the fetch method, no actual data needed here
        $this->assertNotFalse($this->databaseClass->insert(1, 1));
    }

    public function testInsertKo1()
    {
        //We mock that the user is logged in
        $this->session->method('isLoggedIn')
             ->willReturn(false);
        //no need to mock the fetch method because it will not be executed
        $this->assertFalse($this->databaseClass->insert(1, 1));
    }

    public function testInsertKo2()
    {
        //We mock that the user is logged in
        $this->session->method('isLoggedIn')
             ->willReturn(true);
        $this->pdoStatement->method('fetch')
            ->will($this->throwException(new PDOException)); //mock exception on insert
        $this->assertFalse($this->databaseClass->insert(1, 1));
    }
}

P.S : Try to change your classes to adopt the [single responsibility principle1

In short the insert method should just insert (not check if the user is logged - this should be done in another class which has an instance of CaseStatusProvider which checks is the user is logged) and return true or false if error (or throw exception)

P.S.S : The provided code might have typos, I haven't ran it...

Configuration (link):

<phpunit
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"

         <!--bootstrap="/path/to/bootstrap.php"--> 
         [...]
         >
  <!-- ... -->
</phpunit>

You can pass a bootstrap file to have access to all classes in your project (if you are using a framework it is build (ex: Symfony makes bootstrap.cache.php)

Or you can make your own (depends on your projects's file structure).

This way you do not need require or require_once to load classes which you need to test

ka_lin
  • 9,329
  • 6
  • 35
  • 56
  • 1
    This is the longest answer I have ever posted :P – ka_lin Jul 11 '16 at 16:30
  • I appreciate the writeup - I do however have a question. You imply that I don't want to actually communicate with the DB at this point in time in my testing, but this part of the code exists explicitly to talk with the DB to get data. All of the functions of this file go and get information from the DB based on information provided by the function call. From what I've seen/read elsewhere, it seems I would actually want to instantiate the DB object in this situation. Or am I totally crazy and should not listen to those guys? – dmcoding Jul 12 '16 at 16:21
  • 1
    Depends if you want integration tests (real communication with DB) or mock data sets (as I just exemplified above - look for the line `$this->pdoStatement->method('fetch')`, here I am telling what the DB should return without actually executing a sql statement) – ka_lin Jul 12 '16 at 17:20
  • Right, I get how that works now. I'm just in the position where we want to actually access the database information because the functions are simply database calls. We call a function, send it a user_id or some other id, and then do the database call based on that information and return the data set as (normally) an array. That's not saying your answer isn't *helpful*, just that it's not beneficial to my current situation. I'm positive we'll use DI in the future. – dmcoding Jul 12 '16 at 18:16
  • 1
    Well....you just need to pass the real connection to the DB then(one exclusively for tests-you might want to test delete functions as well) :) and still mock the session class (in my opinion), you will just need a class helper (or whatever you want to call it) that builds your database, tables and fills in data for your test cases and also another one that purges your database (so you can have the same data set for each test suite or test case). – ka_lin Jul 13 '16 at 07:39
  • Sounds solid. Looks like I'm going to go the route of dbUnit. This will be an exciting journey. Yippee. Thanks for all the help/feedback. – dmcoding Jul 13 '16 at 19:57
  • Hope the info was useful, happy coding :) – ka_lin Jul 13 '16 at 20:34
  • The longer this internship goes the more I realize there is no such thing as happy coding when you do it for a living thanks for the help, peace brah. – dmcoding Jul 13 '16 at 20:39