We had yet another wonderful lunch discussion involving Test Driven Development with Scott Burns, Chris Graffagnino, Chad Upjohn, Abby Fleming, Dani Adkins, Rainu Ittycheriah, and the one and the only John Berryman. This wasn't a low-level code syntax overview, but a general discussion involving great topics like:
- The (sometimes) unseen benefits
- Typical workflows
- Best practices
- When you shouldn't test
- and how to think through problems in a TDD way.
The (Sometimes) Unseen Benefits
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 (I might get that in writing) and move along. Having your codebase tested (at least somewhat) doesn't just indicate that you've thought your code through, it goes a bit deeper than that. If it's 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 darn confident that nothing will blow in production if all the tests are still passing.
Typical Workflows
Do you write the tests before? After? 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, the writing the code that makes that test pass, then do the refactor. 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 write the test first. This can usually happen with small bug fixes. If it's a feature, on the other hand, it feels impossible. What I typically do is start coding on what I'm trying to implement and write test placeholders along the way. 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.
Best Practices
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.
Mocking
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, of course, 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 get_subtotal.
import groceries # 3rd party lib
def get_subtotal(items):
total = 0
for item in items:
# Make API call
item = groceries.get(item)
total += item.price
return total
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 examples are 100% untested in real life, just basic concepts. Overall, we had a blast. A huge shoutout to John Berryman for everything he did to help out with this and make this meetup possible. If we end up having another discussion on TDD, it will definitely be recorded via google hangouts. I will ultimately turn this into a more fleshed out blog post. We're always here to help, so reach out if you have anything questions, thoughts, concerns, praises, blames, etc.
A few links that were referenced during our conversation:
(I will add more as I remember)