Getting Started with Phoenix (as a Rails Developer) - Part 3
Elizabeth Karst, Former Developer
Article Categories:
Posted on
Comparing testing and development tools for Elixir-Phoenix and Rails apps
Welcome back to this three-part series, which provides an overview of building Elixir apps with Phoenix and how that compares to building Rails apps. Part 3 compares testing and development tools for each framework. Be sure to check out Part 1 and Part 2 as well:
- Part 1: Development Paradigm and Request / Response Cycle
- Part 2: Routing and Model-View-Controller Architecture
Testing #
The Basics
Phoenix tests are written with a built-in testing framework called ExUnit. ExUnit provides a simple testing DSL with a handy set of macros for evaluating the truthiness of a given statement. The most commonly used macros in ExUnit are assert
and refute
, similar to Rails Minitest.
A basic Phoenix test case reads as follows:
test "strings match" do
assert 'hello' == 'hello'
end
Here you are asserting that two strings are equal, and will not see any failures when you run the test. If you try to instead assert that “hi” equals “hello”, you’ll see the following failure.
test "strings match" do
assert 'hi' == 'hello'
end
# Failure
test strings match (MyTest)
test/my_test.exs:2
code: 'hi' == 'hello'
lhs: 'hi'
rhs: 'hello'
stacktrace:
test/my_test.exs:3: (test)
Finished in 0.05 seconds
1 test, 1 failure
Phoenix assert
and refute
macros use pattern matching to determine truthiness. Failure messages provide the values of the left hand side (lhs) and right hand side (rhs) of an attempted match. This makes debugging fairly straightforward. If you want to update this test to verify that “hi” does not equal “hello”, you can use refute
and the test will pass with no failures.
test "strings match" do
refute 'hi' == 'hello'
end
One thing to watch out for is assigning undefined variables in tests. The below example will not cause any failures even though it doesn't really validate anything.
test "strings match" do
assert hello = 'hello'
end
In this example, the variable hello
will simply be bound to the string “hello” and the test will pass. Luckily, you will see a warning that the variable hello
is unused, which is a good signal to go back and review your test. This test should read assert hello == ‘hello’
, where hello
is previously defined.
Having seen the basics, let’s look at a few examples for how you might use ExUnit throughout different Phoenix components.
Testing Views
Phoenix View tests are commonly used to assert that view functions return the expected values. The following test verifies that a title_and_author
function in the BookView
returns the correct string for a given book.
# Phoenix View Test: test/my_book_app_web/views/book_view_test.exs
defmodule MyBookAppWeb.BookViewTest do
alias MyBookApp.BookView
test "title_and_author/1" do
book = insert(:book)
assert BookView.title_and_author(book) == "East of Eden, by John Steinbeck"
end
end
Note that you need to import the BookView
to be able to call its functions. The ability to insert(:book)
comes from ExMachina.Ecto
, which is a module for building and inserting factories in Ecto like you can with gems like factory_bot
in Rails.
The most comparable tests you’ll find in a Rails app are helper specs. Helper specs correlate to any helper modules you may have built to support a view. Helper specs read very similarly to Phoenix view specs. Here’s an example of a Rails BookHelper
spec for comparison:
# Rails Helper Test: spec/helpers/book_helper_spec.rb
RSpec.describe BookHelper do
describe "#title_and_author" do
it "returns expected results" do
let!(:book) { create(:book) }
expect(book.title_and_author.to eq("East of Eden, by John Steinbeck")
end
end
end
Testing Controllers
Phoenix Controller tests are commonly used to assert the expected app flow and verify the expected HTML appears in a rendered template.
One note before diving in: much like Rails, Phoenix tests can be organized in blocks. You can add a setup
block before a set of tests to pull out common setup elements, like creating a book. You can add describe
blocks to group and organize tests.
Here is an example of what a Phoenix Controller test might look:
# Phoenix Controller Test: test/my_book_app_web/controllers/book_controller_test.exs
defmodule MyBookAppWeb.BookControllerTest do
use MyBookAppWeb.ConnCase
setup do
book = insert(:book)
end
describe "show" do
test "renders", %{conn: conn} do
conn
|> get(Routes.book_path(conn, :show, book))
assert html_response(conn, 200)
assert has_selector?(conn, "[data-test='book-show']")
end
end
describe "update" do
test "updates book with valid params" do
params = %{book: %{title: "East of Eden"}}
conn
|> post(Routes.book_path(conn, :update, book.id, params))
assert get_flash(conn, :info) =~ "Successfully updated the book."
assert redirected_to(conn) == Routes.book_path(conn, :show, book.id)
end
end
end
The show
test verifies a successful HTTP response and that expected HTML elements are actually rendered on the page (there’s a few ways to do this - this example looks for CSS selectors created for this purpose). The update
test verifies that posting valid data to update a book
struct results in a successful flash message and a redirect to the book’s show page.
Phoenix controller tests have elements that you might see in Rails controller or feature tests. Here’s what a similar controller test might look like in Rails:
# Rails Controller Test: spec/controllers/books_controller_spec.rb
RSpec.describe BooksController, type: :controller do
let!(:book) { create(:book) }
describe "GET show" do
it 'successfully renders' do
get :show, params: { id: book.id }
expect(reponse).to render_template("show")
expect(response.code).to eq("200")
end
end
describe "POST update" do
it "can handle invalid data" do
post :update, params: {id: book.id, {book_params: title: "East of Eden"}}
expect(response.code).to redirect_to(book_path(book))
end
end
end
There is a bit more magic happening in the get
and post
calls here - Rails infers that these calls are for books
based on the reference to the BooksController
in the first line. Often some of the flows validated in a Rails controller specs are also validated in feature specs. Feature specs (typically written with Capybara) allow you to walk through core app flows as though you were a user, verifying routing and page element rendered along the way. These are a bit slower to run, so it’s typically good to start with controller tests and add feature tests for higher priority user flows.
Testing the Model Layer
Phoenix schema tests are used to validate underlying data structs and their changesets. Here’s an example of what a Book
schema test might look like:
# Phoenix Model Layer Test: test/my_book_app/books/book_test.exs
defmodule MyBookApp.BookTest do
alias MyBookApp.Books.Book
alias MyBookApp.Repo
describe "changeset/2" do
test "requires a title and author" do
book_params = %{}
changeset = Book.changeset(%Book{}, book_params)
assert %{title: ["can't be blank"]} = errors_on(changeset)
end
test "book attributes are saved correctly" do
{:ok, book} = Book.create_book(valid_book_params)
book = Repo.get!(Book, book.id)
assert book.title == valid_book_params[:title]
assert book.author_id == valid_book_params[:author_id]
end
end
end
The first test asserts that a changeset will contain errors if any required fields are missing. The second asserts that valid params passed into a changeset are correctly stored in a book
struct.
Test modules for functions in Phoenix contexts are commonly found in the same test directory as schema tests (e.g. under test/my_app
). These tests feel similar to Phoenix view tests - they typically verify that each function in a context produces the expected value or result. The following test asserts that the new_releases
function in the books
context returns the correct values.
# Phoenix Model Layer Test: test/my_book_app/books/books_test.exs
defmodule MyBookApp.BooksTest do
describe "new_releases/0" do
test "returns books released this month" do
old_book = insert(:book, release_date: Date.utc_today)
new_book = insert(:book, release_date: Date.add(Date.utc_today, -90)))
assert new_book in Book.new_releases
refute old_book in Book.new_releases
end
end
end
The equivalent schema and context tests would be covered in a Book
model spec in Rails. The shoulda-matchers
gem is commonly used to verify validations, like the presence of a book title and author with a should_validate_presence_of
method. Tests for model instance and class methods, like #new_releases
, are also included in the model spec. The #new_releases
test reads very similarly to how it did in the Phoenix test.
# Rails Model Test: spec/models/book_spec.rb
RSpec.describe Book, type: :model do
it { should_validate_presence_of(:title, :author) }
describe "#new_releases" do
it "returns books released this month" do
old_book = create(:book, release_date: DateTime.now - 2.months)
new_book = create(:book, release_date: DateTime.now)
expect(Book.new_releases).to include(new_book)
expect(Book.new_releases).not_to include(old_book)
end
end
end
The biggest difference I've found between writing Phoenix and Rails tests is that Phoenix tests have more emphasis on validating data through pure functions where Rails tests feel a bit more scenario driven overall. This is further emphasized by the hexdocs which highlight testing schemas, controllers and views, but have little on integration testing. This isn't really surprising since Elixir is a functional programming language, but it's worth noting that integration test tools like phoenix_integration
are available for more scenario-driven testing.
Further reading:
Development Tools #
Overall I’ve found the development tools fairly comparable between Rails and Phoenix - it’s just a matter of learning the right commands to get what you need done. Working in the Rails console is a bit different than working in a Phoenix console, but that’s largely due to the differences in writing functional Elixir code vs. object-oriented Ruby code.
Build Tools
Elixir and Rails both come with a build tool for creating, testing and managing projects. Elixir’s tool is Mix, which serves many of the same functions as Ruby’s Rake and built-in Rails commands. Here’s some common commands I use day-to-day:
Elixir | Ruby | |
Create a new app | mix phoenix.new my_new_app | rails new my_new_app |
Generate migration | mix phoenix.gen.migration create_books_table | rails generate migration create_books_table |
Run migrations | mix ecto.migrate | rake db:migrate |
Rollback migrations | mix ecto.rollback | rake db:rollback |
Run local server | mix phoenix.server | rails server |
Run tests | mix test test/test_file/... | rspec spec/spec_file/... |
Interactive Console
Elixir has an interactive console, IEx
, that will feel familiar if you’ve ever used Rails irb
. Running iex
in the terminal will load a basic Elixir console. Running iex -S mix
from a project directory will load your project in an interactive console (like rails console
does for Rails apps).
When working on a Rails app, I often use the interactive console to validate data and try things out as I build. Because Elixir is a functional programming language, I’ve found the console a bit harder to get used to. It’s easy to grab and manipulate data in Rails through simple object methods, but even grabbing a single model struct in Elixir requires quite a bit of typing. A few things to make your life easier:
- Add aliases to an
.iex.exs
file in the project root. If you addalias Myapp.Repo
you can simply callRepo.all
in place ofMyApp.Repo.all
in the interactive console. - It’s worth adding helper functions in contexts that you can call from the console. Grabbing the first user in a list is not as simple as calling
User.first
like in Rails. In Elixir it would look more likeUser |> Repo.all |> List.first
. If you are going to grab any structs frequently during buildout or debugging, it’s worth writing functions for them to avoid lots of typing.
Debugging
It’s fairly easy to debug Rails apps by dropping a binding.pry
anywhere you want to inspect. Elixir has an equivalent IEx.pry
. The main difference in use cases is that you have to also require IEx
anywhere you want to drop an IEx.pry
. You can do this is one line: require IEx; IEx.pry
. It’s a bit more typing, but it’s easy to drop this line anywhere for debugging just like a binding.pry
in Rails.
If you want to drop a pry in an Elixir test, you’ll need to run your text like this: iex -S mix test test/test_file…
instead of just mix test test/test_file
. If you don’t add iex -S
then the test won’t stop at your pry
.
Closing Thoughts #
Elixir / Phoenix and Rails apps both have a lot to offer. I’ve found that Elixir / Phoenix apps often seem more straight forward on paper. The simple, explicit nature of functional programming and the flexibility of composable plugs make Elixir / Phoenix apps extremely easy to work with and debug. However, I can’t help but miss object-oriented programming when I step away from Ruby, or magic like implicit rendering and data interaction through ActiveRecord when I step away from Rails. I expect that I’ll grow more comfortable with functional programming as I spend more time with it but the transition does require a mental shift. Although Ruby / Rails and Elixir / Phoenix apps do have a lot in common, they are fundamentally different languages and frameworks with different approaches to development. I've found it easier to start learning Elixir on a clean slate, rather than bringing any expectations you may have from Rails.
Further reading:
Thanks for reading! You can read more on building Elixir apps with Phoenix vs. Rails in Part 1 and Part 2 of this series: