OK, we are now finally ready for the next level of complexity in a Shoes app!

Shoes.app do
  puts 'Hello World'
end

The app method gets defined for the Shoes class right off the bat. It accepts options and a block to (initialize) and is the first subject we assign for testing. Defining a subject like this is nice for when we'd like to test the same object a few different ways or test several attributes on the same object. In this case we passed to the subject method an optional parameter :app so that we would have a name to call our subject by in addition to the default subject.

Before moving on to testing this subject, we politely ask RSpec to unregister all apps after each test. This is important because during the initialize, the app gets registered with a master list that knows about all the Shoes apps that are running via the registration module (more on this later) which gets pulled in by DSL.

And it is this DSL module that we spec out in great detail here which pulls in all of the dsl tests. We'll save those for a different tutorial.

These DSL tests and the next few lines:

it { is_expected.to respond_to :clipboard }
it { is_expected.to respond_to :clipboard= }
it { is_expected.to respond_to :owner }

use the it syntax where the previously assigned subject is assumed to be the object that is expected to respond to "#a_method". This syntax is made available by the rspec/its require we did back in spec_helper.

Before moving on to spec'ing out individual methods (like initialize), we check that it has the app method which Shoes 3 provided as an alias for self like so

it "exposes self as #app" do
  expect(app.app).to eq(app)
end

This test demonstrates a core idea in RSpec:

  • describe an expected behavior in words
  • write a test using the expect syntax
  • keep it simple

Ideally in test-driven development, we would first write a simple test like this down. Then we'd go back to the codebase and implement the expected behavior.

Looking back to the App class, we can see that one of the first things the author did in Shoes::App was define this alias.

Initialize

With some basic app tests walked through, we're now ready to dig into individual methods that a Shoes::App should have. To do this here, we decided to use a new describe block. In RSpec, we use describe and context to organize the code.

For example, an App may be initialized with no options or with a bunch of options passed in to the opts hash. So we want to describe how the App's initialize should behave in both contexts.

In the defaults context, we first test to see if Shoes::App is getting its properties from the internal app. So there are two questions I want to answer about this: why do we do this, and how do we do it?

Why

We separate App from internal app because App is the "user-facing app". As we'll see later, a part of the magic of Shoes is that whatever you put into the do end block gets evaluated in the context of Shoes::App. So let's say for example that instead of using an internal app like we've done here, we put it directly on the app:

class Shoes
  class App
    attr_accessor :width
  end
end

Then what would happen if a user used Shoes like this:

Shoes.app do
  width = 50
  rect 0, 0, width
end

Instead of declaring a variable called width, that user would have just set the width of the app itself to 50!

We would much prefer if the context in this block was such that the user could write any kind of variable they want for their own program and not worry about what we did behind the scenes.

How

We accomplish this little trick firstly by passing the block and opts onto a new class called InternalApp during the initialize. It's over there that we'll see how the block gets called, and the attributes get set up from the opts.

The next thing we do is use def_delegators (from the Forwardable module) to give App a list of methods to pass on to @__app__ instead of responding to them directly. To get an idea of what this does, check out the following app

Shoes.app do
  width = 50
  rect 0, 0, width
  alert width
  alert self.width
end

Since the method width= isn't delegated, a local variable called width gets set and used in the first two lines. But the app still holds onto its own width and that can be accessed via self.width. That's why specs looking at width, height, etc are checked against a constant that InternalApp holds onto instead of App.

Inspect

The last thing we look at in this context is what inspect returns. One of the purposes of Shoes is to be newcomer friendly and one way to do that is to get nice feedback from to_s, and inspect. Therefore we include the Common::Inspect module and test its output with the known default title.

For this module we've included some empty methods to be overwritten like inspect_details and to_s_details. They are implemented in Shoes::App right here. And you can see that for to_s we expect to get back the title of the app and for inspect we want the title and an object-id (note that the method shoes_object_id_pattern is provided by the InspectHelpers module).

From Opts

This section does what's expected, it checks out that the passed in opts are actually being returned from InternalApp, but then it looks at making sure things get set up correctly. Namely, it verifies that InternalApp receives an App and opts when getting initialized with a call to new. A similar verification checks to see that the main Flow of the app gets set up correctly.

