Use stub of codeception #1694

Closed
dizews opened this Issue Dec 29, 2013 · 22 comments

Projects

None yet

3 participants

@dizews
Contributor
dizews commented Dec 29, 2013

I try to make unit test for User model:

$user = Stub::make($this->userClass, array('save' => function () { return true; }));
$user->save();

When I run this test I get exception:

[yii\base\UnknownPropertyException] Setting unknown property: Mock_User_bc11eae6::__mocked

What did I do wrong?

@Ragazzo
Contributor
Ragazzo commented Dec 29, 2013

yes, this is because Codeception mocks trying to set property on created object instead of on proxy. In this way when invoking Yii2 Model class magic you get exception because there is no such property. You can use phpunit in your testcase:

class UserTest extends TestCase
{

     public function testSomething()
     {
         $mockedUser = $this->getMockBuilder($this->userClass)->setMethods.... ->getMock();
         #or use
         $mockedUser = $this->getMock($this->userClass,....);
     }
}

See phpunit tutorial to get more info. Codeception is compatible with phpunit and all assertions is available to you.

@davertMik will you fix this behavior or it is better to use phpunit in this case?

@qiangxue
Member

Is this a Yii issue?

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

I think no, but lets wait also @DavertMik opinion as a internal core developer. Anyway as i mentioned early it is fine working with phpunit mocking.

@dizews
Contributor
dizews commented Dec 30, 2013

with code:

$user = $this->getMock($this->userClass, ['save'])
    ->expects($this->once())
    ->method('save')
    ->will($this->returnCallback(function() {return true;}));
$user->save();

I still have a problem:

PHP Fatal error:  Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::save()
@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

Will check the basic app.

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

There is no such method in basic\models\User so thats why you get error, if you are using that model.
Also this test with mock for example works fine:

namespace tests\unit\models;

use yii\codeception\TestCase;
use yii\test\DbTestTrait;

class UserTest extends TestCase
{
    use DbTestTrait;

    protected function setUp()
    {
        parent::setUp();
        // uncomment the following to load fixtures for table tbl_user
        //$this->loadFixtures(['tbl_user']);
    }

    public function testMocksAreGood()
    {
        $userMock = $this->getMockBuilder('app\models\User')->setMethods(['getAuthKey'])->getMock();
        $userMock->expects($this->once())->method('getAuthKey')->will($this->returnCallback(function() { return 'some_auth_key'; }));
        $this->assertEquals('some_auth_key',$userMock->getAuthKey());
    }

}

output:

/basic_debug_db_panel$ vendor/bin/codecept run --debug unit models/UserTest.php
Codeception PHP Testing Framework v1.9-dev
Powered by PHPUnit 3.7.28-24-g92e8faf by Sebastian Bergmann.

Unit Tests (1) -------------------------------------------------------------------------
Modules: CodeHelper
----------------------------------------------------------------------------------------
Trying to test mocks are good (tests\unit\models\UserTest::testMocksAreGood)       Ok
----------------------------------------------------------------------------------------


Time: 58 ms, Memory: 8.00Mb

OK (1 test, 2 assertions)
@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

@dizews confirm if this is ok for you :)

@dizews
Contributor
dizews commented Dec 30, 2013

@Ragazzo I use ActiveRecord model.

I add to use codeception into composer.json but still have a same problem

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

can you give table structure? will also check on AR on another table for now.

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

Still cant reproduce, i have this model and this test:

<?php

namespace app\models;

/**
 * This is the model class for table "tbl_user_profile".
 *
 * @property integer $id
 * @property string $first_name
 * @property string $last_name
 */
class UserProfile extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'tbl_user_profile';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['first_name', 'last_name'], 'required'],
            [['first_name', 'last_name'], 'string', 'max' => 255]
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'first_name' => 'First Name',
            'last_name' => 'Last Name',
        ];
    }
}
namespace tests\unit\models;

use yii\codeception\TestCase;
use yii\test\DbTestTrait;

class UserTest extends TestCase
{
    use DbTestTrait;

    protected function setUp()
    {
        parent::setUp();
        // uncomment the following to load fixtures for table tbl_user
        //$this->loadFixtures(['tbl_user']);
    }

    public function testMocksAreGood()
    {
        $user = new \app\models\UserProfile();
        $userMock = $this->getMockBuilder('app\models\UserProfile')->setMethods(['save'])->getMock();
        $userMock->expects($this->once())->method('save')->will($this->returnCallback(function() { return 'model_is_saved'; }));
        $this->assertEquals('model_is_saved',$userMock->save());
    }
    // TODO add test methods here
}

output

/basic_debug_db_panel$ vendor/bin/codecept run --debug unit
Codeception PHP Testing Framework v1.9-dev
Powered by PHPUnit 3.7.28-24-g92e8faf by Sebastian Bergmann.

Unit Tests (1) -------------------------------------------------------------------------
Modules: CodeHelper
----------------------------------------------------------------------------------------
Trying to test mocks are good (tests\unit\models\UserTest::testMocksAreGood)       Ok
----------------------------------------------------------------------------------------


Time: 64 ms, Memory: 9.00Mb

OK (1 test, 2 assertions)
@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

@dizews will wait for you to post here table structure and model, if this question is not solved for you. But as you see i cant reproduce your issue.

@dizews
Contributor
dizews commented Dec 30, 2013

@Ragazzo I find my mistake.

Instead of using:

$user = $this->getMock($this->userClass, ['save'])
    ->expects($this->once())
    ->method('save')
    ->will($this->returnCallback(function() {return true;}));
$user->save();

needed to use:

$user = $this->getMockBuilder($this->userClass)->getMock();
$user->expects($this->once())
    ->method('save')
    ->will($this->returnCallback(function() {return true;}));
$user->save();
@dizews
Contributor
dizews commented Dec 30, 2013

@Ragazzo please, show me the test of LoginForm.

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

right, i also thought about that case, but glad that you found that, experience :) For now there is no test for LoginForm we prefer developers to do it by themselves so they can play-around with TDD in boilerplate. But as i remember @cebe wanted to add some tests in basic boilerplate, so if you will submit PR with them, he merge.

@dizews
Contributor
dizews commented Dec 30, 2013

@Ragazzo I am studying unit tests that why I asked you to write test for LoginForm. I don't know how write a properly test because it call User model inside itself

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

I think it will be better for you to read example first on phpunit docs online. There are good sections there like TestDoubles|Mockes. As for LoginForm simple test can be like this (writing without check)

<?php

namespace tests\unit\models;

use yii\codeception\TestCase;
use app\models\LoginForm;

class UserTest extends TestCase
{

    private $_loginForm;

    protected function setUp()
    {
        parent::setUp();
        $this->_loginForm = new LoginForm();
    }


public function testValidateWrongCredentials()
{
    $this->_loginForm->attributes = ['username' => 'wrong_user_name', 'password' => 'wrong_password'];

    #log attributes to see them in debug mode if needed
    \Codeception\Util\Debug::debug($this->_loginForm->attributes);

    $this->_loginForm->validatePassword();
    $this->assertArrayHasKey('password', $this->_loginForm->errors, 'password error message should be set');
}

public function testValidateWrongPassword()
{
    #demo user exists
    $this->_loginForm->attributes = ['username' => 'demo', 'password' => 'wrong_password'];

    #log attributes to see them in debug mode if needed
    \Codeception\Util\Debug::debug($this->_loginForm->attributes);

    $this->_loginForm->validatePassword();
    $this->assertArrayHasKey('password', $this->_loginForm->errors, 'password error message should be set');
}


public function testValidateCorrectCredentials()
{
    #demo user exists and password is correct
    $this->_loginForm->attributes = ['username' => 'demo', 'password' => 'demo'];

    #log attributes to see them in debug mode if needed
    \Codeception\Util\Debug::debug($this->_loginForm->attributes);

    $this->_loginForm->validatePassword();
    $this->assertArrayNotHasKey('password', $this->_loginForm->errors, 'password error message should not be set');
}

}
@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

I don't know how write a properly test because it call User model inside itself

true, this is very good that your pointed this out, can be solved by IoC method DI, so you pass user instance into LoginForm. But correct way is not to expose class interface and test only methods inputs/outputs and some without deep internal logic.
Currently unavailable due to modifying debug module. Maybe will write article on how to begin basic testing in app, in next few days.
Also if you need database in your tests you can use codeception Db module or create a helper like CodeHelper with config yaml param sqlDump and in your helper in _beforeSuite event do

Yii::$app->db->createCommand(file_get_contents($this->config['sqlDump']))->execute();
@dizews
Contributor
dizews commented Dec 30, 2013

@Ragazzo, I think that for tests of LoginForm don't need to interact with database. I think we should replace getUser method of LoginForm but I can't do it :)

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

Ahh.... thats a long discussion about clean interface/avoiding db in tests, etc)) lets skip it. You have two options:

  1. make getUser public and mock it.
  2. pass user instance like $loginForm->user = new User(); #this can be mocked or extended from some class

But keep in mind that it is OK to use in tests db, and dont be tdd-infected :)

@dizews
Contributor
dizews commented Dec 30, 2013

@Ragazzo I find why I can't mocked getUser of LoginForm.
MockBuilder can't work with private methods.

@Ragazzo
Contributor
Ragazzo commented Dec 30, 2013

of course, and it should not. Anyway as problem is solved, you could close issue.

@dizews dizews closed this Dec 30, 2013
@dizews
Contributor
dizews commented Dec 30, 2013

thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment