Stub All The Things
March 13, 2016
Continuing with the testing theme, one very sharp tool in your testing toolbox is stubbing. Good stubbing allows you to test one specific piece of your code while easing the pain of setup and state configuration.
The three places I've used this the most are:
- External calls
- Logic outside of the current context
- Tricky setup / state configuration
Assumes you know:
- Basics of testing Rails apps
- The concept of stubbing
- How to use the mocha gem
Stubbing external calls
Stubbing external calls is a great idea for a couple reasons:
- HTTP requests are slow and will slow down your test suite considerably.
- Some services don't have test environments, so you may end up burning through your allotted bandwidth or creating bad data.
Bitly is a service a lot of companies use, and is our first example. Say you have a class that just takes in a URL, sends it to Bitly to shorten it, and returns the shortened URL:
# app/services/bitly_service.rb class BitlyService def self.shorten_url(url) return Bitly.shorten(url) end end
We don't want to actually send a call to Bitly every time we run our tests. So, we can stub the Bitly return:
# test/services/bitly_service_test.rb require 'test_helper' class BitlyServiceTest < ActiveSupport::TestCase def test_shorten_url Bitly.stubs(:shorten).returns("bit.ly/something") assert_match /bit.ly/, BitlyService.shorten_url("some_url") end end
This test is sort of trivial due to the simplicity of the method, but it does speed up our tests and would allow us to test any other logic in that method (if for example we added a
return if url.blank? to the beginning).
I know this works because in my sample project for this post I didn't even have to include the Bitly gem. I just needed a blank class to resolve the reference to the class:
# models/bitly.rb class Bitly end
Stubbing logic outside of the current context
Switching examples, let's say we have an
ActiveRecord class called
Classroom, and a classroom
has_many :students. Let's also assume we want a method that returns the name of the best student in a class:
# models/classroom.rb class Classroom < ActiveRecord::Base has_many :students def best_student_name student = Student.best_student_in_class(self) student.name if student.present? end end
Without stubbing, we would need to setup our fixtures in a way that we know which student was going to be returned as the "best" one. In some cases that may not be very hard, but let's say the "best" student is a combination of the highest grades, best attendance, most class participation, etc. In other words, more logic than we would want to set up and maintain through fixtures.
To keep this test focused on what the
Classroom object does with the result of the
best_student_in_class method and not how that method works, we can look at what we're expecting to get back, and stub the return as that. From the example above, we expect to get a
Student object back with a property or method of
Using mocha, we can stub the return of this method as an object that also stubs the method
We can also use the
with() keyword to tie specific arguments to specific results. This allows us to test the filled classroom and the empty classroom case:
# test/models/classroom_test.rb class ClassroomTest < ActiveSupport::TestCase def test_best_student populated_classroom = classrooms(:one) empty_classroom = classrooms(:two) Student.stubs(:best_student_in_class).with(populated_classroom).returns(stub(name: "jimmy")) Student.stubs(:best_student_in_class).with(empty_classroom).returns(nil) assert_equal "jimmy", populated_classroom.best_student_name assert_equal nil, empty_classroom.best_student_name end end
(It's neat that this works and my
Student class is empty. :))
Stubbing tricky setups
Next, let's say we want to make a
size method on
Classroom that returns small, medium, or large based on the number of students in the classroom:
# models/classroom.rb class Classroom < ActiveRecord::Base has_many :students ... def size number_of_students = self.students.count case number_of_students when 0..25 "small" when 26..50 "medium" else "large" end end end
The method is simple enough: based on the number of students, return a string.
Testing this for the small case is easy. Make a classroom fixture, a student fixture, and call the method:
assert_equal "small", classrooms(one).size.
Now for the medium/large case. Some options:
- Don't test it (bad)
- Use a junior dev to create 50 fixtures (fixture bloat and poor use of time)
- Create 50 objects in the test (slow and against best practices)
- Sweet sweet stubbing. (aw yisss)
Similar to the example above where we stubbed a student, and further stubbed a property on the student (
name), we can stub the
students association and the
# test/models/classroom_test.rb class ClassroomTest < ActiveSupport::TestCase def test_size classroom = classrooms(:one) classroom.stubs(:students).returns([1, 2]) assert_equal "small", classroom.size classroom.stubs(:students).returns(stub(count: 40)) assert_equal "medium", classroom.size classroom.stubs(:students).returns(stub(count: 100)) assert_equal "large", classroom.size end end
In the examples above, we stubbed the method right onto our fixture and called a method on that object later in the test. But sometimes you may not have access to the actual object you want to stub. It might be created or referenced in the middle of the test and done away with afterwards.
In those cases,
mocha comes with a handy method called
any_instance. Just as it sounds, any object created of that type will have the various stubbings applied to them.
Student.any_instance.stubs(:name).returns("bill"). With that line, all students will be named
From my personal experience, tons of
any_instance use is a code smell. This might mean that the code is not structured well. If it's difficult to test individual concerns, then it's a possibility that the code is all one mangled mess. Again not always, but sometimes.