I have a method which will be called multiple times during a single test but only once with each argument. So I want to test that the method only received each argument once. For example, I have here a mkdir function that is called with each directory to create:
The test
$dirs = [
"$parentDir/$siteName/assets/components",
"$parentDir/$siteName/assets/layouts",
];
// iterate each directory
foreach($dirs as $dir) {
// and verify that mkdir was called with that argument only once
$fileSystemMock->expects($this->once())
->method('mkdir')
->with($this->equalTo($dir));
}
The method being tested
public function createSite($siteName) {
$fileSystem = $this->fileSystem;
$parentDir = $this->parentDir;
$componentsDir = "$parentDir/$siteName/assets/components";
$layoutsDir = "$parentDir/$siteName/assets/layouts";
$mediaDir = "$parentDir/$siteName/content/media";
$sectionsDir = "$parentDir/$siteName/assets/sections";
if (!$fileSystem->exists($componentsDir)) {
$fileSystem->mkdir($componentsDir);
}
if (!$fileSystem->exists($layoutsDir)) {
$fileSystem->mkdir($layoutsDir);
}
However, the test fails:
Failed asserting that two strings are equal.
--- Expected
+++ Actual
## ##
-'/path/to/parent/best-widgets/assets/layouts'
+'/path/to/parent/best-widgets/assets/components'
Hopefully it makes sense what I'm trying to. Does the once() not take into consideration the with() argument? I don't know how to just check the method was called once with each argument
You may use withConsecutive:
$fileSystemMock->expects($this->exactly(count($dirs)))
->method('mkdir')
->withConsecutive(...array_map(function (string $dir) {
return [$this->equalTo($dir)];
}, $dirs));
withConsecutive expects one parameter for each set of argument expectations, so array_map combined with the unpacking operator comes in handy.
Note that this should only pass if the calls are made in same order defined by the $dirs array. It seems a bit harder to pull off otherwise (btw, a GitHub issue was recently created about it).
Bonus PHP 7.4 version of the above:
$fileSystemMock->expects($this->exactly(count($dirs)))
->method('mkdir')
->withConsecutive(...array_map(fn(string $dir) => [$this->equalTo($dir)], $dirs));
Related
I am testing an parameter sent to a mocked event handler. The parameter is an object of "Event" sub-type, which itself has some data nested inside it. I want to test the Event and its substructure matches the fixture data I've injected into the code through various mocks.
I can test the "top level" of the event easily enough: the classname, and simple attributes like an event name string. I can also test that an attribute contains the same object, which I believe implicitly tests all the substructure of the object.
The problem I'm having is some of the sub-structure in a more complex example is causing the test to fail but it's irrelevant, so I want to cherry-pick specific properties of the sub-structure, and not just identity-compare the entire object.
I feel like I'm missing something in the attribute assertions: how to access the parameter that the "with" refers to - as variable. Then I could pass it into some of the assert methods like attributeEqualTo which require the item under test to be passed in. Perhaps these just cannot be used in the fluent case I'm using?
I'd like to check the event.data is a certain class.
I'd like to check the event.data.thing1 == X
I'd like to check the event.data.thing2 == Y
and so on.
Simplified code:
class MyEventData{
public $thing1;
public $thing2;
}
class MyEvent{
public $data;
}
// An event gets fired containing this in the tests
$eventData = new MyEventData(1,2);
$this->eventMock->expects($this->exactly(3))
->method('fire')
->with(
$this->logicalAnd(
// THIS WORKS OK
$this->isInstanceOf('\MyApp\MyEvents\SomeEvent'),
// THIS WORKS OK
$this->attributeEqualTo ('name', SomeEvent::EVENT_NAME),
// THIS WORKS in simplified cases only
$this->attributeEqualTo ('data', $eventData),
// HOW DO I GET THE "WITH" PARAMETER CONTEXT "INTO" THE THIRD PARAMETER?
$this->assertAttributeInstanceOf('\MyApp\MyEvents\MyEventData', 'data', -classOrObject- ),
// Then how can I test with attribute data.thing1 == 1 and data.thing2 = 2
)
);
I've got it to work using the callback constraint, but it feels like I've now stepped off the path and lost the power of PHPUnit - I can't seem to use the assertion helpers here anymore.
e.g. If the accumulated tests return false, I don't get any details in the output log beyond "Expectation failed for ... and is accepted by specified callback".
$this->callback(function($subject){
$b = true;
// I tried using this constraint but can't access (autoload) this class? So is it not supposed to be used directly?
//$c = new PHPUnit_Framework_Constraint_IsInstanceOf('\MyApp\MyEvents\MyEventData');
// return $c->matches(subject);
// this is the right assert, but it doesn't return the result, so I cannot use it in a callback constraint.
\PHPUnit_Framework_Assert::assertAttributeInstanceOf('\MyApp\MyEvents\MyEventData', 'data', $subject);
// This works but seems very "Manual"
$b = $b && get_class($subject->data) == '\MyApp\MyEvents\MyEventData';
$b = $b && $subject->data->thing1 == 1;
$b = $b && $subject->data->thing2 == 1;
return $b;
})
I call an object that returns an array given certain chained methods:
Songs::duration('>', 2)->artist('Unknown')->genre('Metal')->stars(5)->getAllAsArray();
The problem lies that every time I want to get this array, for example, in another script, I have to chain everything again. Now imagine that in over 10 scripts.
Is there a way to recall the chained methods for later use?
Since you can't cache the result, you could cache the structure of the call chain in an array.
$chain = [
'duration' => ['>', 2],
'artist' => 'Unknown',
'genre' => 'Metal',
'stars' => 5,
'getAllAsArray' => null
];
You could use that with a function that emulates the chained call using the cached array:
function callChain($object, $chain) {
foreach ($chain as $method => $params) {
$params = is_array($params) ? $params : (array) $params;
$object = call_user_func_array([$object, $method], $params);
}
return $object;
}
$result = callChain('Songs', $chain);
If you can not cache your results as suggested, as I commented, here are a couple ideas. If your application allows for mixing of functions (as in you are permitted by standards of your company's development rules) and classes, you can use a function wrapper:
// The function can be as complex as you want
// You can make '>', 2 args too if they are going to be different all the time
function getArtists($array)
{
return \Songs::duration('>', 2)->artist($array[0])->genre($array[1])->stars($array[2])->getAllAsArray();
}
print_r(getArtists(array('Unkown','Metal',5)));
If you are only allowed to use classes and __callStatic() is not forbidden in your development and is also available in the version of PHP you are using, you might try that:
// If you have access to the Songs class
public __callStatic($name,$args=false)
{
// This should explode your method name
// so you have two important elements of your chain
// Unknown_Metal() should produce "Unknown" and "Metal" as key 0 and 1
$settings = explode("_",$name);
// Args should be in an array, so if you have 1 value, should be in key 0
$stars = (isset($args[0]))? $args[0] : 5;
// return the contents
return self::duration('>', 2)->artist($settings[0])->genre($settings[1])->stars($stars)->getAllAsArray();
}
This should return the same as your chain:
print_r(\Songs::Unknown_Metal(5));
It should be noted that overloading is hard to follow because there is no concrete method called Unknown_Metal so it's harder to debug. Also note I have not tested this particular set-up out locally, but I have notated what should happen where.
If those are not allowed, I would then make a method to shorten that chain:
public function getArtists($array)
{
// Note, '>', 2 can be args too, I just didn't add them
return self::duration('>', 2)->artist($array[0])->genre($array[1])->stars($array[2])->getAllAsArray();
}
print_r(\Songs::getArtists(array('Unkown','Metal',5)));
I wrote a lib doing exactly what you're looking for, implementing the principle suggested by Don't Panic in a high quality way: https://packagist.org/packages/jclaveau/php-deferred-callchain
In your case you would code
$search = DeferredCallChain::new_(Songs::class) // or shorter: later(Songs::class)
->duration('>',2) // static syntax "::" cannot handle chaining sadly
->artist('Unknown')
->genre('Metal')
->stars(5)
->getAllAsArray();
print_r( $search($myFirstDBSongs) );
print_r( $search($mySecondDBSongs) );
Hoping it will match your needs!
I'm testing a method with phpunit and I have the following scenario:
method 'setParameter' is called an unkown amount of times
method 'setParameter' is called with different kinds of arguments
among the various arguments method 'setParameter' MUST be called with a set of arguments.
I've tried doing it this way:
$mandatoryParameters = array('param1', 'param2', 'param3');
foreach ($mandatoryParameters as $parameter) {
$class->expects($this->once())
->method('setParameter')
->with($parameter);
}
Unfortunately the test failed because before method is called with these parameters it is called with other parameters too. The error i get is:
Parameter 0 for invocation Namespace\Class::setParameter('random_param', 'random_value')
does not match expected value.
Failed asserting that two strings are equal.
Try using the $this->at() method. You are overwriting your mock each time with your loop.
$mandatoryParameters = array('param1', 'param2', 'param3');
$a = 0;
foreach ($mandatoryParameters as $parameter) {
$class->expects($this->at($a++);
->method('setParameter')
->with($parameter);
}
This will set your mock to expect setParameter to be called a certain number of times and each call will be with a different parameter. You will need to know which call is the specific on for your parameters and adjust the number accordingly. If the calls are not sequential, you can set a key for which index each param.
$mandatoryParameters = array(2 =>'param1', 5 => 'param2', 6 => 'param3');
foreach ($mandatoryParameters as $index => $parameter) {
$class->expects($this->at($index);
->method('setParameter')
->with($parameter);
}
The index is zero based so remember to start your counting from 0 rather than 1.
http://phpunit.de/manual/current/en/phpunit-book.html#test-doubles.mock-objects.tables.matchers
When calling a function is there a way to simplify the argument list? Instead of using $blank.
$subscribe=1;
$database->information($blank,$blank,$blank,$blank,$blank,$blank,$subscribe,$blank,$blank,$blank,$blank,$blank);
function information ($search,$id,$blank,$category,$recent,$comment,$subscribe,$pages,$pending,$profile,$deleted,$reported) {
//code
}
You could pass in an array with the specified keys, and merge it with an array of default values
So instead of
function foo($arg1 = 3, $arg2 = 5, $arg3 = 7) { }
You'd have
function foo($args) {
$defaults = array(
'arg1' => '',
'arg2' => null,
'arg3' => 7
);
// merge passed in array with defaults
$args = array_merge($defaults, $args);
// set variables within local scope
foreach($args as $key => $arg) {
// this is to make sure that only intended arguments are passed
if(isset($defaults[$key])) ${$key} = $arg;
}
// rest of your code
}
Then call it as
foo(array('arg3' => 2));
Yes, pass an array instead, or refactor. A long arguments list is usually a bad smell.
function information(array $params) {....
information(array('search'=>'.....
Twelve arguments are generally too many for one function. It's likely that your code could be simplified (including the argument lists getting shorter) by refactoring function information which looks likely to be a monster.
Stopgap measures you can use in the meantime are
adding default argument values
making the function accept all its arguments as an array
Both of the above will require you to visit all call sites for the function for review and modification.
Adding default arguments is IMHO the poor choice here, as by looking at the example call it seems that you would need to make all arguments default, which in turn means that the compiler will never warn you if you call the function wrongly by mistake.
Converting to an array is more work, but it forces you to rewrite the calls in a way that's not as amenable to accidental errors. The function signature would change to
function information(array $params)
or possibly
function information(array $params = array())
if you want all parameters to be optional. You can supply defaults for the parameters with
function information(array $params) {
$defaults = array('foo' => 'bar', /* ... */);
$params += $defaults; // adds missing values that have defaults to $params;
// does not overwrite existing values
To avoid having to rewrite the function body, you can then use export to pull out these values from the array into the local scope:
export($params); // creates local vars
echo $foo; // will print "bar" unless you have given another value
See all of this in action.
You can make it so the function wil automatically fill the variable with a given value like an empty string:
function information ($subscribe, $search="", $id="", $blank="", $category="", $recent="", $comment="", $pages="", $pending="", $profile="", $deleted="", $reported="") {
//code
}
Yes, there are several ways:
Accept an associative array as a single argument, and pass what you need to that. Throw exceptions if a critical argument is missing.
Place critical arguments at the head of the function definition, and optional ones at the end. Give them a default value so that you don't have to declare them.
Recosinder your function. 12 arguments is much too many for one function. Consider using a class/object, or dividing the work between different functions.
Several ways:
function test($input = "some default value") {
return $input; // returns "some default value"
}
function test($input) {
return $input;
}
test(NULL); // returns NULL
function test() {
foreach(func_get_args() as $arg) {
echo $arg;
}
}
test("one", "two", "three"); // echos: onetwothree
I know it is possible to use optional arguments as follows:
function doSomething($do, $something = "something") {
}
doSomething("do");
doSomething("do", "nothing");
But suppose you have the following situation:
function doSomething($do, $something = "something", $or = "or", $nothing = "nothing") {
}
doSomething("do", $or=>"and", $nothing=>"something");
So in the above line it would default $something to "something", even though I am setting values for everything else. I know this is possible in .net - I use it all the time. But I need to do this in PHP if possible.
Can anyone tell me if this is possible? I am altering the Omnistar Affiliate program which I have integrated into Interspire Shopping Cart - so I want to keep a function working as normal for any places where I dont change the call to the function, but in one place (which I am extending) I want to specify additional parameters. I dont want to create another function unless I absolutely have to.
No, in PHP that is not possible as of writing. Use array arguments:
function doSomething($arguments = array()) {
// set defaults
$arguments = array_merge(array(
"argument" => "default value",
), $arguments);
var_dump($arguments);
}
Example usage:
doSomething(); // with all defaults, or:
doSomething(array("argument" => "other value"));
When changing an existing method:
//function doSomething($bar, $baz) {
function doSomething($bar, $baz, $arguments = array()) {
// $bar and $baz remain in place, old code works
}
Have a look at func_get_args: http://au2.php.net/manual/en/function.func-get-args.php
Named arguments are not currently available in PHP (5.3).
To get around this, you commonly see a function receiving an argument array() and then using extract() to use the supplied arguments in local variables or array_merge() to default them.
Your original example would look something like:
$args = array('do' => 'do', 'or' => 'not', 'nothing' => 'something');
doSomething($args);
PHP has no named parameters. You'll have to decide on one workaround.
Most commonly an array parameter is used. But another clever method is using URL parameters, if you only need literal values:
function with_options($any) {
parse_str($any); // or extract() for array params
}
with_options("param=123&and=and&or=or");
Combine this approach with default parameters as it suits your particular use case.