Tdd Best Practice In Using Restful Api

Hi there,

I’m constantly looking for the best way to use TDD in Yii app development. Nowday most web app are composed by a fronted, an API layer (usually JSON) to provide async calls to the server and a backend. By my side, most used test in this of app are unit tests and functional ones. The latter the most widely showed in guides and books leverage PHPUnit + Selenium, but Behat + Mink seems very cool too (but I’m not really confident with it yet)

If you ever used functional tests that use a browser (like Selenium) you know that the less you have to run them the better you feel. This cause they’re slower, harder to maintain and sometimes (like the popup FB Login using JS SDK) are painful.

When working with a single web page application I care about testing JSON output of my apis. I’d like to test these functionalities with a unit test like approach in order to have faster tests that are easier to maintain. Considering that most of my Controller’s action are availaible to Logged only user using accessControl filter I wondered on the best ways to have my tests up and running.

At this moment I think to have two ways to accomplish this

  • use cUrl toward the desired enpoint to get the JSON

  • directly invoke the controller’s function

In the first scenario I can use fixtures but I got no way to mock CWebUser class (to emulate a logged user), using Apache when the cUrl comes it gets executed by an instance of my CWebApplication that is not the one executed by PHPUnit. I can get rid of this problem by making all my API calls stateless and, as a consequence, removing accessControl filter.

In the second one the only way I found to mock CWebUser class is to override it in the test class that I’m executing. This approach pays until I dont need to test use cases requiring different type of user, and I got no way to change at runtime (or at setup time) my webuser mock. The only way I found to mock my webuser is the one you can find below, this cause $this->getMock(‘WebUser’) doesnt affect anyway CWebApplication’s WebUser (read-only) singleton defined in the configuration file.

Here comes a concrete example:




class UserControllerTest extends CDbTestCase

{

        public $fixtures=array(

                /* NEEDED FIXTURES*/

        );


        public function testUserCanGetFavouriteStore() {

                

                $controller = new UserController(1);

                $result = json_decode($controller->actionAjaxGetFavStore());                    

                $this->assertInternalType('array', $result->data);              

                        

                $model  = $result->data[0];

                $this->assertEquals($model->name, "Nome dello Store");  

                

        }

}


class WebUser extends CWebUser {

        

        public function getId() {

            return 1;

        }


        public function getIsGuest() {

                return false;

        }

};



Anyone can suggest me a better method? There’s anything wrong in this approach?

Best Regards

Depending on how you’ve set up your API, you can log in using a test user account - I assume that you’re using a test db, right?

There would be no need to mock anything if you can authenticate with your api interface, either by an API key or a user/password combo.

It’s been some time since I looked into REST with Yii, but you can definitely authenticate through it.

I hope this is of some help in your case. I’ve had it working fine with Selenium in the past.

Look at "public function testLoginLogout()":


<?php


class SiteTest extends WebTestCase

{

	public function testIndex()

	{

		$this->open('');

		$this->assertTextPresent('Welcome');

	}


	public function testContact()

	{

		$this->open('?r=site/contact');

		$this->assertTextPresent('Contact Us');

		$this->assertElementPresent('name=ContactForm[name]');


		$this->type('name=ContactForm[name]','tester');

		$this->type('name=ContactForm[email]','tester@example.com');

		$this->type('name=ContactForm[subject]','test subject');

		$this->click("//input[@value='Submit']");

		$this->waitForTextPresent('Body cannot be blank.');

	}


	public function testLoginLogout()

	{

		$this->open('');

		// ensure the user is logged out

		if($this->isTextPresent('Logout'))

			$this->clickAndWait('link=Logout (demo)');


		// test login process, including validation

		$this->clickAndWait('link=Login');

		$this->assertElementPresent('name=LoginForm[username]');

		$this->type('name=LoginForm[username]','demo');

		$this->click("//input[@value='Login']");

		$this->waitForTextPresent('Password cannot be blank.');

		$this->type('name=LoginForm[password]','demo');

		$this->clickAndWait("//input[@value='Login']");

		$this->assertTextNotPresent('Password cannot be blank.');

		$this->assertTextPresent('Logout');


		// test logout process

		$this->assertTextNotPresent('Login');

		$this->clickAndWait('link=Logout (demo)');

		$this->assertTextPresent('Login');

	}

}

Thanks Jacmoe, I’m not sure that authenticathed APIs can help in this scenario. If you suppose to be able to retrieve data from your server to populate the frontend of a logged user you need anyway to rely on his session dont you?

This is ok if I move toward a almost stateless API integration, but most of the time I just have controller’s actions that returns Json data to populate the frontend. Maybe it’s just useless to test a JSON output?

Moreover CWebUser login method rely on session or cookies so I cant login while testing just from the command line…

Regards,

Thanks Outrage, I know this way to test my app but I’m trying to minimize the number of test relying on Selenium and the browser, that’s why I’d like to test just the Json endpoint (if makes sense).

Regards,

Ok actually the only solution that me and my team found is creating a stub WebUser class. Rewriting WebUser class in this way you can authenticate a user without having Yii relying on the session.





class WebUserMock extends WebUser {


public function login($identity,$duration=0)

{

    $id=$identity->getId();

    $states=$identity->getPersistentStates();

    if($this->beforeLogin($id,$states,false))

    {

        $this->changeIdentity($id,$identity->getName(),$states);

        $duration = 0;

        if($duration>0)

        {

            if($this->allowAutoLogin)

                $this->saveToCookie($duration);

            else

                throw new CException(Yii::t('yii','{class}.allowAutoLogin must be set true in order to use cookie-based authentication.',

                    array('{class}'=>get_class($this))));

        }


        $this->afterLogin(false);

    }

    return !$this->getIsGuest();

}


public function changeIdentity($id,$name,$states)

{   

    $this->setId($id);

    $this->setName($name);

    $this->loadIdentityStates($states);

}


// Load user model.

protected function loadUser() {

    $id = Yii::app()->user->id;

        if ($id!==null)

            $this->_model=User::model()->findByPk($id);

    return $this->_model;

}

};



In the setUp method of your test class you can login any user (in this case leveraging my fixtures)


//a piece of your setUp() method....

$identity = new UserIdentity($this->users('User_2')->email, md5('demo'));               

$identity->authenticate();      

if($identity->errorCode===UserIdentity::ERROR_NONE)                                     

    Yii::app()->user->login($identity);             



As a final thing to do just override the user component in the test configuration file and tell him to use this one:

protected/config/test.php


'user'=>array(

    'class' => 'application.tests.mock.WebUserMock',

    'allowAutoLogin'=>false,

), 



Still not sure that this is the best way to handle it but seems to work fine