📜 ⬆️ ⬇️

Creating a blog engine with Phoenix and Elixir / Part 10. Channel testing


From the translator: “Elixir and Phoenix are a great example of where modern web development is heading. Already, these tools provide high-quality access to real-time technologies for web applications. Sites with increased interactivity, multiplayer browser games, microservices - those areas in which these technologies will serve a good service. The following is a translation of a series of 11 articles that describe in detail the aspects of development on the Phoenix framework that would seem such a trivial thing as a blog engine. But do not hurry to sulk, it will be really interesting, especially if the articles encourage you to pay attention to the Elixir or become its followers. ”


In this part we will learn how to test channels.


Where did we leave off


At the end of the last part, we completed a cool system of “live” comments for the blog. But to the horror, there was not enough time for the tests! Let's do them today. This post will be clear and short, in contrast to the overly long previous one.


Tidying up trash


Before moving on to the tests, we need to pull up a few places. First let's turn on
approved flag in broadcast call. This way we will be able to check in the tests the change in the status of confirmation of comments.


 new_payload = payload |> Map.merge(%{ insertedAt: comment.inserted_at, commentId: comment.id, approved: comment.approved }) broadcast socket, "APPROVED_COMMENT", new_payload 

You also need to modify the web/channels/comment_helper.ex file so that it reacts to empty data sent to the socket by requests for approval / deletion of comments. After the approve function approve add:


 def approve(_params, %{}), do: {:error, "User is not authorized"} def approve(_params, nil), do: {:error, "User is not authorized"} 

And after the delete function:


 def delete(_params, %{}), do: {:error, "User is not authorized"} def delete(_params, nil), do: {:error, "User is not authorized"} 

This will make the code simpler, error handling better, and testing easier.


Testing comments helper


We will use the factories that we wrote with ExMachina earlier. We need to test the creation of a comment, as well as approval / rejection / deletion of a comment based on user authorization. Create a file test/channels/comment_helper_test.exs , and then add the preparatory code to the beginning:


 defmodule Pxblog.CommentHelperTest do use Pxblog.ModelCase alias Pxblog.Comment alias Pxblog.CommentHelper import Pxblog.Factory setup do user = insert(:user) post = insert(:post, user: user) comment = insert(:comment, post: post, approved: false) fake_socket = %{assigns: %{user: user.id}} {:ok, user: user, post: post, comment: comment, socket: fake_socket} end # Insert our tests after this line end 

Here, the ModelCase module is ModelCase to add the ability to use the setup block. Below are aliases for the Comment , Factory and CommentHelper , so that their functions can be called more easily.


Then comes the setup of some basic data that can be used in each test. As before, a user, post and comment are created here. But pay attention to the creation of a "fake socket", which includes only the assigns key. We can pass it to CommentHelper so that it CommentHelper of it as a real socket.


Then the atom tuple is returned :ok and the dictionary list (as well as in other tests). Let's write the tests themselves!


Let's start with the simplest test to create a comment. Since any user can write a comment, no special logic is required here. We check that the comment was actually created and ... everything!


 test "creates a comment for a post", %{post: post} do {:ok, comment} = CommentHelper.create(%{ "postId" => post.id, "author" => "Some Person", "body" => "Some Post" }, %{}) assert comment assert Repo.get(Comment, comment.id) end 

To do this, we call the create function from the CommentHelper module and pass information to it as if this information was received from a channel.


We proceed to the approval of comments. Since a bit more authorization-related logic is used here, the test will be a bit more complicated:


 test "approves a comment when an authorized user", %{post: post, comment: comment, socket: socket} do {:ok, comment} = CommentHelper.approve(%{"postId" => post.id, "commentId" => comment.id}, socket) assert comment.approved end test "does not approve a comment when not an authorized user", %{post: post, comment: comment} do {:error, message} = CommentHelper.approve(%{"postId" => post.id, "commentId" => comment.id}, %{}) assert message == "User is not authorized" end 

Similar to creating a comment, we call the CommentHelper.approve function and pass the information from the channel to it. We pass the "fake socket" to the function and it accesses the assign value. We test both of them using a valid socket (with the logged on user) and an invalid socket (with an empty assign ). Then just check that we get a comment in a positive outcome and an error message in a negative one.


Now about the tests for deletion (which are essentially identical):


 test "deletes a comment when an authorized user", %{post: post, comment: comment, socket: socket} do {:ok, comment} = CommentHelper.delete(%{"postId" => post.id, "commentId" => comment.id}, socket) refute Repo.get(Comment, comment.id) end test "does not delete a comment when not an authorized user", %{post: post, comment: comment} do {:error, message} = CommentHelper.delete(%{"postId" => post.id, "commentId" => comment.id}, %{}) assert message == "User is not authorized" end 

