by Jamon Holmgren
RubyMotion ships with a built-in fork of MacBacon, which is itself a fork of Bacon, a small pure-Ruby RSpec clone.
RubyMotion's Bacon is fairly capable but not all that well documented. This is a cheat sheet to help bring to mind various strategies for testing RubyMotion apps and gems.
describe
and context
are literally aliases, but you usually use describe
to provide a description for the overall goal of a test, and context
to establish various scenarios you're running your tests.
describe "String description" do
# ...
end
describe UIViewController do
# ...
end
You can provide before
and after
blocks in any describe/context block.
describe PM::TableScreen do
context "with a nav_bar" do
before do
@screen = PM::TableScreen.new(nav_bar: true)
end
after do
@screen = nil
end
it "has a nav_bar" do
@screen.navigationController.should.be.kind_of(PM::NavigationController)
end
end
end
The basic assertation method of Bacon.
describe Hash do
it "is a hash instance" do
obj = {}
obj.should == {}
end
end
Bacon allows you to test the truthiness of a x?
method, such as .kind_of?
. Remove the question mark from the method to test it, like x.should.be.kind_of(Hash)
.
describe Hash do
it "is a hash instance" do
obj = {}
obj.should.be.kind_of(Hash)
end
end
These are mainly just syntactic sugar so you can write something like this:
describe Hash do
it "is a hash instance" do
obj = {}
obj.should.be.a.kind_of(Hash)
end
end
Tests that the opposite is true.
describe Hash do
it "is not an array" do
obj = {}
obj.should.not.be.kind_of(Array)
end
end
describe "Viper::SnakeCase" do
it "has a 'Viper::SnakeCase' module" do
should.not.raise(NameError) { Viper::SnakeCase }
end
end
You can mount a UIViewController into the simulator with the tests
class method. This will look for a controller
method (or provide its own if you don't).
Keep in mind these tests are very slow. Use unit tests (even for view controllers) where possible.
describe MyScreen do
tests MyScreen
def controller
@controller ||= MyScreen.new
end
after { @controller = nil }
it "has the right title" do
view("My Screen").should.be.kind_of(UILabel)
end
end
If you want to have your screen in a navigation controller, make sure your controller
method returns the navigationController.
describe MyScreen do
tests MyScreen
def screen
@screen ||= MyScreen.new(nav_bar: true) # ProMotion-style
end
def controller
screen.navigationController
end
after { @screen = nil }
it "has the right title" do
view("My Screen").should.be.kind_of(UILabel)
end
end
Taps a button on the screen.
describe MyScreen do
tests MyScreen
def controller
@controller ||= MyScreen.new
end
after { @controller = nil }
it "has a button" do
tap("Go Forth And Conquer")
view("Conquered!").should.be.present
end
end
Use wait_till
which keeps trying the block until it returns a truthy value, up to the timeout specified (defaulted to 3 seconds).
describe "HTTP call" do
it "returns a result" do
@ip = nil
AFMotion::JSON.get("http://ip.jsontest.com/") do |result|
@ip = result.object["ip"]
end
wait_till 20 { @ip.nil? == false }
@ip.should == "12.34.56.78"
end
end
Another way to approach this is to use wait_max
and the resume
command, which is what I recommend:
describe "HTTP call" do
it "returns a result" do
@ip = nil
AFMotion::JSON.get("http://ip.jsontest.com/") do |result|
result.should.be.success
@ip = result.object["ip"]
resume
end
wait_max 20 do
@ip.should == "12.34.56.78"
end
end
end
Visual regression testing. You get a lot of value with a small test.
gem "motion-juxtapose"
describe SettingsScreen do
tests SettingsScreen
it "looks like a SettingsScreen" do
views(UIView).length.should.be > 0 # Ensure views are loaded first
it_should_look_like "SettingsScreen", 4 # 4% "fuzz factor"
end
end
gem "motion-stump"
.stub!
will replace a method and return the result you specify. It doesn't care if it's called or not, though.
screen = MyScreen.new(arg: true)
screen.stub!(:foos, return: [])
screen.stub!(:bars, return: [ {}, {} ])
.mock!
is the same as .stub!
, but will fail the test if it's not called.
You can also pass a block to do more stuff, including assertations:
it "does a Google search" do
API::Client.mock!(:get) do |url, params|
url.should == "http://google.com"
params[:q].should == "Nickelback sucks"
resume
end
wait_max 20 {}
end
A good alternative to motion-stump (above). I haven't used it all that much, but its syntax is very pretty. It's fallen a bit out of date, however.
describe 'PersonController' do
extend Facon::SpecHelpers
before do
@konata = mock('konata', :id => 1, :name => 'Konata Izumi')
@kagami = mock('kagami', :id => 2, :name => 'Kagami Hiiragi')
end
it "should find all people" do
Person.should.receive(:find).with(:all).and_return([@konata, @kagami])
Person.find(:all).should == [@konata, @kagami]
end
end
Easily stub out HTTP responses in RubyMotion specs.
it "should allow you to navigate to a website" do
controller = ProMotion::WebScreen.new
stub_request(:get, "https://www.google.com/").
to_return(body: %q{Google! <form action="/search">%}, content_type: "text/html")
controller.mock!(:load_finished) do
controller.html.should.include('<form action="/search"')
resume
end
controller.open_url(NSURL.URLWithString("https://www.google.com/"))
wait_max 5 {}
end
Sometimes, you'll get very useless output from a failed test. Try changing your spec output style.
# In your Rakefile
ENV["output"] ||= "tap"