Testing a CakePHP Contact Form with File Uploads

In my previous post I went through the process of creating a contact form which allowed you to upload a file. In this post I'm going to follow on with that code and being writing tests to ensure that it's working as expected.

As I'm just getting started in Test Driven Developement (TDD) so these tests have come after the code was written, normally you would write tests before any application code was even written however these tests are better than no tests!

PHPUnit

CakePHP uses PHPUnit for it's testing so you'll need to have that installed; the official documentation is the best place to go for howto get that up and running. (CakePHP documentation, PHPUnit Manual)

Database

When testing your applications you don't want to be testing on your live database so the first thing todo is replicate your live database just for testing. Once done update the app\Config\database.php file with your new testing database.

public $test = array(
	'datasource' => 'Database/Mysql',
	'persistent' => false,
	'host' => 'localhost',
	'login' => 'cakephp225',
	'password' => 'cakephp225',
	'database' => 'cakephp_2_2_5_testing',
	'prefix' => '',
	//'encoding' => 'utf8',
);

Writing Tests

The majority of the code that I want to test is in the Contact Model, this deals with form validation and uploading of files so I'm going to start there.

Tests live in their own folder and separated out to keep things clean, head over to the app\Test\Case\Model folder and create a ContactTest.php file.

The file below is a very simple example and comes with 2 standard methods that are used before and after any tests that are ran. Here we're loading the Contact Model and making it available in the $this->Contact variable, and finally we're truncating the contacts table so we start a fresh after each test.

<?php
class ContactTest extends CakeTestCase {

	public function setUp() {
		parent::setUp();

		// Load Contact Model
		$this->Contact = ClassRegistry::init('Contact');
	}

	public function tearDown() {
		parent::tearDown();

		// Remove all form submissions
		$this->Contact->query('TRUNCATE TABLE contacts;');
	}
}

For our very first test we're going to check what would happen when a form is submitted without any information. As we have validation rules in place I would expect the form submission to fail.

