Monday, January 01, 2007

Use your test classes to identify smelly code

How can you tell if you have smelly code? Your test classes actually can tell you a lot about it. Here are a few unit test anti-patterns:

Big test class
Hopefully you have one unit test class per class/module/whatever-that-contains-behaviors you write code for. When your test class starts becoming unwieldily difficult to read and understand, your Class Under Test* is doing too much (i.e. God Object). So what is difficult to read? It is different for everyone. For me, when your unit test class stretches more than 4-5 page scrolls, it is starting to become too big. Think small, think manageable.

*By Class Under Test, I mean the class (e.g. class Laptop) that is to be tested by the test class (e.g. class LaptopTest < Test::Unit::TestCase).

Prefix test method names for categorization purposes
When you have to categorize your test methods in your test class by prefixing some text for grouping to improve readability, it is an indication your Class Under Test is doing too much:

class EBayChristmasShopperTest < Test::Unit::TestCase
  def test_browse_fetch_pricing_details_for_item...
  def test_browse_froogle_compare_item_prices...
  def test_browse_amazon_compare_item_prices...
  def test_auction_overbid_item...
  def test_auction_underbid_item...
  def test_payment_paypal_purchase_item...
  def test_payment_paypal_buy_it_now...
  def test_payment_credit_card_purchase_item...
  def test_payment_credit_card_one_click_checkout...

Instead of one EBayChristmasShopper object doing all of these, how about have Shopper to delegate some of these functionalities (browsing, auction, etc.) to other smaller objects (like ItemBrowser, Auctioneer)? This way you can write more well-intended unit tests for ItemBrowser and Auctioneer, and significantly simplify our EBayChristmasShopper class.

To me, one good way to categorize your unit test methods is to group them by the names of your public methods:

class IPodTest < Test::Unit::TestCase
  def test_prev_skips_to_previous_song...
  def test_prev_and_hold_rewinds_during_playback...
  def test_menu_displays_song_list_during_playback...
  def test_play_starts_playback...
  def test_play_pauses_playback_when_playing...
  def test_play_resumes_playback_when_paused...
  def test_next_skips_to_next_song...
  def test_next_and_hold_fast_forwards_during_playback...


class IPod
  def prev...
  def menu...
  def play...
  def next...
end

This way, each of your public methods can have a group of unit tests responsible for its behavior. In addition, it's easy to find out which method is missing tests (psssst....). But most importantly, you can easily see and logically group functionalities together, and refactor them into new classes should your Class Under Test becomes hideous to look at.

Mocks/Stubs frenzy
All too often, developers try to write code to make things work the way they want, at the expense of the complicating the fundamentals of object-oriented programming: object-to-object communication. Sure, the code works, but the tests tell you a different picture about how you get it to work. Take a look at the following example:

class DriverTest < Test::Unit::TestCase
  def test_drive_vehicle_forward_moves_appropriate_car_parts
    car = Car.new
    car.radio.expects(:off)
    car.clutch.expects(:shift_to_drive)
    car.acceleration_paddle.expects(:push)
    car.steering_wheel.expects(:turn).with(:straight)
    car.ignition.expects(:engine_on)
    car.timing_belt.expects(:run)
    car.muffler.expects(:stinks)
    car.battery.expects(:charging)


    driver = Driver.new(car)
    driver.driving_directions = 'FORWARD'
    driver.drive
  end
end

Did I lose you at the mock call for my car radio? Driving forward, in my mind, only needs half of those moving parts (ignition, clutch, steering wheel, acceleration). Others? I don't care (at least not when I only want to move the car forward as a driver). Sure the code works, but an overdosage of Mocks and Stubs is a good indicator that you are breaking encapsulation and/or coupling your class with too many other objects.

Long test methods
This indicates your Class Under Test requires a lot of set up in order to be useful, or your object is very coupled with the rest of the world (remember someone once said decoupling is good?), or your Class Under Test is very long and procedural. Remember the test_drive_vehicle_forward above?

Reduce the size of your test methods by re-thinking your object-object communication path. Maybe introducing an additional object to do all the dirty set up work would help decoupling the Class Under Test from the rest of the world immensely. Maybe this is a common object-coupling design problem that someone already has a design pattern to solve it succinctly, and all you need is google for it. There are always ways around it.

No comments: