Lesson 7 — Testing with Minitest
Why testing Phlex components is different
Testing views in a traditional Rails app is painful. To test an ERB partial you need a full Rails environment, a controller context, instance variables set correctly, and a view context that wires it all together. The setup is fragile, the tests are slow, and most teams simply don’t bother. View logic goes untested by default. Phlex changes this completely — and it’s one of the most underappreciated benefits of the component approach. A Phlex component is a plain Ruby object. It has an initialize method that declares exactly what it needs, and a call method that returns an HTML string. That’s it. There’s no implicit state, no controller context, no view environment required. To test it you do exactly what you’d do with any Ruby object:
- Instantiate it with known inputs
- Call it to get the output
- Assert the output is what you expect
|
|
No fixtures, no factories, no request stubs, no database. The component takes data in and produces HTML out — a pure function in all but name.
What this means in practice:
- Tests run in milliseconds — no Rails boot time
- Tests can run without a database connection
- Components can be tested as you build them, before they ever touch a view
- Refactoring a component’s internals doesn’t break call sites — the test catches regressions immediately
- Edge cases (disabled state, empty content, missing optional props) are trivial to cover
This is the payoff for the explicit interface that prop declarations enforce. Because every component declares exactly what it accepts, you always know what inputs to test. There are no hidden instance variables, no implicit controller state, no partials loaded from unexpected paths.
The test suite for an entire component library can run in under a second. By the time you add components to a Rails page, you already know they work.
We’ll start with a test_helper file to simplify our test suites.
|
|
Testing Components::Button
|
|
Testing Components::Card
|
|
Run tests:
|
|
What to test — and what not to
Good to test:
- Business logic —
initialsinAvatar,change_variantinStat - Conditional rendering — is the dismiss button present only when
dismissible: true? - Accessibility attributes —
role,aria-disabled,alt - Type validation — does passing a wrong type raise
Literal::TypeError?
Less valuable to test:
- Exact HTML structure — breaks on trivial refactors
- Specific class strings — couples tests to styling decisions
Exercise
Here’s a new component we’d like to test:
|
|
Write tests for this new Components::Alert. Make sure you cover:
- The
role="alert"attribute is present - The dismiss button renders when
dismissible: true - The dismiss button is absent when
dismissible: false(the default) - Two variants apply different CSS classes
Solution
|
|
Module 3 summary
Components::Basegives every component a shared inheritance root, Literal property declarations, and theclass_nameshelperextend Phlex::KitinComponentsenablesComponentName(...)syntax — norenderor.newrequired; every component file begins withrequire_relative "base"making each self-contained- Leaf components (
Button,Badge,Avatar,Alert,Link) render entirely from props - Single yield point components (
Panel) use plainyield - Named slot components must call
vanish(&)as the first line ofview_template— this yields the block to populate slot instance variables before HTML is rendered capture(&)stores block output as a string;raw safe(...)replays itvanishalso collects configuration —Tableuses it to gather column definitions before rendering- The block argument (
|t|,|card|,|s|) is always the component instance — Phlex upgradesyieldtoyield(self)automatically
Phlex::UI components built
Components::Button— variant (primary/secondary/danger/ghost), disabledComponents::Badge— colour variants via Pico<mark>Components::Avatar— image with initials fallback, size variantsComponents::Alert— info/success/warning/danger, dismissibleComponents::Card— title shorthand + header/body/footer named slotsComponents::Table— column-based API via vanish configuration patternComponents::Link— default, secondary, button variantsComponents::Heading— dynamic tag level via sendComponents::Stat— composes Badge internallyComponents::Panel— single yield point containerComponents::Section— body and aside named slots