Łukasz Kazimierz Bandzarewicz

@lucassus

Backbone.js TDD With Jasmine Part Two: The Collection

In the previous part Backbone.js TDD with jasmine part one: The model we created a simple TodoList.Models.Task model for handling creating and updating a task. Code from the previous step can be downloaded from github repository: 004-complete-task-model@tdd-with-backbonejs

Now it’s time to create a TodoList.Collections.Tasks collection. Generally in backbone.js collections are ordered sets of models. In our application we will use this collection for fetching tasks from the serer (fetch method) and for creating new task (create method).

Backbone.js TDD With Jasmine Part One: The Model

Initial Ruby on Rails application

The initial rails application can be downloaded from github repo: 000-basic-app@tdd-with-backbonejs

The basic application provides model Task(name: string, complete: boolean) and corresponding controller with RESTFUL json interface:

  • GET /tasks.json
  • POST /tasks.json
  • PUT /tasks/:id.json

Don’t forget about rake db:create:all and rake db:migrate.
You could seed the database with initial tasks: rake db:seed.

Now you can run rails: rails s and navigate to http://localhost:3000.. and you should see nothing special, just an another todo list app without any fancy features and JavaScripts.

Gems used in the project

Gems included in development and test environments:

  • jasmine - Jasmine ruby gem
  • jasminerice - Pain free coffeescript testing under Rails 3.1
  • guard - Guard is a command line tool to easily handle events on file system modifications
  • guard-jasmine - Guard::Jasmine automatically tests your Jasmine specs on Rails when files are modified
Gemfile
1
2
3
4
5
6
7
8
9
10
11
12
gem 'twitter-bootstrap-rails'
gem 'jquery-rails'
gem 'underscore-rails'
gem 'backbone-on-rails'

group :development, :test do
  gem 'jasmine'
  gem 'jasminerice'

  gem 'guard'
  gem 'guard-jasmine'
end

Also in ./vendor/assets/javascripts we have:

  • Sinon.js - standalone test spies, stubs and mocks for JavaScript. No dependencies, works with any unit testing framework. Also it has very nice api for stubbing server responses.
  • jasmine-sinon - A collection of Jasmine matchers for Sinon.JS

Running the tests

Initial application already contains pre-configured Guardfile for jasmine. It can run JavaScript specs for our application without the browser!

Guardfile
1
2
3
4
5
6
group :frontend do
  guard 'jasmine', :phantomjs_bin => './spec/javascripts/support/phantomjs', :specdoc => :always, :console => :always do
    watch(%r{app/assets/javascripts/.+(js\.coffee|js)}) { "spec/javascripts" }
    watch(%r{spec/javascripts/.+(js\.coffee|js)}) { "spec/javascripts" }
  end
end

In order to execute our JavaScript tests just type in the console guard --group frontend wait for rails to boot and after several seconds you should see the following output:

tests results
1
2
3
4
5
6
7
8
9
$ guard --group frontend
Guard is now watching at '/home/lucassus/Projects/tdd-with-backbonejs'
Guard::Jasmine starts webrick test server on port 8888 in development environment.
Jasmine test runner is available at http://localhost:8888/jasmine
Run all Jasmine suites
Run Jasmine suite at http://localhost:8888/jasmine

0 specs, 0 failures
in 0.002 seconds

TIP 1: phantomjs in already included in ./spec/javascipt/support/phantomjs but you may have to compile it on your machine.

TIP 2: you can also see more detailed tests output in the browser, just navigate to http://localhost:8888/jasmine.

Step one: class TodoList.Models.Tasks should be defined and it can be instantiated

Create file ./spec/javascripts/models/task_spec.js with the following content:

spec/javascripts/models/task_spec.js
1
2
3
4
5
6
7
8
9
10
describe('TodoList.Models.Task', function() {
    it('should be defined', function() {
        expect(TodoList.Models.Task).toBeDefined();
    });

    it('can be instantiated', function() {
        var task = new TodoList.Models.Task();
        expect(task).not.toBeNull();
    });
});

