Screen Tests

Until now we were testing applications manually: launching it in a simulator, opening screen that we are interested in, providing data that screen needs, and then waiting for a result. If the result is similar to what we expect, the application is working; otherwise, we need to fix something.

Manual testing is time-consuming, and in most cases you can't test everything. Another problem is that updating the code may bring new bugs in places where you don't expect them at all.

Automated tests are very helpful - you write them once, tell them what needs to be tested and what result would be expected, and then you use them whenever you want. They will test everything for you - how does the screen looks like, what app shows when API request failed, etc.

Another purpose of tests is to help you with code refactoring. When your app is ready, you may want to start refactoring your code: making it more readable, removing useless methods, changing libraries to a better ones, etc. And here tests will always help you to find what has been broken, what does not work as expect, and more.

It is very important to test application properly. And with tests I mean not a manual testing - we need to write code that will test all parts of our apps for us.

If possible, you should start writing tests right after creating a project - in this case, all your classes and objects will be adequately tested, and when the project is bigger it will save you a lot of time.

Tests are very easy to write. Rubymotion already has a /spec folder with one simple test. In this tutorial, we are going to write tests for a superapp's MoodSelectorScreen.

Practice

So, what is the idea of tests? Basically, you just use your own screens, models or other objects, and you compare its expected behavior with actual behavior. For example:

        q = 1
        q.should.be.equal(1)
        q.should.be.kind_of(Integer)
      

Usual test file structure looks like this:

        describe MoodSelectorScreen do
          context "user interface" do
            it "should do something" do
              # your test here
            end
          end
        end
      
Words after describe, context and it keyword can be changed to whatever you like - those are for you only. The will help you to understand in the future which test fails, and which passes. In this example your test will show something like:
        MoodSelectorScreen user interface should do something
      

Also, you can add some actions that will be run before or after each test. To do it, just create before, before_all, after or after_all methods that will have instructions that you want to run at a given time:

        describe MoodSelectorScreen do
          context "user interface" do
            before do
              Mood.delete_all
            end

            # rest of your tests
          end
        end
      

Before writing any tests, we need to tell our App Delegate not to open any screen during tests. This is suggested by a Rubymotion docs:

        def on_load(app, options={})
          return true if RUBYMOTION_ENV == 'test'
          # rest of the code
        end
      
Another step is to add helpful gem that will make output more readable into Gemfile:
        group :spec do
          gem 'motion-redgreen' , '1.0.0'
        end
      
And now update Rakefile a little bit:
        # -*- coding: utf-8 -*-
        $:.unshift("/Library/RubyMotion/lib")
        require 'motion/project/template/ios'

        begin
          require 'bundler'
          if ARGV.join(' ') =~ /spec/
            Bundler.require :default, :spec
          else
            Bundler.require
          end
        rescue LoadError
        end

        Motion::Project::App.setup do |app|
          # rest of Rakefile code ...

          # Tests setup
          if app.spec_mode
            app.redgreen_style = :full
            ENV['device_name'] = "iPhone 5s"
          end
        end
      
And don't forget to run bundle install.

Let's start writing our first test. First create file for it: spec/screens/mood_selector_screen_spec.rb. The first test will check whether MoodSelectorScreen has a label with "How do you feel?" text, and four buttons with correct texts.

        describe "Mood Selector Screen" do
          tests MoodSelectorScreen

          def screen
            window.rootViewController
          end

          context "User Interface" do
            it "should have all elements with right titles" do
              view("How do you feel?").should.be.kind_of(UILabel)

              view("Awesome").should.be.kind_of(UIButton)
              view("Good").should.be.kind_of(UIButton)
              view("Average").should.be.kind_of(UIButton)
              view("Bad").should.be.kind_of(UIButton)
            end
          end
        end
      

The first thing we need to do is to specify which screen we are going to test. To do it, we use tests class method that loads the screen for us.

Then you can see the screen method that return a current visible screen. In our case, it will be an instance of MoodSelectorScreen, that has been loaded with tests method earlier.

In our test, we try to get a view by its title with a view method. If it exists and its class is correct, then everything is fine. Otherwise, the test will return an error. To run the test use rake spec command:

MoodSelectorScreen tests have passed! Let's try to change label title to some other and execute the test again:

If you check tests output, you will see that default test generated by rubymotion is failing. This is because it expects the app to have one screen opened, but if you remember we denied the app to open anything in AppDelegate some minutes ago. You can fix the test by yourself by changing number of screens to zero, or you can remove the default test file ( spec/main_spec.rb )

Interactions

Now when we know how to test user interface, let's try to test interactions. Each time user presses on a button on the screen, it should create new mood object. However, if the screen could not get location yet, it should show an error.

Primary interaction type in iOS is a tap. To simulate it, you just need to use tap method and specify the button title.

Our next test will check if the screen shows an error when a location is not ready yet. If you remember, to create a Mood Object application needs to get forecast first. To do it, we need to get user's location. If location or forecast is not ready, the app will show an error. Let's make sure it works well:

        it "should show an error if a user creates new Mood without location ready" do
          tap("Awesome")
          view("Error").should.not.be == nil
          tap("OK")
          tap("OK")
        end
      

The test will pass, but unfortunately, we can't write a similar test for a successful Mood object creation at this moment. Tests won't wait for a simulator to get forecast from the server, and it won't wait for a geolocation.

To add a test for a successful Mood creation, we will need to fake our geolocation and forecasts data. We could do it in the app source code, but it is a really bad practice. Let's learn how we can do it in a smart way in the next chapter.

Summary

Tests are very important part of the application. It is a good idea to start writing them after creating your project to have everything properly tested.

Lots of interesting info on a MacBacon, default Rubymotion test framework you can find at Macbacon page

One of ProMotion developers has also created an excellent cheat sheet.

Book Index | Next