399 views
# TDD Workshop Test Driven Development ## Background Have you ever written Code, let it sit for 2 weeks and came back to it? When you did, did you think “WTF is this?” If yes, you’re not alone. Have you ever worked with others or in a bigger project, changed a small thing, pushed it to production because all the manual testcases you could think of worked, but then something else broke? If yes, you’re not alone. These are basically the two main scenarios that TDD aims to “fix”, with a bigger focus on the second scenario and the first more of a nice side effect. But first, we need to dive a bit deeper into some background info to understand TDD better. You can skip the background section but I highly recommend reading it. ### Disclaimer You will not change your programming habits completely with one workshop. It is a learning process and therefore will take time, repetitions, failure and small victories. And that’s okay, you can learn this step by step. If you want to start very easy, try to improve on just one aspect mentioned in this handout in your next project, with one aspect meaning just a single sentence, not a whole paragraph. Anything more is a bonus; this can easily seem overwhelming. But it’s worth it, it’s damn satisfactory to write good code and run 100+ tests and have them all pass in under a second total. Plus, some other benefits that will be mentioned later :) ### Clean Code … there’s a lot of memes about bad code. So, what is “good” Code? I like Robert C. Martin’s “Clean Code” definition from the book with the same name (go and read it! It’s really good): It’s intuitively understandable text. It usually is code, but it doesn’t have to be. It can be a YAML, database structure or something else. And as long as it’s intuitively understandable, it’s “clean”. Now why does this matter? Clean code results in more stable and more easily maintainable code bases. This is especially relevant in industry applications since about 80% of the life cycle of a project is maintenance, feature expansion and bug fixing. It means that when a person unfamiliar with a code snippet looks at said snippet, they can understand the code intuitively with only very few “WTFs” – i.e., they don’t have to think about what the code does, they can understand it by reading it. The important thing is, this person could be anyone other than you - or it could be you in two weeks. Now why is this so hard? Requirements from the Project Owner can be unclear or contradicting - the Project Owner can be you, and yes, you can also write bad requirements. Or developers just ignore code quality and explain it with missing experience, missing discipline or too high of an effort to refactor. ### Code Smells “What? How can code ‘smell’? — Well it doesn't have a nose... but it definitely can stink!” – [refactoring.guru](https://refactoring.guru) If a pattern makes code unreadable, it’s probably a code smell. There are lots of patterns already identified and catalogued, go look them up and click yourself through. Here are my top 3: - Long Method: The name is program. If the method is longer than 10 lines, parts can definitely be extracted. If the method is longer than 5 lines, it’s still probable. - Shotgun Surgery: You make a small change at A, then have to fix a thing at B and C, then you have to fix a thing at D, E, F and G, … you get the point. To change something, you have to fix a thousand little things all over the project. - Comments: My favourite one. If a comment describes what a block below does, extract that block. If a comment describes what a variable is there for, rename the variable. Same for methods. Comments are rarely useful. They are on occasions, but usually code can be made self documenting. ### Refactoring How do I make smelly code not smell? By refactoring it. A refactoring is a structure changing, behaviour preserving process. Same as with the code smells, there are lots of refactorings catalogued, go and have a look. Here are my top 3 (they coincide somewhat with the smells, who would have thought): - Extract Method / Variable: Extract a part into a new method / variable. Most IDEs can do this well automatically already, and I use it all the time. Sometimes, declaring a new variable that is only used once will increase readability because a certain aspect of for example a chain call is made explicit. This can also be applied to conditionals to make them easier to read. - Rename Method / Variable: Rename something so it correctly depicts the intent of the method or what the variable is used for. Most IDEs can do this well automatically already, I also use it all the time. - Replace Conditional with Polymorphism: This is a more advanced one, but one that feels very rewarding to me. You basically turn a conditional into a strategy pattern (look that up as well if you want, it’s a very good pattern to know) and therefore make the whole part more readable and extendable. ### Further Resources I really recommend checking out https://refactoring.guru/ , it is a really good catalogue on Code Smells, Refactorings and Programming Patterns. I also really recommend reading Clean Code by Robert C. Martin, it’s a great book that goes into more detail for every topic in this workshop. It’s also freely available in the library. ## TDD The idea of TDD is to convert requirements to test cases and force the requirements to be fulfilled by writing the tests first. There are two approaches: Chicago / Detroid TDD (the OG), and London TDD (the newer one). Chicago TDD is a bottom up approach, London TDD a top down approach. We will focus on the Chicago approach today. Feel free to read up on the London approach on your own :) ### Process 1. Write a test that fails. This test should force you to write the code you already know you want to write. If you do not know what to implement next, a user story prioritization may be due. 2. Write the minimal code that makes the above test pass. Do not write more code for “later” (see [YAGNI](https://en.wikipedia.org/wiki/You_aren't_gonna_need_it)). The passing code can be ugly, we’ll fix that in the next step. 3. Make it beautiful :) Refactor the code until you’re satisfied it’s clean again. 4. GOTO step 1. #### Remarks to step 1 & 2 If you do not know how to implement the next user story, plan ahead before you write the first test. Get a general feel for what is needed in your current user story and break it up into smaller tasks if required. ### Rules - Do not write production code that is more specific than the tests. - Anything you write as a test, makes the whole test suite more specific. - Anything you write as production code, makes the whole production app more general. - Don’t go for the goal. Avoid the central behaviour as long as you can. - Do not bring it to work if you’re not good at TDD. Learn it first in a private environment. Otherwise, it will be extremely frustrating for everyone. - That said, trying to improve code quality and maintainability by writing tests is never wrong. Just don't force it until you're sure of what you're doing. ### Effects - 100% Test coverage. You will know when you break something. - More modular projects. Changing and extending the project is made easier. The factual, measurable effect of TDD on modularity is not scientifically proven because it is very hard to do so. TDD advocates like Robert C. Martin, Martin Fowler and Kent Beck say so though. - Examples of how to use your code. Just go have a look at the tests to see how a code snippet is used. ### When not to do TDD If you're in a professional setting and you're not an expert. If you're in any kind of setting where you're working with others on the same code base and are not an expert. Otherwise? Go for it. ### When to do TDD Private projects without a deadline are perfect for trying out new technologies. When in a non-TDD, existing project: new modules can usually be completely TDD’d, usually works well with mocks. ### How to Write Good Tests #### Differentiate between Integration and Unit Tests Integration tests have the objective to make sure that complete use cases work in a production environment. Unit tests should help you quickly find and isolate broken pieces of code. We will focus on unit tests. Some skills learned with unit tests will translate to integration tests, but not all. #### Isolate the Testable Logic The more logic you're covering with a single test, the harder it is to isolate broken pieces of code. Try to write tests that cover a small but reasonable amount of code. You will get to know what a reasonable amount of code is with experience, there's no single metric. Try to start with small scopes and extend from there - play around with smaller and bigger scopes. You'll learn that it's almost always context dependent what the best scope is, and everyone does it a little differently. Often, your logic has dependencies on other modules or classes that you don't want to include in your testing. Examples include everything with a high latency like IO, network connections or just calls with complex and / or big computations / results. You can circumvent these dependencies with mocks and patches. Often libraries already have these testing patterns included, look them up before you're trying to write your own. #### Follow Arrange, Act, Assert The AAA is a general approach to writing more readable unit tests. In the first step, you **arrange** and set things up for testing. Set variables, create mocks and patches, etc. The second step is to act and execute the function to test and store the outcome in a variable. The third step is to assert that the hypothesis is true by comparing the outcome variable to a hardcoded expected variable. ### Helpful Testing Resources Learn about Mocks and Dependency Injection. If you’re trying to mock a library function, google them first to see if there’s a mock framework for it (examples python `input()` and `request` & `responses`). [Java Unit tests JUnit](https://junit.org/junit5/) [Python unittest](https://docs.python.org/3/library/unittest.html) Or just google - 'your programming language unit tests', - 'your unit test framework mocking', - 'your unit test framework method patching' ### Exercises Here I've collected some exercise (-types) and links for you to work on your own #### 2 Minute TDD 2 Minute TDD is an extreme form of TDD and is not recommended for complete beginners. In 2MTDD the first and second phase of the TDD cycle are limited to 2 minutes. If you succeed to make your tests pass within those two minutes, commit the changes to a VCS #### Katas There are lots of little Katas, here's an incomplete list: - Program a calculator - Program a calculator that uses strings (terms) as inputs - A program that converts ints to roman numerals - A program that converts roman numerals to ints - A complex data structure like a Stack, Red-Black-Tree, ... - Refactoring Katas - With these, you can learn to write tests on an existing code base. This is a perfect training environment that translates pretty well into a professional environment. However, aspects like team work and coordination are not trained. ## Remarks Don't write efficient code. Write code that is maintainable and then make it efficient. This will usually result in maintainable, efficient code. If you really need to do something akin to [Quake III's Fast inverse square root](https://en.wikipedia.org/wiki/Fast_inverse_square_root) because it cannot be written differently, comment it well. This is one of the rare cases where comments really do improve code understandability. Unreadable code has probably more bugs than readable code, which can influence software security by creating exploitable vulnerabilities. Bugs are easier to identify in readable code than unreadable code. Bugs are also easier to fix in maintainable code. Therefore, readable and maintainable code is usually more secure. However, even with tests, this only applies to code level bugs. This does not prevent design level mistakes. E.g. MD5 implementations work as per specification, but are cryptographically broken. This can be seen as a design level mistake as the specification itself is insecure. # References https://github.com/testdouble/contributing-tests/wiki/ https://tdd.mooc.fi/ https://tddmanifesto.com/exercises/ https://refactoring.guru/ ## Books ### The important ones https://www.informit.com/store/refactoring-improving-the-design-of-existing-code-9780134757711 https://www.informit.com/store/test-driven-development-by-example-9780321146533 https://www.amazon.de/-/en/Robert-Martin/dp/0132350882 ### Other cool books https://www.informit.com/store/xunit-test-patterns-refactoring-test-code-9780132800051 https://www.informit.com/store/atdd-by-example-a-practical-guide-to-acceptance-test-9780132763233 https://www.informit.com/store/growing-object-oriented-software-guided-by-tests-9780321503626 https://www.informit.com/store/developer-testing-building-quality-into-software-9780134291062#