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.
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.
Before moving on to the tests, we need to pull up a few places. First let's turn onapproved
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.
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
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
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!
Source: https://habr.com/ru/post/333020/
All Articles