Lesson 6 — Testing components and views in Rails
Why testing Phlex is genuinely different
Testing views in a traditional Rails app is painful. To test an ERB partial you typically need a full controller context, request stubs, and instance variables set up correctly. The setup is fragile, slow, and most teams simply don’t bother. View logic goes untested by default.
Phlex changes this completely. A component or view is a plain Ruby
object. Instantiate it with known inputs, call it, assert the output.
No fixtures, no factories, no request stubs. The test suite for the
entire Phlex::UI library runs in under a second.
There’s some conceptual understanding needed here when we look at the
setup for view_test_helper, but the bottom line is that its both easy
to test views, and the tests execute quickly.
Two test helpers
We have two helpers in test/:
component_test_helper.rb — for Phlex::UI library components.
These are Rails-agnostic by design: they accept plain values as props
and never call Rails infrastructure internally. Route helpers, dom_id,
and current_user are the caller’s responsibility — the component just
receives strings and Ruby objects.
|
|
view_test_helper.rb — for Views:: - classes and any app-specific
component that uses Rails infrastructure. Views always use route helpers
at minimum, so they always need a Rails view context.
We’ll create a view_test_helper here that can be used in this and any future Rails
project you might create. (I’ve tried to add sufficient comments and explanations)
|
|
Four things are happening here:
include Rails.application.routes.url_helpers— makes all route helpers (boards_path, new_board_path, board_path(@board) etc.) available both inside rendered views and directly in test assertions.default_url_options— required by Rails’ URL helpers when called outside a request context. Provides a host so URL generation works. Path helpers don’t use the host but Rails still requires the method to be defined.view_context.render(view)— delegates rendering through Rails’ own view context rather than calling .call directly. This gives the view a proper Rails environment — helpers like csrf_meta_tags, stylesheet_link_tag, turbo_stream_from all work correctly.ActionView::TestCase::TestController— Rails’ built-in test controller, purpose-made for this use case. It provides a minimal but complete Rails context without needing a real request or routing setup.
The simple rule
Phlex::UI components (Button, Badge, Avatar, Card,
Table, Breadcrumb, EmptyState etc.) → ComponentTestHelper
Everything else — views, form components, layout components, anything
that might use Rails helpers — → ViewTestHelper
When in doubt, use ViewTestHelper. Passing a view context to a
component that doesn’t need one is harmless — Phlex accepts and ignores
it. The cost is negligible.
A note on architecture: Where app-specific components like
BoardFormlive — underViews::, a separateApp::Components::namespace, or somewhere else — is an architectural decision that depends on your app’s scale and team preferences. We keep it simple here: the clean separation that matters is between the portablePhlex::UIlibrary (alwaysComponentTestHelper) and everything else (useViewTestHelperwhen in doubt). The Bonus Module revisits this when we extractPhlex::UIas a gem.
Why Phlex::UI components are Rails-agnostic
Every Phlex::UI component is designed so that Rails infrastructure
is the caller’s responsibility, not the component’s. Consider
Breadcrumb:
|
|
The component renders <a href="..."> with whatever string it receives.
It knows nothing about Rails routing. This means:
- Component tests need no Rails context — fast, simple, portable
- The component works in any Ruby context — Rails, Sinatra, plain Ruby
- When extracted to a gem,
component_test_helper.rbtravels with it unchanged
This design is intentional. It’s what makes Phlex::UI a genuine
library rather than a collection of app-specific templates.
Component tests
We already have a collection of component tests. Here’s another to show how we’re
using the component_test_helper.
|
|
Trying to test the components now will probably give an error. Lookbook is only
defined as a development gem and so the initializer will fail. You need to modify the `lookbook.rb’ initializer
to fix this:
|
|
defined?(Lookbook) returns nil if the constant isn’t loaded — which is the case in test and production where the gem isn’t in the bundle. The block is skipped entirely and no error is raised.
This is the correct pattern for any initializer that configures a development-only gem. The same guard should be applied to any other development-only gem configuration you add in future.
Now running our tests gives:
|
|
View tests
Views always use ViewTestHelper. Pass an explicit id to unsaved
ActiveRecord objects so that route helpers (board_path(@board)) and
dom_id(@board) generate correctly without hitting the database:
|
|
|
|
And running the tests gives:
|
|
What to test at each level
| Level | Helper | What to test |
|---|---|---|
Components:: (library) |
ComponentTestHelper |
Props, variants, slots, type validation, conditional rendering, accessibility attributes |
Views:: and app components |
ViewTestHelper |
Data rendering, empty states, conditional sections, route links |
What not to test
- Exact Tailwind class strings — couples tests to styling and breaks when you restyle
- Exact HTML structure — breaks on trivial template refactors
- Things Rails already tests — don’t test that
form_withgenerates a form tag
Focus on behaviour: does the empty state appear when it should? Do the right links appear? Does the wrong type raise? Does the breadcrumb correctly render the last item as text rather than a link?
The future-proof split
When we extract Phlex::UI to a gem in the Bonus Module,
component_test_helper.rb travels with it untouched — it has no
app-specific dependencies. view_test_helper.rb stays in the app.
This clean separation was worth establishing from the start.
Exercise
Write tests for Components::Breadcrumb. Use the correct helper.
- Renders a
navelement witharia-label="Breadcrumb" - The last item renders as plain text, not a link
- All items except the last render as links
- Three items render three
lielements
Solution
|
|
Breadcrumb accepts url: as a plain string — it never calls route
helpers internally. So ComponentTestHelper is the correct choice,
even though the caller would normally pass a route helper result.