Skip to content

Commit

Permalink
make it more obvious that they are step by step tutorials
Browse files Browse the repository at this point in the history
  • Loading branch information
maccman committed Nov 29, 2011
1 parent bdd4a24 commit d03ef36
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 241 deletions.
4 changes: 2 additions & 2 deletions Gemfile
Expand Up @@ -8,13 +8,13 @@ gem 'rails', :git => 'git://github.com/rails/rails.git'
group :assets do
gem 'coffee-rails', :git => 'git://github.com/rails/coffee-rails.git'
gem 'uglifier'
gem 'stylus', :git => 'git://github.com/maccman/ruby-stylus.git'
gem 'stylus'
end

gem 'jquery-rails'
gem 'thin'
gem 'nestful', :git => 'git://github.com/maccman/nestful.git'
gem 'omniauth', :git => 'git://github.com/intridea/omniauth.git'
gem 'omniauth'
gem 'rdiscount'

group :development do
Expand Down
6 changes: 3 additions & 3 deletions app/views/docs/example.md
@@ -1,14 +1,14 @@
<% title 'Example Applications' %>
<% title 'Step by Step Tutorials' %>

Spine has a number of guides that take you step by step building example applications, as well as examples of real-world applications.

##Example guides
##Tutorials

1. If you're using the building your Spine applications in [pure JavaScript](<%= docs_path("started_js") %>), without using Node or Hem, then the [TodoList guide](<%= docs_path("example_tasks") %>) is the one for you. It takes you through building a basic CRUD application, including model persistence using HTML5 Local Storage.

1. Otherwise, if you're using [CoffeeScript and Hem](<%= docs_path("started") %>), you should checkout the [Contacts guide](<%= docs_path("example_contacts") %>), which will take you through all the steps necessary for building a fully fledged contacts manager, including CRUD, Stacks and search.

##Example apps
##Example applications

Once you're familiar with guides, you may want to browse some source code and see Spine in action. I find this is one of the best ways of learning a new language or framework, especially when it comes to best practices.

Expand Down
237 changes: 4 additions & 233 deletions app/views/docs/testing.md
@@ -1,237 +1,8 @@
<% title 'Testing Applications' %>
Hem includes support for running Jasmine specs out of the box.

Testing Spine applications is simple if focus is set on isolating the application logic from the framework plumbing code and restricting testing to the custom code that the DOM, Model or Custom events will trigger.
##Running tests

You shouldn't test the Spine framework itself, such as testing that Spine triggers model callbacks, how Spine serializes objects or how Spine formats and creates Ajax or HTML5 Storage requests from your models. Spine includes its own internal tests for these.
[http://localhost:9294/test](http://localhost:9294/test)

In this guide, we're going to use the [Jasmine](http://pivotal.github.com/jasmine/) testing framework, which is strongly recommended. However, the general concepts and methodology should be applicable to other browser-based Javascript testing frameworks, such as [QUnit](http://docs.jquery.com/Qunit).
##Adding specs

Avoiding side effects of tested function should be done by stubbing any code that isn't relevant to a specific test. To enable this, we're going to use Jasmine spying mechanism, as can be seen in later examples.

##Testing Models

###Persistence

It's often useful to disable Model persistence during testing, as it's often not relevant. You can do this by stubbing out Spine's persistence adapters:

Spine.Model.Local = {};
Spine.Model.Ajax = {};
----------

###Events

Testing for event handling should be done by unit testing the callback functions that are provided to the Model bind function. This will allow for some separation of the application logic from the framework and will make the code easier to test (including testing error handling and edge cases). Executing the callback functions as if the event they are binded to has been fired and then testing that they fulfill their expectations is the proper way to test event handling with Models.

Since most of the event binding on Models is done in the Controller, an example of testing Model event handlers it will be covered in the Controllers section.

###Iterating

We really want to test the callback evaluator function -

//= JavaScript
draw_first = function(con) {
canvas.draw(con.first_name); // canvas.draw is an imaginary API just for this example
}

spyOn(canvas, "draw");
var contact = Contact.init( { first_name: "Alex" } );
draw_first(contact);
expect(canvas, draw).toHaveBeenCalledWith("Alex");

The former way is preferable and concise over trying to test the entire iterating mechanism, since it's simply more verbose -

//= JavaScript
Contact.include({
draw_first: function(con){
canvas.draw(con.first_name);
});
var alex_contact = Contact.init({first_name: "Alex"});
var john_contact = Contact.init({first_name: "John"});
alex_contact.save();
john_contact.save();
spyOn(canvas, draw);
Contact.each(Contact.draw_first);
expect(canvas, draw).toHaveBeenCalledWith("Alex");
expect(canvas, draw).toHaveBeenCalledWith("John");

Now you can use the `is_friend` function safely with the select function. If you want to be really through, test your controller code so that it calls the Model's select method appropriately, supplying the is_friend method as the callback argument.

###Selecting

Same as with iterating -

//= JavaScript
is_friend = function(con) {
return con.friend;
}
var contact = Contact.init( { first_name: "John", friend: true } );
expect(is_friend(contact)).toBeTruthy();


###Validation

Validating that certain model fields exist or given in a certain form is done by providing a custom validation function.
The same logic from before applies here and to test validation we can unit test the validation function alone or rather instantiate the Model under test and provide values that should and should not pass validation.

If validation fails, an error event will be fired and you should test that validation errors are handled properly as you will test any other event - you can test the error handling function by triggering the error event on the model -

Or, again, by defining a error handling function as the handler instead of providing an anonymous function and then unit testing it -

When validation fails, calling the Model save and updateAttributes methods (most likely from some Controller code) will return false. You can then call the Model validation function again to get more information on what happened.
If you wish to simulate this situation, either try to save a Model with invalid fields -

//= JavaScript
var contact = Contact.init({first_name: "Some", phone: "Invalid Phone Number"});
contact.save()

Or stub the validation function using a Jasmine Spy -

//= JavaScript
var contact = Contact.init({first_name: "Some", last_name: "Name"});
spyOn(contact, "validate").andCallFake(function() { return 'Phone number validation error' });
contact.save();

This will trigger the error handling branch in your Controller code.

##Testing Controllers

###Initialization

The init method is called on instantiation and is the suitable place for binding any custom event handlers using the bind method. The binding of DOM events on the root element or it's children is done by specifying events and handlers in the events property. As such, testing a Controller's initialization phase will include instantiating the controller and then checking that both custom and DOM events were binded to handlers properly.

Spying on the Controller's bind method and setting expectations on the events we would like to handle and their handlers -

var ToggleView = Spine.Controller.Create({
init: function() {
this.bind("toggle", this.toggle);
},
toggle: function() { /* … */ },
bind: jasmine.createSpy('bindSpy');
});
var view = ToggleView.init();
expect(view.bind).toHaveBeenCalledWith("toggle");

In most cases you will want to test an existing Controller, and in that case you should -

ExistingToggleView.extend( { bind: jasmine.createSpy('bindSpy'); } )
var view = ToggleView.init();
expect(view.bind).toHaveBeenCalledWith("toggle");

###Events

Continuing with our approach, testing should be done by simulating the event by executing the handler function with appropriate arguments. Since Spine automatically sets the context of DOM event handlers, by default it is possible to use a controller instance properties from within the handlers.

Controller under test:

//= JavaScript
var TasksView = Spine.Controller.Create({
events: { "click .task" : "click" },
init: function() { Contact.bind( "create", this.proxy(this.notify) ); },
click: function(e) {
canvas.draw("Task for " + this.user + ": " + e.target.text()),
},
notify: function(item) {
canvas.draw("Task that is due on " + item.date + " created!");
},
user: current_user.get("name");
});

The test:

//= JavaScript
var view = TasksView.init();
spyOn(canvas, "draw");
var fake_event = { target: { text: function() { return "Learn about testing with Spine"; } };
view.user = "John";
view.click(fake_event);
expect(canvas.draw).toHaveBeenCalledWith("Task for John: Learn about testing with Spine");

Testing Model handlers is done in the same way -

The test:

//= JavaScript
var view = TasksView.init();
spyOn(canvas, "draw");
var expected_date = new Date();
var task = Task.init( { name: "Learn testing", date: expected_date } );
view.user = "John";
view.notify(task);
expect(canvas.draw).toHaveBeenCalledWith("Task that is due on " + expected_date + " created!");

If the handler relies on any instance property for it's operation, testing the handlers might require us to change/add properties to the controller instance that we setup during testing.

###Rendering

Rendering a view is a job best done using the [Render Pattern](http://maccman.github.com/spine/#s-patterns-the-render-pattern).

The rendering method will fetch all or part of a certain Model data, call a content genration method that will generate HTML from a static string or using a template engine and then set that method output (the generated HTML) as the content of controller's root DOM element.

The fact that this pattern is implemented using small functions doing only one thing makes testing easier, one can make it easier by moving the data fetching code into a separate method that will be used in the rendering method.

This can be seen with the following simple test case that tests the render method -

Controller under test:

//= JavaScript
var ContactsView = Spine.Controller.create({
init: function() {
Contact.bind( "refresh change", this.proxy(this.render) );
},
fetch: function() {
return Contact.all();
},
template: function(items) {
return ($("#contactsTemplate").tmpl(items));
} ,
render: function() {
this.el.html(this.template(this.fetch());
} ,
}

The test:

//= JavaScript
var fake_contacts = { Contact.init( { first_name: "John" } ), Contact.init( { first_name: "Alex" } ) } ;
ContactsView.extend( { template: jasmine.createSpy("templateSpy"),
fetch: jasmine.createSpy("fetchStub").andReturn(fake_contacts)
});
var view = ContactsView.init();
view.render();
expect(view.template).toHaveBeenCalledWith(fake_contacts);

Testing the HTML generation method (the template method in this example) will require adding a HTML fixture that will contain the template it self. This can be done manually or by using the [Jasmine jQuery](https://github.com/velesin/jasmine-jquery) plugin setFixtures method -

//= JavaScript
setFixtures( '<script id="contactsTemplate" type="text/x-jquery-tmpl"> <li> <span> ${first_name} </span> </li> </script>' );

And then testing that the template generates the correct HTML -

//= JavaScript
var view = ContactsView.init();
var contacts = { Contact.init( { first_name: "John" } ), Contact.init( { first_name: "Alex" } ) } ;
var content = view.template(contacts);
expect(content).toEqual( "<li> <span> John </span> </li> <li> <span> Alex </span> </li>" );

###Template Helpers

Helpers are simple functions to be used mostly by the template code in order to generate. This moves the logic into the controller or to helper code modules and doesn't couple it with the actual template.

Setting the helper object as a property of the controller object will make it more accessible to testing -

//= JavaScript
var ContactsView = Spine.Controller.create({
helper: { format: function(name) { name.toUpperCase(); } }
}

var view = ContactsView.init();
expect(view.helper.format('john')).toEqual('JOHN');

> *This guide was kindly contributed by [@asfkrs](http://twitter.com/asfkrs).*

0 comments on commit d03ef36

Please sign in to comment.