If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe know that in DinosaurFactoryTest
, we don't need to worry about testing the length anymore because that's done inside DinosaurLengthDeterminator's test. Every test class can stay focused. Which is important when there's dinosaurs running around.
But... what if we accidentally forgot to call the length determinator? Like, we temporarily set the length to a hardcoded value... but forgot to fix it! Well, if you run your tests... surprise! They pass.
If the possibility of making this mistake scares you... don't worry! This is something we can test for!
Open up DinosaurFactoryTest
.
When you create a mock object, by default, PHPUnit overrides all of its methods and makes each return null
... or maybe zero or an empty string, depending on the return type of the function. But, you can teach your mock object to return different values. You can say:
Hey! When somebody calls this method, don't run the real logic, but do return this value.
Then, we can test that this value is in fact set as the length.
To get this setup, create a new property called lengthDeterminator
. And then set our mock onto that. This will give us access to the mock down inside the test functions.
... lines 1 - 9 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 12 - 19 | |
private $lengthDeterminator; | |
... line 21 | |
public function setUp() | |
{ | |
... line 24 | |
$this->factory = new DinosaurFactory($this->lengthDeterminator); | |
} | |
... lines 27 - 76 | |
} |
To get auto-completion, add @var
and then \PHPUnit_Framework_MockObject_MockObject
.
... lines 1 - 9 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 12 - 16 | |
/** | |
* @var \PHPUnit_Framework_MockObject_MockObject | |
*/ | |
private $lengthDeterminator; | |
... lines 21 - 76 | |
} |
Now, scroll down to the specification test. Before we call growFromSpecification
, we can train the length determinator. How? Use $this->lengthDeterminator->method()
and then getLengthFromSpecification
: this is the name of the method that we call and want to control.
... lines 1 - 9 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 12 - 56 | |
public function testItGrowsADinosaurFromSpecification(string $spec, bool $expectedIsCarnivorous) | |
{ | |
$this->lengthDeterminator->method('getLengthFromSpecification') | |
... lines 60 - 65 | |
} | |
... lines 67 - 76 | |
} |
Next, chain off of that with ->willReturn(20)
.
... lines 1 - 56 | |
public function testItGrowsADinosaurFromSpecification(string $spec, bool $expectedIsCarnivorous) | |
{ | |
$this->lengthDeterminator->method('getLengthFromSpecification') | |
->willReturn(20); | |
... lines 61 - 65 | |
} | |
... lines 67 - 78 |
That's it! Whenever that method is called, it will return 20. And that means, at the bottom, we can assert that 20 should match $dinosaur->getLength()
.
... lines 1 - 9 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 12 - 56 | |
public function testItGrowsADinosaurFromSpecification(string $spec, bool $expectedIsCarnivorous) | |
{ | |
... lines 59 - 64 | |
$this->assertSame(20, $dinosaur->getLength()); | |
} | |
... lines 67 - 76 | |
} |
If it does not... something is fishy! Try the tests!
./vendor/bin/phpunit
Yes! They fail! Go back to DinosaurFactory
, and fix the bad length code.
... lines 1 - 7 | |
class DinosaurFactory | |
{ | |
... lines 10 - 21 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
... lines 24 - 25 | |
$length = $this->lengthDeterminator->getLengthFromSpecification($specification); | |
... lines 27 - 35 | |
} | |
... lines 37 - 45 | |
} |
Run the tests again! Of course now, they pass.
This makes our test more strict. You might think that's definitely great! But... that's not always true! Instead of simply testing the return value of the method, we're testing how the code is written internally... which in some ways, we should not care about: that's the business of the function. All we are supposed to care about is the return value. Writing stricter tests take more time, and will break accidentally more often.
So whether or not you will choose to control the return value of a mock is up to you. Sometimes, when something is super important, a strict test is awesome. And also, pretty often, you'll need to control the return value to even make your method work.
In addition to willReturn
, there are a few other ways to control the return value.
Google for "phpunit willreturn" and look for the "Test Doubles" documentation. Look inside this page for willReturn()
to find a few examples. Another method is returnValueMap()
... which is a little weird, but allows you to map different return values for different input arguments. That is important if you call the same method multiple times with different values.
Oh, and in this case, the code is ->will()
and then $this->returnValueMap()
. But there's also a single method called willReturnValueMap()
: each of these return methods can be called with both styles.
There's also one called willReturnCallback()
where you can pass a callback and return whatever value you want. It's got the power of the value map... but is way less weird.
Ok, there's one more cool thing you can do with a mock: let's see it next!
Hey Jack,
Why are you injecting the mocked class into Symfony's container? Are you in a functional test? In this case, you only need to instantiate the DinosaurFactory manually in your tests and pass the mocked DinosaurLengthDeterminator object as an argument
Cheers!
oh okay. So I can't use the "DinosaurFactory::" and do "new DinosaurFactory()" with the mock service as an argument.
Thank you for the help
No errors are thrown btw. Just the tests are failing as the return value is random, not 20. So I assume the main service is loaded instead of the mock.
Hello, and thanks a lot for this tutorial
I am confused with: "Instead of simply testing the return value of the method, we're testing how the code is written internally... which in some ways, we should not care about: that's the business of the function. All we are supposed to care about is the return value. Writing stricter tests take more time, and will break accidentally more often."
Can you precise?
Or I can :)
"Instead of simply testing the return value of the method, we're
testing how the code is written internally..."
But I feel that with $this->assertSame(20, $dinosaur->getLength()); we are testing the return value here, the $dinosaur, don't we?
"which in some ways, we should not care about: that's the business of the function."
You mean the tested function, growFromSpecification, right?
Thanks!
Hey Francois,
As always, it depends :) Sometimes you can just make sure that the return value is exactly like we expected, but sometimes we need to make sure that the code internally works exactly as we want, for example, if we test user creation method, it should return a new User with some specified fields, but it also should send an email to the user. So, in this case we want to check the return value, but also make sure that the code inside of this method that sends the email was called, something like this. Agree, it requires more time to write those tests, it also may break more often, but that's the price to make sure that the internal code works properly too :)
I hope this helps!
Cheers!
Thank you Victor
Thanks for this general explanation, which is useful, but I was more concerned about the specific case which is mentionned in this video :
Here, in this specific example, I can only see a test of the return value, but the speaker says "Instead of simply testing the return value of the method, we're testing how the code is written internally".
This is what I don't understand, I can only see the test of a return result in these lines:
$dinosaur = $this->factory->growFromSpecification($spec);
$this->assertSame(20, $dinosaur->getLength())
What do I understand wrong?
Thanks
Hey Francois,
I believe in this spot the author means that's we're testing how the growFromSpecification() method works internally, i.e. with that asset that checks the return value we're making sure that the growFromSpecification() method created a proper object - it's happen internally in that method, and so we're kind of testing it internally subsequently, something like this. It's not only related to "$this->assertSame(20, $dinosaur->getLength());" but also to the above line as well: "$this->assertSame($expectedIsCarnivorous, $dinosaur->isCarnivorous(), 'Diets do not match');" - both are testing the result of the growFromSpecification() work, i.e. technically how it internally works.
I hope this helps!
Cheers!
Hum yes ok, I guess this exemple is a bit "in the middle", but I get the point anyway.
Thanks a lot!
Guys Can I ask you?
I created a mock of object
$userStub = $this->createMock(User::class);
Set it on return value of repository class:
$userRepositoryMock = $this->createMock(UserRepository::class);
$userRepositoryMock->method('getUserById')->willReturn($userStub);
Then I create a handler and execute it:
$handler = new \Reset\Handler($userRepositoryMock);
$handler->handle();
self::assertEquals($userStub->geUsername(), 'John doe');
Inside of handler i get the user and use username setter! :
public function handle(): void
{
/*here i must get a mocked user*/
$user = $this->users->getUserById(10);
/* change the same*/
$user->setUsername('John doe'');
}
But assertion don't pass. It still empty. How to check it then?
Thanks
Hey Sergei,
Because you mocked the User object - the method was called but it do nothing. PHPUnit just checked that the method you called is exist and that's it. First of all, usually, devs do not mock data objects like User in your case. Data objects are simple enough, so you can easily create real objects instead. Instead "$userStub = $this->createMock(User::class);" do "$user = new User();" and then change $userStub to $user further in the code and try again - it should work this way.
Or, if you want to continue using mocks for data objects, that I'd not recommend in this case, you can assert that "setUsername()" method was called once on that $userStub mocked object and that the argument passed was "John doe", e.g.:
$userStub->expects($this->once())
->method('setUsername')
->with('John doe')
Something like this.
Though your code inside handle() looks weird, as you're setting "John doe" username to every user? Is it ok? :)
Anyway, I hope this helps!
Cheers!
// composer.json
{
"require": {
"php": "^7.0, <7.4",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/doctrine-bundle": "^1.6", // 1.10.3
"doctrine/orm": "^2.5", // v2.7.2
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"sensio/distribution-bundle": "^5.0.19", // v5.0.21
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.28
"symfony/monolog-bundle": "^3.1.0", // v3.1.2
"symfony/polyfill-apcu": "^1.0", // v1.6.0
"symfony/swiftmailer-bundle": "^2.3.10", // v2.6.7
"symfony/symfony": "3.3.*", // v3.3.13
"twig/twig": "^1.0||^2.0" // v2.4.4
},
"require-dev": {
"doctrine/data-fixtures": "^1.3", // 1.3.3
"doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
"liip/functional-test-bundle": "^1.8", // 1.8.0
"phpunit/phpunit": "^6.3", // 6.5.2
"sensio/generator-bundle": "^3.0", // v3.1.6
"symfony/phpunit-bridge": "^3.0" // v3.4.30
}
}
Hi,
I'm using Symfony 5 to follow along with this series.
I created a DinosaurFactory and using it like this in DinosaurFactoryTest
I added following code in setUp method
But the following doesn't work inside testItGrowsADinosaurFromASpecification
I also created a services_test.yaml and made the service public
I'm not sure if Factory can be used like that for this scenario. Could you please suggest how to inject mock dependency