Writing a Blog Engine in Phoenix and Elixir: Part 5, Adding ExMachina

Brandon Richey
HackerNoon.com

--

Latest Update: 07/21/2016

Previous post in this series

Introduction

As we have been writing our code for our blog engine along the way, you may have noticed that we’re using very few libraries in our codebase. This is two-fold: first, we want The first one I’ll be starting with is ExMachina, a factory library in the same vein as Factory Girl for Ruby.

Current Versions

As of the time of writing this, the current versions of our applications are:

  • Elixir: v1.3.1
  • Phoenix: v1.2.0
  • Ecto: v2.0.2
  • ExMachina: v1.0.2

If you are reading this and these are not the latest, let me know and I’ll update this tutorial accordingly.

What is it?

As mentioned before, ExMachina is designed to be a similar Factory-style implementation to Factory Girl from Ruby (also from the fine folks at Thoughtbot). The premise here is that for your tests, it’s nice to have a way to set up various models and their associations for your tests without having to rewrite the creation logic for them over and over. This is something you can accomplish with writing helper modules that provide simple creation functions, but you end up having to create functions and helpers for each variation, each extra association, etc, which can get a bit repetitive.

Getting Started

We’ll begin by opening up mix.exs and introducing ExMachina to our dependency and application lists.

Adding ExMachina to our dependency list is pretty simple, so we’ll just add an entry for ExMachina after comeonin.

defp deps do
[{:phoenix, "~> 1.2.0"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:comeonin, "~> 2.5.2"},
{:ex_machina, "~> 1.0"}]
end

Next, we’ll add :ex_machina to the list of applications in-use by our application:

def application do
[mod: {Pxblog, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :comeonin, :ex_machina]]
end

Run the following command to make sure that everything is ready to go and set up correctly:

$ mix do deps.get, compile

If all went well, you should see some output about fetching ExMachina and finally everything compiling successfully! You should also run mix test and verify everything is green before you start making modifications just to stay extra safe here.

Adding our first factory: Roles

What we need to do is create a factory module and make it accessible to all of our tests. My favorite way to do this without bloating each test too much is to throw the module file containing the factories into test/support and then specifically import it into tests we need.

So, let’s start by creating test/support/factory.ex:

defmodule Pxblog.Factory do
use ExMachina.Ecto, repo: Pxblog.Repo

alias Pxblog.Role
alias Pxblog.User
alias Pxblog.Post

def role_factory do
%Role{
name: sequence(:name, &"Test Role #{&1}"),
admin: false
}
end
end

We’re calling it Factory since that’s essentially what this module is going to do. We then write factory functions that pattern match on an atom for which type of factory to build/create from. Since this is pretty close to Factory Girl, it carries some naming conventions from it that are pretty important to note. The first being build: build means that we are building out the model (not a changeset) but not persisting it to the database. insert, however, DOES persist the model to the database.

We need the use ExMachina.Ecto bit to tell ExMachina that we’re using Ecto as our Repo layer and that it should act accordingly for create/associations/etc. We also need to add aliases for all of our models since we’ll be writing factories for all of them.

The role_factory function should just return a Role struct that defines the default properties we want it to have. This function only supports an arity of 1.

The sequence bit here is pretty interesting. We want to generate a unique name for each role, so for us to be able to do so we define a sequentially-generated name. We use the sequence function, and we pass it two arguments: the first is the name of the field we want to generate a sequence for, and the second is an anonymous function that returns a string and interpolates a value into it. Let’s take a look at this function:

&”Test Role #{&1}”

If you’re comfortable with Elixir, you probably recognize this as another way to write an anonymous functions. This roughly translates to:

fn x ->
"Test Role #{x}"
end

So if we think about what that sequence function is doing, it’s really just doing this:

sequence(:name, fn x ->
"Test Role #{x}"
end)

Finally, we set our admin flag to false, since we’ll use these as a default case and only explicitly create admin roles. We’ll discuss some of the more complicated options for ExMachina later, as well. Now, we’ll spend some time integrating our new Role factory into our controller tests.

Changing our controller tests to use our Role factory

Open up test/controllers/user_controller_test.exs first. At the top, in our setup block, we’ll modify the bits using our TestHelper.create_role functions.

# ...import Pxblog.Factory

@valid_create_attrs %{email: "test@test.com", username: "test", password: "test", password_confirmation: "test"}
@valid_attrs %{email: "test@test.com", username: "test"}
@invalid_attrs %{}

setup do
user_role = insert(:role)
{:ok, nonadmin_user} = TestHelper.create_user(user_role, %{email: "nonadmin@test.com", username: "nonadmin", password: "test", password_confirmation: "test"})

admin_role = insert(:role, admin: true)
{:ok, admin_user} = TestHelper.create_user(admin_role, %{email: "admin@test.com", username: "admin", password: "test", password_confirmation: "test"})

{:ok, conn: build_conn(), admin_role: admin_role, user_role: user_role, nonadmin_user: nonadmin_user, admin_user: admin_user}
end
# ...

We have to start out by importing our Factory module that we created. Line 10 is just us using a simple role using the :role factory. Line 13 is us using the same :role factory, but us specifically overriding the admin flag to true.

