Creating a blog engine using Phoenix and Elixir / Part 5. Connecting ExMachina
- Transfer
- Tutorial

From a translator: “ Elixir and Phoenix are a great example of where modern web development is going. Already, these tools provide quality access to real-time technology for web applications. Sites with increased interactivity, multi-user browser games, microservices are those areas in which these technologies will serve well. The following is a translation of a series of 11 articles that describe in detail aspects of development on the Phoenix framework, it would seem like such a trivial thing as a blog engine. But do not rush to stumble, it will be really interesting, especially if the articles prompt you to pay attention to Elixir or become his followers.
In this part, we will include the ExMachina library to improve the testing process. Now you do not need to copy the identical code to create the tested models, factories will do it for us!
At the moment, our application is based on:
- Elixir : v1.3.1
- Phoenix : v1.2.0
- Ecto : v2.0.2
- Comeonin : v2.5.2
Introduction
As you noticed, in the process of writing this engine we use only a few libraries. Now add another one called ExMachina. She is an analogue of Factory Girl from Ruby.
What is this?
As just mentioned, ExMachina is designed in the image of Factory Girl - an implementation of the Factory pattern from Ruby (also from the wonderful guys from Thoughtbot). We proceed from the fact that it would be great to add various models with links to the tests without rewriting the code to create them from time to time. You can achieve the same yourself with the help of auxiliary modules that include simple functions for generating models. But then it all comes down to the constant creation of such modules for each necessary data set, for each communication, and so on. This will certainly have time to get bored.
Getting down
Let's start by opening the file
mix.exsto add ExMachina to depsand application. To do this, simply insert another entry for ExMachina into the list of dependencies immediately 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"}]
endAnd then add
:ex_machinato the list of used applications:def application do
[mod: {Pxblog, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :comeonin, :ex_machina]]
endRun the following command to verify that the application components are ready and configured correctly:
$ mix do deps.get, compileIf everything goes well, you should see a message on the output about installing ExMachina and successfully compiling the project! Before we begin to change the code, you need to run
mix test and make sure, for the sake of added reliability, that all tests are green.Add the first factory for roles
We need to create a factory module and make it available for all tests. I prefer to do this without inflating the tests. To do this, just drop the module file with the factories into the directory
test/support and then write its import in the tests we need. So, let's start by creating a file
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
endWe named it
Factorybecause such a name reflects the whole essence of this module. Then we will use special factory functions. They compare with the sample the atom supplied to the input, which determines what type of factory to assemble / create . Since this library is pretty close to Factory Girl, it also brings with it some naming conventions that are important to know. The first such name will be build. The function buildmeans that the model ( not the revision ) will be assembled without saving to the database. The second convention will be the name of the function insert, which, on the contrary, stores the model in the database, thereby creating it. We also need to specify
use ExMachina.Ectoso that ExMachina uses Ecto as a Repo layer and behaves accordingly when creating models, associations, etc. We also need to add aliases to all the models for which we will write factories. The function
role_factoryshould simply return a structure Rolethat defines the default properties. This function only supports arity 1. A piece with the function is
sequencequite curious. We need to generate a unique name for each role. Therefore, we will make it sequentially generated. To do this, we take a function sequencein which we pass two arguments: the first is the name of the field for which we want to generate a sequence, the second is the anonymous function that returns a string and interpolates the value inside it. Let's take a look at this function:&”Test Role #{&1}”If you are familiar with Elixir, you may have learned an alternative way to write anonymous functions. This roughly translates to:
fn x ->
"Test Role #{x}"
endSo you can explain the function
sequencein this way:sequence(:name, fn x ->
"Test Role #{x}"
end)Finally, set the admin flag to
false, because we use this value as the default condition. We can create an administrative role by indicating this explicitly. Other more advanced features of ExMachina let's discuss a bit later. Now let's spend some time combining our new factory Rolewith controller tests.Add Role Factory to Controller Tests
Open the file first
test/controllers/user_controller_test.exs. At the top, in the block, setupadd the use of our new function TestHelper.create_role:# ...
import Pxblog.Factory
@valid_create_attrs %{email: "[email protected]", username: "test", password: "test", password_confirmation: "test"}
@valid_attrs %{email: "[email protected]", username: "test"}
@invalid_attrs %{}
setup do
user_role = insert(:role)
{:ok, nonadmin_user} = TestHelper.create_user(user_role, %{email: "[email protected]", username: "nonadmin", password: "test", password_confirmation: "test"})
admin_role = insert(:role, admin: true)
{:ok, admin_user} = TestHelper.create_user(admin_role, %{email: "[email protected]", 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
# ...But before that, we import the factory module itself. On line 10, we simply add the role using the factory
:role. In line 13, we do the same, but redefine the admin flag to a value true. Save the file and restart the tests. All must still pass! Now let's write a factory for users, which also creates connections.
Add a factory for users
Take a look at the factory for users.
def user_factory do
%User{
username: sequence(:username, &"User #{&1}"),
email: "[email protected]",
password: "test1234",
password_confirmation: "test1234",
password_digest: Comeonin.Bcrypt.hashpwsalt("test1234"),
role: build(:role)
}
endBasically, this factory matches what we wrote earlier for creating roles. But there are a couple of pitfalls that we have to deal with. Above, on line 7 , you can see that we are setting the value
password_digestto the password hash value password(since we are simulating user input, we need to add this too). We simply call the Bcrypt module from Comeonin and use the function hashpwsalt, passing the same value to it as to the password/ fields password_confirmation. On the next line, we also set roleas an association. We use the function buildand pass the name of the association we want to collect into it in the form of an atom. Having modified the user factory, let's go back to the file
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}
endNow we will finally replace all calls to
TestHelpercalls to the factory. We take the role and transfer it to the factory to create a user with the correct role. Then we will do the same with the administrator, but we do not need to change our tests! Run them and make sure they are still green. We can go on.
Add a factory for posts
I think we have already gotten our hands on adding new factories, so working on the latter should not cause any difficulties.
There is nothing new here, so let's just change the file
test/controllers/post_controller_test.exs:def post_factory do
%Post{
title: "Some Post",
body: "And the body of some post",
user: build(:user)
}
endOnce again, we run the
importmodule Pxblog.Factoryso that our tests know where the factory is located, to which we direct calls. Then we replace all the steps for creating a post in a block with a setupfactory call. Using the function insert, a structure is created role, which is then used to create the user through the factory, which is finally used to create the post associated with it ... That's all! Run the tests. They turned green again!
From this place, everything else is just extra work. Let's go back and replace all calls to
TestHelpercalls Factory. This is not particularly new or exciting, so I will not pay too much attention to the explanation of the details.Other ways to connect factories
I choose the path of explicitly connecting my factories to each of the tests, but if you do not want to do the same, you can use one of the following methods.
Add an alias to the block
usingin the file 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 file
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
endOther ExMachina features
For the purposes of the small blogging engine, we do not need any other features provided by ExMachina. For example, in addition to
buildand createthere is support for some other functions for convenience (I use buildas an example, but this also works with create):build_pair(:factory, attrs) <- Builds 2 models
build_list(n, :factory, attrs) <- Builds N modelsYou can also save the model that you built using the method
buildinvocation createon it:build(:role) |> insertOther resources
For more information on using ExMachina, visit the Github page . You can also visit the Thoughbot technical blog, where the creators posted the excellent ExMachina announcement and some other ways to use it.
To summarize
At first, I must say, I was a little wary, remembering how I previously implemented some things with the help of Factory Girl. I was afraid that everything would go the same here. But Elixir protects us from ourselves, which helps us find balance when testing. The syntax is clear and clean. The amount of code needed has decreased significantly. Many thanks to the glorious Thoughtbot guys for yet another extremely useful library.
Conclusion from Wunsch
Today is a very short conclusion - just subscribe to our Elixir community and receive interesting articles every week in Russian.
Other articles in the series
- Introduction
- Login
- Add Roles
- We process roles in controllers
- We connect ExMachina
- Markdown Support
- Add comments
- Finish with comments
- Channels
- Channel testing
- Conclusion
Success in the study, stay with us!