action-cable-testing 0.0.1 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e9610f21de83e78f877150a6d5656a48cc526c22
4
- data.tar.gz: 7bf6e9e31197c9726dff819daa36b69ff980b4a8
3
+ metadata.gz: 4b6269f11a5590fddc954a824bd3af62f7a64e83
4
+ data.tar.gz: '079a0c9dbf1507e5238d40085a459649f962131d'
5
5
  SHA512:
6
- metadata.gz: 0d49c7b608daa5456c8f39e17a2f8e3505d8f38c3e9afde9d8ddc6382b28fa5e29e75371d2a5707087a8aed97a1a8835e782f0e65ce7a3ee545445cf97b42f42
7
- data.tar.gz: 78a6f6f97db43d54e8407c8804c9bf7ed35528e5fbca8754c9bf20cc1d2236ea66dc1955aa8a639099f8e821dbea456a42155e50cd3b830da4a5bc9e5cf96a16
6
+ metadata.gz: 98e4529e68c95d98e3ae33c7a8302c4c492cbb8c96dd70cdbe702e9481aadce1fc58d77331e2cd713c5b8a9dcaf2138ac47c291a947542e8d7b6967bad3e13a2
7
+ data.tar.gz: f3238297625a5ef0a9dc38fc536d3d07ba19785de42313f199b3a9d67352f8d0e8910b144885fe4593c221982dcb52d19c89cd09e3e8db13ee0680de0d2162a8
@@ -0,0 +1,2 @@
1
+ --exclude /templates/
2
+ --quiet
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Gem Version](https://badge.fury.io/rb/action-cable-testing.svg)](https://rubygems.org/gems/action-cable-testing) [![Build Status](https://travis-ci.org/palkan/action-cable-testing.svg?branch=master)](https://travis-ci.org/palkan/action-cable-testing)
1
+ [![Gem Version](https://badge.fury.io/rb/action-cable-testing.svg)](https://rubygems.org/gems/action-cable-testing) [![Build Status](https://travis-ci.org/palkan/action-cable-testing.svg?branch=master)](https://travis-ci.org/palkan/action-cable-testing) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/action-cable-testing)
2
2
 
3
3
  # Action Cable Testing
4
4
 
@@ -21,7 +21,117 @@ And then execute:
21
21
 
22
22
  ## Usage
23
23
 
24
- TBD
24
+ [Documentation](http://www.rubydoc.info/gems/action-cable-testing)
25
+
26
+ ### Test Adapter and Broadcasting
27
+
28
+ We add `ActionCable::SubscriptionAdapter::Test` (very similar Active Job and Action Mailer tests adapters) and `ActionCable::TestCase` with a couple of matchers to track broadcasting messages in our tests:
29
+
30
+ ```ruby
31
+ # Using ActionCable::TestCase
32
+ class MyCableTest < ActionCable::TestCase
33
+ def test_broadcasts
34
+ # Check the number of messages broadcasted to the stream
35
+ assert_broadcasts 'messages', 0
36
+ ActionCable.server.broadcast 'messages', { text: 'hello' }
37
+ assert_broadcasts 'messages', 1
38
+
39
+ # Check the number of messages broadcasted to the stream within a block
40
+ assert_broadcasts('messages', 1) do
41
+ ActionCable.server.broadcast 'messages', { text: 'hello' }
42
+ end
43
+
44
+ # Check that no broadcasts has been made
45
+ assert_no_broadcasts('messages') do
46
+ ActionCable.server.broadcast 'another_stream', { text: 'hello' }
47
+ end
48
+ end
49
+ end
50
+
51
+ # Or including ActionCable::TestHelper
52
+ class ExampleTest < ActionDispatch::IntegrationTest
53
+ include ActionCable::TestHelper
54
+
55
+ def test_broadcasts
56
+ room = rooms(:office)
57
+
58
+ assert_broadcast_on("messages:#{room.id}", text: 'Hello!') do
59
+ post "/say/#{room.id}", xhr: true, params: { message: 'Hello!' }
60
+ end
61
+ end
62
+ end
63
+ ```
64
+
65
+ ### Channels Testing
66
+
67
+ Channels tests are written as follows:
68
+ 1. First, one uses the `subscribe` method to simulate subscription creation.
69
+ 2. Then, one asserts whether the current state is as expected. "State" can be anything:
70
+ transmitted messages, subscribed streams, etc.
71
+
72
+ For example:
73
+
74
+ ```ruby
75
+ class ChatChannelTest < ActionCable::Channel::TestCase
76
+ def test_subscribed_with_room_number
77
+ # Simulate a subscription creation
78
+ subscribe room_number: 1
79
+
80
+ # Asserts that the subscription was successfully created
81
+ assert subscription.confirmed?
82
+
83
+ # Asserts that the channel subscribes connection to a stream
84
+ assert "chat_1", streams.last
85
+ end
86
+
87
+ def test_does_not_subscribe_without_room_number
88
+ subscribe
89
+
90
+ # Asserts that the subscription was rejected
91
+ assert subscription.rejected?
92
+ end
93
+ end
94
+ ```
95
+
96
+ You can also perform actions:
97
+
98
+ ```ruby
99
+ def test_perform_speak
100
+ subscribe room_number: 1
101
+
102
+ perform :speak, message: "Hello, Rails!"
103
+
104
+ # `transmissions` stores messages sent directly to the channel (i.e. with `transmit` method)
105
+ assert_equal "Hello, Rails!", transmissions.last["message"]["text"]
106
+ end
107
+ ```
108
+
109
+ You can set up your connection identifiers:
110
+
111
+ ```ruby
112
+ class ChatChannelTest < ActionCable::Channel::TestCase
113
+ include ActionCable::TestHelper
114
+
115
+ def test_identifiers
116
+ stub_connection(user: users[:john])
117
+
118
+ subscribe room_number: 1
119
+
120
+ assert_broadcasts_on("messages_1", text: "I'm here!", from: "John") do
121
+ perform :speak, message: "I'm here!"
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ ### Generators
128
+
129
+ This gem also provides Rails generators:
130
+
131
+ ```sh
132
+ # Generate a channel test case for ChatChannel
133
+ rails generate test_unit:channel chat
134
+ ```
25
135
 
26
136
  ## Development
27
137
 
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/testing"
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/test_case"
5
+ require "active_support/core_ext/hash/indifferent_access"
6
+ require "json"
7
+
8
+ module ActionCable
9
+ module Channel
10
+ class NonInferrableChannelError < ::StandardError
11
+ def initialize(name)
12
+ super "Unable to determine the channel to test from #{name}. " +
13
+ "You'll need to specify it using tests YourChannel in your " +
14
+ "test case definition."
15
+ end
16
+ end
17
+
18
+ # Stub `stream_from` to track streams for the channel.
19
+ # Add public aliases for `subscription_confirmation_sent?` and
20
+ # `subscription_rejected?`.
21
+ module ChannelStub
22
+ def confirmed?
23
+ subscription_confirmation_sent?
24
+ end
25
+
26
+ def rejected?
27
+ subscription_rejected?
28
+ end
29
+
30
+ def stream_from(broadcasting, *)
31
+ streams << broadcasting
32
+ end
33
+
34
+ def stop_all_streams
35
+ @_streams = []
36
+ end
37
+
38
+ def streams
39
+ @_streams ||= []
40
+ end
41
+ end
42
+
43
+ class ConnectionStub
44
+ attr_reader :transmissions, :identifiers, :subscriptions, :logger
45
+
46
+ def initialize(identifiers = {})
47
+ @transmissions = []
48
+
49
+ identifiers.each do |identifier, val|
50
+ define_singleton_method(identifier) { val }
51
+ end
52
+
53
+ @subscriptions = ActionCable::Connection::Subscriptions.new(self)
54
+ @identifiers = identifiers.keys
55
+ @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
56
+ end
57
+
58
+ def transmit(cable_message)
59
+ transmissions << cable_message.with_indifferent_access
60
+ end
61
+ end
62
+
63
+ # Superclass for Action Cable channel functional tests.
64
+ #
65
+ # == Basic example
66
+ #
67
+ # Functional tests are written as follows:
68
+ # 1. First, one uses the +subscribe+ method to simulate subscription creation.
69
+ # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
70
+ # transmitted messages, subscribed streams, etc.
71
+ #
72
+ # For example:
73
+ #
74
+ # class ChatChannelTest < ActionCable::Channel::TestCase
75
+ # def test_subscribed_with_room_number
76
+ # # Simulate a subscription creation
77
+ # subscribe room_number: 1
78
+ #
79
+ # # Asserts that the subscription was successfully created
80
+ # assert subscription.confirmed?
81
+ #
82
+ # # Asserts that the channel subscribes connection to a stream
83
+ # assert "chat_1", streams.last
84
+ # end
85
+ #
86
+ # def test_does_not_subscribe_without_room_number
87
+ # subscribe
88
+ #
89
+ # # Asserts that the subscription was rejected
90
+ # assert subscription.rejected?
91
+ # end
92
+ # end
93
+ #
94
+ # You can also perform actions:
95
+ # def test_perform_speak
96
+ # subscribe room_number: 1
97
+ #
98
+ # perform :speak, message: "Hello, Rails!"
99
+ #
100
+ # assert_equal "Hello, Rails!", transmissions.last["message"]["text"]
101
+ # end
102
+ #
103
+ # == Special methods
104
+ #
105
+ # ActionCable::Channel::TestCase will also automatically provide the following instance
106
+ # methods for use in the tests:
107
+ #
108
+ # <b>connection</b>::
109
+ # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
110
+ # <b>subscription</b>::
111
+ # An instance of the current channel, created when you call `subscribe`.
112
+ # <b>transmissions</b>::
113
+ # A list of all messages that have been transmitted into the connection (without encoding).
114
+ # <b>streams</b>::
115
+ # A list of all created streams subscriptions (as identifiers) for the subscription.
116
+ #
117
+ #
118
+ # == Channel is automatically inferred
119
+ #
120
+ # ActionCable::Channel::TestCase will automatically infer the channel under test
121
+ # from the test class name. If the channel cannot be inferred from the test
122
+ # class name, you can explicitly set it with +tests+.
123
+ #
124
+ # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
125
+ # tests SpecialChannel
126
+ # end
127
+ #
128
+ # == Specifying connection identifiers
129
+ #
130
+ # You need to set up your connection manually to privide values for the identifiers.
131
+ # To do this just use:
132
+ #
133
+ # stub_connection(user: users[:john])
134
+ class TestCase < ActiveSupport::TestCase
135
+ module Behavior
136
+ extend ActiveSupport::Concern
137
+
138
+ include ActiveSupport::Testing::ConstantLookup
139
+ include ActionCable::TestHelper
140
+
141
+ included do
142
+ class_attribute :_channel_class
143
+
144
+ attr_reader :subscription
145
+ delegate :transmissions, to: :connection
146
+ delegate :streams, to: :subscription
147
+
148
+ ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
149
+ end
150
+
151
+ module ClassMethods
152
+ def tests(channel)
153
+ case channel
154
+ when String, Symbol
155
+ self._channel_class = channel.to_s.camelize.constantize
156
+ when Module
157
+ self._channel_class = channel
158
+ else
159
+ raise NonInferrableChannelError.new(channel)
160
+ end
161
+ end
162
+
163
+ def channel_class
164
+ if channel = self._channel_class
165
+ channel
166
+ else
167
+ tests determine_default_channel(name)
168
+ end
169
+ end
170
+
171
+ def determine_default_channel(name)
172
+ channel = determine_constant_from_test_name(name) do |constant|
173
+ Class === constant && constant < ActionCable::Channel::Base
174
+ end
175
+ raise NonInferrableChannelError.new(name) if channel.nil?
176
+ channel
177
+ end
178
+ end
179
+
180
+ # Setup test connection with the specified identifiers:
181
+ #
182
+ # class ApplicationCable < ActionCable::Connection::Base
183
+ # identified_by :user, :token
184
+ # end
185
+ #
186
+ # stub_connection(user: users[:john], token: 'my-secret-token')
187
+ def stub_connection(identifiers = {})
188
+ @connection = ConnectionStub.new(identifiers)
189
+ end
190
+
191
+ # Subsribe to the channel under test. Optionally pass subscription parameters as a Hash.
192
+ def subscribe(params = {})
193
+ @subscription = self.class.channel_class.new(connection, "test_stub", params.with_indifferent_access)
194
+ @subscription.singleton_class.include(ChannelStub)
195
+ @subscription.subscribe_to_channel
196
+ @subscription
197
+ end
198
+
199
+ # Perform action on a channel.
200
+ #
201
+ # NOTE: Must be subscribed.
202
+ def perform(action, data = {})
203
+ check_subscribed!
204
+ subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
205
+ end
206
+
207
+ def connection # :nodoc:
208
+ @connection ||= stub_connection
209
+ end
210
+
211
+ private
212
+ def check_subscribed!
213
+ raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
214
+ end
215
+ end
216
+
217
+ include Behavior
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module SubscriptionAdapter
5
+ # == Test adapter for Action Cable
6
+ #
7
+ # The test adapter should be used only in testing. Along with
8
+ # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
9
+ #
10
+ # To use the test adapter set adapter value to +test+ in your +cable.yml+.
11
+ class Test < Base
12
+ def broadcast(channel, payload)
13
+ broadcasts(channel) << payload
14
+ end
15
+
16
+ def broadcasts(channel)
17
+ channels_data[channel] ||= []
18
+ end
19
+
20
+ def clear_messages(channel)
21
+ channels_data[channel] = []
22
+ end
23
+
24
+ def reset!
25
+ @channels_data = nil
26
+ end
27
+
28
+ private
29
+ def channels_data
30
+ @channels_data ||= {}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/test_case"
4
+
5
+ module ActionCable
6
+ class TestCase < ActiveSupport::TestCase
7
+ include ActionCable::TestHelper
8
+
9
+ ActiveSupport.run_load_hooks(:action_cable_test_case, self)
10
+ end
11
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ # Provides helper methods for testing Action Cable broadcasting
5
+ module TestHelper
6
+ def before_setup # :nodoc:
7
+ server = ActionCable.server
8
+ test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
9
+
10
+ @old_pubsub_adapter = server.pubsub
11
+
12
+ server.instance_variable_set(:@pubsub, test_adapter)
13
+ super
14
+ end
15
+
16
+ def after_teardown # :nodoc:
17
+ super
18
+ ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
19
+ end
20
+
21
+ # Asserts that the number of broadcasted messages to the channel matches the given number.
22
+ #
23
+ # def test_broadcasts
24
+ # assert_broadcasts 'messages', 0
25
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
26
+ # assert_broadcasts 'messages', 1
27
+ # ActionCable.server.broadcast 'messages', { text: 'world' }
28
+ # assert_broadcasts 'messages', 2
29
+ # end
30
+ #
31
+ # If a block is passed, that block should cause the specified number of
32
+ # messages to be broadcasted.
33
+ #
34
+ # def test_broadcasts_again
35
+ # assert_broadcasts('messages', 1) do
36
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
37
+ # end
38
+ #
39
+ # assert_broadcasts('messages', 2) do
40
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
41
+ # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
42
+ # end
43
+ # end
44
+ #
45
+ def assert_broadcasts(channel, number)
46
+ if block_given?
47
+ original_count = broadcasts_size(channel)
48
+ yield
49
+ new_count = broadcasts_size(channel)
50
+ assert_equal number, new_count - original_count, "#{number} broadcasts to #{channel} expected, but #{new_count - original_count} were sent"
51
+ else
52
+ actual_count = broadcasts_size(channel)
53
+ assert_equal number, actual_count, "#{number} broadcasts to #{channel} expected, but #{actual_count} were sent"
54
+ end
55
+ end
56
+
57
+ # Asserts that no messages have been sent to the channel.
58
+ #
59
+ # def test_no_broadcasts
60
+ # assert_no_broadcasts 'messages'
61
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
62
+ # assert_broadcasts 'messages', 1
63
+ # end
64
+ #
65
+ # If a block is passed, that block should not cause any message to be sent.
66
+ #
67
+ # def test_broadcasts_again
68
+ # assert_no_broadcasts 'messages' do
69
+ # # No job messages should be sent from this block
70
+ # end
71
+ # end
72
+ #
73
+ # Note: This assertion is simply a shortcut for:
74
+ #
75
+ # assert_broadcasts 'messages', 0, &block
76
+ #
77
+ def assert_no_broadcasts(channel, &block)
78
+ assert_broadcasts channel, 0, &block
79
+ end
80
+
81
+ # Asserts that the specified message has been sent to the channel.
82
+ #
83
+ # def test_assert_transmited_message
84
+ # ActionCable.server.broadcast 'messages', text: 'hello'
85
+ # assert_broadcast_on('messages', text: 'hello')
86
+ # end
87
+ #
88
+ # If a block is passed, that block should cause a message with the specified data to be sent.
89
+ #
90
+ # def test_assert_broadcast_on_again
91
+ # assert_broadcast_on('messages', text: 'hello') do
92
+ # ActionCable.server.broadcast 'messages', text: 'hello'
93
+ # end
94
+ # end
95
+ #
96
+ def assert_broadcast_on(channel, data)
97
+ # Encode to JSON and back–we want to use this value to compare
98
+ # with decoded JSON.
99
+ # Comparing JSON strings doesn't work due to the order if the keys.
100
+ serialized_msg =
101
+ ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
102
+ new_messages = broadcasts(channel)
103
+ if block_given?
104
+ old_messages = new_messages
105
+ clear_messages(channel)
106
+
107
+ yield
108
+ new_messages = broadcasts(channel)
109
+ clear_messages(channel)
110
+
111
+ # Restore all sent messages
112
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(channel, m) }
113
+ end
114
+
115
+ new_messages = new_messages.map { |msg| ActiveSupport::JSON.decode(msg) }
116
+
117
+ assert_includes new_messages, serialized_msg, "No messages sent with #{data} to #{channel}"
118
+ end
119
+
120
+ def pubsub_adapter # :nodoc:
121
+ ActionCable.server.pubsub
122
+ end
123
+
124
+ delegate :broadcasts, :clear_messages, to: :pubsub_adapter
125
+
126
+ private
127
+ def broadcasts_size(channel) # :nodoc:
128
+ broadcasts(channel).size
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/testing/version"
4
+
5
+ require "action_cable"
6
+
7
+ module ActionCable
8
+ autoload :TestCase
9
+ autoload :TestHelper
10
+
11
+ module Channel
12
+ eager_autoload do
13
+ autoload :TestCase
14
+ end
15
+ end
16
+
17
+ module SubscriptionAdapter
18
+ autoload :Test
19
+ end
20
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActionCable
4
4
  module Testing
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.5"
6
6
  end
7
7
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/test_unit"
4
+
5
+ module TestUnit # :nodoc:
6
+ module Generators # :nodoc:
7
+ class ChannelGenerator < Base # :nodoc:
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ check_class_collision suffix: "ChannelTest"
11
+
12
+ def create_test_file
13
+ template "unit_test.rb.erb", File.join("test/channels", class_path, "#{file_name}_channel_test.rb")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name %>ChannelTest < ActionCable::Channel::TestCase
5
+ # test "the truth" do
6
+ # assert true
7
+ # end
8
+ end
9
+ <% end -%>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action-cable-testing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '5.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ammeter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.1'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rubocop
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -101,10 +115,19 @@ executables: []
101
115
  extensions: []
102
116
  extra_rdoc_files: []
103
117
  files:
118
+ - ".yardopts"
104
119
  - CHANGELOG.md
105
120
  - LICENSE.txt
106
121
  - README.md
122
+ - lib/action-cable-testing.rb
123
+ - lib/action_cable/channel/test_case.rb
124
+ - lib/action_cable/subscription_adapter/test.rb
125
+ - lib/action_cable/test_case.rb
126
+ - lib/action_cable/test_helper.rb
127
+ - lib/action_cable/testing.rb
107
128
  - lib/action_cable/testing/version.rb
129
+ - lib/generators/test_unit/channel/channel_generator.rb
130
+ - lib/generators/test_unit/channel/templates/unit_test.rb.erb
108
131
  homepage: http://github.com/palkan/action-cable-testing
109
132
  licenses:
110
133
  - MIT