#begin
Today we are going to look at the next chapter of the Clean Code book by Robert C. Martin. In the previous blogs of this series we discussed the first eigth chapters of the book, and today I’ve chosen to check out chapter 9. How predictable, right? Chapter nine is about unit tests. So yeah let’s go!
Unit Tests
So let’s talk about unit-tests, although we have just talked about it for quite some time in the previous blog. Everyone who knows Unckle Bob knows he’s a fanatic proposer of TDD. He always talks about how stupid TDD sounded to him until he met up with Kent Beck and conjured up a little Java program at Beck’s house. He was amazed how they could write that program so nicely and never having to touch a debugger. From that moment on, he was sold on TDD and became it’s prophet. He’s been spreading the word about TDD ever since I guess.
So this chapter is not directly about TDD but it’s about well as the title implies, Unit tests. And I think after all these years that testing is even a thing, the debate is still going about what a unit is exactly. Is it a function, as many people say or maybe is a unit a class? If so, what about unit tests in a functional language where classes do not exist, or just older structured programming languages like Fortran, or C even? Let’s see if this chapter shines some light on that, I’m not even sure if Uncle Bob gives a definition of a unit test in this chapter, but we will find out soon enough.
So Uncle bob starts this chapter with a short little story about how he used to test some concurrent code based on a timer by singing a little song while typing the text in, and the test repeating it in the same speed which thus matched the melody. So by repeating the little song, he could check whether the code was correct. I guess that’s a real out of the box solution to such a test. He also talks about how he would test that code today. He says he would cover every nook and cranny with tests. He would isolate the code from the OS, removing dependencies to timing and write his own timing abstraction and schedule commands himself.
He continues on with stating the three laws of TDD which are pretty insane if you have never heard them or experienced their effect. So the first law states that, and I quote: “You may not write production code until you have written a failing unit test”. So you must always write a failing test first, and if taken really strictly, not compiling is a failing test. The second law states, and I quote: “You may not write more of a unit test than is sufficient to fail, and not compiling is failing.” So if you wrote some test, it can never be that long to begin with. Because as soon as you start writing some code that does not exist, you need to stop writing the test code and add some production code. However, the third law states, and I quote, again…: “You may not write more production code than is sufficient to pass the currently failing test”. So you end up is this very short cycle, a very short loop until you have written some code.
Now, OK, I’ve done some TDD before and let me tell you that these laws should not be taken too strictly. I mean, I do write my unit tests first, and may even write some code that does not compile. But often enough I will write my entire test case first, and then add the production code that covers is. So I might have multiple concepts in the test that do not compile but I’m not going back and forth in this minuscule cycle. That’s a bit too much I think. But what do you think about these three laws? Leave me a comment somewhere because I’m pretty interested to know if any of you practice TDD in a Unity3D context and if you follow these laws loosely or strictly.
Next Uncle Bob explains about the necessity that tests should follow the same software design principles as any other code. Don’t break rules and standards because it’s “just the tests”. Because it will come back and bite you, trust me. If you start breaking encapsulation, design rules like dependency management or structures in your test classes you will end up with a big mess. And as with any practicer of TDD, I’ve felt that pain as well. That you have written so many unit tests but now have to change something in the requirements, and lot’s and lot’s of tests break. This is what is known as the fragile test problem. A guy called Ian Cooper has a really nice talk about this at DevTernity, really awesome conference by the way so massive shout out to them. But it’s a talk about TDD, what went wrong by Ian Cooper. Shout out to his beard as well by the way. But Uncle Bob just wants you to know that test code is just as important as production code. Don’t throw all your software design principles overboard while writing test code. Because they will start to slow you down immensely and you will probably delete them and thus loose your tests.
Next he talks about how tests enable the -ilities of your game. By ilities he means the non-functional requirements your game has. Like maintainability, which says something about the easy of maintaining the code or systems in your game. It might even describe how you maintain 3D models etc. But also things like performance is an ility. So you might state that the game should never get lower than 40 fps. Or availability in the context of an online game. Or portability, which defines the easy of porting your game to another platform, which might sound easy in a Unity context since you can often just switch platforms and smack the build button. But if you have created a game for standalone desktops, and switch to mobile, and expect everything to work out of the box, you are greatly mistaken. I mean, what about resolution, text and icon scales, input, and the famous problem that you do not have hover functionality in mobile games. So any logic you bound to a mouse hover, is not unavailable. Once you know about that problem, you start to notice it everywhere because people often don’t think about it. Keep an eye out for that and you will spot it somewhere in an app today, where you know there’s a desktop version that has more information than the mobile version. But enough about the ilities.
Uncle bob says that tests will enable your ilities. Now, why does he say that. He means that if you have a system that is covered with unit tests you are able to change the code as much as you want, run the tests, make sure they pass and thus you can refactor your code without fear to match these ilities. He’s kind of right, although I don’t agree with him on this one in a Unity3D context. There is far more stuff happening in a game than just code. I mean, rendering, duhh. Imagine you have written the most elegant, brilliant code that has ever been produced on the surface of this planet but you throw in a highly detailed model, with some super fancy shaders, your FPS is still going to drop down the abyss. Now, you could write a unit test that instantiates the model and check fps but that’s a bit too clunky. So for these kinds of things there are best practices for 3D-modelling, like low poly models with nice normal, bumb and specular maps. Some nice shaders and maybe add some level of detail into the mix. These have nothing to do with Unit tests, and are really hard to tests for. This is just something that are covered by policies defined by some art director, game designer or producer for example.
But, if we limit the scope of the ilities to just code, than tests can be a great tool to enable and support them.
Next he talks about the concept of clean tests. Test methods should not be bulky and do much logic. A test should preferrably only have three lines of code since a test always follows a pattern of Arrange, Act and Assert. So in a test you arrange some code, some data or object structure, than you act and run the section of the code that is under test, and once that is done you Assert and check whether the code did what you expected. So, if you could cover all test phases in merely three lines of code. And , well, personally I don’t like this limit. I write tests that might be a bit longer. Since I do not want to spread all these small functions around that are only called by one test case. So if you are testing a particular data structure you simply create that thing in the test itself instead of calling a function because you might get functions like: CreatePlayer, CreatePlayerWithHealth, CreatePlayerWithHealthAndWeapon, CreatePlayerWithHealthAndWeaponAndGear, CreatePlayerWithHealthAndWeaponAndGearWithoutShield, this goes on and on, an on. So I often break this rule and simply create that stuff in the test method itself. But I do agree that test code should be simple, readable, short, concise and dense.
Another piece of advise that he gives here, which I do agree with is that you should stick to the same language in your tests. So you create always name functions in the same way. I personally like to write tests out as sentences, with underscores (_) instead of spaces. A space in a function name will not compile ofc, and thus you replace each space with an underscore. This way you can really easily read tests. And don’t forget you can add additional information in the [Test] attribute. Like a description and an author for example. Although, adding the author is worthless because that information is in Git. We talked about such things in previous episodes already so I’m not going to restart that rant again.
But yeah, he says you should create your own little language for testing. So name your functions in the same manner, also create functions and logic that match the Arrange, Act, Assert concept. Uncle bob calls it the Build-Operate-Check pattern in the book by the way. But I think the Arrange Act Assert concept is more widely known.
He then continues his discussion with the fact that test code must be really simple. He says that test code should hide complexity in functions. I remember making the mistake of writing tests against MonoBehaviours. So in my the Arrange part of my test function I would have to instantiate the prefab, than wait for the awake and start functions to finish. Also, if you instantiate some gameobject which requires the scene to be in a specific state, you must encode that in your arrange functions. So you setup the scene, then spawn your object and then wait for awake and or start. Only then, are you even able to start your test. I mean, this is far from simple. Trust me, it caused me many headaches getting these tests running. I’ve felt the pain… So now, I never, ever test MonoBehaviours anymore and always decouple my code from Unity as much as I can. This thus means that there is an area of my code that is not tested, but these MonoBehaviours do not contain any business code and are just treated as views or UI.
But all this testing setup should be wrapped in functions because you probably need to setup the scene more often than once, and you also need to wait for some object’s awake and start more often than once. So wrap this logic in functions and abstract it away. Keep the code in the test functions themselves simple, to you can quickly see what the test case is about, and not have to worry or read the giant arrange logic part of the test.
Next he states that tests should only have one assert. And, although I do agree with him, there are edge cases where I just need more than one assert in test. Like, what if you are testing some logic that swaps things out, like swapping object A for object B. You write two asset statements to test for example the identity of both objects. Are you really going to write two separate tests for checking the identity of A and another test for the identity of B? I would not. I mean, that seems useless to me. So take this rule with a grain of salt. You should of course limit the asserts in a test as much as you can, but still sticking to a hard rule of only one assert is too limiting in some cases.
How do you get to as less asserts in a test case, well you follow Uncle bob’s next piece of advise which is; stick to one concept per test case. This is great advise. I mean, in theory, you could cover 100% of your code, with a single unit test. But would that be helpful? I don’t think so. So keep tests small to cover one tiny piece of the code. This way your code remains simple and readable, and you will most likely end up with just a single assert statement.
He then ends this chapter with some genera rules for tests. These rules come from training materials used in his old company called Object Mentor. I think he’s sold it now and the company is no longer his but still. The rules originate from the Object mentor training materials. It’s an acronym that states F.I.R.S.T. and stands for Fast, Independent, Repeatable, Self-Validating and Timely.
So tests should be fast, because if they are not fast, you will never run them. Imagine going to the short TDD cycle, but at every point you need to wait 15 minutes for all tests to pass. You won’t ever run these tests during development now. So tests must be fast, mere seconds that is.
Next tests must be independent. All tests must run in isolation of each other. This is a really important concept. You do not want tests impacting each other. So this mean, no singletons for example. Or you will have to destroy them in the teardown section of you tests. A singleton can be a great source of pain in any project. Especially for testing! Tests must run in isolation remember that.
I can also fondly remember my UnityTests overlapping with eachother. In Unity3D you can annotate test functions that return an Ienumerator with a special UnityTest attribute which allows the editor to bind them to the editor update loop. But I noticed that since these are IEnumerators they could actually overlap each other like a single frame or so. I’m not sure if this issue is fixed but I use as less of these functions as I can.
Next, tests must be repeatable. I mean, what value does a test have if it’s not repeatable. The fact you have written tests means they are repeatable right. So don’t stick to manual testing, and automate that crap.
Next, test must be self-validating. Test should either pass or fail. There cannot be any in between state. Don’t start manually comparing results by reading debug logs for example. Make sure it is covered by an assert!
And lastly, they should be timely. This relates to the rules of TDD. Tests should be written just before the production code is written. Because then you know you have covered everything. If you write them any time afterwards, you will forget to cover stuff that you either forgot, or just made hard to test and just don’t code and then you will leave holes in the tests making them meaningless. I mean, if you know here are holes in the tests, yet they all pass. Does that convince you that the code is correct and safe to release to your audience? Think about it.
But let’s call this blog a wrap. I’ve been writing far too long by now. But I do want to end this blog with a side-note, sort of a clarification that although we have discussed much about Unit-tests and TDD you should not see it as a “religion”. You should carefully apply TDD and unit testing where it makes sense. Uncle bob has a different opinion about this since he advises people to use it where ever you can. But in many scenario’s in Unity3D, you simply cannot find a nice way of testing things since they are too coupled to the Unity3D API. Imagine having to test some 3th-party asset that has not been designed to be testable. You will end up with the fragile test problem, trust me. Again, watch the talk called “TDD what went wrong” by Ian Cooper about this topic.
And another thing, maybe you don’t need to apply TDD in some scenario’s like when you are rapidly prototyping some gameplay for example. You’re going to make things difficult when you start throwing TDD into the mix most often. Just because the nature of a prototype allows you to cut corners, may that be design wise, code or structure. You might pull in many dependencies, create a horrible spaghetti monster code nest, just to demo and prototype some idea. Just, make sure to throw that prototype into the bin, and start over in a new, clean project and apply the practices you learn from the clean code book and this blog of course.
I hope you enjoyed this episode and learned something new and valuable. Start applying things in your current or new projects and pick the fruits of your learnings so to say. Don’t forget to leave me some feedback or comments if you like.
Thanks for reading, and test you later!
#end
01010010 01110101 01100010 01100101 01101110.
Recent Comments