📜 ⬆️ ⬇️

Unit testing of models in Yii

Now I will talk about the use of TDD technology for developing models using the Yii-framework.
Initially it is assumed that the “Testing” topic was read from the official manual ( http://yiiframework.ru/doc/guide/ru/test.overview ).

So, the environment is set up and now our task will be to create models of categories and products (Category, Product) and cover them with tests.


Suppose the table of categories we have the following fields:

Product table:

')
Using Gii, we create models using these tables. These will be Category and Product models.

Since our models work with the base, test classes will inherit from CDbTestCase.
Create a test class for the Category model. Inside, we create the “category” property for the object of the class being tested and set the assignment in the setUp () method.
class CategoryTest extends CDbTestCase { /** * @var Category */ protected $category; protected function setUp() { parent::setUp(); $this->category = new Category(); } } 

In all models we have field validation, and we will begin testing with it.

We describe what validation rules will have to exist for our model:


So, "title is a required field." Guided by TDD, we first write a test.
  public function testTitleIsRequired() { $this->category->title = ''; $this->assertFalse($this->category->validate(array('title'))); } 



Run test red. We write validation.
  public function rules() { return array( array('title', 'required'), ); } 

Run test green. Refactoring is not required, so go to the next test.

Maximum length 150 characters
  public function testTitleMaxLengthIs150() { $this->category->title = generateString(151); $this->assertFalse($this->category->validate(array('title'))); $this->category->title = generateString(150); $this->assertTrue($this->category->validate(array('title'))); } // generateString(),      function generateString($length) { $random= ""; srand((double)microtime()*1000000); $char_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $char_list .= "abcdefghijklmnopqrstuvwxyz"; $char_list .= "1234567890"; // Add the special characters to $char_list if needed for($i = 0; $i < $length; $i++) { $random .= substr($char_list,(rand()%(strlen($char_list))), 1); } return $random; } 

Run test red. It is necessary to plant the test by adding validation.
  public function rules() { return array( array('title', 'required'), array('title', 'length', 'max' => 150) ); } 

We start, the test is green, everything is ok.

We now turn to validating the relationships of the model. In the “Category” model, we imply the existence of a “parent” connection.
To implement the bond test, we need fixtures.
Create a fixture file in the desired folder.
Let me remind you that the fixture file should be named in the same way as the table in which the fixture data will be stored.
  return array( 'sample' => array( 'id' => 1, ), 'sample2' => array( 'id' => 2, 'parent_id' => 1 ) ); 


Connect fixture to the test.
  class CategoryTest extends CDbTestCase { public $fixtures = array( 'categories' => 'Category' ); ... 


We write a test to check the connection.
  public function testBelongsToParent() { $category = Category::model()->findByPk(2); $this->assertInstanceOf('Category', $category->parent); } 

Run test red. It is necessary to describe the relationship in the model.
  public function relations() { return array( 'parent' => array(self::BELONGS_TO, __CLASS__, 'parent_id'), ); } 

Run test green.
With the advent of fixtures, there was a problem. The setUp () method of the CDbTestCase class calls a resource-intensive method for loading fixtures, even when no test fixtures are needed. This problem can be solved like this.
  class DbTestCase extends CDbTestCase { private static $_loadFixturesFlag = false; /** * Load fixtures one time */ protected function setUp() { if (!self::$_loadFixturesFlag && is_array($this->fixtures)) { $this->loadFixtures(); self::$_loadFixturesFlag = true; } } /** * Load fixtures */ public function loadFixtures($fixtures = null) { if ($fixtures === null) { $fixtures = $this->fixtures; } $this->getFixtureManager()->load($fixtures); } } 


We created a descendant of CDbTestCase and modified it. Now the fixtures will be called only once, but if we change the data of the fixtures in one of the tests, then we reload them by calling the loadFixtures () method manually.

Now I will give the source codes of the final version of the “Category” and “CategoryTest” classes. Writing tests for the model “Product” remains as homework.

  class CategoryTest extends DbTestCase { /** * @var Category */ protected $category; protected function setUp() { parent::setUp(); $this->category = new Category(); } public function testAllAttributesHaveLabels() { $attributes = array_keys($this->category->attributes); foreach ($attributes as $attribute) { $this->assertArrayHasKey($attribute, $this->category->attributeLabels()); } } public function testBelongsToParent() { $category = Category::model()->findByPk(2); $this->assertInstanceOf('Category', $category->parent); } public function testTitleIsRequired() { $this->category->title = ''; $this->assertFalse($this->category->validate(array('title'))); } public function testTitleMaxLengthIs150() { $this->category->title = generateString(151); $this->assertFalse($this->category->validate(array('title'))); $this->category->title = generateString(150); $this->assertTrue($this->category->validate(array('title'))); } public function testParentIdIsExist() { $this->category->parent_id = 'not-exist-value'; $this->assertFalse($this->category->validate(array('parent_id'))); $this->category->parent_id = 1; $this->assertTrue($this->category->validate(array('parent_id'))); } public function testDescriptionMaxLengthIs4000() { $this->category->description = generateString(4001); $this->assertFalse($this->category->validate(array('description'))); $this->category->description generateString(4000); $this->assertTrue($this->category->validate(array('description'))); } public function testStatusIsRequired() { $this->category->status = ''; $this->assertFalse($this->category->validate(array('status'))); } public function testStatusExistsInStatusList() { $this->category->status = 'not-in-list-value'; $this->assertFalse($this->category->validate(array('status'))); $this->category->status = array_rand($this->category->getStatusList()); $this->assertTrue($this->category->validate(array('status'))); } public function testSafeAttributesOnSearchScenario() { $category = new Category('search'); $mustBeSafe = array('title', 'description'); $safeAttrs = $category->safeAttributeNames; sort($mustBeSafe); sort($safeAttrs); $this->assertEquals($mustBeSafe, $safeAttrs); } } /** * This is the model class for table "{{categories}}". * * The followings are the available columns in table '{{categories}}': * @property integer $id * @property integer $parent_id * @property string $title * @property string $description * @property integer $status */ class Category extends CActiveRecord { const STATUS_PUBLISH = 1; const STATUS_DRAFT = 2; /** * Get status list or status label, if key exist * @static * @param string $key * @return array */ public static function getStatusList($key = null) { $arr = array( self::STATUS_PUBLISH => 'Publish', self::STATUS_DRAFT => 'Draft', ); return $key === null ? $arr : $arr[$key]; } /** * @return string the associated database table name */ public function tableName() { return '{{categories}}'; } /** * @return array validation rules for model attributes. */ public function rules() { return array( array('title, status', 'required'), array('title', 'length', 'max' => 150), array('parent_id', 'exist', 'className' => __CLASS__, 'attributeName' => 'id'), array('description', 'length', 'max' => 4000), array('status', 'in', 'range' => array_keys($this->getStatusList())), array('title, description', 'safe', 'on' => 'search') ); } /** * @return array relational rules. */ public function relations() { return array( 'parent' => array(self::BELONGS_TO, __CLASS__, 'parent_id'), ); } /** * @return array customized attribute labels (name=>label) */ public function attributeLabels() { return array( 'id' => 'ID', 'parent_id' => 'Parent ID', 'title' => 'Title', 'description' => 'Description', 'status' => 'Status', ); } /** * Retrieves a list of models based on the current search/filter conditions. * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions. */ public function search() { $criteria=new CDbCriteria; $criteria->compare('title',$this->title,true); $criteria->compare('description',$this->description,true); return new CActiveDataProvider($this, array( 'criteria'=>$criteria, )); } 

Source: https://habr.com/ru/post/147491/


All Articles