How do I unit test a function that loops over objects? - php

Let me explain myself.
function insertObjects($objs) {
foreach ($objs as $obj) {
$this->repository->insert($obj);
}
}
I don't want to test that insertion into the database worked because I assume it works (it's a different unit). I also don't want to test foreach because obviously foreach is going to work. So the only thing to test here is that $objs is a well formed array. But if $objs is the mock data that I will be passing in... so does this mean there is nothing to test for this function?

If there's any chance of invalid input (a not well-formed array, null value, etc.), you need to handle that case in your method by explicitly checking for it.
In your test, you would then try to call your method with various invalid values, and check whether your method responds correctly, i.e. the database insert method is not called, exceptions are thrown, errors are logged, etc.
Other than that, the only thing to test is whether your database insert method is called with the parameters that correspond to the values in the valid test array you pass in.

Related

How to assert input on a chained mock method

Im using PHPunit 8.3.5 and I'm trying to check wether a method gets the right parameter.
Example code:
$this->registry->get($thing)->apply(EXAMPLE::SUBMIT);
$this->registry->get($thing)->apply(EXAMPLE::CANCEL);
I have two functions, functionA uses the first example line, functionB the second. I need to make sure functionA uses SUBMIT and nothing else, and the same for Bs case.
The problem:
I can use a ->method('apply')->with() with a callback to test wether it gets the right input
I can create a willReturn for ->method('get')->with() to return a simple class with apply as function
I can't figure out how to combine the two
$registryMock = $this->createMock(Registry::class);
$registryMock->method('get')->willReturn(new class {
public function apply(){} // <-- I need to assert the input of this method
});
$registryMock->method('apply')->with(self::callback(function($order, $status){
return $status === EXAMPLE::SUBMIT;
}));
How can I combine those two methods? I've also tried get->apply, but that wasnt it.
Please note: Rewriting the actual code is not an option.
Based on the comment of Dirk:
You create a mock first the 2nd function like you would normally. You then create a mock that returns the previous mock:
// first we create a mock for the last in the chain, here '->apply()'
$registryMockApply = $this->createMock(Registry::class);
$registryMockApply->expects(self::once())->method('apply')->with(
self::equalTo(EXAMPLE::SUBMIT),
);
// Then the one before that, here '->get()', which returns the previous mock
$registryMock = $this->createMock(Registry::class);
$registryMock->method('get')->willReturn($registryMockApply);
// Together resulting in '->get()->apply()'

Testing - How this common scenario is classified and handled

I’m just diving into testing/test driven development in php, and am a bit confused about how to handle a very typical scenario in our legacy codebase. Our codebase does not have many classes, it’s very functional programming oriented.
In this example code, which is pseudocode, I want to write a test for getAndOrderUsersByAge(). As you can see it first gets all the users from our database (in a giant array lets say, because we don’t have objects/classes) then sorts them. This is of course a VERY common occurrence in our code, manipulating or analyzing data gotten from the database; not objects, but an array of database rows basically.
Note that I've added an error in the if statement to simulate an error I want my tests to catch.
//gets all users in our db into an array, then sorts them by age
function getAndOrderUsersByAge(){
//get all users from the db
$all_users = getAllUsers();
//sort
$all_users_sorted = sortUsers($all_users)
//notice I've made a mistake here on purpose, left the "d" off of "$all_users_sorted"
//so the function will always return false, a mistake...which I want tests to catch
if(!empty($all_users_sorte){
return $all_users_sorted;
}
else{
return false;
}
}
My questions are:
1.) Is testing the function getAndOrderUsersByAge() unit testing, integration testing, or …?
2.) In my (unit?) test(s) of getAndOrderUsersByAge(), I want to assume that getAllUsers (the function which gets things from the db) behaves correctly because I only want to test getAndOrderUsersByAge() right? I’m not really sure what “concept” this falls under when I’m reading the two books I have on testing. Is this where I would “mock”, e.g. create some fake data for getAllUsers() to return?
3.) What tests would you write for getAndOrderUsersByAge()? I think I would write one that ensures a non-empty array is returned when getAllUsers() returns a non-empty array, as this would catch the error I've created. I'm not sure what else though.
Any tips, recommended reading material, etc. is very welcomed.
1.) Is testing the function getAndOrderUsersByAge() unit testing, integration testing, or …?
Any test that you do at code level is unit testing, basically testing methods , classes, interfaces. Tests that validate a unit or block of code.
2.) In my (unit?) test(s) of getAndOrderUsersByAge(), I want to assume that getAllUsers (the function which gets things from the db)
You can test getAndOrderUsersByAge() by actual data or mock data. Mock data is preferred so you will have more control on the test and you can create more test cases for sortUsers function.
3.) What tests would you write for getAndOrderUsersByAge()?
The test should be of nature that would validate your sortUsers function. I am not sure how your data looks but some tests could be:
1) data set that will have valid data , which will pass the test
2) data set that has invalid data like special characters in name
3) data set that has junk in age field or floating numbers or out of range integers, basically test how your function behaves when age has different type of data
4) data set that has null values
5) data set with 1 element array
6) data set with repeated element array
7) data set with invalid types in array

