I am completely new to PHP unit testing (using PHPUnit) and CakePHP(2) as a framework, and I'm coming back to PHP after 5 years away.
I've got a website up and running and am writing unit tests as I go along as best practice. However, xdebug is showing that one of my clauses is not covered when I believe I am calling it and I just can't see why. I've googled the hell out of all search terms I can think of and re-read the relevant sections of the cookbook and (while I've learned a lot of other useful things) I didn't find an answer so am hoping that a simple answer is forthcoming from someone in the know :)
Here are the relevant sections of code:
Controller:
<?php
App::uses('AppController', 'Controller');
// app/Controller/ClientsController.php
class ClientsController extends AppController {
/* other functions */
public function edit($id = null) {
if (!$id) {
$this->Session->setFlash(__('Unable to find client to edit'));
return $this->redirect(array('action'=>'index'));
}
$client = $this->Client->findById($id);
if(!$client) {
$this->Session->setFlash(__('Unable to find client to edit'));
return $this->redirect(array('action'=>'index'));
}
if ($this->request->is('post')) {
$this->Client->id = $id;
if ($this->Client->saveAll($this->request->data)) {
$this->Session->setFlash(__('Client has been updated.'));
return $this->redirect(array('action'=>'index'));
} else {
$this->Session->setFlash(__('Unable to update client'));
}
}
if (!$this->request->data) {
$this->request->data = $client;
$this->Session->setFlash(__('Loading data'));
}
}
}
Test:
<?php
// Test cases for client controller module
class ClientsControllerTest extends ControllerTestCase {
public $fixtures = array('app.client');
/* other tests */
public function testEdit() {
// Expect success (render)
$result = $this->testAction('/Clients/edit/1');
debug($result);
}
}
?>
The code executes as expected. If I browse to "/Clients/edit/1", the flash message (Loading data) I expect is displayed indicating that there was no request data, so it's loaded from the $client. The correct data displays in the edit form.
When I call from within my test, I get a success message that the test has passed but xdebug code coverage is showing the if (!$this->request->data) { .. } clause is not covered, and no errors are apparent.
This seems counter-intuitive to me, so in a hope to avoid frustration with future (more complex) unit tests - can anyone explain why the test would pass but not execute this clause when it is called during normal access of the page?
(The fixture is correct both in terms of data structure and inserting the data before I'm attempting to edit it. Calling edit() from a test case with no id or an invalid id correctly executes the relevant clauses, as does passing data that does not pass validation.)
I've got a similar problem and I solved by adding the second parameter to testAction():
$this->testAction('/Clients/edit/1', array('method' => 'get'));
Also you may want to change your
if ($this->request->is('post') {
...
}
if (!$this->request->data) {
...
}
To:
if ($this->request->is('post') {
...
} else {
...
}
Hope it helps.
Related
I have code in place so that if the "Add User" page is accessed from anywhere in the "Posts" section of the website, the user will be taken to the "Users" index after adding the user. But if the "Add User" page is accessed from any other section of the website, the user will be taken back to where they were after adding the user. I want to test this, but I don't know how. This is what I have so far:
Controller Code
<?php
App::uses('AppController', 'Controller');
class UsersController extends AppController {
public function add() {
if ($this->request->is('post')) {
$this->User->create();
if ($this->User->save($this->request->data)) {
$this->Session->setFlash(__('The user has been saved'));
return $this->redirect($this->request->data['User']['redirect']);
} else {
$this->Session->setFlash(__('The user could not be saved. Please, try again.'));
}
}
else {
if ($this->referer() == '/' || strpos($this->referer(), '/posts') !== false) {
$this->request->data['User']['redirect'] = Router::url(array('action' => 'index'));
}
else {
$this->request->data['User']['redirect'] = $this->referer();
}
}
}
public function index() {
$this->User->recursive = 0;
$this->set('users', $this->paginate());
}
}
Test Code
<?php
App::uses('UsersController', 'Controller');
class UsersControllerTest extends ControllerTestCase {
public function testAdd() {
$this->Controller = $this->generate('Users');
// The following line is my failed attempt at making $this->referer()
// always return "/posts".
$this->Controller->expects($this->any())->method('referer')->will($this->returnValue('/posts'));
$this->testAction('/users/add/', array('method' => 'get'));
$this->assertEquals('/users', $this->Controller->request->data['User']['redirect']);
}
}
What am I doing wrong?
You aren't mocking any methods
This line
$this->Controller = $this->generate('Users');
Only generates a test controller, you aren't specifying any methods to mock. To specify that some controller methods need to be mocked refer to the documentation:
$Posts = $this->generate('Users', array(
'methods' => array(
'referer'
),
...
));
The expectation is never triggered
Before asking this question, you probably had an internal conversation a bit like: "why is it saying that my expectation is never called? I'll just use $this->any() and ignore it.."
Don't use $this->any() unless it really doesn't matter if the mocked method is called at all. Looking at the controller code, you're expecting it to be called exactly once - so instead use $this->once():
public function testAdd() {
...
$this->Controller
->expects($this->once()) # <-
->method('referer')
->will($this->returnValue('/posts'));
...
}
The full list of available matchers is available in PHPUnit's documentation.
I have the following code in my controller:
class ContactsController extends AppController {
public $helpers = array('Html', 'Form', 'Session');
public $components = array('Session');
public function index() {
$this->set('contacts', $this->Contact->find('all'));
}
public function view($id) {
if (!$id) {
throw new NotFoundException(__('Invalid contact'));
}
$contact = $this->Contact->findById($id);
if (!$contact) {
throw new NotFoundException(__('Invalid contact'));
}
$this->set('contact', $contact);
}
public function add() {
if ($this->request->is('post')) {
$this->Contact->create();
if ($this->Contact->save($this->request->data)) {
$this->Session->setFlash('Your contact has been saved.');
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash('Unable to add your contact.');
}
}
}
}
In the add() method I have the line $this->redirect(array('action' => 'index')); I'm expecting this line to redirect back to my index page within my view. But all I get is a blank white page.
Any help appreciated.
Regards,
Stephen
Your code looks valid; I don't see an error in the code you provided.
There are a few possible causes;
there is an error somewhere else in your code, but cake php doesn't output the error message because debugging is disabled. Enable debugging by setting debugging to 1 or 2 inside app/Config/core.php - Configure::write('debug', 2);
you application is outputting something to the browser before the redirect header was sent. This may be caused by (non visible) white-space before or after any <?php or ?> . This will cause a 'headers already sent' warning, and the browser will not redirect. There are many questions regarding this situation here on StackOverflow, for example: How to fix "Headers already sent" error in PHP
Note;
in CakePHP its good practice to put a return before any redirect statement; this will allow better Unit-testing of your application, i.e.:
return $this->redirect(array('action' => 'index'));
update; additional 'pointers' on locating the cause
Tracking these kind of problems may be troublesome, I'll add some pointers
If you're using the Auth or Security component, it's possible that one of these cause the 'problem' (e.g. authorization failed or the posted data is marked 'invalid', which will be handled by a 'blackHole()' callback. Disable both in your AppController to check if the problem is still present
If the problem is still present, it is possible that headers are being sent (as mentioned before), but no warning/error is presented. To find if and where those headers are sent, add this code to your 'add' action;
Debugging code:
if (headers_sent($file, $line)) {
exit("headers were already sent in file: {$file}, line number: {$line}");
}
After lots of research finally I got a solution for that.
Please use
ob_start(); in AppController.php
ob_start();
class AppController extends Controller {
function beforeFilter() {
parent::beforeFilter();
}
}
I am trying to invalidate a field by a condition in controller instead of Model.
$this->Model->invalidate('check_out_reason', __('Please specify check out reason.', true));
The above won't work to invalidate the field. Instead, I need the below:
$this->Model->invalidate('Model.check_out_reason', __('Please specify check out reason.', true));
However, if I wish get the error message show up in the "field" itself ($this->model->validationErrors), it needs to be "check_out_reason" instead of "Model.check_out_reason". That means, I can't get the error message to show up in the field itself if I wish to invalidate the input in controller.
May I know is this a bug in CakePHP?
i created a test controller called "Invoices", just for testing, and i developed the following function
public function index(){
if (!empty($this->request->data)) {
$this->Invoice->invalidate('nombre', __('Please specify check out reason.'));
if ($this->Invoice->validates()) {
// it validated logic
if($this->Invoice->save($this->request->data)){
# everthing ok
} else {
# not saved
}
} else {
// didn't validate logic
$errors = $this->Invoice->validationErrors;
}
}
}
i think it worked for me
Change the field "nombre" for your field called "check_out_reason" to adapt the function to your code
I found a workaround for manual invalidates from controller. Reading a lot on this issue I found out that the save() function doesn't take in consideration the invalidations set through invalidate() function called in controller, but (this is very important) if it is called directly from the model function beforeValidate() it's working perfectly.
So I recommend to go in AppModel.php file and create next public methods:
public $invalidatesFromController = array();
public function beforeValidate($options = array()) {
foreach($this->invalidatesFromController as $item){
$this->invalidate($item['fieldName'], $item['errorMessage'], true);
}
return parent::beforeValidate($options);
}
public function invalidateField($fieldName, $errorMessage){
$this->invalidatesFromController[] = array(
'fieldName' => $fieldName,
'errorMessage' => $errorMessage
);
}
After that, make sure that your model's beforeValidate() function calls the parent's one:
public function beforeValidate($options = array()) {
return parent::beforeValidate($options);
}
In your controller for invalidating a field use next line:
$this->MyModel->invalidateField('fieldName', "error message");
Hope it helps! For me it's working!
Well, here's what i got. I have a line of code here that imports HttpSocket in my CakePHP-paypal integration.
It is located in my /app/PaypalIpn/Model/DataSource/PaypalIpnSource.php. Here it is:
function __construct(){
if (!class_exists('HttpSocket')) {
if(App::uses('HttpSocket', 'Network/Http')){
$this->log('http socket imported','paypal');
}
}
else{
$this->log('http socket not imported','paypal');
}
$this->Http = new HttpSocket();
}
By the way, my HttpSocket.php is located here:
C:\xampp\htdocs\wifidrivescanportal\lib\Cake\Network\Http\HttpSocket.php
So everytime i try to access this function in my HttpSocket.php:
public function post($uri = null, $data = array(), $request = array()) {
//$this->log('entered post in http socket','paypal');
$request = Set::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request);
return $this->request($request);
}
via this line of code inside my PaypalIpnSource.php:
function isValid($data){
// .... other codes
$response = $this->Http->post($server, $data);
return $response;
}
nothing happens. It doesn't log anything despite that i indicated it to log something particularly in some portions in /app/PaypalIpn/Model/DataSource/PaypalIpnSource.php
Debug the raw response data in the HttpSocket class. In 1.3 I had a few cases in the past were I had the same issue with this class.
Try http://api13.cakephp.org/view_source/http-socket/#l-292 debug($this->request['raw']) after this line.
The raw data might contain something that the 1.3 class does not pick up.
Well here's what I did these line of codes
function isValid($data){
// .... other codes
$response = $this->Http->post($server, $data);
return $response;
}
I made it this way:
function isValid($data){
// .... other codes
return $this->Http->post($server, $data);
}
Not so certain with the difference but it worked out this way. Once I finished my paypal integration in cakephp, i'll make sure i'll provide a tutorial for beginners like me. Hope I could help that way. My next goal is to do the encryption using ppcrypto.
I am using cakePHP 1.26.
The web page turned out blank when I tried to use requestAction to access a function in a COntroller from a .ctp.
Here is the code:
<?php
class TestingController extends AppController {
function hello($id=null){
$IfLoggedIn=$this->Session->check('user');
if($IfLoggedIn){
//search the database
//$result=doing something from the search results
$this->set('userInfo',$result);
return "2";
}
else if(!$IfLoggedIn && $id!=null){
return "1";
}
else if($id==null){
return "0";
}
}
}
and then in default.ctp file, I made use of the function defined above:
<?php
$u = $this->requestAction('/hello');
if($u=="2"){
echo "welcome back, my friend";
}
else{
echo "Hello World";
}
?>
But when I load a web page, it was blank page.
I have no idea what's wrong in the code.
Try to add
$u = $this->requestAction('/hello', array('return'=>true));
Check this
You might try including the controller in the url param of requestAction.
If you spend more time debugging and reading the manual, you'll learn more, more quickly.
I'm new to Cakephp myself, I'm using 2.0 which may be different in your version of Cake.
I found that the following code from the manual was wrong for me:
<?php
class PostsController extends AppController {
// ...
function index() {
$posts = $this->paginate();
if ($this->request->is('requested')) {
return $posts;
} else {
$this->set('posts', $posts);
}
}
}
You need a slight modification (seems the manual was wrong in this case). The following code worked for me:
<?php
class PostsController extends AppController {
// ...
function index() {
$posts = $this->paginate();
if ( !empty($this->request->params['requested']) ) { // line here is different
return $posts;
} else {
$this->set('posts', $posts);
}
}
}
We shouldn't be checking for the request HTTP verb, we should be checking if the request parameter is true.
Here's another relevant link to the manual about request parameters: http://book.cakephp.org/2.0/en/controllers/request-response.html#accessing-request-parameters
Hope that helps.