TDD workshop. Anybody welcome; Data storage abstraction.

Discussion of testing theory and practice, including methodologies (such as TDD, BDD, DDD, Agile, XP) and software - anything to do with testing goes here. (Formerly "The Testing Side of Development")

Moderator: General Moderators

matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

Aha, so what you're saying is that

Code: Select all

 
$dataStore = new DataStore(new XmlModel('file.xml'));
$dataStore->set('whatever', 'xyz');
 
$dataStore->save();
 
Will save the whatever/xyz back to the file.xml
and

Code: Select all

 
$dataStore = new DataStore(new XmlModel('file.xml'));
$dataStore->set('whatever', 'xyz');
 
$dataStore->save(new YamlModel('file.yml'));
 
will
1) read the file.xml and load it's data in as an array
2) set the whatever to xyz
3a) write the data to a new file file.yml
3b) or write the data to an existing file.yml
User avatar
arjan.top
Forum Contributor
Posts: 305
Joined: Sun Oct 14, 2007 4:36 am
Location: Hoče, Slovenia

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by arjan.top »

I was just writing test for such interface, so I'm for it :)

Code: Select all

 
  public function testSavingWithModelPassedAsArgument() {
    $storageModel = $this->_createStorageModel();
    $storageModel->expectOnce('save');
    $this->_dataStore->save($storageModel);
  }
 
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by Chris Corbyn »

~matthijs, you're correct, it would:

a) Load data from XmlModel
b) Modify it by setting 'whatever' to 'xyz'
c) Save the new modified data to YamlStore in just the same way it would save to XmlStore if no argument was passed :)

~arjan.top, that looks like a good first step :)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by Chris Corbyn »

Code to pass your test ~arjan.top:

Code: Select all

 public function save(StorageModel $storageModel = null) {
    if (!isset($storageModel)) {
      $storageModel = $this->_storageModel;
    }
    return $storageModel->save($this->_store);
  }
I'd also like to get some test coverage to ensure that the DataStore doesn't try saving to the StorageModel it was passed in the constructor if an optional one is given. i.e.

Code: Select all

 public function testSavingModelPassedAsArgumentDoesntSaveOriginal() {
    $this->_storageModel->expectNever('save');
    $this->_dataStore->save($this->_createStorageModel());
  }
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by Chris Corbyn »

Does anybody wish to write the test to prove that we're passing a correct array to this optional StorageModel? I think once we're sure about that we'll move onto a new class entirely. We'll limit ourselves to string data types for now for the sake of brevity. We can always expand our options later :)

Two tests:

1. Ensure a simple 1-dimensional array passed to the optional second StorageModel
2. Ensure a multidimensional array is passed to the optional second StorageModel
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

Maybe for the first?

Code: Select all

 
  public function testSetDataAndPassSimpleArrayToSecondModelSavesCorrectData(){
    $storageModel = $this->_createStorageModel();
    $storageModel->expectOnce('load', array( array('foo' => 'bar') ));
    $dataStore = $this->_createDataStore($storageModel);
    $dataStore->set('zip', 'button');
    $secondStorageModel = $this->_createStorageModel();
    $secondStorageModel->expectOnce('save', array( array('foo' => 'bar', 'zip'=>'button') )); 
    $dataStore->save($secondStorageModel);
  }
 
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

Ok, that was not right. This one passes;

Code: Select all

 
  public function testSetDataAndPassSimpleArrayToSecondModelSavesCorrectData(){
    $storageModel = $this->_createStorageModel();
    $storageModel->setReturnValue('load',  array('foo' => 'bar') );
    $dataStore = $this->_createDataStore($storageModel);
    $dataStore->set('zip', 'button');
    $secondStorageModel = $this->_createStorageModel();
    $secondStorageModel->expectOnce('save', array( array('foo' => 'bar','zip'=>'button' ) ));
    $dataStore->save($secondStorageModel);
  }
 
Somehow these arrays keep giving me headaches
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by Chris Corbyn »

Almost right, but your first expectOnce() should actually just be setReturnValue().

Your expectOnce() there is saying that you expect load() to be called with array('foo' => 'bar') as an argument. What you want is that when load() is called, it will return array('foo' => 'bar') as a value :)

