I'm part of a group here in Nashville called Penny University. Essentially it's an “open-door” user group that connects people wanting to learn with people wanting to teach. Usually over lunch, coffee, or breakfast. I offered to meet up with a few people wanting to learn more about test driven development before and it quickly became a hot topic. I was asked to organize another roundtable discussion about TDD, and this is how it went. This wasn't a low-level code syntax overview, but a general discussion involving great topics like:
- The (sometimes) unseen benefits
- Typical workflows
- Useful practices
- When you shouldn't test
- and how to think through problems in a TDD way.
To someone new to testing, be it a project manager, yourself, or a colleague, it can sometimes feel like a waste of time. ”Why can't I just write the code now and be done with it!?", you may ask. This is not an uncommon sentiment to come across. Certainly, if your boss says don't write tests, then you'll have to do what he/she ultimately wants and move along. (Though I might get that in writing) Having your codebase tested, at least somewhat, indicates that you've not only thought your code through, it goes a bit deeper than that. If you're building a library other people are using, having a good test coverage might give them more confidence in your library. If it's purely an internal app/library/whatever, then having a good test coverage gives you confidence in refactoring later. You rip and yank functions as you please, rewrite them entirely or just delete them and, if you have decent coverage, be pretty confident that nothing will blow up in production if all the tests are still passing.
Do you always write the tests before? What does a typical bug fix, or feature request look like? Obviously, we'd all love to be able to follow the Red, Green, Refactor principle. Meaning, it may seem like only rockstar devs are capable of writing a failing test first, then writing the code that makes that test pass, then refactoring to make the code elegant. Well, I'm not a rockstar dev and I think most would agree that all we can give is our best.
What can we do if not try to do better? If it's straightforward enough, sure, I'll follow TDD principles and write the test first. This can usually happen with small bug fixes. If it's a feature, on the other hand, it feels impossible to me but I will do something to compensate for that. What I typically do is start coding on what I'm trying to implement and write test placeholders along the way. This is very simple to do, just a function and pass statement. Maybe a doc string to tell me a bit more. I'll commit it, but I will not submit that PR to review until those tests have been fleshed out more. (If you're afraid you'll forget about them, have them purposefully fail) This isn't pure TDD but it's being cognisant of function design to accommodate testing. More often than not that leads to best practices. Not always, of course. Sometimes testing is more of an art than a science. For example, if I'm writing a tip calculator, I might focus on implementing the functionality first and write a quick test skeleton that looks like this:
class TipCalcTestCase(TestCase): # setup funcs ... def test_tip_calculates_correct(self): """ Test that get_tip_amount() should take a total and a percent then return the amount needed to tip. """ pass
A unit test should test a unit. That sounds redundant, but I'm basically saying that a unit test should test one thing and one thing only. If I have a function that's long and unwieldy, it will probably be a nightmare to test. By taking that function and extracting smaller, more readable functions, I can test every line with ease. Having TDD in mind when writing code thus can help you write cleaner code. Of course, our tests benefitting is only a side effect to the true value, code readability and maintainability ease. Extracting your one long function into smaller, more manageable functions, will prevent a lot of mental friction when you revisit that code months later.
There are times when testing is just redundant or unnecessary. For example, I don't advise testing 3rd party libraries. You can test how you're calling them, and even test that they get called, but testing the internals of 3rd party libraries should be handled by the authors of those libraries. To give a silly (and totally thought-of-on-the-spot) example, let's say I'm writing an app that helps out cashiers. I might have code that looks like below. In this case, groceries is a very nice 3rd party library that has a method that when given an item (bananas, cookies, etc) returns the price. The problem is that I'm not concerned with groceries doing its job. I'm having faith that the author of groceries has done what he needs to do for me. I want to test
get_subtotal(), but I also want to this test to run completely offline. Mocking (basically just telling a function/class what to do manually) helps me get to what I need to do, which is testing
import groceries # 3rd party lib def get_subtotal(items): subtotal = 0 for item in items: # groceries.get() makes an API call item = groceries.get(item) subtotal += item.price return subtotal
and the tests...
import unittest import mock import get_subtotal class SubtotalTestCase(unittest.TestCase): # Setup testcase class.. ... @mock.patch('get_subtotal', return_value=7) def test_get_subtotal(self): items = ['banana', 'hot dog buns'] # Since get_subtotal is mocked, it will only return 7 and do nothing else. result = get_subtotal(items) self.assertEqual(result, 14)
These silly examples are 100% untested (ha!) in real life, just basic concepts. Overall, we had a blast and everyone felt they took away something they didn't know before. If we end up having another discussion on TDD, it will definitely be recorded via google hangouts. Reach out if you have anything questions, thoughts, concerns, praises, blames, etc.
A few links that were referenced during our conversation:
Read more posts on my personal blog @ http://anthonyfox.io/
The name "Penny University" is a reference to the early coffeehouses in Oxford England. These coffeehouses held an important association with the European Age of Enlightenment. For the price of a penny, scholars and laypeople alike would be given admittance to the coffeehouse, enjoy an endless supply of coffee, and more importantly enjoy learning through conversations with their peers. Thus these coffeehouses came to be called "Penny Universities".
Our new group, Penny University, serves as a modern take on this old tradition by connecting those who desire to learn with those who are willing to share what they know. This can certainly be at a coffeehouse, but anywhere else as well, including just a quick Google Hangout.
There are no mentors or mentees, leaders or followers. We are all peers and we are here to both learn and to teach.
Want to learn more? Click here