Obviously this test will fail since:

  • We don’t have TodoList.Models namespace
  • and Task model is not defined within this namespace
  • required JavaScript files and dependencies are not loaded via assets pipeline
tests results
1
2
3
4
5
6
TodoList.Models.Task
  ✘ should be defined
    ➤ ReferenceError: Can't find variable: TodoList in models/task._spec.js on line 3
  ✘ can be instantiated
    ➤ ReferenceError: Can't find variable: TodoList in models/task._spec.js on line 7
ERROR: 2 specs, 2 failures

Create ./app/assets/javascripts/todo_list.js file with the following content. It will be the entry point for our application.

app/assets/javascripts/todo_list.js
1
2
3
4
5
6
7
8
9
10
11
//= require jquery
//= require jquery_ujs
//= require underscore
//= require backbone

//= require_self
//= require_tree ./models

window.TodoList = {
  Models: {}
};

In the first require section we require all necessary javascipt libraries: jQuery, underscore and finnaly Backbone.js Second section loads our application’s JavaScripts.

Add following content to the ./spec/javascripts/spec.js file:

spec/javascripts/spec.js
1
2
//= require todo_list
//= require_tree .

These directives will load our application along with all test and helper files defined in the ./spec/javascripts folder.

And finally define initial TodoList.Models.Task class:

app/assets/javascripts/models/task.js
1
TodoList.Models.Task = Backbone.Model.extend({});

It’s green!

tests results
1
2
3
4
5
TodoList.Models.Task
  ✔ should be defined
  ✔ can be instantiated
2 specs, 0 failures
in 0.031 seconds

Step two: the new instance should have default values for name and complete flag

spec/javascripts/models/task_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe('TodoList.Models.Task', function() {
    // ...

    beforeEach(function() {
        this.task = new TodoList.Models.Task();
    });

    describe('new instance default values', function() {
        it('has default value for the .name attribute', function() {
            expect(this.task.get('name')).toEqual('');
        });

        it('has default value for the .complete attribute', function() {
            expect(this.task.get('complete')).toBeFalsy();
        });
    });
});
tests results
1
2
3
4
5
TodoList.Models.Task
  new instance default values
    ✘ has default value for name
      ➤ Expected undefined to equal ''.
ERROR: 4 specs, 1 failure

Define default values for the model:

app/assets/javascripts/models/task.js
1
2
3
4
5
TodoList.Models.Task = Backbone.Model.extend({
    defaults: {
        name: ''
    }
});
tests results
1
2
3
4
5
6
7
TodoList.Models.Task
  new instance default values
    ✔ should be defined
    ✔ can be instantiated
    ✔ has default value for the .name attribute
    ✔ has default value for the .complete attribute
4 specs, 0 failures

It seems that we don’t have to define default value for the complete flag. It’s false by default.

Step three: define getters

Generally backbone.js for fetching attributes values has a build-in model.get(attribute) method, for instance model.get('name') or model.get('complete') but in my opinion this approach is prone to typos and other strange errors. To avoid this kind of problems in my backbone models I’m creating getters for all model’s attributes, for example the name attribute will have model.getName() method.

Lets create a simple test case for those methods. First of all create a model.getId() method:

spec/javascripts/models/task_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe('TodoList.Models.Task', function() {
    // ..

    describe('getters', function() {
        describe('#getId', function() {
            it('should be defined', function() {
                expect(this.task.getId).toBeDefined();
            });

            it('returns undefined if id is not defined', function() {
                expect(this.task.getId()).toBeUndefined();
            });

            it("otherwise returns model's id", function() {
                this.task.id = 66;
                expect(this.task.getId()).toEqual(66);
            });
        });
    });
});

Test will fail with the following messages:

tests results
1
2
3
4
5
6
7
8
9
10
TodoList.Models.Task
  getters
    #getId
      ✘ should be defined
        ➤ Expected undefined to be defined.
      ✘ returns undefined if id is not defined
        ➤ TypeError: Result of expression 'this.task.getId' [undefined] is not a function. in models/task._spec.js on line 32
      ✘ otherwise returns model's id
        ➤ TypeError: Result of expression 'this.task.getId' [undefined] is not a function. in models/task._spec.js on line 37
ERROR: 7 specs, 3 failures

Let’s add implementation for the missing method

app/assets/javascripts/models/task.js
1
2
3
4
5
6
7
TodoList.Models.Task = Backbone.Model.extend({
  // .. defaults: { }

  getId: function() {
    return this.id;
  }
});

..and it should be green again!

tests results
1
2
3
4
5
6
7
8
9
10
11
12
TodoList.Models.Task
  new instance default values
    ✔ should be defined
    ✔ can be instantiated
    ✔ has default value for name
    ✔ has default value for complete flag
  getters
    #getId
      ✔ should be defined
      ✔ returns undefined if id is not defined
      ✔ otherwise returns model's id
7 specs, 0 failures

Write some specs for model.getName() and model.getComplete() methods. In the following example I’m going to use sinon’s test stubs. In this case backbone’s get(attribute) method is stubbed and in the test I’m asserting that this method was called with valid attribute name.

TIP 1: don’t forget to require sinon.js in our spec helper, just add //= require sinon to the ./spec/javascript/spec.js file.

TIP 2: sinon should be required before our JavaScripts specs.

spec/javascripts/models/task_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
describe('TodoList.Models.Task', function() {
    // ..

    describe('getters', function() {
        // .. describe('#getId', function() {});

        describe('#getName', function() {
            it('should be defined', function() {
                expect(this.task.getName).toBeDefined();
            });

            it('returns value for the name attribute', function() {
                var stub = sinon.stub(this.task, 'get').returns('Task name');

                expect(this.task.getName()).toEqual('Task name');
                expect(stub.calledWith('name')).toBeTruthy();
            });
        });

        describe('#getComplete', function() {
            // TODO try do it by yourself
        });
    });
});

Obviously it will fail:

tests results
1
2
3
4
5
6
7
8
9
10
11
12
13
TodoList.Models.Task
  getters
    #getName
      ✘ should be defined
        ➤ Expected undefined to be defined.
      ✘ returns value for the name attribute
        ➤ TypeError: Result of expression 'this.task.getName' [undefined] is not a function. in models/task._spec.js on line 49
    #getComplete
      ✘ should be defined
        ➤ Expected undefined to be defined.
      ✘ returns value for the complete attribute
        ➤ TypeError: Result of expression 'this.task.getComplete' [undefined] is not a function. in models/task._spec.js on line 62
ERROR: 11 specs, 4 failures

The missing implementation would be very trivial:

app/assets/javascripts/models/task.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TodoList.Models.Task = Backbone.Model.extend({
  // ..

  getId: function() {
    return this.id;
  },

  getName: function() {
    return this.get('name');
  },

  getComplete: function() {
    return this.get('complete');
  }
});

Green again! Not it’s time for something less trivial.

Step four: creating and updating our model via ajax

For creating a new tasks and updating its complete flag we’ll use built-in in backbone save method. Let’s see whether this method meets all our requirements:

spec/javascripts/models/task_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
describe('TodoList.Models.Task', function() {
    // ..

    describe('#save', function() {
        beforeEach(function() {
            this.server = sinon.fakeServer.create();
        });

        afterEach(function() {
            this.server.restore();
        });

        it('sends valid data to the server', function() {
            this.task.save({name: 'A new task to do'});
            var request = this.server.requests[0];
            var params = JSON.parse(request.requestBody);

            expect(params.task).toBeDefined();
            expect(params.task.name).toEqual('A new task to do');
            expect(params.task.complete).toBeFalsy();
        });
    });
});
tests results
1
2
3
4
5
TodoList.Models.Task
  #save
    ✘ sends valid data to the server
      ➤ Error: A "url" property or function must be specified in backbone.js on line 1287
ERROR: 12 specs, 1 failure

It seems that our model hasn’t required url property. Basically url can be a property or a function and it returns the relative URL where the model’s resource would be located on the server. Let’s add this property with some arbitrary value:

app/assets/javascripts/models/task.js
1
2
3
4
5
6
7
TodoList.Models.Task = Backbone.Model.extend({
  // ..

  url: '/something'

  // ..
});

Now we have:

tests results
1
2
3
4
5
6
TodoList.Models.Task
  #save
    ✘ sends valid data to the server
      ➤ Expected undefined to be defined.
      ➤ TypeError: Result of expression 'params.task' [undefined] is not an object. in models/task._spec.js on line 84
ERROR: 12 specs, 2 failures

It seems that our tasks attributes are not wrapped within task property. In order to fix it we should override model’s toJSON method. In the backbone docs for this method we find the following description:

Return a copy of the model’s attributes for JSON stringification. This can be used for persistence, serialization, or for augmentation before being handed off to a view.

app/assets/javascripts/models/task.js
1
2
3
4
5
6
7
8
9
10
11
TodoList.Models.Task = Backbone.Model.extend({
  // ..

  url: '/something',

  toJSON: function() {
    return { task: this.attributes };
  }

  // ..
});

Green again but url attribute definitely is not what we want. For creating task it should be /tasks.json (along with POST request method) and for updating existing task’s attributes it should be /tasks/:id.json (along with PUT request method). Let write some specs for those scenarios:

Let’s override this method:

spec/javascripts/models/task_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
describe('TodoList.Models.Task', function() {
    // ..

    describe('#save', function() {
        // .. fakeServer

        // .. it('sends valid data to the server', function() { });

        describe('request', function() {

            describe('on create', function() {
                beforeEach(function() {
                    this.task.id = null;
                    this.task.save();
                    this.request = this.server.requests[0];
                });

                it('should be POST', function() {
                    expect(this.request.method).toEqual('POST');
                });

                it('should be async', function() {
                    expect(this.request.async).toBeTruthy();
                });

                it('should have valid url', function() {
                    expect(this.request.url).toEqual('/tasks.json');
                });
            });

            describe('on update', function() {
                beforeEach(function() {
                    this.task.id = 66;
                    this.task.save();
                    this.request = this.server.requests[0];
                });

                it('should be PUT', function() {
                    expect(this.request.method).toEqual('PUT');
                });

                it('should be async', function() {
                    expect(this.request.async).toBeTruthy();
                });

                it('should have valid url', function() {
                    expect(this.request.url).toEqual('/tasks/66.json');
                });
            });
        });
    });
});

It will fail since the url is not set correctly.

tests results
1
2
3
4
5
6
7
8
9
10
TodoList.Models.Task
  #save
    request
      on create
        ✘ should have valid url
          ➤ Expected '/something' to equal '/tasks.json'.
      on update
        ✘ should have valid url
          ➤ Expected '/something' to equal '/tasks/66.json'.
ERROR: 18 specs, 2 failures

Try to fix it with the following code snippet:

app/assets/javascripts/models/task.js
1
2
3
4
5
6
7
8
9
10
11
12
13
TodoList.Models.Task = Backbone.Model.extend({
  // ..

  url: function() {
    if (this.isNew()) {
      return '/tasks.json';
    } else {
      return '/tasks/' + this.getId() + '.json';
    }
  }

  // ..
});

Green again!

TIP: we could define custom jasmine matchers in order to make the test cases above more DRY.

Create spec/javascripts/support/request_matchers.js file with the following content:

