Nithin Bekal About

Notes / Rails Testing

My default testing stack:

  • Minitest, with the test 'something' do ... style
  • No fixtures (or at least limit them as much as possible)
  • spring to run tests quickly
  • vim-test for running tests in vim (I’ve mapped ,. to run tests)

Some testing patterns that I follow

Write the assertions first, and then work backwards. For example, when writing tests for products#show, start by writing what you expect.

class ProductsControllerTest < ActionDispatch::IntegrationTest
  test 'GET #show returns JSON product information' do
    # todo

    assert_response :success
    assert_equal 'Product 42', json['title']
    assert_equal 100,          json['price']
  end
end

In the above test, we don’t have the variable json yet, or even the get :show call that calls the method. but we can start filling in the details.

test 'GET #show returns JSON product information' do
  get :show, id: product.id
  json = JSON.parse(response.body)

  # expectations
end

Now that we have filled in the details within the test, we still don’t have the product variable, which we can add at the top of the test.

test 'GET #show returns JSON product information' do
  product = Product.create!(title: 'Product 42', price: 100)
  
  # ...
end

Use bang version of AR::Base#create in the tests. In case where validations fails, this immediately raises an exception, rather than letting invalid data to pass through.

In the above example, if the Product validation were to fail for some reason, Product#create would return an unpersisted instance of Product. As a result, we would get a routing error for products#show with id=nil, which tells us nothing about the actual cause of failure.

Take the case when you were to add a new validation to Product. Suddenly you see a lot of tests failing with routing error. It would make things a lot easier if you could know what vaildation is causing the failure. Instead, if we’re using #create!, we get an exception at that line, and we know immediately what to fix.

Use Enumerable#fetch. Same reason as above - this causes immediate exception rather than propagating nils through the test.

Leave a failing/pending test when you take a break. This is great advice from Kent Beck’s excellent TDD By Example. It gives you a starting point when you come back to the code, rather than having to think about what to do next.

Start with a failing test. Add something like assert false that will always fail if you are writing tests for existing code. Then, after seeing the failing test, you can remove that line and write the proper test.

Avoid instance variables in assert_equal Instance variables do not raise an exception when you have a typo. In the below example, if you typed @title inside the test, that would be nil, and if post.update always sets title to nil, this test would still pass.

setup do
  @new_title = 'New article title'
end

test 'post updates title' do
  post.update(title: @title)
  assert_equal @title, post.title
end

Avoid Hash#dig - use #fetch or #[]

Bookmarks

Rspec

Fixtures, factories, etc.

Testing views