Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.0, <7.4
Subscribe to download the code!Compatible PHP versions: ^7.0, <7.4
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
TDD & Unit, Integration & Functional Tests
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Unit, Functional & Integration Tests
Before we get back to coding... we need to talk about just a little bit of theory! No, don't run away! Give me just 2-ish minutes!
So, there are actually three different types of tests, and we're going to try them all. The first is a unit test - that's what we've created. In a unit test, you test one specific function on a class. It's the purest way to test: you call a function with some input, and test the return value. We'll learn more about this later, but each unit test is done in isolation. If, for example, a class needs a database connection, we're actually going to fake that database connection so that we can focus on testing just the logic of the class itself.
The second type of test is an integration test... or at least, that's my name for it. An integration test is basically a unit test: you call functions and check their return values. But now, instead of faking the database connection, you'll use the real database connection. We'll talk about when and why this is useful later.
The third type of test is a functional test. In our world, this basically means that you're writing a test where you programmatically command a browser. Yep, you literally write PHP code that tells a browser to go to a page, click a link, fill out a form, click submit, and then assert that some text appears on the next page.
More on all of these things later.
How much to Test?
Another question is how much you should test. Does every function need a unit test? Does every page and every validation error of every form need a functional test? Absolutely not! That sounds worse than a raptor claw across a chalkboard!
Especially if you're new to testing, don't worry: a few tests is way better than none. And honestly, I think many people create too many tests. I follow a simple rule: if it scares me, I test it. Too many tests take extra time, add little value, and slow you down later when they fail after you've made a minor change.
In our app, we've tested the getLength()
and setLength()
methods. These do not scare me. In the real world, I would not test them.
Test-Driven Development
A few minutes ago, I mentioned the term "Test-Driven Development" or TDD. TDD breaks coding into three steps. First, create the test. Second, write the minimum amount of code to get that test to pass. And third, now that your tests are passing, you can safely refactor your code to make it fancier.
So, test, code, refactor: these are the steps we're going to follow. Do I always use TDD in my real projects? Um... yes!? No, not really: I'm not a purist. Heck, sometimes you don't even need a test! Gasp! But yea, usually, if I plan to write a test for a new feature I'm working on I'll follow TDD.
Oh, and TDD is about more than just making sure you have a lot of tests. As you'll see, it forces you to think about how you want to design your code.
Enough theory! Let's try it already!
9 Comments
Hey Mohammad!
Indeed - reality is tricky :). Part of it depends on how much you trust other people on your team. And another part depends on just how scary some code is (e.g. does a function calculate the amount of money to transfer to somone's account? Scary! Or the number of comments they have made. Not scary).
You mentioned:
> I'm always worried about the future, if someone in the future sits beside me and starts to refactor my code. what will happen if I did not write a test for the smallest part of the system?
That's fair. However, if developers on your team aren't able to realize that changing (for example) what a function returns will have side-effects on other parts of your system, then you ARE going to have big problems, no matter what. That is a bad situation to be in. You also pay a high cost for writing test for every part of the system. Sure, there is some up-front cost for writing the test initially. But I'm talking about the cost of needing to constantly re-write tests for simple changes. Here's an example: suppose you have a function that today returns a string. Then, you decide you want it to return an array. You realize that this breaks anything that uses this function - so you find the 2 places that use it and update both to expect an array. None of this code is scary - so the change was simple. But if you had unit tested the function itself... you would now need to update it... which I find "not ideal" if the logic there is very simple. Also, if you had tests for the other 2 functions that used this function, those tests may *not* have failed, if you had forgotten to update them - you may have mocked out the first function's logic, causing the other 2 functions to happily pass, even though they are now incorrect.
That's a long way of saying: I totally understand what you're thinking. Just don't forget about the cost of writing tests for simple functions. And also know that if you can't trust your team to think critically when they are refactoring things, you're going to be in trouble anyways ;). I'm definitely on the more pragmatic side of testing. I write tests to protect the "core things" are site does or our "core user experiences" - e.g. make sure the checkout page works perfectly. On a lower-level, I test functions that scare me or are very complex (and so testing actually helps me write them and get all the edge case). I do not test so that I can help catch mistakes from team members.
Cheers!
The subtitles don't match what's being said.
Hmm, thanks Thao L.! It looks like something got de-synced. We'll take a look and fix it!
Cheers!
Hey Thao L.
The subtitles are fixed now, if you still see them de-synced, you may have to clear your browser cache for this page
Thanks!
good idea about - if it scares you - test. I have worked in a company where they tried to cover with unit tests everything. And I thought I am better programmer because I tried also. I tried even test every line of code of integration tests but and during code reviews I tried to encourage team also to test same as me that really used to take lot of time, so I guess now if I would go back to that company, I would just test as much as company requires and if I feel code is scary and probably would be better that way. The problem is that I have read that robert martin saying you need to cover asymptotically 100 % of the code. Maybe he should edit this book and also publicly tell to those who own the book - dont do it, it takes too much time.
Plus - I have tried TDD. But TDD also tells - before writing code - write a test. That meant that I cannot write a code without writing test, and so it leads to 100 % coverage. TDD made me slow down hell lot, and after couple of months the teamlead said to not do TDD and I think he was right and made me faster again. TDD maybe works for simple functions as in the video, but for real complicated stuff - I do no think it always is good. I stopped seeing a benefit of it, especially after learned that no need 100 % of coverage.
Lijana Z. Since you're talking a lot about being "fast" as something that is of the utmost value, let me remind you another thing that Bob Martin said "The only way to go fast is to go well." Hurrying will come back to kick you right up the arse one day, and it will kick you hard. Like Lijana Z. said, you must find your balance. I've been in the situation where every deploy is a "close your eyes and fingers crossed". Fragile and rigid code is the arch enemy of every programmer and tests are a great way to avoid it. I would also say that what you said that TDD is not good for complicated stuff, is not true. Quite the contrary - you want your complicated stuff tested and testable. TDD greatly helps with that. Of course you don't need 100% test coverage, but you need to test your logic. No need to test, for example, getters and setters, if they do not contain logic, which they by all means shouldn't.
Hey Lijana Z.!
You share my experience exactly, and I think you need to go through this process to really "feel" the pros and cons of testing. I'm very happy to hear this! And yea, there are certainly people (smart people, smarter than me!) that disagree. But in the real world, with budgets, goals, deadlines, etc - you *must* find your balance.
Anyways, thanks for the message - it's great to hear other experiences that mirror my own!
Cheers!
Interesting to hear for me also. I was also really annoyed when I saw in one company when people write bad code, and without tests, but they have more experience, so I assumed they should write tests, or they should be valued less than me. But they were valued a lot by director, and I was doing poor because of low productivity. I did not attend all the classes in my univercity, but I believe there was no productivity classes, agile, or similar stuff classes. One thing there was - a psychology, which I probably should have not skipped as much as I did, maybe would have learned something useful, because psychology is also big part of success I believe.
This really should be teached in all univercities and colleges to not get so much bad experience. I was even losing trust, if I can have good life ever with strugling so much at work. But I did not give up, because I did not want to do most of other jobs. And also if I would want some other, I did not have a skill.
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// 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
}
}
You mentioned something about "No need to write tests for each and every method or function or piece of code". Personally, when I start writing tests, I'm always worried about the future, if someone in the future sits beside me and starts to refactor my code. what will happen if I did not write a test for the smallest part of the system?
Well, the superficial consequences would be that part of one big feature is not going to work. But wait a min why it's not going to work? because that other guy who sits beside me who tried to refactor my code, basically he/she changed the function logic in a way, in which that function now is returning different results. results which my code have never expected it. so it's needed to write a test for every and each piece of code. and don't worry about too many tests. if you are supposed to change the function logic for a reason and that change has a very big effect on the tests which leads you to change the code in the whole bunch of tests. then just look at it from this perspective that you are creating a new feature not changing the old one. But all of these explanation is what I have understood from what is the main reason of writing tests.