spec/javascripts/support/request_matchers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
beforeEach(function() {
  this.addMatchers({
    toBeGET: function() {
      var actual = this.actual.method;
      return actual === 'GET';
    },
    toBePOST: function() {
      var actual = this.actual.method;
      return actual === 'POST';
    },
    toBePUT: function() {
      var actual = this.actual.method;
      return actual === 'PUT';
    },
    toHaveUrl: function(expected) {
      var actual = this.actual.url;
      this.message = function() {
        return "Expected request to have url " + expected + " but was " + actual
      };
      return actual === expected;
    },
    toBeAsync: function() {
      var actual = this.actual.async;
      return actual;
    }
  });
});

And now instead:

1
2
3
it('should be POST', function() {
  expect(this.request.method).toEqual('POST');
});

We could write:

1
2
3
it('should be POST', function() {
  expect(this.request).toBePOST();
});

TIP: Try to refactor other test scenarios.

We can also do one more step further and create the following macros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
window.itShouldBePOST = function() {
  it('should be POST', function() {
    expect(this.request).toBePOST();
  });
};

window.itShouldBePUT = function() {
  it('should be PUT', function() {
    expect(this.request).toBePUT();
  });
};

window.itShouldBeAsync = function() {
  it('should be async', function() {
    expect(this.request).toBeAsync();
  });
};

window.itShouldHaveUrl = function(url) {
  it('should have url ' + url, function() {
    expect(this.request).toHaveUrl(url);
  });
};

Now our test case is really DRY!

spec/javascripts/models/task_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
describe('TodoList.Models.Task', function() {
    // ..

    describe('#save', function() {
        // .. fakeServer

        // .. it('sends valid data to the server', function() { });

        describe('request', function() {
            beforeEach(function() {
                this.performRequest = function() {
                    this.task.save();
                    return this.server.requests[0];
                };
            });

            describe('on create', function() {
                beforeEach(function() {
                    this.task.id = null;
                    this.request = this.performRequest();
                });

                itShouldBePOST();
                itShouldBeAsync();
                itShouldHaveUrl('/tasks.json');
            });

            describe('on update', function() {
                beforeEach(function() {
                    this.task.id = 66;
                    this.request = this.performRequest();
                });

                itShouldBePUT();
                itShouldBeAsync();
                itShouldHaveUrl('/tasks/66.json');
            });
        });
    });
});

Our model is ready! Now you can open firebug or goggle-chrome console and test it:

1
2
3
4
5
var task = new TodoList.Models.Task();
task.set('name', 'Something new to do');
task.save();
task.save({ complete: true });
// .. and so on

TIP: You could try to write some test scenarios for backbone model’s validations.

Try it in jsfiddle

Feel free to fork it!

Stay tuned, there will be more: “Part two: The collection” and “Part three: The view”.. and maybe something about Routers.

Jasmine Matchers Disassembled

Disassembled matchers for jasmine 1.1.0

Go to matcher: toBeTruthy() | toBeFalsy() | toBeDefined() | toBeUndefined() | toBeNull() | toEqual() | toContain() | toBeLessThan(), toBeGreaterThan() | toMatch() | toThrow()

toBeTruthy

Matcher that boolean not-nots the actual. Home

source link
1
2
3
jasmine.Matchers.prototype.toBeTruthy = function() {
  return !!this.actual;
};

toBeFalsy

Matcher that boolean nots the actual. Home

source link
1
2
3
jasmine.Matchers.prototype.toBeFalsy = function() {
  return !this.actual;
};

toBeDefined

Matcher that compares the actual to jasmine.undefined. Home

source link
1
2
3
jasmine.Matchers.prototype.toBeDefined = function() {
  return (this.actual !== jasmine.undefined);
};

toBeUndefined

Matcher that compares the actual to jasmine.undefined. Home

source link
1
2
3
jasmine.Matchers.prototype.toBeUndefined = function() {
  return (this.actual === jasmine.undefined);
};

toBeNull

Matcher that compares the actual to null. Home

source link
1
2
3
jasmine.Matchers.prototype.toBeNull = function() {
  return (this.actual === null);
};

toEqual

Compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. Home

toContain

