action-cable-testing 0.0.1 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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