SitePoint Sponsor

User Tag List

Page 2 of 2 FirstFirst 12
Results 26 to 42 of 42
  1. #26
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    This seriously lacks functionality (configuration) but here we go:

    Test
    PHP Code:
    class TestOfFileCache extends UnitTestCase
    {
        public function 
    setUp()
        {
            if (
    file_exists('/tmp/something')) @unlink('/tmp/something');
        }

        public function 
    testUnsetKeyIsValidReturnsFalse()
        {
            
    $cache = new FileCache;
            
    $this->assertFalse($cache->get('something'));
        }

        public function 
    testIsValidTrueAfterSet()
        {
            
    $cache = new FileCache;
            
    $this->assertFalse($cache->isValid('something'));
            
    $cache->set('something''some data');
            
    $this->assertTrue($cache->isValid('something'));
        }

        public function 
    testGettingUnsetKeyReturnsFalse()
        {
            
    $cache = new FileCache;
            
    $this->assertFalse($cache->get('something'));
        }

        public function 
    testGettingSetKeyContainsExactData()
        {
            
    $cache = new FileCache;
            
    $this->assertFalse($cache->get('something'));
            
    $cache->set('something''some data');
            
    $this->assertIdentical('some data'$cache->get('something'));
        }

        function 
    testCacheIsAMonoStateObject()
        {
            
    $cache = new FileCache;
            
    $this->assertFalse($cache->get('something'));
            
    $value rand(1100);
            
    $cache->set('something'$value);
            
    $this->assertEqual($value$cache->get('something'));

            
    $cache2 = new FileCache;
            
    $this->assertEqual($value$cache2->get('something'));
        }

    The Code...
    PHP Code:
    interface Cache
    {
        public function 
    set($key$value);
        public function 
    get($key);
        public function 
    isValid($key);
    }

    class 
    FileCache implements Cache
    {
        public function 
    set($key$value)
        {
            if (
    $handle = @fopen('/tmp/'.$key'w+'))
            {
                if (@
    fwrite($handle$value))
                {
                    
    fclose($handle);
                    return 
    true;
                }
            }

            return 
    false;
        }

        public function 
    get($key)
        {
            if (
    $this->isValid($key))
            {
                return 
    file_get_contents('/tmp/'.$key);
            }
            else return 
    false;
        }

        public function 
    isValid($key)
        {
            return 
    file_exists('/tmp/'.$key);
        }


  2. #27
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    How about this next?

    PHP Code:
    function testKeysWithSpecialCharacters() {
      
    $keys = array('this/that''something & something else''sales > profit');
      
    $cache = new FileCache;
      foreach (
    $keys as $key) {
        
    $value rand(0,100);
        
    $cache->set($key,$value);
        
    $this->assertEqual($value$cache->get($key));  
      }

    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  3. #28
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by sweatje
    How about this next?

    PHP Code:
    function testKeysWithSpecialCharacters() {
      
    $keys = array('this/that''something & something else''sales > profit');
      
    $cache = new FileCache;
      foreach (
    $keys as $key) {
        
    $value rand(0,100);
        
    $cache->set($key,$value);
        
    $this->assertEqual($value$cache->get($key));  
      }

    Goddammit.... see, this is where I'd fall down. I need to learn to think about all the things that could go wrong, rather than focussing entirely on perfect use.

    Slight (only small) risk of collisions. I considered base64_encode() but that contains "=" and "/".

    PHP Code:
    class FileCache implements Cache
    {
        public function 
    set($key$value)
        {
            if (
    $handle = @fopen('/tmp/testcache/'.md5($key), 'w+'))
            {
                if (@
    fwrite($handle$value))
                {
                    
    fclose($handle);
                    return 
    true;
                }
            }

            return 
    false;
        }

        public function 
    get($key)
        {
            if (
    $this->isValid($key))
            {
                return 
    file_get_contents('/tmp/testcache/'.md5($key));
            }
            else return 
    false;
        }

        public function 
    isValid($key)
        {
            return 
    file_exists('/tmp/testcache/'.md5($key));
        }

    What next? Cache directory specification? Cleaning the cache? Age?

    EDIT | I had to change the setUp() method by the way.

    PHP Code:
        public function setUp()
        {
            if (!
    is_dir('/tmp/testcache')) mkdir('/tmp/testcache');
            
    $handle opendir('/tmp/testcache');
            while (
    $file readdir($handle))
            {
                if (
    $file != '.' && $file != '..' && !is_dir('/tmp/testcache/'.$file))
                  @
    unlink('/tmp/testcache/'.$file);
            }
            
    closedir($handle);
        } 

  4. #29
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    That setup method knows an awful lot about how your FileCache works. I knows where the directory is, it creates the directory if it does not exist

    Time to pause and refactor. All of that kind of code belongs in FileCache::__construct().

    Move it there and we should still pass.

    Next, it seems like we should add a conveniance method to the API of the FileCache, something like FileCache::clearAll() which will purge all cached keys. This should work for any kind of a caching mechanism. Once you have it working, you could just have call FileCache::clearAll() without the test knowing the details of FileCache's implementation.

    What would function testClearAll() look like?
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  5. #30
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by sweatje
    That setup method knows an awful lot about how your FileCache works. I knows where the directory is, it creates the directory if it does not exist

    Time to pause and refactor. All of that kind of code belongs in FileCache::__construct().

    Move it there and we should still pass.

    Next, it seems like we should add a conveniance method to the API of the FileCache, something like FileCache::clearAll() which will purge all cached keys. This should work for any kind of a caching mechanism. Once you have it working, you could just have call FileCache::clearAll() without the test knowing the details of FileCache's implementation.

    What would function testClearAll() look like?
    Point noted about the setUp() method... It felt a little wrong whilst I was writing it.

    I'm not sure how a static call to a method for purging like that would handle this since the directory would be specified in the constructor. If called statically the function will know nothing about the location of the cache. Maybe instantiate FileCache in setUp() and run a method $cache->purgeAll(); ?

  6. #31
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by d11wtq
    I'm not sure how a static call to a method for purging like that would handle this since the directory would be specified in the constructor. If called statically the function will know nothing about the location of the cache. Maybe instantiate FileCache in setUp() and run a method $cache->purgeAll(); ?
    Now you are using TDD is it's role of exploring and defining the API. When I said FileCache::clearAll(), I just meant the clearAll method of the FileCache class, not nescesarily that you had to call it statically. It does raise an interesting point though, how should your object be configured?

    Perhaps it is time to start working of a second caching mechanism, say a MySQL cache. How could you reuse these tests to test a second concrete cache mechanism. What would be unique about each of these? Can you preserve a common API between them so they are truely interchangable?
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  7. #32
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Well I see it like this. Each mechanism can have a different configuration that's specific to itself. The main class that makes use of the mechanism needn't know how it was set up, it only needs to know how to use the features of the API. So, for a DB mechanism something like:

    PHP Code:
    $cacheMechanism = new DBCache('localhost''user''pass''my_db''my_table');
    $cache = new Cache($cacheMechanism); 
    It would be down to the developer to know how the meachanism is set up, but not how it works.

  8. #33
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Okay, perhaps time to do a little bit of refactoring on our tests to make it easier to test multiple caching mechanisms.

    PHP Code:
    abstract class CacheBaseTestCase extends UnitTestCase {
      function 
    setUp() {
        
    $this->getCache()->clearAll();
      }
      abstract function 
    getCache() {
      }
      function 
    testUnsetKeyIsValidReturnsFalse() {
        
    $cache $this->getCache();
        
    $this->assertFalse($cache->isValid('as of yet unset key'));
      }
      
    // other common test functions
    }

    class 
    FileCacheTestCase extends CacheBaseTestCase {
      function 
    getCache() {
        return new 
    FileCache;
      }
      
    // FileCache specific tests can go here
    }

    class 
    DbCacheTestCase extends CacheBaseTestCase {
      function 
    getCache() {
        return new 
    DbCache(/* setup parms */);
      }
      
    // DbCache specific tests can go here

    And then change your test runner to a Group test running the two concrete test cases.

    Here we are basically using the TemplateMethod pattern, defining all the tests but having specific concrete unit test cases replace the getCache method with a real factory producing the subject test object.
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  9. #34
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Chris, have you continued to make progress here?
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  10. #35
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by sweatje
    Chris, have you continued to make progress here?
    Hi sorry I haven't visited in the last few days. Gimme a few mins and I'll continue with this

  11. #36
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    OK, I haven't implemented the DBCache just yet so that I can double check what I'm doing with refactoring here

    Test(s):

    PHP Code:
    abstract class CacheBaseTestCase extends UnitTestCase
    {
        public function 
    setUp()
        {
            
    $this->getCache()->purgeAll();
        }

        abstract public function 
    getCache();

        public function 
    testUnsetKeyIsValidReturnsFalse()
        {
            
    $cache $this->getCache();
            
    $this->assertFalse($cache->get('something'));
        }

        public function 
    testIsValidTrueAfterSet()
        {
            
    $cache $this->getCache();
            
    $this->assertFalse($cache->isValid('something'));
            
    $cache->set('something''some data');
            
    $this->assertTrue($cache->isValid('something'));
        }

        public function 
    testGettingUnsetKeyReturnsFalse()
        {
            
    $cache $this->getCache();
            
    $this->assertFalse($cache->get('something'));
        }

        public function 
    testGettingSetKeyContainsExactData()
        {
            
    $cache $this->getCache();
            
    $this->assertFalse($cache->get('something'));
            
    $cache->set('something''some data');
            
    $this->assertIdentical('some data'$cache->get('something'));
        }

        function 
    testCacheIsAMonoStateObject()
        {
            
    $cache $this->getCache();
            
    $this->assertFalse($cache->get('something'));
            
    $value rand(1100);
            
    $cache->set('something'$value);
            
    $this->assertEqual($value$cache->get('something'));

            
    $cache2 $this->getCache();
            
    $this->assertEqual($value$cache2->get('something'));
        }

        function 
    testKeysWithSpecialCharacters() {
            
    $keys = array('this/that''something & something else''sales > profit');
            
    $cache $this->getCache();
            foreach (
    $keys as $key) {
                
    $value rand(0,100);
                
    $cache->set($key,$value);
                
    $this->assertEqual($value$cache->get($key));
            }
        }

    PHP Code:
    class TestOfFileCache extends CacheBaseTestCase
    {
        public function 
    getCache()
        {
            return new 
    FileCache('/tmp/testcache');
        }

        public function 
    testWritingToUnwritableDirectoryThrowsException()
        {
            try {
                
    $cache = new FileCache('/root/no-way-ho-say');
            } catch (
    Exception $e) {
                
    //
            
    }
            
    $this->assertIsA($e'Exception');
        }

    PHP Code:
    //I'll do this next
    class TestOfDBCache extends CacheBaseTestCase
    {
        public function 
    getCache()
        {
            return new 
    DBCache();
        }

    Code:
    PHP Code:
    interface Cache
    {
        public function 
    set($key$value);
        public function 
    get($key);
        public function 
    isValid($key);
        public function 
    purgeAll();
    }

    class 
    FileCache implements Cache
    {
        protected 
    $path;

        public function 
    __construct($path='/tmp/testcache')
        {
            
    $this->path $path;
            if (!
    is_dir($path))
            {
                if (!@
    mkdir($path)) throw new Exception('Cannot create directory '.$this->path.' for caching');
            }
        }

        public function 
    set($key$value)
        {
            if (
    $handle = @fopen($this->path.'/'.md5($key), 'w+'))
            {
                if (@
    fwrite($handle$value))
                {
                    
    fclose($handle);
                    return 
    true;
                }
            }

            return 
    false;
        }

        public function 
    get($key)
        {
            if (
    $this->isValid($key))
            {
                return 
    file_get_contents($this->path.'/'.md5($key));
            }
            else return 
    false;
        }

        public function 
    isValid($key)
        {
            return 
    file_exists($this->path.'/'.md5($key));
        }

        public function 
    purgeAll()
        {
            
    $handle opendir($this->path);
            while (
    $file readdir($handle))
            {
                if (
    $file != '.' && $file != '..' && !is_dir($this->path.'/'.$file))
                  @
    unlink($this->path.'/'.$file);
            }
            
    closedir($handle);
        }
    }

    class 
    DBCache implements Cache
    {
        public function 
    set($key$value) {}
        public function 
    get($key) {}
        public function 
    isValid($key) {}
        public function 
    purgeAll() {}

    As for actually passing the tests on a DB class. I was thinking a Mock would be a plan, but is using a real database probably for the best here?

    Thanks

  12. #37
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by d11wtq
    OK, I haven't implemented the DBCache just yet so that I can double check what I'm doing with refactoring here
    Those tests all look good to me

    Quote Originally Posted by d11wtq
    As for actually passing the tests on a DB class. I was thinking a Mock would be a plan, but is using a real database probably for the best here?

    Thanks
    How about lets do both and see which suits your needs better.
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  13. #38
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Hokey cokey here it is. Seems like I had to do a lot but since I'd already done the same sort of thing for the file cache the logic is near enough the same.

    I used my DB class and resultset objects... that will open up the possibility for Mocking easier

    Non-mocked...

    Tests (excluding what I already posted above):
    PHP Code:
    class TestOfDBCache extends CacheBaseTestCase
    {
        public function 
    getCache()
        {
            return new 
    DBCache(new DB('localhost''testuser''testpass''testcache'));
        }

        public function 
    testNonValidDbThrowsException()
        {
            try {
                
    $cache = new DBCache(new DB('0.0.0.0''baduser''badpass''baddb'));
            } catch (
    Exception $e) {
                
    //
            
    }
            
    $this->assertIsA($e'Exception');
        }

    Code:
    PHP Code:
    class DBCache implements Cache
    {
        protected 
    $db;

        public function 
    __construct(DB $db_conn)
        {
            
    $this->db $db_conn;
            if (!
    $this->db->isConnected()) throw new Exception('Database provided but not connected');
        }

        public function 
    set($key$value)
        {
            if (
    $this->isValid($key))
            {
                
    $this->db->query("
                update cache
                set
                    cache_data = '"
    .$this->db->escape($value)."',
                    cache_modified = '"
    .time()."'
                where
                    cache_key = '"
    .$key."'");
            }
            else
            {
                
    $this->db->query("
                insert into cache (
                    cache_key,
                    cache_data,
                    cache_modified
                ) values (
                    '"
    .$this->db->escape($key)."',
                    '"
    .$this->db->escape($value)."',
                    '"
    .time()."'
                )"
    );
            }
        }

        public function 
    get($key)
        {
            if (
    $this->isValid($key))
            {
                
    $result $this->db->query("
                    select
                        cache_data
                    from
                        cache
                    where
                        cache_key = '"
    .$this->db->escape($key)."'");
                return 
    $result->cache_data;
            }
        }

        public function 
    isValid($key)
        {
            
    $result $this->db->query("
            select
                id
            from
                cache
            where
                cache_key = '"
    .$this->db->escape($key)."'");
            if (
    $result->length() > 0) return true;
            else return 
    false;
        }

        public function 
    purgeAll()
        {
            
    $this->db->query("
            delete from cache"
    );
        }

    Do I need to post my DB class? It should be obvious what we would mock and what we wouldn't. In fact, I'll just steam ahead and do it if you like? Unless you have some comments on the above?

    Thanking you!

  14. #39
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Hmmm.... OK so this:

    PHP Code:
        public function getCache()
        {
            return new 
    DBCache(new DB('localhost''testuser''testpass''testcache'));
        } 
    Passes the type hint:

    PHP Code:
    __construct(DB $db_conn
    But when Mocked, the object passed will be of type "MockDB". Pointers? I can obviously remove the type hint but given the dependency on the methods expected it doesn't seem wise. Unless I change the type hint to look for an interface rather than an actual class...

  15. #40
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    First a warning, Though your DB class is a clear external resource bondary, SQL itself is somewhat tough to mock, considering things like whitespace and even column order often do not matter in the SQL itself.

    Second, you type hint is spot on. Then you only need to mock the interface itself. I will call that "iDB"

    That said, lets do it and see what happens:

    PHP Code:
    Mock::generate('iDB');
    Mock::generate('resultset'); //whatever your resultset class or interface is

    class DbCacheMockTestCase extends CacheBaseTestCase {
      function 
    getCache($db) {
        return new 
    DbCache($db);
      }

    Of course, all of the CacheBaseTest methods expect to have the getCache have no parameter So lets think about that again.

    PHP Code:
    class DbCacheMockTestCase extends CacheBaseTestCase {
      function 
    getCache($db=false) {
        if (
    $db) {   
          return new 
    DbCache($db);
        }
        return new 
    DbCache($this->getMockDb());
      }
      protected 
    $db;
      function 
    getMockDb() {
        if (
    $this->db) {
          return 
    $this->db;
        } 
        return new 
    MockiDB;
      }
      function 
    setMockDb($mock) {
        
    $this->db $mock;
      }

    So now we can override the MockiDB passed into the DbCache when requested by the getCache() method. Now we have to tackle any failing test cases. Of course, I don't have it running, but I will take a crack at what I think it might look like and you can hopefully run from there.

    PHP Code:
    class DbCacheMockTestCase extends CacheBaseTestCase {
      
    //...
      
    function testUnsetKeyIsValidReturnsFalse() {
        
    $rs = new MockResultSet;
        
    $rs->expectOnce('length');
        
    $rs->setReturnValue('length'0);

        
    $db = new MockiDB;
        
    $db->expectOnce('query', array( new WantedPatternExpectation('/select.*from.*cache/ims'));
        
    $db->setReturnValue('query'$rs);

        
    $this->setMockDb($db); 
        
    parent::testUnsetKeyIsValidReturnsFalse(); 
      }
    //...

    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  16. #41
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I'm taking a shot at implementing this now but it looks like I need to override a fair few methods in my abstract class in order to get this working since we need all the mock setup stuff in the tests in order to have each instance of the mock provide the correct data. It doesn't feel quite right for some reason.

    Now, if the getCache() method knew which test was being run it could provide correctly set up mocks and we could leave the code in our abstract class untouched/non-overridden but that's... well... ughh

  17. #42
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    You could start out just stubing in empty methods so the abstract ones are not called. You should at least do one or two with the mock db class to get the feel for what is going on. You are recreating a relativly complex relationship: what sql is being passed to the DB class, what resultset object you are returning, what methods will be called on that result set, etc.

    My database access classes have been refactored to the point were all access is a sincle executeCursor call to the DB class returning a single resultset. This is resonably easy to mock and if read only, I sometimes do integration testing against live data as well. Even in these cases, both the mock based (complete unit tests) and live db (integration tests) both end up living in the test cases permanently.

    Regards,
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •