Skip to content

Commit 1313ecd

Browse files
authored
Add opt-in CodeTeams test helpers (#23)
* feat: add CodeTeams::Testing helpers Adds an opt-in testing helper for creating in-memory teams and exposing them via CodeTeams.all/find. * feat: add RSpec helper integration Provides an opt-in RSpec helper (code_team_with_config) and per-example cleanup for in-memory teams. * feat: make RSpecHelpers inclusion opt-in Stop auto-including CodeTeams::RSpecHelpers; clients can include it in their own RSpec config. The testing integration remains opt-in via requiring code_teams/rspec. * refactor: make Testing.enable! client-controlled Move RSpec configuration into enable! method so clients explicitly opt-in rather than having it auto-run on require. This gives consumers control over when testing infrastructure is activated. * test: update specs to call enable! explicitly Specs now call CodeTeams::Testing.enable! at the top level before RSpec.describe, matching the intended client usage pattern. * docs: add Testing section to README Document how to use code_teams/rspec and Testing.enable! for creating temporary teams in specs. * chore: update unicode-emoji gem version * refactor: have testing.rb require rspec.rb This eliminates the conditional check for RSpecHelpers and simplifies client usage - they only need to require code_teams/testing. * refactor: rename rspec.rb to rspec_helpers.rb File name now matches the module it defines. * refactor: move deep_stringify_keys to Utils Consolidates utility methods in one place. * refactor: improve type signatures - Add typed: strict sigil and signatures to Utils - Use T::Hash[Symbol, T.untyped] for create_code_team attributes - Fix nilable string handling in Plugin.default_data_accessor_name * chore: remove accidentally committed local files These belong in .git/info/exclude, not in the repo. * test: remove redundant before/after blocks The enable! around hook handles cleanup automatically. * docs: simplify testing example to use built-in properties only * refactor: improve more type signatures - inherited(base) now typed as T.class_of(Plugin) instead of T.untyped - raw_hash now typed as T::Hash[String, T.untyped] since YAML produces string keys * chore: bump version to 1.2.0 * Install 1.2.0 * refactor: nest RSpecHelpers under Testing namespace Move RSpecHelpers to CodeTeams::Testing::RSpecHelpers as suggested in PR review. This better reflects the module hierarchy since RSpecHelpers is part of the testing utilities.
1 parent 250a271 commit 1313ecd

File tree

9 files changed

+200
-8
lines changed

9 files changed

+200
-8
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ GEM
9999
thor (1.3.2)
100100
unicode-display_width (3.1.4)
101101
unicode-emoji (~> 4.0, >= 4.0.4)
102-
unicode-emoji (4.0.4)
102+
unicode-emoji (4.2.0)
103103
yard (0.9.37)
104104
yard-sorbet (0.9.0)
105105
sorbet-runtime

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,32 @@ if errors.any?
111111
end
112112
```
113113

114+
## Testing
115+
116+
`code_teams` provides test helpers for creating temporary teams in your specs without writing YML files to disk. Add the following to your `spec_helper.rb` (or `rails_helper.rb`):
117+
118+
```ruby
119+
require 'code_teams/testing'
120+
121+
CodeTeams::Testing.enable!
122+
```
123+
124+
This gives you:
125+
- A `code_team_with_config` helper method available in all specs
126+
- Automatic cleanup of testing teams between examples
127+
128+
Example usage in a spec:
129+
130+
```ruby
131+
RSpec.describe 'my feature' do
132+
it 'works with a team' do
133+
team = code_team_with_config(name: 'Test Team')
134+
135+
expect(CodeTeams.find('Test Team')).to eq(team)
136+
end
137+
end
138+
```
139+
114140
## Contributing
115141

116142
Bug reports and pull requests are welcome!

lib/code_teams.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def self.from_yml(config_yml)
8484
)
8585
end
8686

87-
sig { params(raw_hash: T::Hash[T.untyped, T.untyped]).returns(Team) }
87+
sig { params(raw_hash: T::Hash[String, T.untyped]).returns(Team) }
8888
def self.from_hash(raw_hash)
8989
new(
9090
config_yml: nil,
@@ -103,7 +103,7 @@ def self.register_plugins
103103
end
104104
end
105105

106-
sig { returns(T::Hash[T.untyped, T.untyped]) }
106+
sig { returns(T::Hash[String, T.untyped]) }
107107
attr_reader :raw_hash
108108

109109
sig { returns(T.nilable(String)) }
@@ -112,7 +112,7 @@ def self.register_plugins
112112
sig do
113113
params(
114114
config_yml: T.nilable(String),
115-
raw_hash: T::Hash[T.untyped, T.untyped]
115+
raw_hash: T::Hash[String, T.untyped]
116116
).void
117117
end
118118
def initialize(config_yml:, raw_hash:)

lib/code_teams/plugin.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ def self.data_accessor_name(key = default_data_accessor_name)
2525
sig { returns(String) }
2626
def self.default_data_accessor_name
2727
# e.g., MyNamespace::MyPlugin -> my_plugin
28-
Utils.underscore(Utils.demodulize(name))
28+
Utils.underscore(Utils.demodulize(T.must(name)))
2929
end
3030

31-
sig { params(base: T.untyped).void }
31+
sig { params(base: T.class_of(Plugin)).void }
3232
def self.inherited(base) # rubocop:disable Lint/MissingSuper
33-
all_plugins << T.cast(base, T.class_of(Plugin))
33+
all_plugins << base
3434
end
3535

3636
sig { returns(T::Array[T.class_of(Plugin)]) }

lib/code_teams/testing.rb

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# frozen_string_literal: true
2+
#
3+
# typed: strict
4+
5+
require 'securerandom'
6+
require 'code_teams'
7+
require 'code_teams/testing/rspec_helpers'
8+
9+
module CodeTeams
10+
# Utilities for tests that need a controlled set of teams without writing YML
11+
# files to disk.
12+
#
13+
# Opt-in by requiring `code_teams/testing`.
14+
module Testing
15+
extend T::Sig
16+
17+
THREAD_KEY = T.let(:__code_teams_collection, Symbol)
18+
@enabled = T.let(false, T::Boolean)
19+
20+
sig { void }
21+
def self.enable!
22+
return if @enabled
23+
24+
CodeTeams.prepend(CodeTeamsExtension)
25+
@enabled = true
26+
27+
return unless defined?(RSpec)
28+
29+
T.unsafe(RSpec).configure do |config|
30+
config.include CodeTeams::Testing::RSpecHelpers
31+
32+
config.around do |example|
33+
example.run
34+
# Bust caches because plugins may hang onto stale data between examples.
35+
if CodeTeams::Testing.code_teams.any?
36+
CodeTeams.bust_caches!
37+
CodeTeams::Testing.reset!
38+
end
39+
end
40+
end
41+
end
42+
43+
sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(CodeTeams::Team) }
44+
def self.create_code_team(attributes)
45+
attributes = attributes.dup
46+
attributes[:name] ||= "Fake Team #{SecureRandom.hex(4)}"
47+
48+
code_team = CodeTeams::Team.new(
49+
config_yml: 'tmp/fake_config.yml',
50+
raw_hash: Utils.deep_stringify_keys(attributes)
51+
)
52+
53+
code_teams << code_team
54+
code_team
55+
end
56+
57+
sig { returns(T::Array[CodeTeams::Team]) }
58+
def self.code_teams
59+
existing = Thread.current[THREAD_KEY]
60+
return existing if existing.is_a?(Array)
61+
62+
Thread.current[THREAD_KEY] = []
63+
T.cast(Thread.current[THREAD_KEY], T::Array[CodeTeams::Team])
64+
end
65+
66+
sig { void }
67+
def self.reset!
68+
Thread.current[THREAD_KEY] = []
69+
end
70+
71+
module CodeTeamsExtension
72+
extend T::Sig
73+
74+
sig { params(base: Module).void }
75+
def self.prepended(base)
76+
base.singleton_class.prepend(ClassMethods)
77+
end
78+
79+
module ClassMethods
80+
extend T::Sig
81+
82+
sig { returns(T::Array[CodeTeams::Team]) }
83+
def all
84+
CodeTeams::Testing.code_teams + super
85+
end
86+
87+
sig { params(name: String).returns(T.nilable(CodeTeams::Team)) }
88+
def find(name)
89+
CodeTeams::Testing.code_teams.find { |t| t.name == name } || super
90+
end
91+
end
92+
end
93+
end
94+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
#
3+
# typed: false
4+
5+
require 'securerandom'
6+
require 'code_teams/testing'
7+
8+
module CodeTeams
9+
module Testing
10+
module RSpecHelpers
11+
def code_team_with_config(team_config = {})
12+
team_config = team_config.dup
13+
team_config[:name] ||= "Fake Team #{SecureRandom.hex(4)}"
14+
CodeTeams::Testing.create_code_team(team_config)
15+
end
16+
end
17+
end
18+
end

lib/code_teams/utils.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
# frozen_string_literal: true
2+
#
3+
# typed: strict
4+
15
module CodeTeams
26
module Utils
7+
extend T::Sig
8+
39
module_function
410

11+
sig { params(string: String).returns(String) }
512
def underscore(string)
613
string.gsub('::', '/')
714
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -10,8 +17,24 @@ def underscore(string)
1017
.downcase
1118
end
1219

20+
sig { params(string: String).returns(String) }
1321
def demodulize(string)
14-
string.split('::').last
22+
T.must(string.split('::').last)
23+
end
24+
25+
# Recursively converts symbol keys to strings. Top-level input should be a Hash.
26+
sig { params(value: T.untyped).returns(T.untyped) }
27+
def deep_stringify_keys(value)
28+
case value
29+
when Hash
30+
value.each_with_object({}) do |(k, v), acc|
31+
acc[k.to_s] = deep_stringify_keys(v)
32+
end
33+
when Array
34+
value.map { |v| deep_stringify_keys(v) }
35+
else
36+
value
37+
end
1538
end
1639
end
1740
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'code_teams/testing'
2+
3+
CodeTeams::Testing.enable!
4+
5+
RSpec.describe CodeTeams::Testing::RSpecHelpers do
6+
it 'exposes code_team_with_config and makes the team discoverable' do
7+
code_team_with_config(name: 'RSpec Team')
8+
9+
expect(CodeTeams.find('RSpec Team')).not_to be_nil
10+
end
11+
12+
it 'cleans up testing teams between examples' do
13+
expect(CodeTeams::Testing.code_teams).to be_empty
14+
expect(CodeTeams.find('RSpec Team')).to be_nil
15+
end
16+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require 'code_teams/testing'
2+
3+
CodeTeams::Testing.enable!
4+
5+
RSpec.describe CodeTeams::Testing do
6+
describe '.create_code_team' do
7+
it 'adds the team to CodeTeams.all and CodeTeams.find' do
8+
team = described_class.create_code_team({ name: 'Temp Team', extra_data: { foo: { bar: 1 } } })
9+
10+
expect(CodeTeams.all).to include(team)
11+
expect(CodeTeams.find('Temp Team')).to eq(team)
12+
expect(team.raw_hash.dig('extra_data', 'foo', 'bar')).to eq(1)
13+
end
14+
end
15+
end

0 commit comments

Comments
 (0)