This post continues on from the first one. That first post was a setup to getting the tools you needed. Here we’re going to put Sinatra through its paces and make sure we understand how it works so we can get on to creating an application.
As a reminder: one of the reasons I wanted to do this series of posts was because, as a tester, I often found myself wanting to write web applications, but without having to utilize massive frameworks that would take me a long time to learn before I could get anything working. As an example one app I would like to write is a simple test application that can be used by test logic I am building using libraries like Watir, Selenium, Capybara, whatever. Yes, I can find plenty of sites out there to do this with but I wanted my own so I could change it as need be. Second, I would like to write a web application that helps manage test specifications. (That’s a bit further in the future for me.)
Your ability to be a test engineer is broadened when you can write these applications yourself. So what I’m doing here is giving you a peek into how I learned just enough to be dangerous.
Handling HTTP Verbs – The Sinatra DSL
Given the details in the first post, you should be set up at this point. So first create a project directory (name it whatever you want) and in that directory create the file app.rb. In that file, put the following:
1 2 3 4 5 |
require 'sinatra' get '/' do "Test App" end |
You have to require the Sinatra gem. The line starting with ‘get’ is nothing more than a method. This particular type of method is called a handler by Sinatra because it handles routes and actions. The first part (get) states which HTTP method, or verb, is being handled. In this case, that would be the HTTP GET verb. This means you are handling the actin of ‘getting’ a page when it is requested from your server. The next part is a string that is the route, which I have here as ‘/’. That corresponds to the root URL of the application.
What you are seeing here is the Sinatra DSL syntax in action, which tends to be expressed like this:
verb 'route' do
So in my code, I’m instructing the application to respond to HTTP GET requests to the path ‘/’. The response I want to give is composed by the do block. That block contains the behavior that should occur when the route is followed.
This is pretty cool because what I did here is create a very simple web application that will run on a very simple web server. More technically, what I did is create a Sinatra application that has a single HTTP endpoint exposed as a route. Routes are the primary way that users interact with your application and thus the core of any Sinatra application is the ability to respond to one or more routes.
Sinatra applications respond to one or more routes to provide their functionality. The route is a URL and the block specifies what happens when the user visits the URL. In this case, I’m doing nothing more than returning a simple text string which will be rendered on the page. The last line of a handler’s code block is always what will be rendered by the browser and that’s important to understand.
To try it, execute the following command from the directory where you placed the file:
app.rb -p 9292
The default port is 4567. Here I’m specifying a different one. After Sinatra starts up, you can view this simply by going to your http://localhost:9292 URL. It’s not very exciting but it does work.
Now let’s say I want to make another route. Add this to your file:
1 2 3 |
get '/to_do' do "Test App - To Do Page" end |
To see this in action, you’ll have to kill the server and start it up again. That’s pretty annoying, right? Go ahead and kill the server but before starting it up again let’s fix it so that you don’t have to start and stop the server each time you make a change. That’s where the reloader that we installed comes into play. Change your script so that it looks like this:
1 2 3 |
require 'sinatra' require 'sinatra/reloader' ... |
Now you can make changes to your routes and view them without having to do a server restart.
With the above changes in place, start up your server, and then go to http://localhost:9292 and http://localhost:9292/to_do. In each case, you will see the text that was specified as part of the route.
Serving Static and Dynamic Content
You’re not bound to route creation as a way just to deliver simple text messages. Let’s serve up an HTML page. Add this route to the script:
1 2 3 |
get '/index.html' do "Test App - Index Page - From Route" end |
You can access this by going to http://localhost:9292/index.html. Notice here that even though I’ve listed a page as part of the route, there is no actual page being called up. All I said here is that there is a route called “index.html” — and that by itself means nothing to Sinatra, any more than if I had said the route was called “index.jeff”.
So when going to this route and thus this URL, you’re just going to get the string. An actual page like index.html is a static resource. Sinatra will serve static resources from a public folder if it exists. So let’s create a folder called public within your project and create a file called index.html in it. You can put whatever you want in there. Here’s what I did:
1 2 3 4 5 6 7 8 9 10 11 |
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Test App</title> </head> <body> <h1>Test App</h1> <p>Index Page - From Public</p> </body> </html> |
Go to http://localhost:9292/index.html again and you’ll see the contents of the page. Wait. I just said that “index.html” was nothing special to Sinatra. Well, in fact, that’s sort of true. What happens here is that Sinatra will look for a file in your public directory. If a file is found that matches the route, that file will be displayed.
I had mentioned that I could have used “index.jeff”. Had I made my route that and put a file called “index.jeff” in my public folder, when I went to http://localhost:9292/index.jeff, Sinatra would attempt to render index.jeff in the browser. Since “.jeff” is not a recognized extension, the browser will most likely try to download the file.
Okay so index.html is a static resource. And even though the route specifies certain text to be displayed, the static resource overrides that since it is now present. That’s another important thing to understand. Incidentally, you’ll notice that the “public” folder is omitted from the URL.
Now let’s serve up a page that has dynamic content. This means that it’s no longer a static resource. In Sinatra this would be called a view. Views in Sinatra are HTML templates that can optionally contain data passed from the application. There are numerous template engines available: ERB, Haml, Markaby, Slim and plenty more. You can do what are called inline views or external views. I never bother with inline — look them up if you’re curious — so I’m just going to go external.
First, add a new route to your script:
1 2 3 |
get '/index' do erb :index end |
What I’ve done here is tell Sinatra that when the “index” route is visited, the ERB template engine should be started and it should call up a resource called “index”.
To get this to work, create a views directory within your project directory. In that new directory, create a file called index.erb. Notice that in the route we’re not specifying “index.html” or even “index.erb”. The reason for this is because the template engine you use will supply the extension for you. Here is what you can use for the content of the index.erb file:
1 2 3 4 5 6 7 8 9 10 11 |
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Test App</title> </head> <body> <h1>Test App</h1> <p>Index Page - From Views</p> </body> </html> |
Looks remarkably like what you put in the HTML file, no? ERB is a templating engine but it will output pure HTML. Where ERB becomes powerful is when you put dynamic content in place. First make sure you can render the view by going to http://localhost:9292/index.
Even though I have a dynamic content file, I still have a static resource in that nothing dynamic happens! Let’s do the most simple thing: have the page display the current time. Add the following to index.erb:
1 2 3 4 5 |
... <h1>Test App</h1> <p>Index Page - From Views</p> <p>Time is: <%= Time.now %></p> ... |
Now if you view the page you will see that it displays the current time. (Audience goes: ooooo! aaaaah!) Okay, I’ll grant it’s not much — but it is dynamic content. The point here is that you can use a dynamic templating language to allow you to create an application that responds in various ways. The reason I’m not focusing on ERB too much is because I’m not going to use it for long.
If you want to clean up at this point, you can delete the index.html file from your public folder. (Keep the folder, though. We’ll use it again.) Then clean up your app.rb file so it looks like this:
1 2 3 4 5 6 |
require 'sinatra' require 'sinatra/reloader' get '/' do erb :index end |
Note a change there! The root url — “/” — is now the index rather than using “/index”. This means you can just go to http://localhost:9292/ for what we’re doing.
Getting Information from the App
Let’s now add a form that we can do something with. Put this HTML in the index.erb file:
1 2 3 4 5 6 |
... <form action="/" method="post"> <input type="text" name="message"> <input type="submit"> </form> ... |
The form has a post method and that will trigger the action of calling up the root url page again. POST is another HTTP verb, just as GET is. What that means is that we have to handle this post HTTP verb just as we did the get HTTP verb. However, to see what happens when you don’t have a route, call up your page and enter some text in the form, clicking the submit button.
Sinatra will fail and tell you that you need to add a route. So add the following to app.rb:
1 2 3 |
post '/' do "You said '#{params[:message]}'." end |
Now if you submit the form with some text, you will see that whatever you typed is displayed back for you. This works because you have now told Sinatra what to do when a post action occurs on the root url. The “params” thing is a Sinatra convention in that any arguments passed to a page request are held in a hash called params. The symbol :message refers to the name that you gave to the input field in the index.erb file. To see this in action, modify the string as such:
1 2 3 |
post '/' do "You said '#{params[:message]}' (Params is a #{params.class} with #{params.inspect})." end |
You can basically see what Sinatra is dealing with: a hash.
Stop and consider what you did here. You basically built a small web application and server that can respond to GET and POST requests. You did this in about ten lines of code in a single file. Everything else was just basic HTML.
This seems like a pretty good place to stop. In the next part, we’ll actually structure the application a little more and we will get it ready for deployment to a cloud-based solution.
Following along ( with a bit of pain getting stuff installed on my new Mac but a good learning experience ) and feeling a great sense of accomplishment 🙂
Brilliant tutorial, still relevant today as we have a legacy system at my new workplace built on Ruby, Sinatra, etc. This so neat and plainly laid out it makes it a breeze to understand. Kudos.