loading...

How do you like to unit test your code?

joshualjohnson profile image Joshua Johnson ・1 min read

I've always been stuck on how I like to unit test my code. Sometimes I like to isolate with mocks and test pure units. Sometimes I like to do full integration testing. Meaning that I will not mock any dependencies. Except for external library dependencies. At the end of the day, the integrated testing seems to be more valuable.

How do you like to unit test? What seems to be more valuable for you?

Discussion

pic
Editor guide
 

I adhere pretty strongly to the Test Pyramid, and approaching my code from a test-first perspective means that I avoid a lot of the difficult to mock scenarios that you'd typically encounter when approaching writing code from a different direction.

I still have a strong suite of Integration tests in my codebases, but as I climb the test pyramid they become broader in scope, and far fewer in number. There's no hard and fast rules about what I do and don't cover in an integration test. But typically I'll cover just what's needed to confirm that the integrated components play nicely together, and not much more, because their internal behaviour has be exercised well elsewhere.

A trivial example is if I've written 20 unit test cases for how an input field validates, I'll only exercise one or two cases of that validation in an integration test for the form that contains that field.

Extending this example, I likely won't test that validation at all in a browser based functional test.

One of the simpler reasons that I prefer to test things in isolation is the time it takes to diagnose the reason for a failure in a test. With a big 'ol integration test, a failure means that one of n things is broken. In a well isolated unit test a failure means one thing is broken.

I know that I've been burned before by two independent issues appearing in the same integration test, costing me far more time than I'd like to admit to correct.

 

How do I like to unit test?

I like to write unit tests on a "use case" level...

Normally I divide my code in modules that expose some kind of business functions like...

module Restaurant
  def self.get_menu(items_store)
  end

  def self.create_empty_order(orders_store)
  end

  def self.get_order(id, orders_store)
  end

  def self.get_orders(orders_store)
  end

  def self.add_item_to_order(order_id, item_id, items_store, order_items_store)
  end

  def self.update_order_item_quantity(order_item_id, quantity, order_items_store)
  end
end

To implement the expected behavior I normally will need to create some "internal" modules and clases... I don't write tests for those internal modules and clases, I try to always test through the interface of these modules. And I use mocks or doubles for everything that is passed as argument.

For example...

module Restaurant
  RSpec.describe "Get menu" do
    def item_with(attrs)
      ItemRecord.new(attrs)
    end

    def store_with(records)
      ItemsStore.new(records)
    end

    def get_menu(store)
      Restaurant.get_menu(store)
    end

    it "returns all the stored items" do
      store = store_with([item_with(name: "D1"), item_with(name: "D2")])
      items = get_menu(store)
      expect(items.count).to eq 2
    end

    describe "returns each item" do
      it "with name, price and description" do
        store = store_with([
          item_with(
            name: "D1",
            price: 110,
            description: "D1 desc"
          )
        ])

        item = get_menu(store).first
        expect(item.name).to eq "D1"
        expect(item.price).to eq 110
        expect(item.description).to eq "D1 desc"
      end

      it "knowing when is a dish" do
        store = store_with([item_with(category: "dishes")])
        item = get_menu(store).first
        expect(item).to be_dish
      end

      it "knowing when is not a dish" do
        store = store_with([item_with(category: "beverages")])
        item = get_menu(store).first
        expect(item).not_to be_dish
      end

      it "knowing when is a beverage" do
        store = store_with([item_with(category: "beverages")])
        item = get_menu(store).first
        expect(item).to be_beverage
      end

      it "knowing when is not a beverage" do
        store = store_with([item_with(category: "dishes")])
        item = get_menu(store).first
        expect(item).not_to be_beverage
      end
    end
  end
end

or...

module Restaurant
  RSpec.describe "Add item to order" do
    def order_with(attrs)
      OrderRecord.new(attrs)
    end

    def item_with(attrs)
      ItemRecord.new(attrs)
    end

    def order_item_with(attrs)
      OrderItemRecord.new(attrs)
    end

    def items_store_with(records)
      ItemsStore.new(records)
    end

    def order_items_store_with(records)
      OrderItemsStore.new(records)
    end

    def add_item_to_order(order_id, item_id, items_store, order_items_store)
      Restaurant.add_item_to_order(order_id, item_id, items_store, order_items_store)
    end

    attr_reader :order, :item, :items_store

    before do
      @order = order_with(id: 1)
      @item = item_with(name: "D1", price: 110, description: "D1 desc")
      @items_store = items_store_with([item])
    end

    it "creates an order item record" do
      order_items_store = order_items_store_with([])

      expect(order_items_store).
        to receive(:create).
        with(order_id: order.id, item_id: item.id, quantity: 1, name: "D1", price: 110)

      add_item_to_order(order.id, item.id, items_store, order_items_store)
    end

    it "updates the order item record, when an item is added more than once" do
      order_item = order_item_with(id: 1234, order_id: order.id, item_id: item.id, quantity: 1, price: 110)
      order_items_store = order_items_store_with([order_item])

      expect(order_items_store).
        to receive(:update).
        with(order_item.id, quantity: 2)

      add_item_to_order(order.id, item.id, items_store, order_items_store)
    end
  end
end

What seems to be more valuable for me?

I think what I value most is...

  • Fast tests, to know that the system is working after every change
  • Be able to make changes fast
  • Have a place to clearly see the expected behavior
  • Be able to remove code without regrets
 

It depends...
It depends on whether it's a single class \ method that does a single thing, such as calculations or validations, or you need to validate a process \ workflow.

I think integration testing is to validate that the overall behaviour of an aspect of your code works as you think it should; thus an integration of classes, packages and libraries.

I think it's better to use as much real code as possible and rely as little as possible on mocks.

 

Love your explanation. I feel the same way about testing. I much rather prefer not to mock at all. Unless you rely on an environmental dependency that HAS to be mocked.

 

Thanks!
Mocking stuff can be a real pain and takes a lot of time to work out if everything needed for the test is available (mocked).

Lol! Takes more work sometimes to do that than to just run it fully integrated.

 

I'm a huge fan of Test Driven Development, I've been working with Scott Hanselman's setup

So once I've done some experimenting and I know how I think the shape of my class/service/api will look, I define the interface, then I enter a test driven inner loop which consists of:

  1. Write test for a method (Red)
  2. Code the method until pass (Green)
  3. Refactor until happy (Refactor)
  4. Commit.

The same loop applies when I'm fixing bugs, Step 1 is to write an automated test that replicates the bug, then continues on as normal. Now with the setup described above, all of the building and testing happens automatically while I'm typing. so I'm writing code and red and green are just blinking, until I get to a point where I've got nothing but green.

I haven't managed to get this setup working slick testing parts of the app like controllers, but I'm still new at testing against views, so I imagine as I get more comfortable with it, I can get into a nice system.

I mock services to test business logic at the unit level, and use an in memory database to integration test my services both with and without business logic.

 

Here's how TDD is helpful.

I sometimes write code and when I want to test it later I find that testing it can be hard. I end up refactoring to get nice, small methods to test. When I do write the tests upfront I feel like the code ends up much more user friendly.

But sometimes we don't do TDD and follow this practice:

Instead, our process is "pull requests' need accompanying test in order to be merged". So, how the developer goes about it is up to them so long as they exist during the pull request.

The newer software followed this methodology from day 1, and it's been far less buggy and stayed cleaner than other teams who decided to not adopt this methodology (until the data presented itself and they were folded into this working standard)

 

github.com/ua1-labs/firebug/blob/m... is an example of a test I just wrote. I'm testing functionality rather than testing for specific methods.