yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.23k stars 6.91k forks source link

Use stub of codeception #1694

Closed dizews closed 10 years ago

dizews commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

Is this a Yii issue?

Ragazzo commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

Will check the basic app.

Ragazzo commented 10 years ago

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 commented 10 years ago

@dizews confirm if this is ok for you :)

dizews commented 10 years ago

@Ragazzo I use ActiveRecord model.

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

Ragazzo commented 10 years ago

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

Ragazzo commented 10 years ago

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 commented 10 years ago

@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 commented 10 years ago

@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 commented 10 years ago

@Ragazzo please, show me the test of LoginForm.

Ragazzo commented 10 years ago

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 commented 10 years ago

@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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

@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 commented 10 years ago

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 commented 10 years ago

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

Ragazzo commented 10 years ago

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

dizews commented 10 years ago

thanks!