Ruby
Article

Cells: A Deeper Look into Dependency Injection and Testing

By Nick Sutterer

In my previous post, you and Scott learned the basic of Cells, a view model layer for Ruby and the Rails framework.

Where there used to be stacks of partials that access controller instances variables, locals, and global helper functions, there’s now stacks of cells. Cells are objects. So far, so good.

Scott now understands that every cell represents a fragment of the final web page. Also, the cell helps by embracing the logic necessary to present that very fragment and by providing the ability to render templates, just as we used to do it in controller views.

Great, but what are we gonna do with all that now?

Back in the days when Cells was a very young project, many developers got intrigued by using cells for sidebars, navigation headers, or other reusable components. The benefit of proper encapsulation and the reusability coming with it is an inevitable plus for these kinds of project.

However, cells can be used for “everything”, meaning they can replace the entire ActionView render stack and provide entire pages, a lot faster and with a better architecture.

This intrigues Scott.

A User Listing

Why not implement a page that lists all users signed up for Scott’s popular web application?

In Rails, this feature normally sits in the UsersController and its #index action. Instead of using a controller view, let’s use Cells for that.

Page Cell Without Layout

For a better understanding, we should start off with the UsersController and see how a page cell is rendered from there.

Please note that we still use the controller’s layout to wrap around the user listing. That’s because we want to learn how to use Cells step-wise. Scott likes that. But Scott needs to keep in mind that Cells can also render the application layout, making ActionView completely redundant! This we will explore at a later point.

class UsersController < ApplicationController
  def index
    render html: cell("user_index_cell"), layout: true
  end
  ...

All the controller does is rendering the UserIndexCell that we have to implement now. Did you notice that there’s no model passed into the cell call? This is because cells can aggregate data on their own, if desired. We’ll shortly learn what’s the best way of handling dependencies.

Using render with the :html option will simply return the passed string to the browser. With layout: true it – surprisingly – wraps that string in the controller’s layout.

This is all Rails specific. Now, let’s get to the actual cell. The new UserIndexCell would go into app/cells/user_index_cell.rb in a conventional setup:

In Trailblazer, cells have a different naming structure and directory layout. Scott’s user cell would be called User::Cell::Index and sit in app/concepts/user/cell/index.rb, but that’s a whole different story for a follow-up post.

class UserIndexCell < Cell::ViewModel
  def show
    @model = User.all
    render
  end
end

With the introductory post in the back of your head, this doesn’t look too new. The cell’s show method will assign the @model instance variable by invoking User.all and then render its view.

Iterations in Views

In the view, we can use the user collection and display a nicely formatted list of users. In conventional Cells, the view resides in app/cells/user_index/show.haml and looks as follows:

%h1 All Users

%ul
  - model.each do |user|
    %li
      = link_to user.email, user
      = image_tag user.avatar

Since we assigned @model earlier, we can now use Cells’ built-in model method in the view to iterate over the collection and render the list.

Scott, being a dedicated and self-appointed software architect, narrows his eyes to slits. Imaginary tumbleweed passes behind his 23″ external monitor. Silence.

There’s two things he doesn’t like right now:

Why does the cell fetch its model? Couldn’t this be a dependency passed in from the outer world, such as, the controller?

And, why is the cell’s view so messy? Didn’t we say that cell views should be logicless? This looks just like a partial from a conventional Rails project.

You’re right, Scott, and your architect intuition has led you to ask the right questions.

It’s not good practice to keep data aggregation knowledge in cells, unless it really makes sense and you understand your cell as a stand-alone widget.

External Dependencies

Whenever you assign @model you must ask yourself: “Wouldn’t it be better to let someone else grab my data?”. Here’s how that is done in the controller:

class UsersController < ApplicationController
  def index
    users = User.all
    render html: cell("user_index_cell", users), layout: true
  end
  ...

Now it’s the controller’s responsibility to find the appropriate input for the cell. Even though Rails MVC is far from the real MVC, this is what a controller is supposed to do.

We can now simplify the cell, too:

class UserIndexCell < Cell::ViewModel
  def show
    render
  end
end

Remember, the first argument passed to cell is always available as model within the cell and its view. Please don’t get confused with the term “model”, though. Rails has misapprehended us that a “model” is always one particular entity. In OOP, a model is just an object, and in our context, this is an array of persistent objects.

Let’s see how we can now polish up the view and have less logic in it. The next version of it is going to use instance methods as “helpers”. A bit better, but not perfect:

%h1 All Users

%ul
  - model.each do |user|
    %li
      = link user
      = avatar user

Instead of keeping model internals in the view, two “helpers” link and avatar now do the job. Since we’re iterating, we still have to pass the iterated user object to the method – a result of a suboptimal object design we will soon fix.

Helpers == Instance Methods

In order to make link and avatar work, we need to add instance methods to the cell:

class UserIndexCell < Cell::ViewModel
  def show
    render
  end

private
  def link(user)
    link_to(user.email, user)
  end

  def avatar(user)
    image_tag(user.avatar)
  end
end

All presentation logic is now nicely encapsulated as instance methods in the cell. The view is tidied up, sort of, and only delegates to “helpers”.

Well, sort of, because neither does Scott like the explicit passing of the user instance to every helper, nor is he a big fan of the manual each loop. He scrunches up his nose…there must be a better way to do this.

Nesting Cells

In OOP, when you start passing around objects in succession, it often is an indicator for a flawed object design.

If we need to pass a single user instance to all those helpers, why not introduce another cell? This cell has the responsibility to present a single user only, and embrace all successive helper calls in one object?

Scott’s puts on his white architect hat, again. “Yes, that sounds like good OOP.”

The logical conclusion is to introduce a new cell for one user. It will live in app/cells/user_index_detail_cell.rb.

We all know, that name is more than odd and a result of Rails’ missing convention of namespacing. Let’s go with it for now, but keep in mind that the next post will introduce Trailblazer cells, where namespaces and strong conventions make this look a lot more pleasant:

class UserIndexDetailCell < Cell::ViewModel
  def show
    render
  end

private
  def link
    link_to(model.email, model)
  end

  def avatar
    image_tag(model.avatar)
  end
end

We removed link and avatar from UserIndexCell (yes, delete that code, good-bye) and moved it to UserIndexDetailCell. Since the latter is supposed to present one user only, we can safely use model here and do not need to pass anything around.

Here’s the view in app/cells/user_index_detail/show.haml – again, Scott, bear with me. The next post will show how this can be done in a much more streamlined structure:

%li
  = link
  = avatar

Scott loves this. Simple views can’t break, can they?

Now that we have implemented two cells (one for the page, one per listed user), how do we connect them? A simple nested cell invocation will do the trick, as illustrated in the following app/cells/user_index/show.haml view:

%h1 All Users

%ul
  - model.each do |user|
    = cell("user_index_detail", user)

Where there was the hardcoded item view, we now dispatch to the new cell. As you might have guessed, this new detail cell is really instantiated and invoked every time this array’s iterated. And it’s still faster than partials!

Do not confuse that with helpers, though. The detail cell does not have any access to the index cell, and visa-versa. Dependencies have to be passed around explicitly, no cell can access another cell’s internals, instance variables or even helper methods.

Anyway, rendering collections is something the Cells authors have thought about already.

Rendering Collections

Cells provides a convenient API to render collections without having to iterate through them manually. Scott likes simple APIs as much as he adores simple, logicless views:

%h1 All Users

%ul
  = cell("user_index_detail", collection: model)

When providing the :collection option, Cells will do the iteration for you! And, good news, in the upcoming Cells 5.0, this will have another massive performance boost, thanks to more simplifications.

Scott is very happy about his new view architecture. He has a sip of his icey-cold beer, a reward for his hard-earned thirst, and freezes. No, it’s not the chilled beverage that makes him turn into a pillar of salt. It’s tests! He has not written a single line of them.

Testing Cells