Returning Different Results in PHPUnit Mock Object

I've been working on getting our systems more compatible with PHPUnit so we can do more unit testing of our classes and have managed to get some of them working with mock objects, but I've run across a problem which I can't seem to get around.
One of the classes we have (which I'm creating a mock version of) is for queries. You pass the query string into it's "query()" method, it logs the query, runs it and returns the result. It also wraps the mysql_fetch_assoc with a method called "get_row()", which returns an array value much like the original.
The problem is that, in some methods, there's more than one query being passed to the "query()" method and as a result it needs to run through multiple while loops to load the data into different variables. I've created a simplified version below:
class object{
public function __construct($query){
$this->query = $query;
}
public function loadData(){
$data1 = queryDataSource("SELECT * FROM data1");
$data2 = queryDataSource("SELECT * FROM data2");
return Array(
"data1" => $data1,
"data2" => $data2,
);
}
private function queryDataSource($query){
$this->query->query($query)
while($row = $this->query->get_row()){
$result[] = $row;
}
return $result
}
}
class testObject extends PHPUnit_Framework_TestCase{
method testLoadData(){
$test_data = Array('name' => 'Bob', 'number' => '98210');
$query = $this->getMock('Query');
$query->expects($this->any())->method('query');
$query->expects($this->at(1))->method('get_row')->will($this->returnValue($test_data);
$query->expects($this->at(2))->method('get_row')->will($this->returnValue(False);
$query->expects($this->at(3))->method('get_row')->will($this->returnValue($test_data);
$query->expects($this->at(4))->method('get_row')->will($this->returnValue(False);
}
}
In order to escape the first while loop in $object->queryDataSource() I'm returning a boolean FALSE value, as would happen when doing mysql_fetch_assoc. The problem is that, when it tries to run the second query and fetch the data through get_row(), the mock object seems to keep returning FALSE ratehr than moving on to the at(3) point. This happens even with 4 objects, only the first will get the test data as a return value then get FALSE the second time, the others will get FALSE every time.
Does anyone know if there's a way to get around this? I tried removing the FALSE flags and just having the odd values in at(), but that had the same problem, and I tried just having it return the data for at(1-2), but that just passed all the data into the first while loop and nothing for the other.
Thanks for any help you can give, hope the description of the problem's clear enough
I can't run the code as it seems to only be pseudocode but from what I understood is that you are trying to mock like this:
Call to query, get_row, get_row, query, get_row, get_row.
The issue you seem to have run into is that the number in the ->at() matcher doesn't count up per method but per object.
So what you probably want to write is:
$query->expects($this->any())->method('query');
$query->expects($this->at(1))->method('get_row')->will($this->returnValue($test_data);
$query->expects($this->at(2))->method('get_row')->will($this->returnValue(False);
$query->expects($this->at(4))->method('get_row')->will($this->returnValue($test_data);
$query->expects($this->at(5))->method('get_row')->will($this->returnValue(False);
Or to make it a litte easer to read maybe even:
$query->expects($this->at(0))->method('query');
$query->expects($this->at(1))->method('get_row')->will($this->returnValue($test_data);
$query->expects($this->at(2))->method('get_row')->will($this->returnValue(False);
$query->expects($this->at(3))->method('query');
$query->expects($this->at(4))->method('get_row')->will($this->returnValue($test_data);
$query->expects($this->at(5))->method('get_row')->will($this->returnValue(False);
With your mocks you ran into the issue that the second call to "query" was counting up one "call" and hence skipping over the second return($test_data);.
Unfortunately the at() binds your tests to the implementations very strongly.
Imagine if you rearranged 2 method calls inside a tested method, the functionality is exactly the same but all tests using at() would now fail, often with cryptic messages such as method doesn't exist at index N
On the occasions you want to specifically say "this called exactly this way then this called exactly this way" that's great, but if you just want assertions then one of the PHPUnit Mock Extensions seems more friendly, particularly Mockery and a guide here (a touch out of date I believe)
There are others also.

Setting an array of related models in Yii from form input

In Yii, the following doesn't work (the relation array remains empty) and doesn't return an error:
foreach ($data as $k => $relatedModelData){
//construct the related model from the data passed in
...
$model->arrayOfRelatedModels[] = $relatedModel;
}
Instead, I have to do this:
foreach ($data as $k => $relatedModelData){
//construct the related model from the data passed in
...
$tempArray[] = $relatedModel;
}
$model->arrayOfRelatedModels = $tempArray;
I am wondering why this is the case, or whether I have got something slightly wrong in the first example?
#o_nix is right, you should be getting the:
Indirect modification of overloaded property error. It's something i've come across a lot recently.
It means that Yii is returning you a magic attribute via the __get function, the object doesn't really exist on the class, and when you set this object it goes through the magic __set function. This means if you try and change something inside the object itself (inner array values for example) it has no idea what to do with them and so it throws up that notice and leaves it alone.
To get round this you did the right thing, modify a new local variable and set the whole object to this once you're done.
P.S
You might have your PHP configuration set to hide notices, which is why it's silent.
Hope that clears it up

foreach $key=>$value to set ZF form values / OOP PHP

I currently have the following code in a ZF application, to set the values of a Form based on a rowset retrieved from a Db-Table.
$form->forename->setValue($footerContactDetails->forename);
$form->surname->setValue($footerContactDetails->surname);
$form->telephone->setValue($footerContactDetails->telephone);
$form->mobile->setValue($footerContactDetails->mobile);
$form->fax->setValue($footerContactDetails->fax);
$form->email->setValue($footerContactDetails->emailAddress);
$form->address1->setValue($footerContactDetails->address1);
$form->address2->setValue($footerContactDetails->address2);
$form->address3->setValue($footerContactDetails->address3);
$form->townCity->setValue($footerContactDetails->townCity);
$form->region->setValue($footerContactDetails->region);
As the object element names from the rowset match the form elements, what I'd like to do is the following:
foreach ($footerContactDetails as $key=>$value) {
$form->$key->setValue($value);
}
However this provides the following error message:
Fatal error: Call to a member function setValue() on a non-object
I expect this question is more related to OOP PHP in general and not the just ZF.
So how can I set these form values using a foreach?
Many thanks
The foreach is fine, but the names dont match:
$form->email->setValue($footerContactDetails->emailAddress);
So, when iterating over $footerContactDetails, your code will eventually try to
$form->emailAddress->setValue()
but since there is no element of that name in the Zend_Form (it's called email instead), you'll get the error. So change it accordingly and it will work.
On a sidenote, Zend_Form has a method for bulk setting:
$form->populate((array) $footerContactDetails);
It expects an array though, hence the typecast.
Try this:
foreach($footerContactDetails as $key=>$value){
$form->{$key}->setValue($value);
}
Foreach over object variables takes all variables. That means primitive types too. Since primitive type is not an object, you cannot call method on it.
I suggest you use is_object() to check if variable is an object before calling a method, or maybe even think of a different approach.
Did you try something like this?
foreach ($footerContactDetails as $key=>$value) {
$form->${$key}->setValue($value);
}

Categories