Cells: A Deeper Look into Dependency Injection and Testing
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 inapp/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:
- Setup the test environment, e.g. using fixtures.
- Invoke the actual cell.
- 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!