Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking/stubbing FTP operations in PHPUnit

Tags:

php

phpunit

People also ask

How to mock method in PHPUnit?

PHPUnit provides methods that are used to automatically create objects that will replace the original object in our test. createMock($type) and getMockBuilder($type) methods are used to create mock object. The createMock method immediately returns a mock object of the specified type.

What is PHPUnit used for?

PHPUnit is a unit testing framework for the PHP programming language. It is an instance of the xUnit design for unit testing systems that began with SUnit and became popular with JUnit. Even a small software development project usually takes hours of hard work.


Two approaches that come to mind:

  1. Create two adapters for your FTP class:

    1. The "real" one that uses PHP's ftp functions to connect to the remote server, etc.
    2. A "mock" one that does not actually connect to anything and only returns seeded data.

      The FTP class' connect() method then looks like this:

      public function connect($name, $opt=array())
      {
        return $this->getAdapter()->connect($name, $opt);
      }
      

      The mock adapter might look something like this:

      class FTPMockAdapter
        implements IFTPAdapter
      {
        protected $_seeded = array();
      
        public function connect($name, $opt=array())
        {
          return $this->_seeded['connect'][serialize(compact('name', 'opt'))];
        }
      
        public function seed($data, $method, $opt)
        {
          $this->_seeded[$method][serialize($opt)] = $data;
        }
      }
      

      In your test, you would then seed the adapter with a result and verify that connect() is getting called appropriately:

      public function setUp(  )
      {
        $this->_adapter = new FTPMockAdapter();
        $this->_fixture->setAdapter($this->_adapter);
      }
      
      /** This test is worthless for testing the FTP class, as it
       *    basically only tests the mock adapter, but hopefully
       *    it at least illustrates the idea at work.
       */
      public function testConnect(  )
      {
        $name    = '...';
        $opt     = array(...);
        $success = true
      
        // Seed the connection response to the adapter.
        $this->_adapter->seed($success, 'connect', compact('name', 'opt'));
      
        // Invoke the fixture's connect() method and make sure it invokes the
        //  adapter properly.
        $this->assertEquals($success, $this->_fixture->connect($name, $opt),
          'Expected connect() to connect to correct server.'
        );
      }
      

    In the above test case, setUp() injects the mock adapter so that tests can invoke the FTP class' connect() method without actually triggering an FTP connection. The test then seeds the adapter with a result that will only be returned if the adapter's connect() method were called with the correct parameters.

    The advantage to this method is that you can customize the behavior of the mock object, and it keeps all of the code in one place if you need to use the mock across several test cases.

    The disadvantages to this method are that you have to duplicate a lot of functionality that has already been built (see approach #2), and arguably you've now introduced another class that you have to write tests for.

  2. An alternative is to use PHPUnit's mocking framework to create dynamic mock objects in your test. You'll still need to inject an adapter, but you can create it on-the-fly:

    public function setUp(  )
    {
      $this->_adapter = $this->getMock('FTPAdapter');
      $this->_fixture->setAdapter($this->_adapter);
    }
    
    public function testConnect(  )
    {
      $name = '...';
      $opt  = array(...);
    
      $this->_adapter
        ->expects($this->once())
        ->method('connect')
        ->with($this->equalTo($name), $this->equalTo($opt))
        ->will($this->returnValue(true));
    
      $this->assertTrue($this->_fixture->connect($name, $opt),
        'Expected connect() to connect to correct server.'
      );
    }
    

    Note that the above test mocks the adapter for the FTP class, not the FTP class itself, as that would be a rather silly thing to do.

    This approach has advantages over the previous approach:

    • You're not creating any new classes, and PHPUnit's mocking framework has its own test coverage, so you don't have to write tests for the mock class.
    • The test acts as documentation for what is happening 'under the hood' (although some might contend this is not actually a good thing).

    There are some disadvantages to this approach, however:

    • It's rather slow (in terms of performance) compared to the previous approach.
    • You have to write a lot of extra code in every test that uses the mock (although you can refactor common operations to mitigate some of this).

    See http://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects for more information.


Your approach seems to be O-K. There are always limitations to what you can unit test considering eventually you will be at low level functions that interact directly with externals.

I would recommend you base your FTP on adapters that you can mock out, and then you can cover the actual adapter testing via integration testing.


While one option would be to mock the FTP server and connect to it in tests (you would only need to change the ftp-server details/connection configuration to the mock server and not change any code) there is another one: You don't need to unit-test PHP's functions.

Those functions are not components written by you, so you should not test them.

Otherwise you could start to write test for if or even operators like + - but you don't.

To say more, it would be good to see your code. If you've got procedural style code, it's hard to mock/stub/dupe each FTP function. Actually this is not easily possible with PHP anyway.

But if you instead create a FTP connection object that wraps all these functions, you can create a test dupe for that FTP connection object. This requires refactoring of your code however.