The biggest observation that got me to use TDD mostly is the realization that TDD is always what you are doing, just that if you don't have a formal test, your "test" is the object's use in your code itself. So if you find you need another object, there WILL be tests and an expected interface, just those tests are now embedded in your code, and you can only know it works with your specific code and specific situation, not in all conditions you will inevitably end up changing your code into. Separated unit tests ensure your object works as expected in all conditions it should face, and having those tests make it so you can confidently optimize or enhance functionality without losing our breaking old functionality.
0:27 - testing the implementation and not the behaviour 2:00 - your tests never fail 3:20 - your tests aren't clear 6:25 - not running your tests as part of a build process
Good takes, would love to see solid code examples that aren't contrived "shape" classes or reading a character string and making sure it isn't a number. That's what I'm always looking for on RUclips with coding. Good code that makes me think differently about how I implement/approach problems. :)
You should rewrite those functions so they don't return void. If that's not possible then you'd want to validate whatever side effect the `void` function causes actually happens as expected.
In programming there’s a distinction between functions and procedures. Some languages (like pascal) actually have those keywords. A void (aka a procedure) just executed a set of commands and changes the state of the system. This per definition means it’s not a single unit of work and thus not a unit test. It’s potentially a candidate for a integration- or end to end test. This is why I’m more in favor of integration and end2end tests as it will implicit with also test your small functions. A function is not supposed to change the state of your system. It merely performs logic on variables and therefor doesn’t change the state of the system. I like that very strict difference, I don’t know why that separation became less and less clear as program languages got younger - well I do know just don’t really approve. As I tend to really hold firm to these two principles: A void changes my system a function merely changes program data. A void used functions to achieve that system change, a function only contributes the variables for that. So for example updating a file where you’ll remove say or Unix and for windows. You will have a function returning either or depending on the OS. You pass that in as a variable to a void (procedure) that opens the file reads the content and does the replacement and reads it back. That’s the way to separate the two. Now the astute among you will have seen immediately a little gap in that hard separation. What if you want to know if the procedure was successful? Or that opening that file was successful. You can (and should) check if the file exists and the rights are okay (two functions as they don’t change the state) but then there’s still runtime errors for example a disk failure. You could throw an error which is technically is not a return value, but ironically there was no try catch in pascal. And this is where the lines start to blur between the two. In C I tend to pass in a reference to a status variable and have that updated and checked. So it’s still a void but I have the status either in an int that I passed in or a pointer to status structure, which I update each step of the process, so that I also know and can report where a process (procedure failed). This very strict distinction I only hold dear on embedded systems as you have little to no means to actually check the state of the system and you simple have to trust that when you change an output pin or piece of hardware that it actually did that. With operating systems at the abstractions of file systems etc you can test this and I do use a return value in a “procedure” from time to time. But it’s a great practice to quickly see what changes the state of your system/program and what does not. But as with everything in CS there are edge cases that blur the edges.
Just a side note. Don't be afraid to write functions that return multiple values. Whatever you have to do to avoid "out parameters" (most often seen with functions that return void or bool) is _usually_ justified.
@@khatdubell I’m not a fan of returning more than one value. It is a better practice to then return a structure with the values in that structure. This also is far more portable to other languages, because most (sane) languages do not allow to return multiples values. And when you do it truly properly, aka in the C way. You allocate the structure outside of the function call and pass it in my reference and have the function update the values. But you can also return the same pointer. And of course free the pointer after your done. This is the reason to allocate it outside of the function. Because then you know you created it and you need to free it. The function can’t free it because then it’s out of scope. I’d it’s an object then it could be freed when the destructor is called but nobody knows if it is when it’s a library. And this is why I advocate first learning C and/or assembly because then you know what happens with memory management and you can make better judgement when using interpreted (ugghhhhhn) 🤮) languages.
Try developing software that controls external resources like controlling cloud services or even more difficult, hardware like microcontrollers controlling AD/DA converters or i2c devices, motor controllers 😉You can’t write useful tests for that. Your mocks will be a “assumption” and thus useless. You can then only do end to end tests. But you can only write those when you’ve written the implementation 😄
@@etorty_dev assumption is the mother of all f-ups. You should develop against a test instance. That’s the only way to cover all basis. The devil is always in the details.
Interesting fact I have observed: If you test your initial version of your function, it is guaranteed to not work (unless it is a trivial function, in which case, testing that function might be overkill). So test that initial version, (which fails), and then start fixing it. P.S. by "initial" version of the function before you ever ran it ever.
Mocking is not bad. If you already have good unit tests for some functions, you will sometimes want to assert that those functions were called with certain parameters in the functions that call them. Otherwise you’d get a combinatorial explosion of cases you should test.
It depends but I almost always write the test first, then the code to satisfy the test, and then I repeat that until the task is done. For projects that are more in crunch mode I will forego writing tests but that code is really intended to only exist for a short period of time until we can go back and update it to satisfy technical requirements. I write the bare minimum required to get things to work so that ideally discourages folks from adding onto the ball of mud.
What about a scenario where the function that's being tested doesn't produce an output but does a side effect. Is it okay to use "verifys" there to ensure that when that function was called the code took a certain path in the function's logic?
You can, but if it's a side effect that's in the background it's important to ask why you're validating that it happens. If it's a side effect with no observable behavior then it may not be that important for the system to function and could just be removed. Or if it's only logging a message then it's good to ask if it's worth validating the log happened, sometimes it is important (say if it's logging an error or warning) other times it's not.
The biggest observation that got me to use TDD mostly is the realization that TDD is always what you are doing, just that if you don't have a formal test, your "test" is the object's use in your code itself. So if you find you need another object, there WILL be tests and an expected interface, just those tests are now embedded in your code, and you can only know it works with your specific code and specific situation, not in all conditions you will inevitably end up changing your code into.
Separated unit tests ensure your object works as expected in all conditions it should face, and having those tests make it so you can confidently optimize or enhance functionality without losing our breaking old functionality.
Yesssss! I love this observation.
0:27 - testing the implementation and not the behaviour
2:00 - your tests never fail
3:20 - your tests aren't clear
6:25 - not running your tests as part of a build process
Doing gods work 🙏
Good takes, would love to see solid code examples that aren't contrived "shape" classes or reading a character string and making sure it isn't a number. That's what I'm always looking for on RUclips with coding. Good code that makes me think differently about how I implement/approach problems. :)
Great video! Thank you for sharing.
Thanks for watching!
How should we test functions that return void?
You should rewrite those functions so they don't return void. If that's not possible then you'd want to validate whatever side effect the `void` function causes actually happens as expected.
@@CodyEngelCodes Ah looks like this might answer the comment I just wrote 😅
In programming there’s a distinction between functions and procedures. Some languages (like pascal) actually have those keywords.
A void (aka a procedure) just executed a set of commands and changes the state of the system.
This per definition means it’s not a single unit of work and thus not a unit test. It’s potentially a candidate for a integration- or end to end test. This is why I’m more in favor of integration and end2end tests as it will implicit with also test your small functions.
A function is not supposed to change the state of your system. It merely performs logic on variables and therefor doesn’t change the state of the system.
I like that very strict difference, I don’t know why that separation became less and less clear as program languages got younger - well I do know just don’t really approve.
As I tend to really hold firm to these two principles: A void changes my system a function merely changes program data. A void used functions to achieve that system change, a function only contributes the variables for that.
So for example updating a file where you’ll remove say
or Unix and
for windows.
You will have a function returning either
or
depending on the OS.
You pass that in as a variable to a void (procedure) that opens the file reads the content and does the replacement and reads it back.
That’s the way to separate the two.
Now the astute among you will have seen immediately a little gap in that hard separation. What if you want to know if the procedure was successful?
Or that opening that file was successful. You can (and should) check if the file exists and the rights are okay (two functions as they don’t change the state) but then there’s still runtime errors for example a disk failure.
You could throw an error which is technically is not a return value, but ironically there was no try catch in pascal.
And this is where the lines start to blur between the two.
In C I tend to pass in a reference to a status variable and have that updated and checked. So it’s still a void but I have the status either in an int that I passed in or a pointer to status structure, which I update each step of the process, so that I also know and can report where a process (procedure failed).
This very strict distinction I only hold dear on embedded systems as you have little to no means to actually check the state of the system and you simple have to trust that when you change an output pin or piece of hardware that it actually did that. With operating systems at the abstractions of file systems etc you can test this and I do use a return value in a “procedure” from time to time.
But it’s a great practice to quickly see what changes the state of your system/program and what does not. But as with everything in CS there are edge cases that blur the edges.
Just a side note.
Don't be afraid to write functions that return multiple values.
Whatever you have to do to avoid "out parameters" (most often seen with functions that return void or bool) is _usually_ justified.
@@khatdubell I’m not a fan of returning more than one value. It is a better practice to then return a structure with the values in that structure. This also is far more portable to other languages, because most (sane) languages do not allow to return multiples values. And when you do it truly properly, aka in the C way. You allocate the structure outside of the function call and pass it in my reference and have the function update the values. But you can also return the same pointer. And of course free the pointer after your done. This is the reason to allocate it outside of the function. Because then you know you created it and you need to free it. The function can’t free it because then it’s out of scope. I’d it’s an object then it could be freed when the destructor is called but nobody knows if it is when it’s a library.
And this is why I advocate first learning C and/or assembly because then you know what happens with memory management and you can make better judgement when using interpreted (ugghhhhhn)
🤮) languages.
Writing code without tests is a foreign concept to me at this point.
Try developing software that controls external resources like controlling cloud services or even more difficult, hardware like microcontrollers controlling AD/DA converters or i2c devices, motor controllers 😉You can’t write useful tests for that. Your mocks will be a “assumption” and thus useless. You can then only do end to end tests. But you can only write those when you’ve written the implementation 😄
The mocks won't be useless if they help test your assumptions of expected behavior and help your code robust.
@@etorty_dev assumption is the mother of all f-ups. You should develop against a test instance. That’s the only way to cover all basis. The devil is always in the details.
Interesting fact I have observed:
If you test your initial version of your function, it is guaranteed to not work (unless it is a trivial function, in which case, testing that function might be overkill). So test that initial version, (which fails), and then start fixing it.
P.S. by "initial" version of the function before you ever ran it ever.
Yep that's fantastic advice!
Mocking is not bad. If you already have good unit tests for some functions, you will sometimes want to assert that those functions were called with certain parameters in the functions that call them. Otherwise you’d get a combinatorial explosion of cases you should test.
Nice video. I'm curious, do you write test first or test after in your day job?
It depends but I almost always write the test first, then the code to satisfy the test, and then I repeat that until the task is done.
For projects that are more in crunch mode I will forego writing tests but that code is really intended to only exist for a short period of time until we can go back and update it to satisfy technical requirements. I write the bare minimum required to get things to work so that ideally discourages folks from adding onto the ball of mud.
How can you mock in a way that won’t break when you change internal implementation?
Just avoid mocking everything
What about a scenario where the function that's being tested doesn't produce an output but does a side effect. Is it okay to use "verifys" there to ensure that when that function was called the code took a certain path in the function's logic?
You can, but if it's a side effect that's in the background it's important to ask why you're validating that it happens. If it's a side effect with no observable behavior then it may not be that important for the system to function and could just be removed. Or if it's only logging a message then it's good to ask if it's worth validating the log happened, sometimes it is important (say if it's logging an error or warning) other times it's not.