Testing the Bundle
Keep on Learning!
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 SubscribeHey! Someone already made some tests for our bundle!
| // ... lines 1 - 2 | |
| namespace App\Tests\Service; | |
| // ... lines 4 - 7 | |
| class KnpUIpsumTest extends TestCase | |
| { | |
| public function testGetWords() | |
| { | |
| $ipsum = new KnpUIpsum(); | |
| $words = $ipsum->getWords(1); | |
| $this->assertInternalType('string', $words); | |
| $this->assertCount(1, explode(' ', $words)); | |
| $words = $ipsum->getWords(10); | |
| $this->assertCount(10, explode(' ', $words)); | |
| $words = $ipsum->getWords(10, true); | |
| $this->assertCount(10, $words); | |
| } | |
| // ... lines 24 - 65 | |
| } |
Tip
The assertInternalType() method has been removed, you can use assertIsString() instead:
$this->assertIsString($words);
If you want to know more about this: https://github.com/sebastianbergmann/phpunit/issues/3369
So nice! Right now, they live in the app, but moving them into the bundle is our next job! But first... let's make sure they're still working.
Find the terminal tab for the application and run:
./vendor/bin/simple-phpunit
The first time you run this, it'll download PHPUnit behind the scenes. Then... it does not pass!
Class
App\Service\KnpUIpsumnot found
Of course! When we moved this class into the new namespace, we did not update the test! No problem - just re-type KnpUIpsum and hit tab to auto-complete and get the new use statement.
| // ... lines 1 - 4 | |
| use KnpU\LoremIpsumBundle\KnpUIpsum; | |
| // ... lines 6 - 67 |
Perfect! But... I can already see another problem! When we added the first constructor argument to KnpUIpsum, we also didn't update the test. I could use mocking here, but it's just as easy to say new KnpUWordProvider. Repeat that in the two other places.
| // ... lines 1 - 5 | |
| use KnpU\LoremIpsumBundle\KnpUWordProvider; | |
| // ... lines 7 - 8 | |
| class KnpUIpsumTest extends TestCase | |
| { | |
| public function testGetWords() | |
| { | |
| $ipsum = new KnpUIpsum(new KnpUWordProvider()); | |
| // ... lines 14 - 23 | |
| } | |
| // ... line 25 | |
| public function testGetSentences() | |
| { | |
| $ipsum = new KnpUIpsum(new KnpUWordProvider()); | |
| // ... lines 29 - 37 | |
| } | |
| // ... line 39 | |
| public function testGetParagraphs() | |
| { | |
| // ... lines 42 - 43 | |
| for ($i = 0; $i < 100; $i++) { | |
| $ipsum = new KnpUIpsum(new KnpUWordProvider()); | |
| // ... lines 46 - 64 | |
| } | |
| } | |
| } |
Ok, try those tests again!
./vendor/bin/simple-phpunit
Got it!
Adding Tests to your Bundle & autoload-dev
Time to move this into our bundle. We already have a src/ directory. Now create a new directory next to that called tests/. Copy the KnpUIpsumTest and put that directly in this new folder. I'm putting it directly in tests/ because the KnpUIpsum class itself lives directly in src/.
And the test file is now gone from the app.
But really... we shouldn't need to update much... or anything in the test class itself. In fact, the only thing we need to change is the namespace. Instead of App\Tests\Services, start with the same namespace as the rest of the bundle. So, KnpU\LoremIpsumBundle\Tests.
| // ... lines 1 - 2 | |
| namespace KnpU\LoremIpsumBundle\Tests; | |
| // ... lines 4 - 67 |
But, if we're going to start putting classes in the tests/ directory, we need to make sure that Composer can autoload these files. This isn't strictly required to make PHPUnit work, but it will be needed if you add any helper or dummy classes to the directory and want to use them in your tests.
And, it's easy! We basically want to add a second PSR-4 rule that says that the KnpU\LoremIpsumBundle\Tests namespace lives in the tests/ directory. But... don't! Instead, copy the entire section, paste and rename it to autoload-dev. Change the namespace to end in Tests\\ and point this at the tests/ directory.
| // ... lines 1 - 19 | |
| "autoload-dev": { | |
| "psr-4": { | |
| "KnpU\\LoremIpsumBundle\\Tests\\": "tests/" | |
| } | |
| } | |
| // ... lines 25 - 26 |
Why autoload-dev? The issue is that our end users will not be using anything in the tests/ directory: this config exists just to help us when we are working directly on the bundle. By putting it in autoload-dev, the autoload rules for the tests/ directory will not be added to the autoload matrix of our users' applications, which will give them a slight performance boost.
Installing symfony/phpunit-bridge
Ok: our test is ready. So let's run it! Move over to the terminal for the bundle and run... uh... wait a second. Run, what? We haven't installed PHPUnit! Heck, we don't even have a vendor/ directory yet. Sure, you can run composer install to get a vendor/ directory... but with nothing inside.
This should be no surprise: if we want to test our bundle, the bundle itself needs to require PHPUnit. Go back to the terminal and run:
composer require symfony/phpunit-bridge --dev
Two important things. First, we're using Symfony's PHPUnit bridge because it has a few extra features... and ultimately uses PHPUnit behind-the-scenes. Second, just like with autoloading, our end users do not need to have symfony/phpunit-bridge installed in their vendor directory. We only need this when we're working on the bundle itself. By adding it to require-dev, when a user installs our bundle, it will not also install symfony/phpunit-bridge.
Ignoring composer.lock
Now that we've run composer install, we have a composer.lock file! So, commit it! Wait, don't! Libraries and bundles should actually not commit this file - there's just no purpose to lock the dependencies: it doesn't affect our end-users at all. Instead, open the .gitignore file and ignore composer.lock. Now when we run git status, yep! It's gone.
| // ... line 1 | |
| composer.lock |
phpunit.xml.dist
Ok, let's finally run the tests!
./vendor/bin/simple-phpunit
It - of course - downloads PHPUnit behind the scenes the first time and then... nothing! It... just prints out the options??? What the heck? Well... our bundle doesn't have a phpunit.xml.dist file yet... so it has no idea where our test files live or anything else!
A good phpunit.xml.dist file is pretty simple... and I usually steal one from a bundle I trust. For example, Go to github.com/knpuniversity/oauth2-client-bundle. Find the phpunit.xml.dist file, view the raw version and copy it. Back at our bundle, create that file and paste it in.
| <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" | |
| backupGlobals="false" | |
| colors="true" | |
| bootstrap="./vendor/autoload.php" | |
| > | |
| // ... lines 9 - 26 | |
| </phpunit> |
Oh, and before I forget, in .gitignore, also ignore phpunit.xml. The .dist version is committed, but this allows anyone to have a custom version on their local copy that they do not commit.
| // ... lines 1 - 2 | |
| phpunit.xml |
Check out the new file: the really important thing is that we set the bootstrap key to vendor/autoload.php so that we get Composer's autoloading. This also sets a few php.ini settings and... yes: we tell PHPUnit where our test files live.
Now I think it will work. Find your terminal and try it again:
./vendor/bin/simple-phpunit
It passes! Woo!
After seeing these fancy green colors, you might be thinking that our bundle is working! And if you did... you'd be half right. Next, we'll build a functional test... which is totally going to fail.
12 Comments
I have a question i have not been able to find an answer to for a long time: how can I make a relation with a bundles entity?
consider: a one-to-many relation (from my app to the bundle) specifically, this would require that the bundles entity contains the foreign key and therefore editing the bundle code. However, we shouldn't edit the bundle code as other apps that use the bundle will not have this particualr, app specific relation. So editing the bundle code for this apps needs is not suitable.
I've tried: a lot of things, in particular, I've tried extending the bundle class to add the relation. The problem is that you also have other entities within the bundle that have a relation to the base class. These entities will continue to refer to the base class and not be updated to refer to the (new) extended class - resulting in errors.
I've also tried a compositional approach with object reflection: when I build a class to contain the bundle entity and route the method calls to the contained bundle entity (through reflection I think). I aimed to "expand" the api of the "extending" entity with relation method calls, but I couldnt get this to work either.
Any thoughts?
Also I realized this problem doesn't just apply to people looking to extend their own bundles but also 3rd party bundles they don't control and cant edit. So how can people extend entities in 3rd party bundles?
Or is the solution an in-elegant one of copying the git repo (to "own" the code) and directly inserting the relations into the entity. This would work but it creates problems.
Extending a bundle entity would be a common need I would expect. So I'm suprised I havent been able to find an answer.
Would really love to find how to solve this!
Hey Cameron,
Complex question :) Well, I would recommend you to provide models instead of entities in your bundles, and users will have to extend those models in their entities to bring some boilerplate code from the bundles into their own projects. The benefit would be to have all the entities in your project, i.e. inside of your
src/Entity/where you have full control of everything and can create any additional relations you want. You may also want to provide in your bundles some interfaces/abstract classes/traits that uses will have to implement/extend/use in order the bundle could interact with the project's code.Maybe take a look at the FOSUserBundle implementation: https://github.com/FriendsOfSymfony/FOSUserBundle or other popular bundles that have to work with entities to get more ideas. Otherwise, probably using some configuration options that users can change via the configuration files that will dynamically will set the correct mapping for the entities, IIRC it should be possible, but I have never tried to do it and also it might be kinda confusing for users. IMO, asking users to implement an interface or extend a base class or use a trait would be more straightforward for this.
I hope this helps! :)
Cheers!
Thanks victor. By “model instead of entities”, it sounds like you could simply use @mappedsuperclass to achieve the same thing?
I found another possible alternative; the relations can be remapped at runtime via a doctrine listener. allowing for all the other entities to refer to the newly extended class. Although I think the methods will need to use interfaces in their signature to handle the extended class.
I’ll see if I can put together a sample repo.
This seems like an issue that a lot of devs looking to write modular, reusable code will run into. It would be great to see a video on how to do this within symfony.
Hey Cameron,
Well, mapped superclass is a bit more advanced and complex, I just mean about providing some code that you would extend with your entities in your src/ dir, something simpler.
Yeah, nice catch.. I believe that's what you need if you make it complex, otherwise just provide some base classes you can extend.
It might be a little know fact... but not sure that many devs may need that kind of complexity, but if you're trying to create really complex bundle that provides some entities - yeah, that the way I believe.
Cheers!
Thanks, it's worth me explaining my thoughts as I think it's quite a common need for developers and something I think should be included in the bundle training as not knowing this is a show stopper. I've personally run into this requirement multiple times as a dev and my projects have not been that exotic.
Maybe you can suggest something I'm missing here, but being able to extend (specifically: to add a relation to a third party bundle entity or my own bundle - without editing the code) is common requirement - and one that's proven quite frustrating to find an answer to.
Simply put: devs cannot write modular & reusable code without the ability to extend (add relations to) non-editable entites.
If I write code that I could re-use in another library and want to create a re-usable bundle: I first need to decouple / generalize the bundle code from my app's specific use (practically, this invovles removing relation code that exists between my app and the re-usable bundle), so this bundle code can be used in another app. These other app's are likely to need to add their own relations to between their app's entities and the bundles entities also.
This leads to the problem I'm having: it's not possible to add a many-to-one relation to a bundle's entity without editing the code (e.g. cloning the repo and editing it directly). You can't simply extend a bundle class to add a relation, create a "containing class" to do composition using reflection (to mimic the entities interface).
This simply means: devs can't create a re-usable bundle where there's a requirement to have a relation with a bundle entity.
More over, it's a nightmare trying to find a solution - many lost hours/days trying to design and implement a solution. Symfonycasts could cover this in their training to prevent others falling down the rabbit hole of this problem. Also: cloning the repo and adding in my app specific relations is a poor solution for this problem.
I did find the solution:
doctrine.orm.resolve_target_entities
it's pretty cool and makes sense that this feature comes from doctrine.
Hey Cameron,
That is a perfect catch, I personally didn't know about that feature because I never needed something like this. Thanks for sharing it with others, and here's a nice example of how to use it: https://symfony.com/doc/current/doctrine/resolve_target_entity.html
Cheers!
nice article - good explaination.
When i run ./vendor/bin/simple-phpunit it raises an exception
Fatal error: Uncaught RuntimeException: Could not find https://github.com/sebastianbergmann/phpunit/archive/6.5.zip in C:\www\sf_bundles_scrach\vendor\symfony\p<br />hpunit-bridge\bin\simple-phpunit:65<br />Stack trace:<br />#0 {main}<br /> thrown in C:\www\sf_bundles_scrach\vendor\symfony\phpunit-bridge\bin\simple-phpunit on line 65<br />It seem that the corresponding branches have been deleted from github.
You can then install manually <b>symfony/phpunit-bridge</b> to resolve.
Hey Helmi,
Sorry for the very long reply! Yeah, it looks like PHPUnit 6.5 support was dropped, you need to upgrade "symfony/phpunit-bridge" package, i.e. call "composer update symfony/phpunit-bridge" to be able to call "bin/phpunit" or "./vendor/bin/simple-phpunit". Thank you for reporting this! I upgraded the tutorial's code so it should work out of the box now.
Cheers!
Hello, could you tell me, from what course this PHPUnit test exists here?
Hey Dmitriy,
This test was added in *this* course, but we did it behind the scene because we don't want to cover testing in this course. If you're interested in testing with PHPUnit, check out our PHPUnit course: https://symfonycasts.com/sc...
Cheers!
it's clear
"Houston: no signs of life"
Start the conversation!