The Ultimate N00b’s Guide to Testing
Novice programmers don’t yet have the skills to write simple code.
- Sandi Metz, Practical Object-Oriented Design in Ruby, Chapter 9
I first encountered testing in September of 2015 at Ruby DCamp. A few months prior, I had learned Ruby in my free time–enough to build a library adventure game, but not much beyond that. Prior to that, I didn’t even know that this programming language existed.
At Ruby DCamp, I was in the woods with over 70 Rubyists including industry veterans and coding school alum, and evidently the most novice person there.
During the first day of camp, we convened as a group only to break off into pairing sessions that required we write tests for “The Game of Life” (not the board game). I didn’t even know what pair programming entailed, or what was the purpose of two people huddling over one laptop, much less know what to do when I was expected to be the “driver.” We paired with at least six different people and I remember freezing up when asked, “Oh, do you prefer Minitest or RSpec?” Panic-stricken, I did my best to convey to my pairing partners exactly how new to coding I was.
Here are some key takeaways from Metz’s chapter on testing. This is everything that I wish I knew about testing. Given that there is much I don’t know, subsequent blog posts will focus on the nuances of testing.
Metz asserts that writing changeable code requires three core skills: an understanding of object-oriented design principles, the ability to refactor, and knowledge of how to write tests. I imagine that, objectively speaking, these are the golden rules to writing high-quality and clean code. Frankly, while other Ruby DCampers had cultivated such a warm and welcoming environment, I did not grasp the purpose behind writing tests until recently. It wasn’t until after my coding school days that I practiced with Mocha and RSpec, and saw the value of writing tests as a way to affirm a function’s objective and output. Tests have the benefit of reducing bugs, serving as documentation for others to understand the code’s purpose, and most importantly, reducing costs.
Writing Test-First Code
During the process of creating my tic-tac-toe game, I first determined the methods required for each class. Then I wrote tests to support the utility of those methods. However, after being encouraged to pursue true test-driven development, I did so. It was very slow-moving at first, but helped me tackle this relatively complex game by hacking at bits and pieces of it. But there were questions that lingered: how many tests should I write? Is there a ratio of method to tests? What is worth testing?
Had I read the final chapter of POODR earlier, I would have learned the value of writing fewer tests. “Test everything just once and in the proper place,” Metz says. Admittedly, I wish I had. Not only did I duplicate many of my tests, I also coupled objects in such a way that there was a dependency of one object as an argument of another. When stated, it makes absolute sense: tests should focus on the incoming or outgoing messages that cross an object’s boundaries. After all, it is in these instances that a function somehow changes or morphs the message. To me, being able to test what you receive as an output against your expectations is at the heart of testing. But according to Metz, it is a little more nuanced than that. Incoming and outgoing messages should be tested for different things; the former should be tested for their return state and the latter should be tested that they get sent.
An Overview
This is everything that I wish I knew about testing. Because there is still so much that I don’t know, subsequent blog posts will focus on the nuances of testing.
- Styles of testing: there is test driven development (TDD) and behavior driven development (BDD). Both encourage test-first approaches, but TDD progresses “inside-out” and starts with tests of domain objects whereas BDD works “outside-in” with objects at the boundary.
- Frameworks: as previously mentioned, there is MiniTest and RSpec. My only experience is with the latter, but Metz uses the former in her examples. The only difference would be a matter of personal preference; this opinion piece details how the main differentiator is the user interface. In addition to the formatting and readability of RSpec, I also learned about other features such as being able to run just one test, color formatting, and ability to sequentially or randomly run tests. I dig it! (Although, to be fair, it seems like MiniTest is more modular).
- What to test: the application’s objects should fall into one of two categories 1) an object that you’re testing, or 2) everything else. Your tests should know about the first category, but not much beyond that. Even then, the tests should not have complete access to all of the object’s private methods; they should know only about and test incoming and outgoing messages. If you take at the example below, `get_empty_cells` is an incoming message that `Board` responds to and is, in turn, sent by `ComputerPlayer`.
class Board
def initialize
...
end def get_empty_cells
...
end
endclass ComputerPlayer
def make move
empty_cells = board.get_empty_cells
end
end
- What not to test: tests should ignore messages sent to `self` (considered private methods) and outgoing query messages (which have no side effects and in which no other objects depend on its execution).
Test Doubles
A test double is an instance of a fake object that is used for testing. It is said that a double stubs another method, which essentially implements a version of that method by returning a canned answer. This is particularly useful for creating testable conditions, such as producing the same outcome for testing and reducing the need to invoke and initialize other functions.
There are multiple words associated with doubles. After reading POODR, I investigated further to understand the distinction between the terms. “Fakes” refer to a class that implements an interface but has no logic. It will return a value that is considered good or bad data and is very straightforward. “Mocks” test behavior and operate by defining an expectation that a message will get sent. Their only role is to prove messages get sent. And lastly, “stubs” return a certain value (e.g. boolean) that will be passed in to avoid any setup. They are similar to mocks, but cannot verify whether methods have been called. And then there are “spies” which serves as a sort of flag, to ensure that a method was called. It can record other things such as the number of invocations, a list of arguments passed in, or to see the inner working of an algorithm.
I won’t front: writing tests can be hard. When I did manage to do TDD and go through the red-green-refactor mode with relative ease, I got into the flow of TDD’ing. But then, there were times when I was sure testing would not be plausible. It’s hard to not just write methods that accomplish things first and foremost. It almost feels non-intuitive–like, shouldn’t you write a method first and then test it to ensure its robustness afterward? But alas, I am turning back the clock and re-working through various parts of my tic-tac-toe game. I look forward to reflecting on various design decisions and doing true TDD all the way through. Slow and steady does it.