views:

514

answers:

3

I'm building unit tests for class Foo, and I'm fairly new to unit testing.

A key component of my class is an instance of BarCollection which contains a number of Bar objects. One method in Foo iterates through the collection and calls a couple methods on each Bar object in the collection. I want to use stub objects to generate a series of responses for my test class. How do I make the Bar stub class return different values as I iterate? I'm trying to do something along these lines:

$stubs = array();
foreach ($array as $value) {
    $barStub = $this->getMock('Bar');
    $barStub->expects($this->any())
            ->method('GetValue')
            ->will($this->returnValue($value));
    $stubs[] = $barStub;
}
// populate stubs into `Foo`

// assert results from `Foo->someMethod()`

So Foo->someMethod() will produce data based on the results it receives from the Bar objects. But this gives me the following error whenever the array is longer than one:

There was 1 failure:

1) testMyTest(FooTest) with data set #2 (array(0.5, 0.5))
Expectation failed for method name is equal to <string:GetValue> when invoked zero or more times.
Mocked method does not exist.
/usr/share/php/PHPUnit/Framework/MockObject/Mock.php(193) : eval()'d code:25

One thought I had was to use ->will($this->returnCallback()) to invoke a callback method, but I don't know how to indicate to the callback which Bar object is making the call (and consequently what response to give).

Another idea is to use the onConsecutiveCalls() method, or something like it, to tell my stub to return 1 the first time, 2 the second time, etc, but I'm not sure exactly how to do this. I'm also concerned that if my class ever does anything other than ordered iteration on the collection, I won't have a way to test it.

A: 

This should satisfy the requirement to return a series of values in order as it's called if you're comfortable with the use of global. It has no idea which Bar is called but if each Bar is called by Foo once in order then it shouldn't be too hard to populate the test data.

$barTestData = array('empty',1,2,3,4,5,6);

function barDataCallback(){
    global $barTestData;
    return next($barTestData);
}
dtanders
A: 

I noticed you have an extra parenthesis after "->method('GetValue')" in your code. Don't know if you copied and pasted that or not.

Sean
Fixed the typo, thanks. This wasn't a direct copy-paste. I have a few other things going on in my code that would have just muddied the waters.
keithjgrant
I'm unable to recreate your problem with the code you posted. I think you'll have to post what you're actually doing to get a good answer.
Sean
Interesting. I poked around with it some more, and I'm not getting the same error message I quoted above. I wonder if that resulted from another error I must have resolved. Now my tests seem to run just fine, but fail when they should pass.
keithjgrant
+2  A: 

I'm unfortunately not sure if you can solve your actual question using getMock(), but my experience with getMock() itself is slim.

Only thing I can think of offhand, but not knowing your Bar class, this may not help: The third parameter of getMock() lets you pass constructor arguments (as an array).

I'd create my own mock class extending Bar as a test helper (fancy name for 'just another class that so happens to be used only in tests') that does exactly what I like and inject a series of them into your Foo object. That gives you all the control you'd want, since you can outright replace the methods in question, which getMock() does not do. Of course that also means you're not testing the Bar class in this test, which may not be what you want - though I'd recommend writing a separate test class per tested class anyway, but there are cases where that's unnecessarily purist.

$stubs = array();
foreach ($array as $value) {
    $stubs[] = new MyBarTestHelper($value);
}

That aside, I'm honestly surprised you're only seeing the exception described when you have more than one array element. I've observed that PHPUnit actually expects you to declare any method you want it to be able to track as a getMock() parameter, and will stolidly error out otherwise, since essentially what it does internally is create its own extension of the class, wrapping each method that you expressly declare with logic that lets it determine whether it was called (= adding the method name into a logical list).

So colour me naive (seriously, I probably am, I'm a test newbie, myself), but see if this helps you any:

$stubs = array();
foreach ($array as $value) {
    $barStub = $this->getMock('Bar', array('GetValue'));
    $barStub->expects($this->any())
            ->method('GetValue')
            ->will($this->returnValue($value));
    $stubs[] = $barStub;
}
pinkgothic
Creating my own test class was basically what I had to do. In my case I was able to use the `Bar` class itself, since the methods I used were simple getters, but a hand-made mock class would work with more complex methods.
keithjgrant
Regarding the array of valid methods passed as the second parameter to `getMock()`: According to documentation, you can omit this parameter, and any method call will be allowed.
keithjgrant
That was my intuitive understanding, too, but I did actually get errors omitting it that I didn't get expressly mentioning the called methods (of course only in combination with `->expects()`); hence mentioning it. What I observed might be a very version-specific bug-ism, though. But, either way, I'm very glad you got your test to work as desired. :)
pinkgothic