Code: Select all

 public function testSetDataAndPassSimpleArrayToSecondModelSavesCorrectData(){
    $storageModel = $this->_createStorageModel();
    $storageModel->setReturnValue('load', array('foo' => 'bar'));
    $dataStore = $this->_createDataStore($storageModel);
    $dataStore->set('zip', 'button');
    $secondStorageModel = $this->_createStorageModel();
    $secondStorageModel->expectOnce('save', array( array('foo' => 'bar', 'zip'=>'button') ));
    $dataStore->save($secondStorageModel);
  }
We could also write this test without loading any existing data for brevity:

Code: Select all

 public function testSetDataAndPassSimpleArrayToSecondModelSavesCorrectData() {
    $this->_dataStore->set('foo', 'bar');
    $storageModel = $this->_createStorageModel();
    $storageModel->expectOnce('save', array( array('foo' => 'bar') ));
    $this->_dataStore->save($storageModel);    
  }
EDIT | Damn, I'm too slow :P
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

Yeah I discovered that :) ([edit]meaning I discovered my error, not that you are slow!)

Setting deeper also works, it seems:

Code: Select all

 
  public function testSetDataAndPassComplexArrayToSecondModelSavesCorrectData(){
    $storageModel = $this->_createStorageModel();
    $storageModel->setReturnValue('load',  array('foo' => 'bar') );
    $dataStore = $this->_createDataStore($storageModel);
    $dataStore->set('bar/zip', 'button');
    $secondStorageModel = $this->_createStorageModel();
    $secondStorageModel->expectOnce('save', array( array('foo' => 'bar', 'bar'=>array('zip'=>'button') ) ));
    $dataStore->save($secondStorageModel);
  }
 
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by Chris Corbyn »

Brilliant, let's document this class and move onto the first StorageModel.

I've attached the files. Anybody wanna write the Doc comments for the DataStore class? :) This is also important; although not really anything to with TDD!
Attachments
data_store.zip
(137.17 KiB) Downloaded 252 times
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

Cool!

I'd like to do the doc comments. Is there any official documentation on which style we use?
Edit: the one from phpdoc.org?
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

Something like

Code: Select all

 
<?php
 
/**
 * DataStore package
 * 
 * @package DataStore
 */
 
/**
 * Class DataStore
 * 
 * @package DataStore
 */
 
require_once dirname(__FILE__) . '/StorageModel.php';
 
class DataStore {
 
  /**
   * The storage model
   * @access private
   * @var object
   */
  private $_storageModel;
 
  /**
   * The private store of data
   * @access private
   * @var array
   */
  private $_store = array();
 
  /**
   * Constructor sets up {@link $_storageModel} and {@link $_store}
   */
  public function __construct(StorageModel $storageModel) {
    $this->_storageModel = $storageModel;
    $this->_store = (array) $this->_storageModel->load();
  }
 
  /**
   * Sets a value based on input $key and $value
   * @param $key 
   * @param $value
   */
  public function set($key, $value) {
    $container =& $this->_getContainer($key, true);
    $container[$this->_getEndKey($key)] = $value;
  }
  
  /**
   * Gets a value based on input $key
   * @param $key 
   * @return $value
   */
  public function get($key) {
    if ($this->has($key)) {
      $container =& $this->_getContainer($key);
      return $container[$this->_getEndKey($key)];
    }
  }
 
  /**
   * Returns true or false if input $key exists
   * @param $key 
   * @return boolean 
   */
  public function has($key) {
    $container =& $this->_getContainer($key);
    return (is_array($container)
      && array_key_exists($this->_getEndKey($key), $container));
  }
 
  /**
   * Removes a $value from the store based on input $key
   * @param $key 
   */
  public function remove($key) {
    if ($this->has($key)) {
      $container =& $this->_getContainer($key);
      unset($container[$this->_getEndKey($key)]);
    }
  }
  
  /**
   * Save the current state of the store
   * @param $storageModel  
   * @return boolean 
   */
  public function save(StorageModel $storageModel = null) {
    if (!isset($storageModel)) {
      $storageModel = $this->_storageModel;
    }
    return $storageModel->save($this->_store);
  }
 
  // -- Private methods
  
  /**
   * @access private
   * @param $key 
   * @return ? 
   */
  private function _getEndKey($key) {
    $keys = explode('/', $key);
    return array_pop($keys);
  }
 
