zenspec 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +637 -0
- data/Rakefile +12 -0
- data/examples/progress_loader_demo.rb +113 -0
- data/lib/zenspec/formatters/progress_bar_formatter.rb +433 -0
- data/lib/zenspec/formatters/progress_formatter.rb +146 -0
- data/lib/zenspec/helpers/graphql_helpers.rb +103 -0
- data/lib/zenspec/helpers.rb +10 -0
- data/lib/zenspec/matchers/graphql_matchers.rb +358 -0
- data/lib/zenspec/matchers/graphql_type_matchers.rb +554 -0
- data/lib/zenspec/matchers/interactor_matchers.rb +216 -0
- data/lib/zenspec/matchers.rb +12 -0
- data/lib/zenspec/progress_loader.rb +155 -0
- data/lib/zenspec/railtie.rb +26 -0
- data/lib/zenspec/shoulda_config.rb +17 -0
- data/lib/zenspec/version.rb +5 -0
- data/lib/zenspec.rb +14 -0
- data/sig/zenspec.rbs +4 -0
- metadata +121 -0
data/README.md
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
# Zenspec
|
|
2
|
+
|
|
3
|
+
A comprehensive RSpec matcher library for testing GraphQL APIs, Interactor service objects, and Rails applications. Includes Shoulda Matchers integration for clean, expressive tests.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "zenspec"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install zenspec
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **GraphQL Matchers** - Test GraphQL queries, mutations, types, and schemas
|
|
28
|
+
- **Interactor Matchers** - Test service objects with clean syntax
|
|
29
|
+
- **GraphQL Helpers** - Execute queries and mutations with ease
|
|
30
|
+
- **Progress Loader** - Docker-style progress bars for terminal output
|
|
31
|
+
- **Shoulda Matchers** - Automatically configured for Rails models and controllers
|
|
32
|
+
- **Rails Integration** - Automatically loads in Rails applications
|
|
33
|
+
- **Non-Rails Support** - Works in any Ruby project
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# In your spec/rails_helper.rb or spec/spec_helper.rb
|
|
39
|
+
require "zenspec"
|
|
40
|
+
|
|
41
|
+
# Everything is automatically configured!
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Progress Loader
|
|
47
|
+
|
|
48
|
+
Docker-style progress bars for displaying terminal progress.
|
|
49
|
+
|
|
50
|
+
### RSpec Formatters
|
|
51
|
+
|
|
52
|
+
Zenspec includes two custom RSpec formatters for displaying test progress:
|
|
53
|
+
|
|
54
|
+
#### Progress Bar Formatter (Recommended)
|
|
55
|
+
|
|
56
|
+
Clean, colorful output with status icons:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Use the progress bar formatter
|
|
60
|
+
rspec --require zenspec/formatters/progress_bar_formatter \
|
|
61
|
+
--format ProgressBarFormatter
|
|
62
|
+
|
|
63
|
+
# Or add to .rspec file:
|
|
64
|
+
# --require zenspec/formatters/progress_bar_formatter
|
|
65
|
+
# --format ProgressBarFormatter
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Output example:
|
|
69
|
+
```
|
|
70
|
+
✔ user_spec.rb creates a new user successfully [10% 1/10]
|
|
71
|
+
✔ user_spec.rb validates email presence [20% 2/10]
|
|
72
|
+
⠿ user_spec.rb --> sends welcome email [30% 3/10]
|
|
73
|
+
✗ post_spec.rb publishes post with valid data [40% 4/10]
|
|
74
|
+
⊘ auth_spec.rb authenticates with OAuth [50% 5/10]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Format:
|
|
78
|
+
- **Running tests** show an arrow (-->) between filename and description
|
|
79
|
+
- **Completed tests** (passed/failed/pending) show only a space between filename and description
|
|
80
|
+
|
|
81
|
+
Colors:
|
|
82
|
+
- **Green** ✔ - Passed tests
|
|
83
|
+
- **Red** ✗ - Failed tests
|
|
84
|
+
- **Yellow** ⠿ - Currently running test (with arrow)
|
|
85
|
+
- **Cyan** ⊘ - Pending/skipped tests
|
|
86
|
+
|
|
87
|
+
#### Docker-Style Progress Formatter
|
|
88
|
+
|
|
89
|
+
Alternative formatter with Docker-style progress bars:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Use the Docker-style formatter
|
|
93
|
+
rspec --require zenspec/formatters/progress_formatter \
|
|
94
|
+
--format Zenspec::Formatters::ProgressFormatter
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Output example:
|
|
98
|
+
```
|
|
99
|
+
[=====================> ] 65% 13/20 spec/models/user_spec.rb:42
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Programmatic Usage
|
|
103
|
+
|
|
104
|
+
Use the progress loader in your code:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Basic usage
|
|
108
|
+
loader = Zenspec::ProgressLoader.new(total: 100, description: "Processing files")
|
|
109
|
+
|
|
110
|
+
100.times do |i|
|
|
111
|
+
# Do some work...
|
|
112
|
+
loader.update(i + 1, description: "Processing file #{i + 1}/100")
|
|
113
|
+
sleep(0.1)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
loader.finish(description: "Processing complete!")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Custom Width
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# Custom progress bar width (default is 40)
|
|
123
|
+
loader = Zenspec::ProgressLoader.new(
|
|
124
|
+
total: 50,
|
|
125
|
+
width: 60,
|
|
126
|
+
description: "Downloading layers"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
loader.increment(description: "Layer 1/50")
|
|
130
|
+
loader.increment(description: "Layer 2/50")
|
|
131
|
+
# ...
|
|
132
|
+
loader.finish
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Time Estimates
|
|
136
|
+
|
|
137
|
+
The progress loader automatically calculates and displays time information:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
loader = Zenspec::ProgressLoader.new(total: 1000)
|
|
141
|
+
|
|
142
|
+
loader.update(250)
|
|
143
|
+
# Output: [==========> ] 25% 250/1000 1s/4s
|
|
144
|
+
|
|
145
|
+
loader.update(500)
|
|
146
|
+
# Output: [====================> ] 50% 500/1000 2s/4s
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Interactor Matchers
|
|
152
|
+
|
|
153
|
+
### Basic Usage
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
RSpec.describe CreateUser do
|
|
157
|
+
subject(:result) { described_class.call(params) }
|
|
158
|
+
|
|
159
|
+
context "with valid params" do
|
|
160
|
+
let(:params) { { name: "John", email: "john@example.com" } }
|
|
161
|
+
|
|
162
|
+
# Shoulda-style one-liners
|
|
163
|
+
it { is_expected.to succeed }
|
|
164
|
+
it { is_expected.to set_context(:user) }
|
|
165
|
+
it { is_expected.to succeed.with_data(kind_of(User)) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context "with invalid params" do
|
|
169
|
+
let(:params) { { name: "", email: "invalid" } }
|
|
170
|
+
|
|
171
|
+
it { is_expected.to fail_interactor }
|
|
172
|
+
it { is_expected.to fail_interactor.with_error("validation_failed") }
|
|
173
|
+
it { is_expected.to have_error_code("validation_failed") }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Available Matchers
|
|
179
|
+
|
|
180
|
+
#### `succeed`
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
expect(result).to succeed
|
|
184
|
+
expect(result).to succeed.with_data("expected value")
|
|
185
|
+
expect(result).to succeed.with_context(:user_id, 123)
|
|
186
|
+
expect(CreateUser).to succeed # Works with classes too
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `fail_interactor`
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
expect(result).to fail_interactor
|
|
193
|
+
expect(result).to fail_interactor.with_error("invalid_input")
|
|
194
|
+
expect(result).to fail_interactor.with_errors("error1", "error2")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### `have_error_code` / `have_error_codes`
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
expect(result).to have_error_code("not_found")
|
|
201
|
+
expect(result).to have_error_codes("error1", "error2")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### `set_context`
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
expect(result).to set_context(:user)
|
|
208
|
+
expect(result).to set_context(:user_id, 123)
|
|
209
|
+
expect(result).to set_context(:data).to(expected_value)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## GraphQL Response Matchers
|
|
215
|
+
|
|
216
|
+
### Basic Usage
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
RSpec.describe "User Queries" do
|
|
220
|
+
subject(:result) { graphql_execute_as(user, query, variables: { id: user.id }) }
|
|
221
|
+
|
|
222
|
+
let(:query) do
|
|
223
|
+
<<~GQL
|
|
224
|
+
query GetUser($id: ID!) {
|
|
225
|
+
user(id: $id) {
|
|
226
|
+
id
|
|
227
|
+
name
|
|
228
|
+
email
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
GQL
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Shoulda-style
|
|
235
|
+
it { is_expected.to succeed_graphql }
|
|
236
|
+
it { is_expected.to have_graphql_data("user") }
|
|
237
|
+
it { is_expected.to have_graphql_data("user", "name").with_value("John") }
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Available Matchers
|
|
242
|
+
|
|
243
|
+
#### `have_graphql_data`
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# Basic checks
|
|
247
|
+
expect(result).to have_graphql_data
|
|
248
|
+
expect(result).to have_graphql_data("user")
|
|
249
|
+
expect(result).to have_graphql_data("user", "id")
|
|
250
|
+
|
|
251
|
+
# With specific value
|
|
252
|
+
expect(result).to have_graphql_data("user", "name").with_value("John Doe")
|
|
253
|
+
|
|
254
|
+
# Partial matching (subset)
|
|
255
|
+
expect(result).to have_graphql_data("user").matching(
|
|
256
|
+
id: "123",
|
|
257
|
+
name: "John"
|
|
258
|
+
# other fields can exist but aren't checked
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Inclusion (contains these values)
|
|
262
|
+
expect(result).to have_graphql_data("user").that_includes(name: "John")
|
|
263
|
+
|
|
264
|
+
# Presence checks
|
|
265
|
+
expect(result).to have_graphql_data("users").that_is_present
|
|
266
|
+
expect(result).to have_graphql_data("deletedAt").that_is_null
|
|
267
|
+
|
|
268
|
+
# Array count
|
|
269
|
+
expect(result).to have_graphql_data("users").with_count(5)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### `have_graphql_errors`
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
# Basic error check
|
|
276
|
+
expect(result).to have_graphql_errors
|
|
277
|
+
|
|
278
|
+
# With specific message
|
|
279
|
+
expect(result).to have_graphql_error.with_message("Not found")
|
|
280
|
+
|
|
281
|
+
# With extensions
|
|
282
|
+
expect(result).to have_graphql_error.with_extensions(code: "NOT_FOUND")
|
|
283
|
+
|
|
284
|
+
# At specific path
|
|
285
|
+
expect(result).to have_graphql_error.at_path(["user", "email"])
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
#### `succeed_graphql`
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
expect(result).to succeed_graphql
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `have_graphql_field` / `have_graphql_fields`
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
data = result.dig("data", "user")
|
|
298
|
+
|
|
299
|
+
expect(data).to have_graphql_field("name")
|
|
300
|
+
expect(data).to have_graphql_field(:email)
|
|
301
|
+
|
|
302
|
+
expect(data).to have_graphql_fields(
|
|
303
|
+
id: "123",
|
|
304
|
+
name: "John Doe",
|
|
305
|
+
email: "john@example.com"
|
|
306
|
+
)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## GraphQL Type Matchers
|
|
312
|
+
|
|
313
|
+
Test your GraphQL schema types, queries, and mutations.
|
|
314
|
+
|
|
315
|
+
### Type Field Testing
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
RSpec.describe UserType do
|
|
319
|
+
subject { described_class }
|
|
320
|
+
|
|
321
|
+
# Shoulda-style
|
|
322
|
+
it { is_expected.to have_field(:id).of_type("ID!") }
|
|
323
|
+
it { is_expected.to have_field(:name).of_type("String!") }
|
|
324
|
+
it { is_expected.to have_field(:email).of_type("String") }
|
|
325
|
+
it { is_expected.to have_field(:posts).of_type("[Post!]!") }
|
|
326
|
+
|
|
327
|
+
# With arguments
|
|
328
|
+
it do
|
|
329
|
+
is_expected.to have_field(:posts)
|
|
330
|
+
.with_argument(:limit, "Int")
|
|
331
|
+
.with_argument(:offset, "Int")
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Enum Testing
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
RSpec.describe StatusEnum do
|
|
340
|
+
subject { described_class }
|
|
341
|
+
|
|
342
|
+
it { is_expected.to have_enum_values("ACTIVE", "INACTIVE", "PENDING") }
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Schema Query/Mutation Testing
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
RSpec.describe AppSchema do
|
|
350
|
+
subject { described_class }
|
|
351
|
+
|
|
352
|
+
# Query fields
|
|
353
|
+
it { is_expected.to have_query(:user).with_argument(:id, "ID!") }
|
|
354
|
+
it { is_expected.to have_query(:users).of_type("[User!]!") }
|
|
355
|
+
|
|
356
|
+
# Mutation fields
|
|
357
|
+
it { is_expected.to have_mutation(:createUser).with_argument(:input, "CreateUserInput!") }
|
|
358
|
+
it { is_expected.to have_mutation(:deleteUser).of_type("Boolean!") }
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Field Argument Testing
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
RSpec.describe "UserType posts field" do
|
|
366
|
+
subject(:field) { UserType.fields["posts"] }
|
|
367
|
+
|
|
368
|
+
it { is_expected.to have_argument(:limit).of_type("Int") }
|
|
369
|
+
it { is_expected.to have_argument(:status).of_type("Status!") }
|
|
370
|
+
end
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## GraphQL Execution Helpers
|
|
376
|
+
|
|
377
|
+
Execute GraphQL queries and mutations with ease.
|
|
378
|
+
|
|
379
|
+
### `graphql_execute`
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
# Basic execution
|
|
383
|
+
result = graphql_execute(query, variables: { id: "123" }, context: { current_user: user })
|
|
384
|
+
|
|
385
|
+
# With custom schema
|
|
386
|
+
result = graphql_execute(query, schema: MySchema)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### `graphql_execute_as`
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
# Automatically adds user to context
|
|
393
|
+
result = graphql_execute_as(user, query, variables: { id: "123" })
|
|
394
|
+
|
|
395
|
+
# Custom context key
|
|
396
|
+
result = graphql_execute_as(user, query, context_key: :admin)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### `graphql_mutate`
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
# Execute mutations
|
|
403
|
+
result = graphql_mutate(mutation, input: { name: "John" }, context: { current_user: user })
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### `graphql_mutate_as`
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
# Execute mutation as user
|
|
410
|
+
result = graphql_mutate_as(user, mutation, input: { name: "John" })
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Shoulda Matchers
|
|
416
|
+
|
|
417
|
+
Automatically includes and configures [Shoulda Matchers](https://github.com/thoughtbot/shoulda-matchers) for Rails projects.
|
|
418
|
+
|
|
419
|
+
### Model Validations
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
RSpec.describe User do
|
|
423
|
+
subject { build(:user) }
|
|
424
|
+
|
|
425
|
+
# Validations
|
|
426
|
+
it { is_expected.to validate_presence_of(:email) }
|
|
427
|
+
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
|
|
428
|
+
it { is_expected.to validate_length_of(:password).is_at_least(8) }
|
|
429
|
+
|
|
430
|
+
# Associations
|
|
431
|
+
it { is_expected.to have_many(:posts).dependent(:destroy) }
|
|
432
|
+
it { is_expected.to belong_to(:organization) }
|
|
433
|
+
|
|
434
|
+
# Database columns
|
|
435
|
+
it { is_expected.to have_db_column(:email).of_type(:string) }
|
|
436
|
+
it { is_expected.to have_db_index(:email).unique }
|
|
437
|
+
end
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Controller Testing
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
RSpec.describe UsersController do
|
|
444
|
+
describe "GET #index" do
|
|
445
|
+
it { is_expected.to respond_with(:success) }
|
|
446
|
+
it { is_expected.to render_template(:index) }
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
describe "POST #create" do
|
|
450
|
+
it { is_expected.to permit(:name, :email).for(:create) }
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Complete Examples
|
|
458
|
+
|
|
459
|
+
### Full Interactor Test Suite
|
|
460
|
+
|
|
461
|
+
```ruby
|
|
462
|
+
RSpec.describe CreateUser do
|
|
463
|
+
subject(:result) { described_class.call(params) }
|
|
464
|
+
|
|
465
|
+
describe "success cases" do
|
|
466
|
+
let(:params) { { name: "John Doe", email: "john@example.com" } }
|
|
467
|
+
|
|
468
|
+
it { is_expected.to succeed }
|
|
469
|
+
it { is_expected.to set_context(:user) }
|
|
470
|
+
it { is_expected.to succeed.with_context(:user, kind_of(User)) }
|
|
471
|
+
|
|
472
|
+
it "creates a user with correct attributes" do
|
|
473
|
+
expect(result).to succeed
|
|
474
|
+
expect(result.user.name).to eq("John Doe")
|
|
475
|
+
expect(result.user.email).to eq("john@example.com")
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
describe "failure cases" do
|
|
480
|
+
context "with blank name" do
|
|
481
|
+
let(:params) { { name: "", email: "john@example.com" } }
|
|
482
|
+
|
|
483
|
+
it { is_expected.to fail_interactor }
|
|
484
|
+
it { is_expected.to fail_interactor.with_error("blank_name") }
|
|
485
|
+
it { is_expected.to have_error_code("blank_name") }
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
context "with duplicate email" do
|
|
489
|
+
before { create(:user, email: "john@example.com") }
|
|
490
|
+
|
|
491
|
+
let(:params) { { name: "John", email: "john@example.com" } }
|
|
492
|
+
|
|
493
|
+
it { is_expected.to fail_interactor.with_error("duplicate_email") }
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Full GraphQL Test Suite
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
RSpec.describe "User Queries" do
|
|
503
|
+
let(:user) { create(:user, name: "John Doe", email: "john@example.com") }
|
|
504
|
+
|
|
505
|
+
describe "user query" do
|
|
506
|
+
subject(:result) { graphql_execute_as(user, query, variables: { id: user.id }) }
|
|
507
|
+
|
|
508
|
+
let(:query) do
|
|
509
|
+
<<~GQL
|
|
510
|
+
query GetUser($id: ID!) {
|
|
511
|
+
user(id: $id) {
|
|
512
|
+
id
|
|
513
|
+
name
|
|
514
|
+
email
|
|
515
|
+
posts {
|
|
516
|
+
id
|
|
517
|
+
title
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
GQL
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
it { is_expected.to succeed_graphql }
|
|
525
|
+
it { is_expected.to have_graphql_data("user") }
|
|
526
|
+
|
|
527
|
+
it "returns correct user data" do
|
|
528
|
+
expect(result).to have_graphql_data("user").matching(
|
|
529
|
+
id: user.id,
|
|
530
|
+
name: "John Doe",
|
|
531
|
+
email: "john@example.com"
|
|
532
|
+
)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it { is_expected.to have_graphql_data("user", "posts").that_is_present }
|
|
536
|
+
|
|
537
|
+
context "when user has posts" do
|
|
538
|
+
before { create_list(:post, 3, user: user) }
|
|
539
|
+
|
|
540
|
+
it { is_expected.to have_graphql_data("user", "posts").with_count(3) }
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
context "when not authenticated" do
|
|
544
|
+
subject(:result) { graphql_execute(query, variables: { id: user.id }) }
|
|
545
|
+
|
|
546
|
+
it { is_expected.to have_graphql_error.with_message("Authentication required") }
|
|
547
|
+
it { is_expected.to have_graphql_error.with_extensions(code: "UNAUTHENTICATED") }
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Full GraphQL Type Test Suite
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
RSpec.describe Types::UserType do
|
|
557
|
+
subject { described_class }
|
|
558
|
+
|
|
559
|
+
# Basic fields
|
|
560
|
+
it { is_expected.to have_field(:id).of_type("ID!") }
|
|
561
|
+
it { is_expected.to have_field(:name).of_type("String!") }
|
|
562
|
+
it { is_expected.to have_field(:email).of_type("String") }
|
|
563
|
+
it { is_expected.to have_field(:createdAt).of_type("ISO8601DateTime!") }
|
|
564
|
+
|
|
565
|
+
# Associations
|
|
566
|
+
it { is_expected.to have_field(:posts).of_type("[Post!]!") }
|
|
567
|
+
it { is_expected.to have_field(:organization).of_type("Organization") }
|
|
568
|
+
|
|
569
|
+
# Fields with arguments
|
|
570
|
+
describe "posts field" do
|
|
571
|
+
subject(:field) { described_class.fields["posts"] }
|
|
572
|
+
|
|
573
|
+
it { is_expected.to have_argument(:limit).of_type("Int") }
|
|
574
|
+
it { is_expected.to have_argument(:offset).of_type("Int") }
|
|
575
|
+
it { is_expected.to have_argument(:status).of_type("PostStatus") }
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
RSpec.describe Types::PostStatusEnum do
|
|
580
|
+
subject { described_class }
|
|
581
|
+
|
|
582
|
+
it { is_expected.to have_enum_values("DRAFT", "PUBLISHED", "ARCHIVED") }
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
RSpec.describe AppSchema do
|
|
586
|
+
subject { described_class }
|
|
587
|
+
|
|
588
|
+
describe "queries" do
|
|
589
|
+
it { is_expected.to have_query(:user).with_argument(:id, "ID!") }
|
|
590
|
+
it { is_expected.to have_query(:users).of_type("[User!]!") }
|
|
591
|
+
it { is_expected.to have_query(:currentUser).of_type("User") }
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
describe "mutations" do
|
|
595
|
+
it { is_expected.to have_mutation(:createUser).with_argument(:input, "CreateUserInput!") }
|
|
596
|
+
it { is_expected.to have_mutation(:updateUser).with_argument(:input, "UpdateUserInput!") }
|
|
597
|
+
it { is_expected.to have_mutation(:deleteUser).of_type("Boolean!") }
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Configuration
|
|
605
|
+
|
|
606
|
+
Zenspec works out of the box with sensible defaults. For Rails applications, it automatically configures itself through a Railtie.
|
|
607
|
+
|
|
608
|
+
### Manual Configuration (Non-Rails)
|
|
609
|
+
|
|
610
|
+
If you're not using Rails, the matchers are still automatically included when you require zenspec:
|
|
611
|
+
|
|
612
|
+
```ruby
|
|
613
|
+
# spec/spec_helper.rb
|
|
614
|
+
require "zenspec"
|
|
615
|
+
|
|
616
|
+
# That's it! All matchers and helpers are available
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
## Development
|
|
622
|
+
|
|
623
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
624
|
+
|
|
625
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
626
|
+
|
|
627
|
+
## Contributing
|
|
628
|
+
|
|
629
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/zyxzen/zenspec. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/zyxzen/zenspec/blob/main/CODE_OF_CONDUCT.md).
|
|
630
|
+
|
|
631
|
+
## License
|
|
632
|
+
|
|
633
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
634
|
+
|
|
635
|
+
## Code of Conduct
|
|
636
|
+
|
|
637
|
+
Everyone interacting in the Zenspec project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zyxzen/zenspec/blob/main/CODE_OF_CONDUCT.md).
|