In my opinion, writing unit tests is not about discovering bugs as much as it’s a tool for designing your code in a robust way. Unit tests are in principle small tests for nimble isolated parts of your code, such as a class or even just one method.

Writing unit tests is a design process

The unit test defines a recipe of how the isolated code is supposed to respond to the given input, and thereby it is a way of designing your application.

Defining the expected outcome of a class or method with a unit test is often a lot easier than actually implementing the code that should produce the outcome, therefore unit tests are cheaper to edit or even throw away than implemented code.

By following a Test-driven Development principle, you always write a failing test before you implement anything, and if those tests are good, you will maintain a high level of test coverage on all the different components that make up your awesome application. Bugs happen either because you got some input you didn’t expect, or because the different units (with green tests) does not work together as intended. These kinds of bugs are better discovered with automated integration tests, or manual testing.

Good vs. Bad unit tests

A little over a decade ago, Michael Feathers made a good list of some features a unit test should not include. He argued that a test is not a unit test if:

  1. It talks to the database
  2. It communicates across the network
  3. It touches the file system
  4. It can’t run correctly at the same time as any of your other unit tests
  5. You have to do special things to your environment (such as editing config files) to run it.

By following these, you get good unit tests, and you also benefit from having fast tests. Having a fast test suite is essential when following TDD.

I found some examples of RSpec model specs for a Rails application, on which I think that the principles for unit testing should apply.

describe User, '.active' do
  it 'returns only active users' do
    active_user = create(:user, active: true)
    non_active_user = create(:user, active: false)
    result = User.active
    expect(result).to eq [active_user]
  end
end

This first example tests a scope on the User class for finding only active users. It requires database access, and would fail in a weird edgecase where some other unit test create a user at the same time. While some might argue that testing scopes this way is a necessary evil, this is better tested with a integration test. We should assume that the scope functionality provided by ActiveRecord is properly tested already.

describe User, '#name' do
  it 'returns the concatenated first and last name' do
    user = build(:user, first_name: 'Josh', last_name: 'Steiner')
    expect(user.name).to eq 'Josh Steiner'
  end
end

This is a good example of a good unit test. Although the example is simple, it provides a clear description of how the #name method on an instantiated User should behave, and does not violate any of the five rules. If you want to be explicit, you should also write some test-code for how the method should behave if first_name and/or last_name is nil.