As I mentioned earlier, our tests are almost identical, with the exception of a positive outcome, in which we see that the comment has been deleted and is no longer represented in the database.


Let's check that we cover the code with tests properly. To do this, run the following command:


 $ mix test test/channels/comment_helper_test.exs --cover 

It will create a report in the [project root]/cover directory, which will tell us which code is not covered by the tests. If all tests are green, open the file in the browser ./cover/Elixir.Pxblog.CommentHelper.html . If you see red, then this code is not covered by tests. No red means 100% coverage.


The complete file with the comments helper tests is as follows:


 defmodule Pxblog.CommentHelperTest do use Pxblog.ModelCase alias Pxblog.Comment alias Pxblog.CommentHelper import Pxblog.Factory setup do user = insert(:user) post = insert(:post, user: user) comment = insert(:comment, post: post, approved: false) fake_socket = %{assigns: %{user: user.id}} {:ok, user: user, post: post, comment: comment, socket: fake_socket} end # Insert our tests after this line test "creates a comment for a post", %{post: post} do {:ok, comment} = CommentHelper.create(%{ "postId" => post.id, "author" => "Some Person", "body" => "Some Post" }, %{}) assert comment assert Repo.get(Comment, comment.id) end test "approves a comment when an authorized user", %{post: post, comment: comment, socket: socket} do {:ok, comment} = CommentHelper.approve(%{"postId" => post.id, "commentId" => comment.id}, socket) assert comment.approved end test "does not approve a comment when not an authorized user", %{post: post, comment: comment} do {:error, message} = CommentHelper.approve(%{"postId" => post.id, "commentId" => comment.id}, %{}) assert message == "User is not authorized" end test "deletes a comment when an authorized user", %{post: post, comment: comment, socket: socket} do {:ok, comment} = CommentHelper.delete(%{"postId" => post.id, "commentId" => comment.id}, socket) refute Repo.get(Comment, comment.id) end test "does not delete a comment when not an authorized user", %{post: post, comment: comment} do {:error, message} = CommentHelper.delete(%{"postId" => post.id, "commentId" => comment.id}, %{}) assert message == "User is not authorized" end end 

Testing the comment channel


The generator has already created for us the basis of channel tests, it remains to fill them with meat. Let's start by adding the Pxblog.Factory alias to use the factories in the setup block. Actually, everything is as before. Then you need to set up a socket, namely, introduce yourself as a created user and connect to the comments channel of the created post. Leave the ping and broadcast tests in place, but remove the shout tests, since we no longer have this handler. In the file test/channels/comment_channel_test.exs :


 defmodule Pxblog.CommentChannelTest do use Pxblog.ChannelCase alias Pxblog.CommentChannel alias Pxblog.Factory setup do user = Factory.create(:user) post = Factory.create(:post, user: user) comment = Factory.create(:comment, post: post, approved: false) {:ok, _, socket} = socket("user_id", %{user: user.id}) |> subscribe_and_join(CommentChannel, "comments:#{post.id}") {:ok, socket: socket, post: post, comment: comment} end test "ping replies with status ok", %{socket: socket} do ref = push socket, "ping", %{"hello" => "there"} assert_reply ref, :ok, %{"hello" => "there"} end test "broadcasts are pushed to the client", %{socket: socket} do broadcast_from! socket, "broadcast", %{"some" => "data"} assert_push "broadcast", %{"some" => "data"} end end 

We have written quite complete tests for the CommentHelper module, so here we’ll leave the tests directly related to the functionality of the channels. Let's create a test for three messages: CREATED_COMMENT , APPROVED_COMMENT and DELETED_COMMENT .


 test "CREATED_COMMENT broadcasts to comments:*", %{socket: socket, post: post} do push socket, "CREATED_COMMENT", %{"body" => "Test Post", "author" => "Test Author", "postId" => post.id} expected = %{"body" => "Test Post", "author" => "Test Author"} assert_broadcast "CREATED_COMMENT", expected end 

If you have never seen channel tests before, then everything seems new. Let's understand the steps.


We start by passing to the test socket and the post created in the setup block. In the next line, we send a CREATED_COMMENT event to the socket along with an associative array, similar to what the client actually sends to the socket.


