I have a method that creates a large multi-dimensional array. I am trying to run a series of unit tests on this method. I'm trying to do both positive tests (testing that certain array keys get set) and negative tests (testing that certain array keys are absent). The problem is that there is a lot of code required to set up the object and there are a lot of different arguments that this method accepts that I want to test. For these reasons, I want to use data providers to run a series of tests on the method. This way I could set up the object once and use the data provider to get the array arguments and expected array values.
I can do the positive tests by calling $this->assertArraySubset()
and including the expected array structure in the data provider. But I can't think of a good way to test that certain array keys do not exist (my negative test), because these array keys are at different levels of the array.
Here is an example of my code so you can see what I'm dealing with:
<?php
class MyClassTest {
public function providerForFunctionThatCreatesArray() {
return [
[
'{foo:bar}', # some data returned by service A
'{foo:baz}', # some data returned by service B
'c' # checking that this key does not exist in the array
],
[
'{foo:barbaz}',
'{foo:bazbar}',
'd' # I also want to check that this key does not exist but in a different level of the array (i.e. $array['b'])
],
]
}
/**
* @dataProvider providerForFunctionThatCreatesArray
*/
public function testFunctionThatCreatesArray($dataFromServiceA, $dataFromServiceB, $expectedKeyNotExists) {
$serviceA = $this
->getMockBuilder(ServiceA::class)
->setMethods(['get_data'])
->getMock();
$serviceA->expects($this->any())->method('get_data')->willReturnValue($dataFromServiceA);
$serviceB = $this
->getMockBuilder(ServiceB::class)
->setMethods(['get_data'])
->getMock();
$serviceB->expects($this->any())->method('get_data')->willReturnValue($dataFromServiceB);
$myClass = new MyClass($serviceA, $serviceB);
$array = $myClass->functionThatCreatesArray();
// This is the function that checks that keys do not exist in the array
$this->assertArrayNotHasKey($expectedKeyNotExists, $array['a']);
}
}
The {foo:...}
stuff is data that is returned by some services that my function uses. The different values influence the array that my function creates. I have created mocks for these services and use the data provider to force the value that the services return.
As you can see, my data provider also returns a key as the third argument to my test function ($expectedKeyNotExists
). This is the key that I'm checking does not exist in my array. However, the d
key is one that I want to check at a different part of my array, such as $array['b']
instead of $array['a']
. If I run the above test, it will check that 'd' does not exist at $array['a']
, which is not what I want. What would be a good way to structure my tests to dynamically check that my keys do not exist in different parts of the array?
I thought about having my data provider return a fourth key which is the parent key to use. Like this:
return [
[
'{foo:bar}', # some data returned by service A
'{foo:baz}', # some data returned by service B
'c', # checking that this key does not exist in the array
'a' # parent key
],
[
'{foo:barbaz}', # some data returned by service A
'{foo:bazbar}', # some data returned by service B
'd', # checking that this key does not exist in the array
'b' # parent key
]
]
And then I could do my tests like this:
public function testFunctionThatCreatesArray($dataFromServiceA, $dataFromServiceB, $expectedKeyNotExists, $parentKey) {
// ... snip ...
$this->assertArrayNotHasKey($expectedKeyNotExists, $array[$parentKey]);
}
The problem with the above method is that it's not very flexible in the case of checking keys at different levels of the array. For example, what if I want to check that keys to not exist at $array['a']['e']['f']
and $array['a']['g']['h']
.
As far as I know Phpunit does not offer an assertion for an array key recursively.
You can extend Phpunit with your own assertions, but I would start lightly and add a private helper method to the test-case that returns a bool whether or not the array as the key recursively (check existing Q&A material like Search for a key in an array, recursively and others on how to check an array for a key recursively), and then do an assertion for false like:
$this->assertFalse(
$this->arrayHasKeyRecursive($array, $expected),
"key must not exist"
);
just keep in mind when you write code to support your tests, to make it quite dumb (and sometimes you need to put helper routines under test as well so that your tests do not lie to you on bugs).
Cross reference