This kind of expectation used in these two tests is different than the ones we've seen so far because the code that's being checked actually happens on the following line with the call to subject. When you ask RSPect to expect an object to receive a method call, you are stubbing out that method so that it doesn't actually get called, plugging and_call_original on the end makes sure that the actual method does get called. So the idea is that if you want to watch an object (not an instance of an object) for certain calls, you should mock it by stubbing out the method you want to call, and then pass the work back on to the actual object so that we're testing the actual codebase as much as possible. Here's a little more info on the topic.

When Registering

The last thing to check during the initialize is registration. As discussed a bit ago, Shoes contains a list of all apps that are running so during initialization of the app this app needs to be added to the list. Now, if you scroll to the end of the specs, you'll see that there are more registration specs there. The purpose for splitting registration up like this, is that here we are still focused on initialization. That means we want to focus on the work that gets done by initialization here and nothing more; however, since registration is just complex enough to deserve its own module, we also want to spec it out separately. Hence, we give it its own section at the bottom of the specs.

Styles

The Style module will need its own tutorial, so I won't go deeply into what is being tested here, and these styles actually belong to the InternalApp so you won't see much in app.rb about this. But it is nevertheless important to make sure these things are all available from the Shoes.app block. So I will just make a few points for these tests

  • These tests are testing the style method which is avaialble in the block thanks to DSL
  • We ensure that any styles set for one app are not also set in other apps
  • We check that styles can be set and passed on to objects inside an app.

So please take a look at these and hold off detailed questions for when we look at the style method of DSL and the Style module.

GUI

Next we check that @__app__ (the InternalApp) can talk to the gui (the backend) and perform some basic functions.

In the first test, we want to make sure that when the app is asked to do something with clipboard that call gets passed back to the gui. Here's how that works: clipboard is a method provided by the DSL, which directs a call to @__app__. A quick look inside InternalApp shows that clipboard is forwarded on to gui as expected.

The pattern of putting work that the gui should do (clipboard, quitting, fullscreen etc) on the backend is a common pattern in Shoes design. And it's an important distinction to pay attention to throughout these lessons because its not always so clearly cut! Even here we're essentially testing gui methods from the frontend. Getting these two things clearly separated is a primary goal of excellent shoes development.


Moving Forward

At this point, I'll stop explaining each test. Hopefully some of the guidance and explanation above makes it easier to understand how a lot of RSpec tests work, where to look for methods not found directly in app.rb, and how to navigate around. From here out, I'd like to focus on tests that look at code found directly in the app.rb file and only stray to explain new types of tests or large groups of related tests.

Additional Context

The next major section of the App and for its tests is the handling of contexts. Let's start again in the tests to see what we hope to get out of this "context" stuff.

The tests are telling us that we expect App to throw and error when given an unknown method call unless something else (an additional_context) is provided that might understand the call. To dig into why a tool like this was created, I searched on github for the method name (eval_with_additional_context) and found this pull request on the matter. So this method is provided in app so that Shape can use it here;

Now you can reread that code and the tests with the idea in mind that Shape is evaluating self as the context so that methods which Shape understands (like arc_to) are available inside of the Shape do block.

It's also useful to point out the use of double here.

We don't want to actually create an execution context with an asdf method, so instead, we create a test double called context and tell RSpec that we want it to respond to the asdf method by returning nil

Subscribing to DSL Methods

The last thing we need to cover in this tour through Shoes::App is DSL method subscription. As before, we want to read the specs and look at how it's used before looking at how a solution was implemented.

For this test, we want to make sure objects that "subscribe" to the DSL have certain behaviors. So instad of testing those objects (URL, Widget) we create a test class. The idea is that we want to separate the idea of subscription from the implementation. Test classes like these are great for that!

The tests tell us that an object which has subscribed to DSL methods should have access to a specific sub-list of these methods and pass them on to the app for handling. It also provides a way that a new method can be added to the dsl (i.e. through a widget) and the subscription will still work (so one widget is available from another).

The problem is solved by creating a class method on Shoes::App which first extends Forwardable to the class and then uses our old friend def_delegators to forward a list of methods to the app. In addition, a list of classes that want a DSL subscription is kept so that any methods added in the future can get the same Forwardable treatment. Nice!

Wrap-up

So we took a quick tour through app. We saw that the heavy lifting is spread between App and InternalApp, along with DSL and some Common modules. We also learned a bit about how testing with RSpec works and some good examples of well-written tests. I think the next place to go, while App is fresh in mind would be InternalApp and DSL. These two classes will continue the theme of gently maneuvering through some of the tricky spots that make Shoes tough to build, but easy to use.