It's day two of my apprenticeship at 8th Light, and directly after being assigned a video on testing, I've been assigned one on boundaries.
My mission: Listen to Gary Bernhardt take notes, and be less terrible.
Fine, OK, that's just my insecurity chiming in. It is my second day. I'm still worried this is all some wild dream, that any minute they'll discover they've somehow made a mistake. But enough of me psyching myself out. I have some boundaries to establish.
I was having a lot of trouble with the tests on my tic-tac-toe game. I had written it for the command line and there were numerous problems that arose in testing regarding the print statement.
I need the print statement. I can't get rid of it. It's how the system communicates state to the user. But those print statements kept getting in the way of my tests. They'd show up among the elegant green dots, marring the display, or cause other kinds of trouble, especially when testing whether they themselves worked.
So Dave Moore, my mentor, showed me this talk by Gary Bernhardt, and there were a few things that really clicked with me.
One of the hacky solutions I came up with for testing my print statements was to create a bunch of stubs, test doubles that overrode the print statements and made them simply return a value to be tested. The problem was that I was testing stubs, not the real thing. That meant that the tests could return false results. There were bugs that could escape into production, or I could waste time fixing the tests when no production code was actually broken.
There were a couple of key points I pulled from the video to help here, and the two I'll talk about in this post can be exemplified in the philosophy behind Unix.
Bernhardt talks a lot about letting values be your boundaries, and that's certainly true in the Unix Pipeline. The idea is that each shell function doesn't require any special conditions to operate.
touch doesn't care what argument you pass it, and it doesn't print anything unless something goes wrong. It accepts an argument and returns a command. The power comes with the Unix pipeline, which allows you to pass the return value of one function to the next allowing you to create complex transformations out of very simple, decoupled individual functions. Here is a clear example of letting the value being passed be the boundary. If you're using Mac or Linux its how your machine functions.
Another way of checking if a boundary is needed is to say out loud what the class or function does. Where you use the word "and" is where a new division needs to be.
By coding software this way, it makes tests much easier. They don't need any mocks or stubs because data in is your setup, and data out is your result. The tests run fast and can drive the development of clean code easily.
But what to do about that flingin-flangin print statement!?
Another key idea is the Core and Shell pattern.
I was under the impression that code was code and that's all there is. But in fact, you can divide code into two kinds. A core, which handles decision making and data transformations, and a shell, which integrates with dependencies.
The core is where you keep all of your decision making. All of the code is discrete and encapsulated making it easy to change, and fast to test.
The Shell is where that print statement goes. Which might not even need a test, but can be covered with a stripped down integration test, which again takes a lot less time than trying to test the integration with all of the logical paths the core generates.
Next time I write a large project, I want to try this core-shell technique. I want to see if it eases my testing pain.
But besides making me chomp at the bit to start diving into code some more, Between this and yesterday I've learned something important:
When your tests are becoming a pain, it's a symptom of poor design. In that case don't spend all of your time trying to make the test work. Modify the production code. If it's easy to test, it's easy to maintain.