  /**
   * @access private
   * @param $key 
   * @param $creatIfNotFound
   * @return $container
   */
  private function &_getContainer($key, $createIfNotFound = false) {
    $container =& $this->_store;
    foreach (array_slice(explode('/', $key), 0, -1) as $nextKey) {
      if (!array_key_exists($nextKey, $container)) {
        if (!$createIfNotFound) {
          return false;
        }
        $container[$nextKey] = array();
      }
      $container =& $container[$nextKey];
    }
    return $container;
  }
  
}
 
 
I quickly put this together, there's probably errors in it. I haven't finished the - may I say not so well documented - documentation on phpdocs yet ;)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by Chris Corbyn »

Very good :) I'd throw the types in there too:

i.e.

@param string $key

or

@param StorageModel $storageModel

I don't believe {@link} will work for things that are declared private too so I'd probably not mention the private members in the documentation for the constructor :) I'm a picky bugger eh? Nah, but if you're reading an API you don't really want to know about private members. I'd change the constructor comment to:

Code: Select all

 /**
   * Constructor takes a {@link StorageModel} for reading and writing.
   * @param StorageModel $storageModel
   */
  public function __construct(StorageModel $storageModel) {
    $this->_storageModel = $storageModel;
    $this->_store = (array) $this->_storageModel->load();
  }
:)

_getEndKey() always returns a string (the segment after the last "/").
&_getContainer() always returns an array
get() returns mixed

Okie doke. Where do we want to go from here?

XmlStorageModel, YamlStorageModel or ArrayStorageModel?
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

What would be the easiest step up? ArrayStorageModel? We basically already have that isn't it?
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: TDD workshop. Anybody welcome; Data storage abstraction.

Post by matthijs »

Here's the file after suggested changes

Code: Select all

 
<?php
 
/**
 * DataStore package
 * 
 * @package DataStore
 */
 
/**
 * Class DataStore
 * 
 * @package DataStore
 */
 
require_once dirname(__FILE__) . '/StorageModel.php';
 
class DataStore {
 
  /**
   * The storage model
   * @access private
   * @var object
   */
  private $_storageModel;
 
  /**
   * The private store of data
   * @access private
   * @var array
   */
  private $_store = array();
 
  /**
   * Constructor takes a {@link StorageModel} for reading and writing.
   * @param StorageModel $storageModel
   */
  public function __construct(StorageModel $storageModel) {
    $this->_storageModel = $storageModel;
    $this->_store = (array) $this->_storageModel->load();
  }
 
  /**
   * Sets a value based on input $key and $value
   * @param string $key 
   * @param string $value
   */
  public function set($key, $value) {
    $container =& $this->_getContainer($key, true);
    $container[$this->_getEndKey($key)] = $value;
  }
  
  /**
   * Gets a value based on input $key
   * @param string $key 
   * @return mixed $value
   */
  public function get($key) {
    if ($this->has($key)) {
      $container =& $this->_getContainer($key);
      return $container[$this->_getEndKey($key)];
    }
  }
 
  /**
   * Returns true or false if input $key exists
   * @param string $key 
   * @return boolean 
   */
  public function has($key) {
    $container =& $this->_getContainer($key);
    return (is_array($container)
      && array_key_exists($this->_getEndKey($key), $container));
  }
 
  /**
   * Removes a $value from the store based on input $key
   * @param string $key 
   */
  public function remove($key) {
    if ($this->has($key)) {
      $container =& $this->_getContainer($key);
      unset($container[$this->_getEndKey($key)]);
    }
  }
  
  /**
   * Save the current state of the store
   * @param StorageModel $storageModel  
   * @return boolean 
   */
  public function save(StorageModel $storageModel = null) {
    if (!isset($storageModel)) {
      $storageModel = $this->_storageModel;
    }
    return $storageModel->save($this->_store);
  }
 
  // -- Private methods
  
  /**
   * @access private
   * @param string $key 
   * @return string
   */
  private function _getEndKey($key) {
    $keys = explode('/', $key);
    return array_pop($keys);
  }
 
  /**
   * @access private
   * @param $key 
   * @param $creatIfNotFound
   * @return array
   */
  private function &_getContainer($key, $createIfNotFound = false) {
    $container =& $this->_store;
    foreach (array_slice(explode('/', $key), 0, -1) as $nextKey) {
      if (!array_key_exists($nextKey, $container)) {
        if (!$createIfNotFound) {
          return false;
        }
        $container[$nextKey] = array();
      }
      $container =& $container[$nextKey];
    }
    return $container;
  }
  
}
 
 
Post Reply