The following describes our "expectations". For now, you cannot define a list that refers to any other variables inside the assert_broadcast function , so you should develop a habit of defining the expected values ​​separately and passing the expected variable to the assert_broadcast call. Here we expect the values ​​of body and author match what we passed inside.


Finally, we check that the CREATED_COMMENT message was translated along with the expected associative array.


Now go to the APPROVED_COMMENT event:


 test "APPROVED_COMMENT broadcasts to comments:*", %{socket: socket, post: post, comment: comment} do push socket, "APPROVED_COMMENT", %{"commentId" => comment.id, "postId" => post.id, approved: false} expected = %{"commentId" => comment.id, "postId" => post.id, approved: true} assert_broadcast "APPROVED_COMMENT", expected end 

This test is largely similar to the previous one, except that we pass the approved value of false to the socket and expect to see the value of approved equal to true after execution. Notice that in the variable expected we use commentId and postId as pointers to comment.id and post.id These expressions will cause an error, so you need to use the separation of the expected variable in the assert_broadcast function.


Finally, take a look at the test for the DELETED_COMMENT message:


 test "DELETED_COMMENT broadcasts to comments:*", %{socket: socket, post: post, comment: comment} do payload = %{"commentId" => comment.id, "postId" => post.id} push socket, "DELETED_COMMENT", payload assert_broadcast "DELETED_COMMENT", payload end 

Nothing particularly interesting. We transfer the standard data to the socket and check that we are broadcasting the event about deleting the comment.


Just as we did with CommentHelper , CommentHelper 's run tests specifically for this file with the --cover option:


 $ mix test test/channels/comment_channel_test.exs --cover 

You will receive warnings that the expected variable is not used , which can be safely ignored.


 test/channels/comment_channel_test.exs:31: warning: variable expected is unused test/channels/comment_channel_test.exs:37: warning: variable expected is unused 

If you have opened the ./cover/Elixir.Pxblog.CommentChannel.html file and do not see anything red, then you can shout "Hurray!". Full coverage!


The final version of the CommentChannel test should look like this:


 defmodule Pxblog.CommentChannelTest do use Pxblog.ChannelCase alias Pxblog.CommentChannel import Pxblog.Factory setup do user = insert(:user) post = insert(:post, user: user) comment = insert(:comment, post: post, approved: false) {:ok, _, socket} = socket("user_id", %{user: user.id}) |> subscribe_and_join(CommentChannel, "comments:#{post.id}") {:ok, socket: socket, post: post, comment: comment} end test "ping replies with status ok", %{socket: socket} do ref = push socket, "ping", %{"hello" => "there"} assert_reply ref, :ok, %{"hello" => "there"} end test "broadcasts are pushed to the client", %{socket: socket} do broadcast_from! socket, "broadcast", %{"some" => "data"} assert_push "broadcast", %{"some" => "data"} end test "CREATED_COMMENT broadcasts to comments:*", %{socket: socket, post: post} do push socket, "CREATED_COMMENT", %{"body" => "Test Post", "author" => "Test Author", "postId" => post.id} expected = %{"body" => "Test Post", "author" => "Test Author"} assert_broadcast "CREATED_COMMENT", expected end test "APPROVED_COMMENT broadcasts to comments:*", %{socket: socket, post: post, comment: comment} do push socket, "APPROVED_COMMENT", %{"commentId" => comment.id, "postId" => post.id, approved: false} expected = %{"commentId" => comment.id, "postId" => post.id, approved: true} assert_broadcast "APPROVED_COMMENT", expected end test "DELETED_COMMENT broadcasts to comments:*", %{socket: socket, post: post, comment: comment} do payload = %{"commentId" => comment.id, "postId" => post.id} push socket, "DELETED_COMMENT", payload assert_broadcast "DELETED_COMMENT", payload end end 

Final touches


Since the test coverage report can be easily created using Mix, it doesn't make sense to include it in Git history, so open the .gitignore file and add the following line to it:


 /cover 

That's all! Now we have a completely test-covered channel code (with the exception of the Javascript tests, which are a separate world that does not fit into this series of lessons). In the next part, we will move on to work on the UI, make it a little nicer and more functional, and also replace the standard styles, logos, etc., to make the project look more professional. In addition, the ease of use of our site is now absolutely no. We will fix this too, so that people would like to use our blogging platform!


Other series articles


  1. Introduction
  2. Authorization
  3. Adding Roles
  4. Process roles in controllers
  5. We connect ExMachina
  6. Markdown support
  7. Add comments
  8. We finish with comments
  9. Channels
  10. Channel testing
  11. Conclusion

')

Source: https://habr.com/ru/post/333020/


All Articles