Code a testEmptyForm method (all tests start with the word 'test' followed by something descriptive of your choosing), create some empty data in the normal CakePHP format (if you don't know this then pr($this->request->data) in your Controller on a form submission) attempt to save the data and then the actual test we're doing is expecting the result to be false using the assertFalse PHPUnit method.

<?php
class ContactTest extends CakeTestCase {

	public function setUp() {
		...
	}

	public function tearDown() {
		...
	}

	public function testEmptyForm() {
		// Build the data to save
		$data = array(
			'Contact' => array(
				'name' => '',
				'email' => '',
				'message' => '',
			)
		);

		// Attempt to save
		$result = $this->Contact->save($data);

		// Test save failed
		$this->assertFalse($result);
	}
}

Great our first test is complete, in your browser head over to the test page (http://localhost/cakephp-2.2.5/test.php) in my example. Here you should see the test suite, on the left the 'app' and 'core' tests and in the main area 'App Test Cases:' followed by a link to the newly created 'Model / Contact' test file. Click the link and your tests will run; for more information click the 'Show Passes' link and this shows each of your tests in turn and whether they passed or not. As we've only the one 'testEmptyForm' test at the moment that should be green and passing!

Testing a Contact Form with CakePHP

The next test is going to check the email validation so I'm going to provide an invalid email address and again I expect the test to fail as the form shouldn't be allowed to save if the email is invalid:

public function testInvalidEmail() {
	// Build the data to save
	$data = array('Contact' => array(
		'name' => 'James Fairhurst',
		'email' => 'infojamesfairhurst.co.uk',
		'message' => 'Test Message',
	));

	// Attempt to save
	$result = $this->Contact->save($data);

	// Test save failed
	$this->assertFalse($result);
}

Next I'm going to test with a valid email, this should allow the form to save so we can write a few more assertions to ensure that the form submitted and that it's in the database. Instead of using assertFalse on the result we can see if the Contact data has been returned; this happens on a successful insert so we can use the assertArrayHasKey method (For a full list of what's available check out the chapter in the official docs).

After testing that the insert was successful I'm also going to see if it's in the database, we'll use a normal find call to get the count of entries with our submitted data. I'm expecting this to be one!

public function testValidEmail() {
	// Build the data to save
	$data = array('Contact' => array(
		'name' => 'James Fairhurst',
		'email' => 'info@jamesfairhurst.co.uk',
		'message' => 'Test Message',
	));

	// Attempt to save
	$result = $this->Contact->save($data);

	// Test successful insert
	$this->assertArrayHasKey('Contact', $result);

	// Get the count in the DB
	$result = $this->Contact->find('count', array(
		'conditions' => array(
			'Contact.email' => 'info@jamesfairhurst.co.uk',
			'Contact.name' => 'James Fairhurst',
			'Contact.message' => 'Test Message',
		),
	));

	// Test DB entry
	$this->assertEqual($result, 1);
}

Let's run our tests again and see the results:

Testing a Contact Form with CakePHP

We haven't touched file uploads yet as it's a little tricky and will require some modifications to the Contact Model so that we can test correctly. However before we do there's a test we can run to check that the contact form works with an empty file. I'm going to spoof the file array and simply enter it along with the form data (Again if you want an example just pr($this->request->data) in the Contact Controller). Exactly the same as our previous test but with an empty file.

public function testFormWithEmptyFile() {
	// Build the data to save along with an empty file
	$data = array('Contact' => array(
		'name' => 'James Fairhurst',
		'email' => 'info@jamesfairhurst.co.uk',
		'message' => 'Test Empty File Upload',
		'filename' => array(
			'name' => '',
			'type' => '',
			'tmp_name' => '',
			'error' => 4,
			'size' => 0,
		)
	));

	// Attempt to save
	$result = $this->Contact->save($data);

	// Test successful insert
	$this->assertArrayHasKey('Contact', $result);

	// Get the count in the DB
	$result = $this->Contact->find('count', array(
		'conditions' => array(
			'Contact.email' => 'info@jamesfairhurst.co.uk',
			'Contact.name' => 'James Fairhurst',
			'Contact.message' => 'Test Empty File Upload',
		),
	));

	// Test DB entry
	$this->assertEqual($result, 1);
}

Testing File Uploads

Testing file uploads is quite tricky but can be done a number of ways, as I'm new to this I've gone down the path of least resistance (or so I think). In our Contact Model we make use of the standard PHP is_uploaded_file and move_uploaded_file functions. The only issue with this is that they don't work well with testing as essentially we're trying to spoof a file upload and rightly so those functions will fail.

To get around this we're going to refactor the processUpload method and move those PHP functions into their own Model methods like below. In our tests we're going to use something called stubs which will override the methods and replace them with other logic.

public function processUpload($check=array()) {
	// deal with uploaded file
	if (!empty($check['filename']['tmp_name'])) {

		// check file is uploaded
		//if (!is_uploaded_file($check['filename']['tmp_name'])) {
		if (!$this->is_uploaded_file($check['filename']['tmp_name'])) {
			return FALSE;
		}

		// build full filename
		$filename = WWW_ROOT . $this->uploadDir . DS . Inflector::slug(pathinfo($check['filename']['name'], PATHINFO_FILENAME)).'.'.pathinfo($check['filename']['name'], PATHINFO_EXTENSION);

		// @todo check for duplicate filename

		// try moving file
		//if (!move_uploaded_file($check['filename']['tmp_name'], $filename)) {
		if (!$this->move_uploaded_file($check['filename']['tmp_name'], $filename)) {
			return FALSE;

		// file successfully uploaded
		} else {
			// save the file path relative from WWW_ROOT e.g. uploads/example_filename.jpg
			$this->data[$this->alias]['filepath'] = str_replace(DS, "/", str_replace(WWW_ROOT, "", $filename) );
		}
	}

	return TRUE;
}

public function is_uploaded_file($tmp_name) {
	return is_uploaded_file($tmp_name);
}

public function move_uploaded_file($from, $to) {
	return move_uploaded_file($from, $to);
}

We're next going to create a method to test a valid upload, we first create a stub of the Contact Model class which will essentially create a fake copy of it. We're telling it we're going to replace the 'is_uploaded_file' and 'move_uploaded_file' methods with our own.

We do this by telling what that method will return, for testing purposes the 'is_uploaded_file' will always return TRUE and the 'move_uploaded_file' will copy our file instead of the usual checking with file uploads.

Next up we do the usual setup of building form data and supplying it with a spoof file, I've put some files in the app\Test\Case\Model. One is a valid image file which our Contact Form will allow and a txt file which our form should reject due to the mimetype validation rules.

public function testFormWithValidFile() {
	// Create a stub for the Contact Model class
	$stub = $this->getMock('Contact', array('is_uploaded_file','move_uploaded_file'));

	// Always return TRUE for the 'is_uploaded_file' function
	$stub->expects($this->any())
		->method('is_uploaded_file')
		->will($this->returnValue(TRUE));
	// Copy the file instead of 'move_uploaded_file' to allow testing
	$stub->expects($this->any())
		->method('move_uploaded_file')
		->will($this->returnCallback('copy'));

	// Build the data to save along with a sample file
	$data = array('Contact' => array(
		'name' => 'James Fairhurst',
		'email' => 'info@jamesfairhurst.co.uk',
		'message' => 'Test File Upload',
		'filename' => array(
			'name' => 'TestFile.jpg',
			'type' => 'image/jpeg',
			'tmp_name' => ROOT.DS.APP_DIR.DS.'Test'.DS.'Case'.DS.'Model'.DS.'TestFile.jpg',
			'error' => 0,
			'size' => 845941,
		)
	));

	// Attempt to save
	$result = $stub->save($data);

	// Test successful insert
	$this->assertArrayHasKey('Contact', $result);

	// Get the count in the DB
	$result = $this->Contact->find('count', array(
		'conditions' => array(
			'Contact.email' => 'info@jamesfairhurst.co.uk',
			'Contact.name' => 'James Fairhurst',
			'Contact.message' => 'Test File Upload',
			'Contact.filename' => 'uploads/TestFile.jpg'
		),
	));

	// Test DB entry
	$this->assertEqual($result, 1);

	// Test uploaded file exists
	$this->assertFileExists(WWW_ROOT.'uploads'.DS.'TestFile.jpg');
}

Along with the normal tests on the insert and whether data is in our database we're also checking that the file has been successfully moved to the expected directory. Next up is a test exactly like before but with an invalid file:

public function testFormWithInvalidFile() {
	// Create a stub for the Contact Model class
	$stub = $this->getMock('Contact', array('is_uploaded_file','move_uploaded_file'));

	// Always return TRUE for the 'is_uploaded_file' function
	$stub->expects($this->any())
		->method('is_uploaded_file')
		->will($this->returnValue(TRUE));
	// Copy the file instead of 'move_uploaded_file' to allow testing
	$stub->expects($this->any())
		->method('move_uploaded_file')
		->will($this->returnCallback('copy'));

	// Build the data to save along with a sample file
	$data = array('Contact' => array(
		'name' => 'James Fairhurst',
		'email' => 'info@jamesfairhurst.co.uk',
		'message' => 'Test File Upload',
		'filename' => array(
			'name' => 'TestFile.txt',
			'type' => 'text/plain',
			'tmp_name' => ROOT.DS.APP_DIR.DS.'Test'.DS.'Case'.DS.'Model'.DS.'TestFile.txt',
			'error' => 0,
			'size' => 19,
		)
	));

	// Attempt to save
	$result = $stub->save($data);

	// Test failure
	$this->assertFalse($result);

	// Test uploaded file does not exists
	$this->assertFileNotExists(WWW_ROOT.'uploads'.DS.'TestFile.txt');
}

Lets run our tests again and hopefully we should be rocking!

Testing a Contact Form with CakePHP

I originally had those overridden methods as protected however was having issues with getting them to work. Seems CakePHP's __call() Model method wasn't playing nicely and I was getting an SQL error. If you any solutions please feel free to update my question on StackOverflow.

Someone commented and mentioned using Reflection however I've yet to try that as I'll need to upgrade my PHP version which could be a hassle. Another solution which could possibly work is to create a new MyContact class within the test file and override the methods there like this:

<?php
App::uses('Contact', 'Model');
App::uses('AppModel', 'Model');

class ContactTest extends CakeTestCase {

	public function setUp() {
		parent::setUp();

		// Load Contact Model
		$this->Contact = ClassRegistry::init('MyContact');

		// Used if we're overriding the Model my MyContact
		$this->Contact->alias = 'Contact';
	}
	...
}
class MyContact extends Contact {
	public function is_uploaded_file($tmp_name) {
		return TRUE;
	}
	public function move_uploaded_file($from, $to) {
		return TRUE;
	}
}

Wrapping Up

As mentioned I'm relatively new to testing and PHPUnit so this also helps me getting to grips with everything and hopefully can help out other people too. The full source code is available on Github, still playing around with Git so I've used a separate branch to make all these changes.

Posted on 2nd February 2013
4 years, 3 weeks, 5 days ago

comments powered by Disqus