Save the file and rerun our tests, and we’re still green! Now, let’s implement our user factory and introduce a factory that builds associations too!

Adding our next factory: Users

Let’s take a look at the factory for users.

def user_factory do
%User{
username: sequence(:username, &"User #{&1}"),
email: "test@test.com",
password: "test1234",
password_confirmation: "test1234",
password_digest: Comeonin.Bcrypt.hashpwsalt("test1234"),
role: build(:role)
}
end

For the most part, this factory matches what we created for roles. There are a couple of gotchas we’ll have to deal with first. On line 7 of the above, you can see that we have to set the password_digest value to the hashed password value (since we simulate logins for users, you’ll want to include this). We just call the Bcrypt module under Comeonin and specifically invoke the hashpwsalt function, passing it the same password that we use the in password/password_confirmation values. On line 8, we also have to specify that role is actually an association; we use the build function and pass in via an atom the name of the association we want to build for this.

Having modified our user factory, let’s go back to test/controllers/user_controller_test.exs.

setup do
user_role = insert(:role)
nonadmin_user = insert(:user, role: user_role)

admin_role = insert(:role, admin: true)
admin_user = insert(:user, role: admin_role)

{:ok, conn: build_conn(), admin_role: admin_role, user_role: user_role, nonadmin_user: nonadmin_user, admin_user: admin_user}
end

Now we’ve completely replaced all of our calls to TestHelper with calls to the Factory instead. We’re using the user role and passing that to our user factory so that it knows which role to use. We then do the same thing for the admin user, but other than that we don’t need to modify our tests at all! Run our tests, make sure everything is still green, and then we can proceed!

Adding our next factory: Posts

Now that we’ve gotten so good at adding new factories, adding our final factory for posts should be pretty simple.

def post_factory do
%Post{
title: "Some Post",
body: "And the body of some post",
user: build(:user)
}
end

Not really anything new here, so we’ll jump right into modifying test/controllers/post_controller_test.exs:

# ...import Pxblog.Factory

@valid_attrs %{body: "some content", title: "some content"}
@invalid_attrs %{}

setup do
role = insert(:role)
user = insert(:user, role: role)
post = insert(:post, user: user)
conn = build_conn() |> login_user(user)
{:ok, conn: conn, user: user, role: role, post: post}
end
# ...

Once again, we import Pxblog.Factory so our module knows where the factory calls are coming from. Then, we replace all of our creation steps in our setup block with Factory calls instead. role gets populated from our insert statement creating our role, which then is used to build our user from the factory, and finally that user is used to create a post associated with that user…but that’s it! Run our tests again and it’s back to being green!

Beyond this point, everything else is just grunt-work; going back in and replacing calls to TestHelper with Factory calls instead. It’s nothing particularly new or exciting, so I won’t bother with the excruciating details for each.

Other ways to include your factories

I chose the route of explicitly importing my factory into each of my tests, but if you did not want to do this, you could instead do one of the following:

Add our alias statement to the using blocks in test/support/model_case.ex:

using do
quote do
alias Pxblog.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Pxblog.ModelCase
import Pxblog.Factory
end
end

And test/support/conn_case.ex:

using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest

alias Pxblog.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query

import Pxblog.Router.Helpers
import Pxblog.Factory

# The default endpoint for testing
@endpoint Pxblog.Endpoint
end
end

Other functions you can perform with Ex Machina

For purposes of the little blog engine we’re writing we didn’t have an explicit need for some of the other features that ExMachina provides out of the box. For example, both build and create support a few other functions for sake of convenience (I’m using build as the example, but these work with create too):

build_pair(:factory, attrs)    <- Builds 2 models
build_list(n, :factory, attrs) <- Builds N models

You can also persist a model that you used a factory build method by calling create on it:

build(:role) |> insert

Other resources

For more information on how to use ExMachina, visit the Github page for it (https://github.com/thoughtbot/ex_machina). You can also visit thoughtbot’s tech blog where the creators have a great write up announcing ExMachina and some other ways to use it! The post in question is at https://robots.thoughtbot.com/announcing-ex-machina.

Wrapping up

At first I was admittedly a little wary as I’ve seen some things that have been implemented with Factory Girl in the past and I was afraid to get right back into that same scenario, but Elixir does such a nicer job of protecting us from ourselves that I think it balances out quite nicely. The syntax is crisp and clean, the amount of code that I had to write dropped dramatically, and it unties our tests just a little bit more from Ecto, so overall I’m very pleased. A big thank you to the fine folks at Thoughtbot for giving us another very handy library to use.

I’d also love to know what people thought of this article. I’ve been toying with the idea of publishing a series of posts on different libraries available for Elixir and Phoenix, and ExMachina seemed like a great way to start! You can usually find me on the Elixir Slack group or the Elixir IRC room, or of course you can find me on twitter at @diamondgfx.

Next post in this series

Check out my new book!

Hey everyone! If you liked what you read here and want to learn more with me, check out my new book on Elixir and Phoenix web development:

I’m really excited to finally be bringing this project to the world! It’s written in the same style as my other tutorials where we will be building the scaffold of a full project from start to finish, even covering some of the trickier topics like file uploads, Twitter/Google OAuth logins, and APIs!

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.

To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

--

--