Matcher that checks that the expected item is an element in the actual Array. Home

toBeLessThan, toBeGreaterThan

Home

source link
1
2
3
4
5
6
7
jasmine.Matchers.prototype.toBeLessThan = function(expected) {
  return this.actual < expected;
};

jasmine.Matchers.prototype.toBeGreaterThan = function(expected) {
  return this.actual > expected;
};

toMatch

Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes a pattern or a String. Home

source link
1
2
3
jasmine.Matchers.prototype.toMatch = function(expected) {
  return new RegExp(expected).test(this.actual);
};

toThrow

Matcher that checks that the expected exception was thrown by the actual. Home

source link
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
jasmine.Matchers.prototype.toThrow = function(expected) {
  var result = false;
  var exception;
  if (typeof this.actual != 'function') {
    throw new Error('Actual is not a function');
  }
  try {
    this.actual();
  } catch (e) {
    exception = e;
  }
  if (exception) {
    result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected));
  }

  var not = this.isNot ? "not " : "";

  this.message = function() {
    if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {
      return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' ');
    } else {
      return "Expected function to throw an exception.";
    }
  };

  return result;
};

Jasmine Cheat Sheet

BDD for JavaScript

Jasmine is a behavior-driven development framework for testing your JavaScript code. It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.

KRUG the Perfect RSpec

Slajdy z prezentacji: The perfect RSpec

Przydatke linki

RSpec best practices

Domain objects

Testing Named Scopes in Ruby on Rails 3.x

The code

1
2
3
4
5
6
7
8
9
10
11
12
13
class Topic < ActiveRecord::Base
  STATUS_PENDING = 'pending'
  STATUS_ACCEPTED = 'accepted'
  STATUS_DONE = 'done'
  STATUSES = [STATUS_PENDING, STATUS_ACCEPTED, STATUS_DONE].freeze

  ## Scopes
  default_scope :order => 'created_at DESC'
  # statuses
  scope :pending, where(:status => STATUS_PENDING)
  scope :accepted, where(:status => STATUS_ACCEPTED)
  scope :done, where(:status => STATUS_DONE)
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe Topic do

  describe "scopes" do
    describe :default do
      specify { Cse::Topic.scoped.arel.orders.should == ['created_at DESC'] }
    end

    describe "statuses" do
      Cse::Topic::STATUSES.each do |status|
        describe status do
          specify { Cse::Topic.send(status).where_values_hash.should == { :status => status } }
        end
      end
    end
  end

end

Rails Flash Messages Helper

The code

1
2
3
4
5
6
7
8
9
10
11
12
13
module ApplicationHelper

  def flash_messages
    return if flash.empty?

    content_tag(:ul, :id => 'flash-messages') do
      flash.collect do |type, message|
        content_tag(:li, message, :class => "flash-message #{type}")
      end.join("\n").html_safe
    end
  end

end

The spec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require 'spec_helper'

describe ApplicationHelper do

  describe "#flash_messages" do
    context "when there is no flash messages" do
      it "should return nothing" do
        helper.flash_messages.should be_nil
      end
    end

    context "when there are some flash messages" do
      let(:flashes) do
        { :notice => 'Battlestation operational',
          :error => 'Hudson, we have a problem!',
          :warning => "I'm sorry Dave, I'm afraid I can't do that" }
      end

      before do
        flashes.each { |type, message| flash[type] = message }
      end

      subject { helper.flash_messages }
      it "should render a list with flash messages" do
        should have_selector('ul', :id => 'flash-messages')

        flashes.each do |type, message|
          should have_selector("li", :class => "flash-message #{type}", :content => message)
        end
      end
    end
  end

end

The usage

1
<%= flash_messages %>

..and the output

1
2
3
<ul id="flash-messages">
  <li class="flash-message notice">Vote was added.</li>
</ul>

The Content Is Coming…

Coming soon…

“my_blog_spec.rb”
1
2
3
describe MyBlog do
  it "should soon have a content"
end