Cells are objects and objects are very easy to test.

Now, where does one start with so many objects? We could start testing a single detail cell, just for the sake of writing tests. Scott prefers Minitest over Rspec. This doesn’t mean Scott wants to start another religious war over test frameworks, though.

A cell test consists of three steps:

  1. Setup the test environment, e.g. using fixtures.
  2. Invoke the actual cell.
  3. Test the output. Usually, this is done using Capybara’s fine matchers.

Speaking of Capybara, in order to use this gem properly in Minitest, it’s advisable to include the appropriate gem in your Gemfile:

 group :test do
  gem "minitest-rails-capybara"
  ...
 end

In test_helper.rb, some Capybara helpers have to be mixed into your Spec base class. This is to save Scott a terrible headache, or even a migrane:

Minitest::Spec.class_eval do
  include ::Capybara::DSL
  include ::Capybara::Assertions
end

Now for the actual test. This test file could go in test/cells/user_index_detail_test.rb.

class UserCellTest < MiniTest::Spec
  include Cell::Testing
  controller UsersController

  let (:user) { User.create(email: "g@trb.to", avatar: image_fixture) }

  it "renders" do
    html = cell(:user_index_detail, user).()

    html.must_have_css("li a", "g@trb.to")
    html.must_have_css("img")
  end
end

This is, if you have a closer look, really just a unit test. A unit test where you invoke the examined object, and assert the side effects.

The side effects, when rendering a cell, should be emitted HTML, which can be easily tested using Capybara. Scott is impressed.

Cell Test == Unit Test

The fascinating fact here is that no magic is happening anywhere.

Where a conventional Rails helper test and its convoluted magic can trick you into thinking that your code’s working, this test will break if you don’t pass the correct arguments into the cell.

You have to aggregate the correct data, instantiate and invoke the object, and then you can inspect the returned HTML fragment.

Scott scratches his head. He now understands what a cell test looks like. Invocation and assertion is all it needs. However, does it make sense to unit-test every little cell? Wouldn’t it make more sense to test the system as a whole, where we only render the UserIndexCell and see if that runs?

Correct, Scott.

As a rule of thumb, start testing the uppermost cell and try to assert as many details from there as possible. If composed, nested cells yield a high level of complexity, then there’s nothing wrong with breaking down tests to a lower level.

The benefit of the top-down approach is, when changing internals, you won’t have to rewrite a whole bunch of tests. Does this feel familiar from “normal” OOP testing? Yes it does, because cells are just objects.

Here’s how a complete top-bottom test could be written. Instead of worrying about internals, the index cell is rendered directly:

it "renders" do
  html = cell(:user_index, collection: [user, user2]).()

  html.must_have_css("li", count: 2)
  html.must_have_css("li a", "g@trb.to")
  html.must_have_css("li a", "2@trb.to")
  # ..
end

Note how we now render the user collection, and as a logical conclusion, assert an entire list, not just a single item.

Testing view components is no pain. The opposite is the case: it’s identical to using a cell. This behavior comes for free when you write clean, simple objects with a well-defined API.

With a few Capybara assertions, you can quickly write tests that make sure your cells will definitely work in production, making your view layer rock-solid.

What’s Next?

We’re set to write cells for all the small things, embrace them as collections with any level of complexity, and, the most important part, test those objects so it won’t break anymore.

In the next post we will discuss some expert features of Cells, such as packaging CSS and Javascript assets into the Rails assets pipeline, view inheritance, caching, and how Trailblazer::Cell introduces a more intuitive file and naming structure.

Well done, Scott. Keep those objects coming!

  • Kirill Mitskevich

    Cells is the coolest thing for frontend developer who is familiar with Rails. It’s the most powerful tool for organizing frontend structure I’ve seen. I can’t imagine how people can normally write( mb they can’t ) their frontend without something like modules( e.g. Cells, React components )…

    • apotonick

      Thank you, Kirill! And, yes, I agree, I am struggling to understand how people can write server-side views in Rails without Cells, just because it’s so backwards.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.