Back to Home

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

elixir · phoenix · wunsh · wunch · exmachina

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

Original author: Brandon Richey
  • 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"}]
end

And 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]]
end

Run the following command to verify that the application components are ready and configured correctly:

$ mix do deps.get, compile

If 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
end

We 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 specifyuse 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}"
end

So 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)
  }
end

Basically, 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}
end

Now 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)
  }
end

Once 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
end

Other 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 models

You can also save the model that you built using the method buildinvocation createon it:

build(:role) |> insert

Other 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


  1. Introduction
  2. Login
  3. Add Roles
  4. We process roles in controllers
  5. We connect ExMachina
  6. Markdown Support
  7. Add comments
  8. Finish with comments
  9. Channels
  10. Channel testing
  11. Conclusion


Success in the study, stay with us!

Read Next